Skip to main content

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

#StageVerdictThe thing that stops it being a PASS
AIntake / createPARTIALNo UI creates units, so every project is building-wide. A building tracked with no client silently discards a deep-linked buildingId/eventId.
BDecidePASS (violation origin only)Strongest surface in the app. But with no linked orders it is a terminal dead end — see §3.2.
CWork / tracksPASS as of this passWas the worst navigation gap; now linked to the field job. Inspector picker still depends on licenses seed.
DProposalFAIL (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.
EField executionPARTIAL for XRF, FAIL for the restReport generation is XRF-only. Paint-chip / dust-wipe / abatement visits capture data that no UI can turn into a document.
FFile / docsPARTIALStrong required-slot checklist. But the "filing package" produces no merged PDF and never consults the checklist — it can assemble with required slots empty.
GMoneyFAIL (silent)Same rate_cards defect: invoice totals $0, still numbered, still markable paid.
HPhase advancementMANUAL ONLYA 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 tableSymptomCreatable in-app?
licensesInspector 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_cardsProposals and invoices total $0, silently/settings only, gated on manage_invoices
xrf_instrumentsXRF instrument dropdown is emptyNo UI at all — SQL only
unitsUnit picker never renders; every project is building-wideNo UI at allsrc/data/units.ts has no insert
laboratory_partnersChain of custody cannot be createdNo UI at allsrc/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)

  1. generate-proposal-pdf / generate-invoice-pdf skip 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.
  2. assemble-filing-package never 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.
  3. Phase advancement is unchecked. docs_qa → billing is one dropdown click regardless of documents, licenses, or signatures.
  4. XRF ingest warnings are computed and thrown away. ingest-xrf-csv runs the domain's calibration-cadence, sequence-gap and time-span checks server-side; ingestXrfCsv reads only readingsInserted, 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 onlycreateChainOfCustody 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

  1. Event-linking UI in Decide's empty state. Unblocks two of three project origins. linkProjectToEvent already exists.
  2. Make the $0 path fail loud. 422 from both PDF functions plus a "no rate card for X" link to Settings.
  3. 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.
  4. Seed or expose xrf_instruments and laboratory_partners.
  5. Make assemble-filing-package refuse (or report) unfilled required slots, and render the package contents.
  6. Lab-result entry, without which clearance cannot be closed in-app.
  7. A units create path, or an explicit decision that unit-scoped projects are out of scope.
  8. Confirm the nightly Layer 2 pg_cron sync's first run succeeds (sync-orchestrator-nightly-delta, installed 2026-09-03) and finish the in-progress manual historical backfill.
  9. Fix the HPD success-watermark poisoning and ECB SAFETY_CAP_OFFSET 550k truncation before trusting Layer 2 as fully loaded.
  10. Get email actually sending: RESEND_API_KEY in the verified path (stops at queued_unsent today) and a real Svix signature on handle-email-suppression.
  11. Subdomain routing (tenant_branding.portal_domain) — today one origin, tenant from the logged-in profile.
  12. 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:31 carries continue-on-error: true. npm run lint currently reports 1,448 errors and 78 warnings, of which 1,330 are @typescript-eslint/no-explicit-any, overwhelmingly in supabase/functions/**. Only 4 errors are --fix-able. Clear it, then drop the continue-on-error.
  • CI invokes two npm scripts that do not exist. ci.yml:150 and :155 run npm run check:tenant-guard and npm run check:entitlement-guard, but neither script is defined in any package.json. The underlying scripts/check-tenant-guard.mjs and scripts/check-entitlement-guard.mjs are real — they were just never wired up. Both steps are continue-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_transitions has 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 (vitest is scoped to src/**); the real suite is cd domain && npm test. Don't read a green root npm test as broad coverage.