Skip to main content

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.md and HPD_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 firms table, no deployments table. RULEBOOK's "firm" (SecureEnv, Abated NYC) maps 1:1 to a tenants row — firm_id was retired in favor of tenant_id, and RULEBOOK's deployments concept (§0) was folded into the inspections table (one row per field visit, carrying tenant_id) — see owner decision "Contradiction 13", supabase/migrations/20260821080900_inspections.sql. A single project can be worked by multiple tenants via project_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 to domain/src/programs/nyc-lead-paint/ — that package is the code twin of the rulebook, not archive/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)

TermMeaning
TenantSecureEnv or Abated — the white-label org running the app
ClientProperty owner / managing agent (portal login at /portal/*)
BuildingCitywide BIN record — shared across tenants
Tenant building"We service this BIN for this client"
Public eventOne agency row (HPD violation, complaint, lien, …)
ProjectOne unit of billable work — always has origin + program_id
InspectionOne scheduled field visit on a project (can be several per project)
ServiceDerived 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).

OriginWhen to useWhat happens at createLinked data
violationHPD (or other) citation drives the jobcreateProjectFromViolation(s) + project_event_linksOne or more public_events rows at create
obligationRecurring duty came due (LL31, RPO, annual notice)createProjectFromObligation — label only todayNo obligation engine — and no post-create event-link UI, so Decide opens empty
occupant_requestVoluntary / fear-of-lead / owner requestcreateProjectFromOccupantRequestSame empty Decide as obligation unless events were linked at create

Prerequisites before create:

  1. Building exists (or is found via address search on the new-project form).
  2. 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-linked buildingId/eventId.
  3. Optional unit_id when 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 (buildDecidedFormBundlesservicesForTracksprojectTypeLabel, 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 nameTypical ownerWhat people actually doPrimary UI
intakePMTrack, create, Decide paths, signed PROPOSALDecide + Work → Proposal
schedulingDispatch / PMEach track's field visits scheduled on inspectionsWork → Tracks (schedule drawer)
fieldInspectorCapture data; XRF report is the only generated field doc/field/:inspectionId wizard
docs_qaBack officeRequired slots, HPD forms, affidavits, filing-package rowFile tab
billingBillingInvoice numbered + sent + paid; vendor paymentsMoney tab
closedBadge tone only — not read-onlyHeader 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.

ActorPermissions (typical)Responsibilities
Project managermanage_projects, advance_projectsTrack building, create project, Decide paths, proposal, schedule visits, set phase label
Dispatchmanage_projectsAssign inspector, schedule date, access — per visit
Inspectormanage_inspections/field queue + wizard: XRF CSV, samples, floor-plan sketch, complete visit
Back office / compliancemanage_documents, manage_licensesGenerate HPD forms, sign/notarize affidavits, assemble filing package row
Billingmanage_invoicesGenerate invoice PDF, mark sent/paid, vendor payments
Client (portal)client_user JWTRead own buildings, projects, documents (signed URLs), non-draft invoices
Collaborator tenantproject_collaborators grantfield_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.

ShapeMeaningTypical servicesTabs usually used
S1 Contest (records only)Age, exemption, tenancy — no field testnoneDecide → File → Money
S2 XRF dismiss / contestProve no LBP on presumed surfacesxrfDecide → Work → File → Money
S3 Paint-chipInconclusive surfaces (624, 625 violation surface)xrf, paint_chipFull; lab report UI not wired
S4 Abate + clearancePositive hazard or blocked turnoverabatement, dust_wipeFull; often two inspections; abatement/CoC reports not wired
S5 Records / LL31RPO 618–620 or 626 report submissionnone or xrf onlyDecide → 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).

OrderShapeService bundleTenant
616S2 (XRF)xrfSecureEnv (inspection EPA)
617S4 (abate)abatement + dust_wipeAbated 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

StepBuilt?Where
See both violations on building/buildings/:id events list, /violations
Link both to one project✅ at create/projects/newnot on project detail after create
Bundle recommendation (616 vs 617)✅ domain + DecidebuildHpdFormBundles / Options Explorer
Persist chosen pathsproject_order_decisions
Auto-split inspections per tenantcreateInspection() takes tenantId as a caller-supplied argument — staff create one inspection per tenant/service
Per-visit tenant on inspectionsinspections.tenant_id
Union ABAT-AFF violation numbers⚠️ partialEdge generate-hpd-document; staff must verify
XRF reuse from prior LL31⚠️ logic onlydomain/.../xrf/reuse.tsno UI
Abatement / dust-wipe / CoC reports❌ in UIEdge functions exist, no invoker
Lab-result entryClearance 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

StepBuilt?Notes
Blocker detectionblockingViolations in domain; fidelity tests vs archive
Warn PM not to pick XRF for 622⚠️ partialBundles 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 onlyproject_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

StepBuilt?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 generatorNo generate-rpo-filing in promoted supabase/functions/
Obligation-driven auto-create for RPONo evaluate-obligations, no /obligations page
unit_rpo_records tableNot 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 stateMeaningHow you get there
closedOps label — commercially intended completeHeader dropdown, any time
cancelledJob abandonedSide-state
on_hold / blockedPausedSide-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):

StageVerdict
Intake / createPARTIAL — no units UI; unassigned client drops deep links
DecidePASS for violation origin; dead end otherwise
Work / tracksPASS — field job is linked; inspector picker needs licenses seed
ProposalFAIL (silent) — $0 without rate_cards
Field executionPARTIAL XRF, FAIL other services (no report UI)
File / docsPARTIAL — checklist yes, filing package is a row with no PDF
MoneyFAIL (silent) — $0 invoices still markable paid
PhaseMANUAL 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_idtenant_id), and deployments was folded into inspections. archive/src/lib/hpd/* is archaeology only — use domain/ instead.

Pending rulebook ↔ code gaps (from RULEBOOK §7):

  1. 625 XRF-clear path — owner ruling 2026-07-13; verify orderRegistry has xrfClearable: true for 625. DESIGN-C also flags 625's missing blockedByOpen: ['617'].
  2. 616/624 four-state instance fieldsinstance_state column exists; domain does not consume it yet.

13. Quick reference — files to read

QuestionRead
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
Schemadomain/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.

#OrderFamilyPlain EnglishSurface status
V1616HazardPresumed LBP on living-room window framePresumed
V2617HazardHPD XRF positive on bedroom doorPositive
V3622TurnoverTurnover work on that same positive doorTied to 617
V4624HazardKitchen window — HPD XRF was inconclusiveInconclusive
V5625TurnoverTurnover inconclusive on kitchen friction surfacesTied 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 (abatementdust_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.