Project lifecycle — initiation to completion
What this is. The operational guide for how work moves through Complied today: how projects are created, who does what, which screens exist. For HPD legal rules (per-order resolution paths, document codes, which professional/EPA license a service requires), see
RULEBOOK.mdandHPD_SYSTEM_MODEL.md.Pass/fail of a real walk-through lives in
LIFECYCLE_ASSESSMENT.md. If this guide and that file disagree on what is built, the assessment wins.No
firmstable, nodeploymentstable. RULEBOOK's "firm" (SecureEnv, Abated NYC) maps 1:1 to atenantsrow —firm_idwas retired in favor oftenant_id, and RULEBOOK'sdeploymentsconcept (§0) was folded into theinspectionstable (one row per field visit, carryingtenant_id) — see owner decision "Contradiction 13",supabase/migrations/20260821080900_inspections.sql. A single project can be worked by multiple tenants viaproject_collaborators(owning tenant grants a scoped role to a collaborator tenant on one project) — see §4 and §7 below.Not Laravel. The pre-rebuild app lives under
archive/(React + Supabase). The rule engine was ported todomain/src/programs/nyc-lead-paint/— that package is the code twin of the rulebook, notarchive/src/lib/hpd/orderRegistry.ts.Last updated: 2026-09-03 (doc consolidation — merged in
CLIENT_COMPLEX_PROJECT_WALKTHROUGH.md).
1. The big picture
Complied turns agency data on a building into a billable project, records a decision per linked HPD order, derives field tracks, produces field evidence + HPD paperwork, invoices the client, and closes commercially. Filing with HPD is always human-out — the app generates individual PDFs; staff mail or upload them. Assembling a "filing package" today writes a DB row, not a merged PDF.
NYC Open Data ──► public_events (on buildings)
│
Staff tracks building ──┤
▼
┌─────────────────────────┐
│ PROJECT (one job) │
│ origin + program │
│ + linked events │
│ + order decisions │
└─────────┬───────────────┘
│
Decide → Work → File → Money
(phase is a header label, not this pipeline)
│
filing package row (human files with HPD)
Vocabulary (do not mix up)
| Term | Meaning |
|---|---|
| Tenant | SecureEnv or Abated — the white-label org running the app |
| Client | Property owner / managing agent (portal login at /portal/*) |
| Building | Citywide BIN record — shared across tenants |
| Tenant building | "We service this BIN for this client" |
| Public event | One agency row (HPD violation, complaint, lien, …) |
| Project | One unit of billable work — always has origin + program_id |
| Inspection | One scheduled field visit on a project (can be several per project) |
| Service | Derived work type: xrf, paint_chip, dust_wipe, abatement |
Full glossary: domain/OVERVIEW.md. Phase-as-label: domain/GATING.md.
2. How projects are created (three origins)
Every project has an immutable origin. Creation UI: /projects/new (ProjectNewPage.tsx).
| Origin | When to use | What happens at create | Linked data |
|---|---|---|---|
violation | HPD (or other) citation drives the job | createProjectFromViolation(s) + project_event_links | One or more public_events rows at create |
obligation | Recurring duty came due (LL31, RPO, annual notice) | createProjectFromObligation — label only today | No obligation engine — and no post-create event-link UI, so Decide opens empty |
occupant_request | Voluntary / fear-of-lead / owner request | createProjectFromOccupantRequest | Same empty Decide as obligation unless events were linked at create |
Prerequisites before create:
- Building exists (or is found via address search on the new-project form).
- For violation origin: building is tracked (
tenant_buildings) and has open violations visible on the form. A tracked building with no client silently drops a deep-linkedbuildingId/eventId. - Optional
unit_idwhen work is apartment-scoped — no units-create UI, so this is rare.
Not automatic: ingest does not create projects. sync-orchestrator fills public_events; a PM
must consciously open a project from a violation. /violations navigates to the building, not to
project create — one extra hop.
Project type + services (derived, not declared)
project_type and project_services are gone. Neither is set by staff anywhere — both are
derived live from the project's linked HPD orders and their project_order_decisions
(buildDecidedFormBundles → servicesForTracks → projectTypeLabel,
domain/src/field/serviceRequirements.ts), the same derivation generate-proposal-pdf uses to
price the proposal and the Work tab's Tracks panel uses for each track's field-visit tasks.
Before a decision exists, the derivation falls back to the cheapest-cure guess
(buildHpdFormBundles); once decided, the real chosen service overrides the guess. There is no
separate manual-declaration step.
UI: /projects/:id → Work tab → Proposal card (read-only project type + service chips,
naming any undecided orders with a link to Decide) and Tracks (per-track field-visit tasks
with schedule-visit drawers and Open field job).
3. Phase is a label, not a pipeline
domain/src/phases/gates.ts was deleted in Rung 6. projects.phase is a manually-set
operational label (setProjectPhase from the client). Any phase may be set from any other, in
either direction. History is still captured via the projects_log_phase_transition trigger.
intake → scheduling → field → docs_qa → billing → closed
These names remain useful as ops vocabulary. They do not unlock or lock tabs, documents, or field work. The workspace is always Decide / Work / File / Money.
Side states (on_hold, cancelled, blocked) are orthogonal; the header can set or clear them
without touching phase.
| Phase name | Typical owner | What people actually do | Primary UI |
|---|---|---|---|
| intake | PM | Track, create, Decide paths, signed PROPOSAL | Decide + Work → Proposal |
| scheduling | Dispatch / PM | Each track's field visits scheduled on inspections | Work → Tracks (schedule drawer) |
| field | Inspector | Capture data; XRF report is the only generated field doc | /field/:inspectionId wizard |
| docs_qa | Back office | Required slots, HPD forms, affidavits, filing-package row | File tab |
| billing | Billing | Invoice numbered + sent + paid; vendor payments | Money tab |
| closed | — | Badge tone only — not read-only | Header dropdown |
Important: setting closed does not wait for HPD to accept a filing, and does not
check documents, licenses, invoices, or signatures. Legal closure with the agency is outside the
app. Candidate close-time warnings (undecided orders, empty slots, unpaid vendors) are proposed
in domain/GATING.md / DESIGN-C and have not shipped.
4. Who does what (roles)
Roles are permission bundles (profiles.permission_bundle_id), not a fixed enum. Default bundles
map to the lanes below.
| Actor | Permissions (typical) | Responsibilities |
|---|---|---|
| Project manager | manage_projects, advance_projects | Track building, create project, Decide paths, proposal, schedule visits, set phase label |
| Dispatch | manage_projects | Assign inspector, schedule date, access — per visit |
| Inspector | manage_inspections | /field queue + wizard: XRF CSV, samples, floor-plan sketch, complete visit |
| Back office / compliance | manage_documents, manage_licenses | Generate HPD forms, sign/notarize affidavits, assemble filing package row |
| Billing | manage_invoices | Generate invoice PDF, mark sent/paid, vendor payments |
| Client (portal) | client_user JWT | Read own buildings, projects, documents (signed URLs), non-draft invoices |
| Collaborator tenant | project_collaborators grant | field_execution: inspections only — no invoices, documents, or licenses |
5. HPD resolution shapes → workspace (bridge to RULEBOOK)
Every HPD order resolves via one of five shapes (see RULEBOOK §2). The shape tells you which services and tabs matter — not every project runs field work.
| Shape | Meaning | Typical services | Tabs usually used |
|---|---|---|---|
| S1 Contest (records only) | Age, exemption, tenancy — no field test | none | Decide → File → Money |
| S2 XRF dismiss / contest | Prove no LBP on presumed surfaces | xrf | Decide → Work → File → Money |
| S3 Paint-chip | Inconclusive surfaces (624, 625 violation surface) | xrf, paint_chip | Full; lab report UI not wired |
| S4 Abate + clearance | Positive hazard or blocked turnover | abatement, dust_wipe | Full; often two inspections; abatement/CoC reports not wired |
| S5 Records / LL31 | RPO 618–620 or 626 report submission | none or xrf only | Decide → File → Money |
Overdue shifts hazard orders to S4 (AF-5 + fee). Turnover overdue keeps the cheapest shape but files dismissal to the Audit Unit instead of cert.
Domain derives bundles: buildHpdFormBundles(linkedOrders, asOfDate) — shown on Decide when
events are linked. After a decision, buildDecidedFormBundles takes over.
6. Swimlane — any violation project (happy path)
The full actor-by-actor flow — PM → Dispatch → Inspector → Back office → Money, then a human
files with HPD — is diagrammed once, canonically, in
domain/SWIMLANES.md §1 ("Staff project: violation → commercially
done"). Solid edges there are built UI; dashed are manual or outside the app.
Two things worth calling out without re-drawing it here: filing is always human-out (the app
builds PDFs; staff mail or upload them), and setting phase to closed is a header click, not a
gate — it does not wait on HPD acceptance and does not check documents, licenses, or invoices.
7. Complex example A — 616 + 617 co-issue (split bundles)
The rule behind all three examples below: contest or test before you abate — abatement is
the expensive last resort. orderRegistry lists each order's cure methods cheapest-first;
deriveServiceKey takes the first viable one, then overrides to forced abatement when the order
is an overdue hazard (AF-5 + fee) or is blocked by another open order (blockedByOpen).
Scenario: Same apartment, HPD cited 616 (presumed) and 617 (positive) on friction surfaces. Owner wants cheapest path: XRF-contest the 616, abate the 617.
Rulebook: C7 in RULEBOOK §5 — two bundles, two service shapes, one project (not two projects).
| Order | Shape | Service bundle | Tenant |
|---|---|---|---|
| 616 | S2 (XRF) | xrf | SecureEnv (inspection EPA) |
| 617 | S4 (abate) | abatement + dust_wipe | Abated NYC + clearance sampler |
Decision spine: Both violations link to one project at create. Decide records contest-616
/ cure-617; Tracks derive xrf (616, presumed → cheap XRF clear) plus abatement + dust_wipe
(617, already positive → no content dismissal). SecureEnv runs the XRF visit; Abated NYC runs the
abatement visit; clearance is a third, independent visit (never the abating firm) — and if the
two are cured together, the abatement supervisor affidavit unions both violation numbers. End
state: CERT-H hazard cert + proof set in the filing package row; a human files it with HPD.
UI today
| Step | Built? | Where |
|---|---|---|
| See both violations on building | ✅ | /buildings/:id events list, /violations |
| Link both to one project | ✅ at create | /projects/new — not on project detail after create |
| Bundle recommendation (616 vs 617) | ✅ domain + Decide | buildHpdFormBundles / Options Explorer |
| Persist chosen paths | ✅ | project_order_decisions |
| Auto-split inspections per tenant | ❌ | createInspection() takes tenantId as a caller-supplied argument — staff create one inspection per tenant/service |
| Per-visit tenant on inspections | ✅ | inspections.tenant_id |
| Union ABAT-AFF violation numbers | ⚠️ partial | Edge generate-hpd-document; staff must verify |
| XRF reuse from prior LL31 | ⚠️ logic only | domain/.../xrf/reuse.ts — no UI |
| Abatement / dust-wipe / CoC reports | ❌ in UI | Edge functions exist, no invoker |
| Lab-result entry | ❌ | Clearance cannot be recorded passed in-app |
8. Complex example B — 622 blocked by open 617 (forced abate)
Scenario: Turnover 622 issued alongside hazard 617 on the same positive friction surface. HPD cert forbids XRF Option 4 while 617 is open. Cheapest path is blocked → must abate.
Decision spine: orderRegistry marks 622.blockedByOpen = ['617']; blockingViolations()
sees the open 617 and deriveServiceKey forces abatement_dustwipe — Decide never offers an
XRF-dismiss path for 622, it shows one abatement bundle. Field runs abatement then clearance,
filing CERT-T Option 3. Back office unions both violation numbers on a single ABAT-AFF, then
sequences the filing by hand: dismiss 617 first, then 622.
UI today
| Step | Built? | Notes |
|---|---|---|
| Blocker detection | ✅ | blockingViolations in domain; fidelity tests vs archive |
| Warn PM not to pick XRF for 622 | ⚠️ partial | Bundles show abatement; Decide is the decision surface (no leftover service checkboxes) |
| Per-option walkthrough (contest vs cure) | ⚠️ | Decide / Options Explorer — richer than at Rung 0, still not the old archive wizard |
| Instance state (contest came back positive) | ⚠️ column only | project_order_decisions.instance_state exists; domain does not read it yet |
Without units | ⚠️ | unitKeyForEvent falls back to building:<id> — blockers are over-broad on multi-unit buildings |
9. Complex example C — RPO 618 + hazard 616 (parallel tracks)
Scenario: Audit triggered 618 (records) and unit has 616 hazard. Records bundle is independent of field testing (S5 + S2).
Decision spine: Owner practice is usually one project with two tracks, not two projects. The records track (618, S5) needs no field visit — staff gather AF-RPO packets (annual notice, investigation, turnover docs) and file to the Audit Unit through the File tab. The hazard track (616, S2) schedules an inspector, XRFs the cited components, then contests or certifies. Back office assembles two document sets into one filing package row; billing may still be one invoice.
UI today
| Step | Built? | Notes |
|---|---|---|
| Create from violation | ✅ | /projects/new |
records as a service type | ⚠️ | Not in SERVICE_TYPE_IDS (xrf / dust_wipe / paint_chip / abatement) — records work is File-tab documents, not a field visit |
| RPO-specific generator | ❌ | No generate-rpo-filing in promoted supabase/functions/ |
| Obligation-driven auto-create for RPO | ❌ | No evaluate-obligations, no /obligations page |
unit_rpo_records table | ❌ | Not in rebuild schema |
Documents & professionals across these three examples: owner/agent signs cert, contest, and AF-5 covers; the inspector/RA produces XRF and paint-chip reports; the abatement supervisor signs the proof-set (ABAT-AFF + ABAT-EPA + WORK-DESC + DUST-LAB + SAMP-AFF + SAMP-EPA — same set every time, only the cover form changes if overdue); the NYS ELAP lab supplies paint-chip and clearance lab results; a notary witnesses certs, the XRF affidavit, the proof-set, and the clearance affidavit.
10. Project end states
| End state | Meaning | How you get there |
|---|---|---|
closed | Ops label — commercially intended complete | Header dropdown, any time |
cancelled | Job abandoned | Side-state |
on_hold / blocked | Paused | Side-state |
HPD acceptance is not modeled. Staff may keep a project closed while HPD still shows open.
Client portal: /portal/projects/:id — read-only status, documents, invoices.
11. Built vs not built
Do not maintain a second matrix here. The pass/fail record is
LIFECYCLE_ASSESSMENT.md. Architecture and production gaps:
domain/OVERVIEW.md, domain/CUTOVER.md.
Quick orientation (that assessment's stage verdicts, 2026-09-01):
| Stage | Verdict |
|---|---|
| Intake / create | PARTIAL — no units UI; unassigned client drops deep links |
| Decide | PASS for violation origin; dead end otherwise |
| Work / tracks | PASS — field job is linked; inspector picker needs licenses seed |
| Proposal | FAIL (silent) — $0 without rate_cards |
| Field execution | PARTIAL XRF, FAIL other services (no report UI) |
| File / docs | PARTIAL — checklist yes, filing package is a row with no PDF |
| Money | FAIL (silent) — $0 invoices still markable paid |
| Phase | MANUAL ONLY — dropdown, no gates |
Shipped surfaces that older docs called missing: Complied Map (/map), Violations explorer
(/violations), Field jobs (/field), Decide/Work/File/Money tabs, order-path decisions,
document slots.
12. How this relates to the old rulebook
RULEBOOK.md, HPD_SYSTEM_MODEL.md,
domain/SWIMLANES.md, and domain/GATING.md all
still apply. The one resolved structural gap is RULEBOOK §0's firm/deployments model: no
firms table survives this rebuild (firm_id → tenant_id), and deployments was folded into
inspections. archive/src/lib/hpd/* is archaeology only — use domain/ instead.
Pending rulebook ↔ code gaps (from RULEBOOK §7):
- 625 XRF-clear path — owner ruling 2026-07-13; verify
orderRegistryhasxrfClearable: truefor 625. DESIGN-C also flags 625's missingblockedByOpen: ['617']. - 616/624 four-state instance fields —
instance_statecolumn exists; domain does not consume it yet.
13. Quick reference — files to read
| Question | Read |
|---|---|
| How do I create a project? | src/pages/ProjectNewPage.tsx, src/data/projects.ts |
| What does phase do? | domain/GATING.md, setProjectPhase in src/data/projects.ts |
| How are services derived? | domain/src/field/serviceRequirements.ts |
| What docs does HPD need for order 624? | domain/.../orderRegistry.ts, RULEBOOK.md §1 |
| How are bundles computed? | domain/.../hpdFormBundles.ts, decidedFormBundles.ts |
| Can we run a real project? | LIFECYCLE_ASSESSMENT.md |
| What's NOT shipped / prod-ready? | domain/CUTOVER.md |
| Schema | domain/SCHEMA.md |
14. Illustration — one complex unit, five open orders
A single apartment can carry several HPD orders at once, each following a different resolution shape, all inside one project. Fictional but rulebook-faithful (combinations C6 + C7 + C8): Apt 3B, a pre-1960 Brooklyn multiple dwelling, has five open lead orders after an HPD inspection following a complaint involving a child under six.
| # | Order | Family | Plain English | Surface status |
|---|---|---|---|---|
| V1 | 616 | Hazard | Presumed LBP on living-room window frame | Presumed |
| V2 | 617 | Hazard | HPD XRF positive on bedroom door | Positive |
| V3 | 622 | Turnover | Turnover work on that same positive door | Tied to 617 |
| V4 | 624 | Hazard | Kitchen window — HPD XRF was inconclusive | Inconclusive |
| V5 | 625 | Turnover | Turnover inconclusive on kitchen friction surfaces | Tied to 624 |
Running each order through the cheapest-path-first rule (§7 above): 616 (presumed) clears via
XRF — the cheap path. 617 (positive) has no content dismissal, so it must be abated. 622
shares 617's door and inherits the abatement bundle (blockedByOpen: ['617'] bars cert Options
4/5 while 617 is open). 624 (inconclusive) needs paint-chip — re-XRF proves nothing. 625
stays blocked until 624 clears.
That collapses into three service bundles inside the one project: Bundle A (xrf, 616,
SecureEnv), Bundle B (paint_chip, 624 → then unlocks 625, SecureEnv), Bundle C (abatement →
dust_wipe, 617 + 622, Abated NYC → an independent clearance sampler). SecureEnv owns the
project end to end — Abated NYC is invited only via a project_collaborators grant scoped to
field execution, so it runs the abatement visit but never sees invoices, documents, licenses, or
the filing package; had SecureEnv performed the abatement itself, clearance would have to go to
another firm (the abating firm can never clear its own job). Back office assembles three document
sets — XRF, paint-chip, abatement proof-set — into one filing package row; billing can still be a
single invoice.