Lifecycle assessment — can we run a real NYC lead-paint project through this?
Assessed 2026-09-01 against src/, domain/, supabase/ (not archive/). Re-checked 2026-09-02
for docs accuracy: stage letters A–H did not change; collaboration directory and Work-tab drawers
do not flip a verdict.
Short answer: yes, for one shape of project — a violation-origin project with linked HPD lead orders, taken down the XRF path — and only after three pieces of seed data that no UI can create are inserted by hand. Every other service path (paint chip, dust wipe, abatement) can be decided, scheduled and captured, then hits a wall at document generation.
This document is the pass/fail record. Architecture lives in docs/domain/. The operational
guide is docs/PROJECT_LIFECYCLE_GUIDE.md. Those were rewritten 2026-09-02 to match this
assessment; if a later sprint changes the product, refresh this file, then the others.
1. Stage-by-stage verdict
| # | Stage | Verdict | The thing that stops it being a PASS |
|---|---|---|---|
| A | Intake / create | PARTIAL | No UI creates units, so every project is building-wide. A building tracked with no client silently discards a deep-linked buildingId/eventId. |
| B | Decide | PASS (violation origin only) | Strongest surface in the app. But with no linked orders it is a terminal dead end — see §3.2. |
| C | Work / tracks | PASS as of this pass | Was the worst navigation gap; now linked to the field job. Inspector picker still depends on licenses seed. |
| D | Proposal | FAIL (silent) | With no rate_cards rows, generate-proposal-pdf skips every service, returns 200, and the UI toasts "Proposal generated — $0". Also un-regenerable once created. |
| E | Field execution | PARTIAL for XRF, FAIL for the rest | Report generation is XRF-only. Paint-chip / dust-wipe / abatement visits capture data that no UI can turn into a document. |
| F | File / docs | PARTIAL | Strong required-slot checklist. But the "filing package" produces no merged PDF and never consults the checklist — it can assemble with required slots empty. |
| G | Money | FAIL (silent) | Same rate_cards defect: invoice totals $0, still numbered, still markable paid. |
| H | Phase advancement | MANUAL ONLY | A bare dropdown, any phase to any phase, forward or backward. No gate engine exists. |
2. Data prerequisites no UI can satisfy
These are the reason a fresh tenant looks broken rather than empty.
| Missing table | Symptom | Creatable in-app? |
|---|---|---|
licenses | Inspector picker shows only "Unassigned", with no explanation | /settings only, gated on manage_licenses, unlinked from the flow. Now seeded by npm run seed:dev. |
rate_cards | Proposals and invoices total $0, silently | /settings only, gated on manage_invoices |
xrf_instruments | XRF instrument dropdown is empty | No UI at all — SQL only |
units | Unit picker never renders; every project is building-wide | No UI at all — src/data/units.ts has no insert |
laboratory_partners | Chain of custody cannot be created | No UI at all — src/data/labPartners.ts has zero importers |
Consequence worth calling out: with no units, unitKeyForEvent falls back to building:<id>, so
blockingViolations treats every open order on a building as being in the same unit. Blocker
detection is over-broad on multi-unit buildings.
3. Dead ends
3.1 Work → field job — FIXED in this pass
The only route into the inspection wizard was the "All inspections" drawer. VisitEditor now
carries an Open field job link, and there is a dedicated /field queue.
3.2 Decide with no linked orders — still a dead end
OptionsExplorer renders "No HPD lead orders linked to this project yet." with no action.
linkProjectToEvent / unlinkProjectFromEvent exist in src/data/projects.ts but have zero
callers outside project creation. So an obligation or occupant_request project — two of the
three documented origins — can never reach Decide, and therefore never gets tracks, a priced
proposal, or a document checklist. This is the highest-value remaining fix.
3.3 /violations → project create — still a detour
A violation row navigates to the building, not to project create. "Create project" is one hop further on.
3.4 File → Money → closed
No forward affordance between them; only the tab strip and the header dropdown.
3.5 Filing package contents
No view, no download, no per-document list. listFilingPackageDocuments has no caller.
4. Silent-failure defects (ranked)
generate-proposal-pdf/generate-invoice-pdfskip unpriced services and return 200. A $0 proposal can be signed and a $0 invoice can be numbered and marked paid. Both should 422 with the unpriced service list, and the UI should link to the rate-card settings.assemble-filing-packagenever reads the required-documents checklist. It includes only HPD cover documents it can generate plus affidavit-coded documents, then toasts success. A package can be "assembled" with every lab report slot empty.- Phase advancement is unchecked.
docs_qa → billingis one dropdown click regardless of documents, licenses, or signatures. - XRF ingest warnings are computed and thrown away.
ingest-xrf-csvruns the domain's calibration-cadence, sequence-gap and time-span checks server-side;ingestXrfCsvreads onlyreadingsInserted, so the client never sees them.
5. Unreachable backend
Edge functions on disk with no frontend invoker: generate-paint-chip-report,
generate-dust-wipe-report, generate-abatement-report, generate-coc.
Data modules with zero importers: src/data/labPartners.ts.
src/data/chainOfCustody.ts is imported by the wizard for list only — createChainOfCustody
still has no UI caller. Orphaned functions elsewhere: insertXrfReadings, updateDustWipeSampleResult,
listCalibrationRules, listExclusionPhrases, addApartmentExclusion, listInvoiceLineItems,
listProposalLineItems, listFilingPackageDocuments, getXrfReportReview, listAuditFindings.
Domain logic with no UI: ServiceFieldCaptureContract was unread until this pass (now consumed by
fieldJobReadiness); xrf/reuse.ts's routeStoredReading still has no consumer anywhere.
There is no lab-result entry UI, so a clearance can never be recorded as passed from the app.
7. Sidebar
11 of 17 nav items were dead stubs. "Schedule" is now the live Field jobs route; the remaining
dead entries are Dashboard, Tasks, Sales, Deadlines, Reports, Billing, Communications,
Compliance/Lab, Notifications, Admin, Knowledge. /settings is routed but has no sidebar entry —
reachable only from the avatar dropdown, which matters because three empty pickers can only be
fixed there.
8. What to fix next, in order
- Event-linking UI in Decide's empty state. Unblocks two of three project origins.
linkProjectToEventalready exists. - Make the $0 path fail loud. 422 from both PDF functions plus a "no rate card for X" link to Settings.
- Wire the three non-XRF report generators + chain of custody into the field job. The edge functions and the data layer both already exist; this is a button and a dialog each.
- Seed or expose
xrf_instrumentsandlaboratory_partners. - Make
assemble-filing-packagerefuse (or report) unfilled required slots, and render the package contents. - Lab-result entry, without which clearance cannot be closed in-app.
- A
unitscreate path, or an explicit decision that unit-scoped projects are out of scope. - Confirm the nightly Layer 2
pg_cronsync's first run succeeds (sync-orchestrator-nightly-delta, installed 2026-09-03) and finish the in-progress manual historical backfill. - Fix the HPD success-watermark poisoning and ECB
SAFETY_CAP_OFFSET550k truncation before trusting Layer 2 as fully loaded. - Get email actually sending:
RESEND_API_KEYin the verified path (stops atqueued_unsenttoday) and a real Svix signature onhandle-email-suppression. - Subdomain routing (
tenant_branding.portal_domain) — today one origin, tenant from the logged-in profile. - Owner sign-off on remaining CARRY gaps (obligations, units UI, photos) as deferred or as new work.
9. Repo and CI debt
Not product gaps, but they weaken the gates that protect everything in §8. Measured 2026-09-03.
- Lint is advisory, not blocking.
.github/workflows/ci.yml:31carriescontinue-on-error: true.npm run lintcurrently reports 1,448 errors and 78 warnings, of which 1,330 are@typescript-eslint/no-explicit-any, overwhelmingly insupabase/functions/**. Only 4 errors are--fix-able. Clear it, then drop thecontinue-on-error. - CI invokes two npm scripts that do not exist.
ci.yml:150and:155runnpm run check:tenant-guardandnpm run check:entitlement-guard, but neither script is defined in anypackage.json. The underlyingscripts/check-tenant-guard.mjsandscripts/check-entitlement-guard.mjsare real — they were just never wired up. Both steps arecontinue-on-error: true, so CI has been silently green on a command that cannot run. Wire the scripts up, confirm they pass, then make them blocking. project_phase_transitionshas no free-text reason/note column — flagged since Phase 2 and still open. Transitions record the what, never the why.- Root test coverage is thin by design (
vitestis scoped tosrc/**); the real suite iscd domain && npm test. Don't read a green rootnpm testas broad coverage.