Rewritten 2026-08-02 — the original framing ("no named-contractor directory") came from a mis-transcription of the IA audit and is withdrawn. Revised again the same day after verifying the estimator against production and the code: the feature is already built end to end. See the trail below.
What the IA audit actually said
plans/2026-07-23-ia-remaining-37-roadmap.md:560-566 did not report a missing directory. It reported the same trade concept living in more than one vocabulary:
| vocabulary |
shape |
DEFECT_TRADES (server/types/defect-fields.ts:15) |
20 stable slugs, drive the live {{trade}} Mustache render, append-only |
contractor_types |
tenant-editable free text, no stable slug — 10 seeded rows per tenant |
recommendation-categories.ts |
50 hardcoded entries |
Its prescription was vocabulary consolidation, and it deferred that itself, with the blocker written out: defect trade stores a stable slug driving live Mustache; reading contractor_types instead (per-tenant ids, free text) breaks that render pipeline, forces the synchronous sanitizeDefectStates enum guard to become an async DB read, and requires reconciling two heterogeneous taxonomies.
"Only a contractor_types taxonomy exists" was true. "Therefore a named-contractor directory is missing" was added in transcription.
One correction to the audit itself: recommendation-categories.ts is not a trade vocabulary. Its 50 entries are keyed by component/action (roof-leak, water-heater, sewer-scope), and the trade appears only inside the generated sentence — "Recommend a qualified plumber service or replace the water heater." Only two vocabularies actually collide.
Scope, corrected
1. Repair estimates — already built; what is missing is the switch
Verified 2026-08-02 against the codebase and production.
The carry-through already works:
| stage |
location |
| authoring |
app/routes/repair-items.tsx:197, app/routes/resources/comments-library.tsx:93 |
| snapshot into results |
server/lib/collab/results-doc.types.ts:74 — estimateSnapshotMin/Max |
| aggregation + totals |
server/lib/aggregate-recommendations.ts:64 — estimateMinSum / estimateMaxSum |
| report render |
app/components/portal/sections/ReportView.tsx:670,688; report/ReportRepairPanel.tsx:59 |
| repair request |
app/components/portal/sections/repair/RepairDefectRow.tsx:95 |
| analytics |
server/services/inspection/inspection-analytics.service.ts:244 |
What is actually missing is a checkbox. tenant_configs.is_estimates_shown gates the render and defaults to false. Its two sibling flags, declared adjacent in both the schema and UpdateBrandingSchema — enableRepairList and enableCustomerRepairExport — each have a checkbox in the Report features group of app/routes/settings-workspace.tsx. showEstimates appears in that file zero times, and the action builds its body field by field, so the form would not forward it even if posted.
In production, one tenant has it enabled: the developer's own test tenant. No real tenant has it on, and none can turn it on.
Correcting an earlier claim in this issue: the "627 comments carrying an estimate (17%)" figure is not evidence of use. Those rows come from server/data/recommendation-seeds.ts (80 entries, 57 with estimates) via RecommendationService.bulkSeed. Every tenant has exactly 57, created within seconds of each other, and every distinct value's row count is a multiple of the tenant count. comment_usage is empty. No user has authored an estimate.
That matters for what ships: the seeded numbers are national medians (the file header cites "2024-2026 InterNACHI / IRC / NFPA convention text + median US contractor pricing"), and that provenance is visible only in a source comment. Turning the switch on without marking them would put someone else's national median in front of a buyer as the inspector's local figure.
comments.library_id is already the provenance field — "Set when MarketplaceService.importLibrary inserts rows; null for tenant-authored comments" — and bulkSeed (server/services/recommendation.service.ts:127) does not set it, so seeded rows are indistinguishable from authored ones. Setting it also makes the seeded price table updatable, since replace-mode already deletes only same-library_id rows.
2. Named-contractor directory — CLOSED, not deferred
No inspection-software competitor ships one. Contractor referral lists are maintained by inspection companies on their own websites, organised by specialty. Putting the platform into a recommendation chain carries liability, disclosure and conflict-of-interest exposure that nothing in the product currently justifies.
Reopen only if a tenant asks — and then the first question is whose recommendation it is.
3. Trade vocabulary consolidation — still deferred; baseline alignment is not
Consolidation stands deferred on the audit's own blocker analysis. But the seeded contractor_types list is not merely unconsolidated — it is a partial, differently-worded subset of the immutable slug vocabulary, and that is visible to users today:
| seed |
slug |
|
| Licensed Electrician |
licensed-electrician |
ok |
| HVAC Technician |
hvac-technician |
ok |
| General Contractor |
general-contractor |
ok |
| Structural Engineer |
structural-engineer |
ok |
| Chimney Sweep |
chimney-sweep |
ok |
| Plumber |
licensed-plumber |
wording differs |
| Roofer |
licensed-roofer |
wording differs |
| Pest/Termite |
pest-control |
wording differs |
| Foundation Specialist |
— |
no slug |
| Grading/Drainage |
— |
no slug |
Twelve slugs have no seeded entry at all: mold-remediation-specialist, septic-contractor, arborist, garage-door-technician, appliance-technician, waterproofing-contractor, mason, landscaper, painter, flooring-contractor, glazier, qualified-handyman.
So a defect flagged mold-remediation-specialist renders a trade the repair-item dropdown cannot offer, and the inspector types their own wording for it.
Aligning the seed costs a column and a backfill and does not touch the slug vocabulary or the Mustache pipeline. That is in scope here. Merging the two vocabularies is not.
Stale comment to remove while in there: the seed at starter-content.service.ts:87 says it "MUST stay in sync with the contractor-type backfill in 0000_baseline.sql" — no such backfill exists in that file any more.
Implementation surface
- add the
showEstimates checkbox to the Report features group in settings-workspace.tsx, and forward it in the action
- tenant-level estimate disclaimer, configured directly beneath that checkbox; new nullable column at the end of
tenant_configs, NULL falling back to platform copy
bulkSeed sets library_id on seeded rows; backfill the existing rows, matched on the same (category, text) key bulkSeed already dedupes on
- surface the provenance in the library UI, with a way to clear the seeded set in one action
- realign the
CONTRACTOR_TYPES seed to DEFECT_TRADES, add trade_slug to contractor_types, backfill existing rows (rename three, keep the two unmapped rows with NULL, seed the twelve missing)
Default stays false. Whether an estimate goes into a negotiation is the tenant's call, not the platform's.
Out of scope, tracked separately
The CRUD audit that produced this revision also found: #291 (canned comments have no delete path), #292 (templates.version incremented but never pinned), #293 (marketplace library lifecycle; contractor-type delete has no reference check).
deleteTemplate guarding only inspections.templateId while services.templateId is also a FK belongs to the multi-service work, not here.
Design
docs/superpowers/specs/2026-08-02-oi-repair-estimates-design.md
Rewritten 2026-08-02 — the original framing ("no named-contractor directory") came from a mis-transcription of the IA audit and is withdrawn. Revised again the same day after verifying the estimator against production and the code: the feature is already built end to end. See the trail below.
What the IA audit actually said
plans/2026-07-23-ia-remaining-37-roadmap.md:560-566did not report a missing directory. It reported the same trade concept living in more than one vocabulary:DEFECT_TRADES(server/types/defect-fields.ts:15){{trade}}Mustache render, append-onlycontractor_typesrecommendation-categories.tsIts prescription was vocabulary consolidation, and it deferred that itself, with the blocker written out: defect trade stores a stable slug driving live Mustache; reading
contractor_typesinstead (per-tenant ids, free text) breaks that render pipeline, forces the synchronoussanitizeDefectStatesenum guard to become an async DB read, and requires reconciling two heterogeneous taxonomies."Only a
contractor_typestaxonomy exists" was true. "Therefore a named-contractor directory is missing" was added in transcription.One correction to the audit itself:
recommendation-categories.tsis not a trade vocabulary. Its 50 entries are keyed by component/action (roof-leak,water-heater,sewer-scope), and the trade appears only inside the generated sentence — "Recommend a qualified plumber service or replace the water heater." Only two vocabularies actually collide.Scope, corrected
1. Repair estimates — already built; what is missing is the switch
Verified 2026-08-02 against the codebase and production.
The carry-through already works:
app/routes/repair-items.tsx:197,app/routes/resources/comments-library.tsx:93server/lib/collab/results-doc.types.ts:74—estimateSnapshotMin/Maxserver/lib/aggregate-recommendations.ts:64—estimateMinSum/estimateMaxSumapp/components/portal/sections/ReportView.tsx:670,688;report/ReportRepairPanel.tsx:59app/components/portal/sections/repair/RepairDefectRow.tsx:95server/services/inspection/inspection-analytics.service.ts:244What is actually missing is a checkbox.
tenant_configs.is_estimates_showngates the render and defaults tofalse. Its two sibling flags, declared adjacent in both the schema andUpdateBrandingSchema—enableRepairListandenableCustomerRepairExport— each have a checkbox in the Report features group ofapp/routes/settings-workspace.tsx.showEstimatesappears in that file zero times, and the action builds its body field by field, so the form would not forward it even if posted.In production, one tenant has it enabled: the developer's own test tenant. No real tenant has it on, and none can turn it on.
Correcting an earlier claim in this issue: the "627 comments carrying an estimate (17%)" figure is not evidence of use. Those rows come from
server/data/recommendation-seeds.ts(80 entries, 57 with estimates) viaRecommendationService.bulkSeed. Every tenant has exactly 57, created within seconds of each other, and every distinct value's row count is a multiple of the tenant count.comment_usageis empty. No user has authored an estimate.That matters for what ships: the seeded numbers are national medians (the file header cites "2024-2026 InterNACHI / IRC / NFPA convention text + median US contractor pricing"), and that provenance is visible only in a source comment. Turning the switch on without marking them would put someone else's national median in front of a buyer as the inspector's local figure.
comments.library_idis already the provenance field — "Set when MarketplaceService.importLibrary inserts rows; null for tenant-authored comments" — andbulkSeed(server/services/recommendation.service.ts:127) does not set it, so seeded rows are indistinguishable from authored ones. Setting it also makes the seeded price table updatable, since replace-mode already deletes only same-library_idrows.2. Named-contractor directory — CLOSED, not deferred
No inspection-software competitor ships one. Contractor referral lists are maintained by inspection companies on their own websites, organised by specialty. Putting the platform into a recommendation chain carries liability, disclosure and conflict-of-interest exposure that nothing in the product currently justifies.
Reopen only if a tenant asks — and then the first question is whose recommendation it is.
3. Trade vocabulary consolidation — still deferred; baseline alignment is not
Consolidation stands deferred on the audit's own blocker analysis. But the seeded
contractor_typeslist is not merely unconsolidated — it is a partial, differently-worded subset of the immutable slug vocabulary, and that is visible to users today:licensed-electricianhvac-techniciangeneral-contractorstructural-engineerchimney-sweeplicensed-plumberlicensed-rooferpest-controlTwelve slugs have no seeded entry at all:
mold-remediation-specialist,septic-contractor,arborist,garage-door-technician,appliance-technician,waterproofing-contractor,mason,landscaper,painter,flooring-contractor,glazier,qualified-handyman.So a defect flagged
mold-remediation-specialistrenders a trade the repair-item dropdown cannot offer, and the inspector types their own wording for it.Aligning the seed costs a column and a backfill and does not touch the slug vocabulary or the Mustache pipeline. That is in scope here. Merging the two vocabularies is not.
Stale comment to remove while in there: the seed at
starter-content.service.ts:87says it "MUST stay in sync with the contractor-type backfill in0000_baseline.sql" — no such backfill exists in that file any more.Implementation surface
showEstimatescheckbox to the Report features group insettings-workspace.tsx, and forward it in the actiontenant_configs, NULL falling back to platform copybulkSeedsetslibrary_idon seeded rows; backfill the existing rows, matched on the same(category, text)keybulkSeedalready dedupes onCONTRACTOR_TYPESseed toDEFECT_TRADES, addtrade_slugtocontractor_types, backfill existing rows (rename three, keep the two unmapped rows with NULL, seed the twelve missing)Default stays
false. Whether an estimate goes into a negotiation is the tenant's call, not the platform's.Out of scope, tracked separately
The CRUD audit that produced this revision also found: #291 (canned comments have no delete path), #292 (
templates.versionincremented but never pinned), #293 (marketplace library lifecycle; contractor-type delete has no reference check).deleteTemplateguarding onlyinspections.templateIdwhileservices.templateIdis also a FK belongs to the multi-service work, not here.Design
docs/superpowers/specs/2026-08-02-oi-repair-estimates-design.md