Skip to content

Repair estimates: the estimator is built but has no settings switch; align the contractor-type seed to the trade slugs #277

Description

@important-new

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:74estimateSnapshotMin/Max
aggregation + totals server/lib/aggregate-recommendations.ts:64estimateMinSum / 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 UpdateBrandingSchemaenableRepairList 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions