diff --git a/docs/README.md b/docs/README.md index 55ba4f7cc..85a09b279 100644 --- a/docs/README.md +++ b/docs/README.md @@ -31,6 +31,8 @@ - [Product Overview](product/PRODUCT_OVERVIEW.md) - [Product Roadmap](product/PRODUCT_ROADMAP.md) - [Roadmap](product/ROADMAP.md) +- [Product PRD, October 2026](product/PRODUCT_PRD_2026-10.md) +- [FSMA 204 Readiness PRD gap analysis, 2026-10-28](product/FSMA_204_READINESS_GAP_ANALYSIS_2026-10-28.md) - [Positioning](product/POSITIONING.md) - [Pitch Deck](product/PITCH_DECK.md) - [Website Copy](product/WEBSITE_COPY.md) diff --git a/docs/product/FSMA_204_READINESS_GAP_ANALYSIS_2026-10-28.md b/docs/product/FSMA_204_READINESS_GAP_ANALYSIS_2026-10-28.md new file mode 100644 index 000000000..29b1b76fe --- /dev/null +++ b/docs/product/FSMA_204_READINESS_GAP_ANALYSIS_2026-10-28.md @@ -0,0 +1,203 @@ +# FSMA 204 Readiness PRD (external, 2026-10-28): gap analysis and reconciliation with PRODUCT_PRD_2026-10 + +Date: 2026-10-28. Status: proposal for founder review; this document decides nothing. Owner: founder / product. +Relation: subordinate to `docs/product/PRODUCT_PRD_2026-10.md` (2026-10-27). Where the two conflict, the 2026-10-27 decisions of record (its section 9, Q1 to Q6) and non-goals (its section 7) stand, and the conflicting item is marked "deferred, conflicts with 2026-10-27 decision". Supersedes nothing. +Basis: the summary of the external "RegEngine FSMA 204 Readiness PRD" (about 6,500 words, produced 2026-10-28 by an external assistant from a competitor-checklist research memo). The full text is not in this repository, so every statement about that PRD below is a statement about its summary. +Verification basis: repo state checked against the files cited inline on 2026-10-28 at commit 28e10bd (main 8548c09 merged in). The first pass used af5beee; of the files cited here, `services/ingestion/app/webhook_models.py`, `services/ingestion/app/csv_templates.py` and `docs/product/PRODUCT_PRD_2026-10.md` changed between the two commits and their claims were rechecked at 28e10bd. Production status comes from `PRODUCTION_ENABLED_ROUTERS` in `server/router_policy.py`, `PRODUCTION_SURFACE.md` and the copy registry `frontend/src/lib/capabilities.ts` (live: CSV upload, JSON events API; preview: EPCIS intake, EPCIS export; off: EDI, SFTP). Test mappings name files that exist; whether a test asserts the whole gate is stated per row. Jev answers (TypeSafe jev-1.14.0, request req_01a18223290f7a5d9b3824b5a17703a6) are planning inputs, not verified facts. Anything else is marked "not verified". + +## Summary + +- The two documents conflict on two items. The external P0 list names EDI 856 and SFTP intake; the 2026-10-27 PRD lists both as non-goals (section 7) and removes the `edi` router from the production allowlist before the first pilot ingest (R7, Q6, SEL-592). Jev: conflict yes, p(yes) 0.93 (planning input). +- Adopting the full external P0 list as current work would put the pilot goal at risk. Jev scored that risk 2.92 on a 0 to 3 scale, "critical", confidence 0.93 (planning input). Goal 2 of the 2026-10-27 PRD (one paid pilot by 2027-01-31) is the control. +- Of the 18 external P0 rows in section 3: 7 are already covered by R1 to R7, Q4 to Q6 or P1 items; 9 are partial and become proposals in section 9; 1 is absent (tenant exit export); 1 is deferred as conflicting (EDI 856 and SFTP). +- Positioning agrees. The external exclusion of QMS, MES, EHS and HACCP suites matches PRD section 7 and `docs/product/POSITIONING.md`. The external name "Nulogy" appears nowhere in this repository, so there is no checklist text to reconcile. +- Nothing in this document changes `frontend/src/lib/capabilities.ts`, `docs/PRODUCT_DESCRIPTION.md`, Linear, or code. Section 9 proposes stories; the founder files or discards them. +- One open question for the founder (section 10): whether the tenant exit export moves from P2 to P1 because the pilot paper (R5) relies on `docs/DATA_PROCESSING_AGREEMENT.md`, which promises a machine-readable export. + +## 1. Relation and precedence + +This document is subordinate to `docs/product/PRODUCT_PRD_2026-10.md`. Precedence when texts disagree: the 2026-10-27 decisions of record (section 9) and non-goals (section 7) first, then that PRD's P0, P1 and P2 lists, then the proposals here. + +"Deferred, conflicts with 2026-10-27 decision" means: no Linear story is proposed, the copy registry does not change, and the item is revisited only if the founder reopens section 7 of the PRD. The repo facts for such items are recorded in neutral terms so the decision can be revisited with evidence. + +Jev was asked how the external document should relate to the PRD. Its answer was unsure: reject 0.50, long-horizon spec 0.48, replace 0.01 (confidence 0.33, planning input). The founder chose on 2026-10-28 to keep the 2026-10-27 decisions and to produce this reconciliation rather than file the external document. That choice is the answer of record. + +## 2. Positioning + +| External summary | Repo today | Finding | +| --- | --- | --- | +| "Vendor-neutral traceability readiness and evidence layer between operational systems and the records a company must produce" | `docs/product/POSITIONING.md`: "traceability evidence platform that ingests supplier and ERP data, preflights CTE/KDE quality, routes fixes, and turns validated records into tenant-scoped audit and export evidence" | Same wedge, narrower words. No repo change needed. | +| "Supplier data in. Traceability problems exposed. Verifiable evidence out." | PRD section 1 problem statement; `docs/PRODUCT_DESCRIPTION.md` talking-track opener | Consistent. | +| "Proof of traceability on demand" | `docs/PRODUCT_DESCRIPTION.md`: hash or Merkle verification establishes artifact integrity, not source-data accuracy or regulatory approval | "Proof" must stay inside what the verifier shows: bundle integrity. The external summary itself excludes any claim that RegEngine proves source data is true, so the two agree once the word is scoped. | +| Not a QMS, MES, EHS, HACCP, maintenance or plant-operations suite | PRD section 7 non-goals; `REPO_PURPOSE.md` non-goals | Consistent. | + +## 3. P0 gap matrix + +The external summary lists its ingestion channels in one bullet. It is split here into row 3 (channels the 2026-10-27 PRD covers) and row 4 (SFTP and EDI 856), so the conflicting channels sit in their own row. That gives 18 rows. + +Disposition vocabulary: `already covered` (the 2026-10-27 PRD already carries the work), `fold into` a named SEL issue or feature, `propose new story` (section 9), `deferred, conflicts with 2026-10-27 decision`, `out of scope`. + +Production status vocabulary: PROD means the router is in `PRODUCTION_ENABLED_ROUTERS`; NON-PROD means the code exists but production does not mount it; registry means the label in `frontend/src/lib/capabilities.ts`. + +| # | External P0 capability | Repo state | Production status | Evidence | Relation to 2026-10-27 PRD | Disposition | +| --- | --- | --- | --- | --- | --- | --- | +| 1 | FDA-rule applicability and exemption scoping | partial | FTL scoping PROD inside the rules engine; exemptions frontend only | `services/shared/rules/ftl.py` (18 FTL categories); `services/shared/rules/engine.py` filters rules by `ftl_scope` and returns no verdict when FTL status is unknown. Exemptions under 21 CFR 1.1305 exist only as the questionnaire `EXEMPTION_QUESTIONS` in `frontend/src/app/tools/ftl-checker/components/FTLCheckerClient.tsx` and the text in `frontend/src/lib/ftl-data.ts`; no backend exemption logic | unlisted (the free tools are E4) | propose new story, P2 (row 1 in section 9) | +| 2 | Final-rule CTE, KDE and TLC model | partial | PROD on the canonical path | The canonical model carries the seven final-rule CTEs (`services/shared/canonical_event.py`). Drift elsewhere: `services/ingestion/app/webhook_models.py` adds `GROWING` ("legacy, not a CTE"); `CTEType` in `services/shared/schemas.py` has `CREATION` and `INITIAL_PACKING` only; graph and NLP enums use `CREATION`; the CHECK constraint in `alembic/versions/20260415_v057_schema_additions.py` allows `growing`; `services/ingestion/app/epcis/validation.py` collapses harvesting, cooling and initial packing to `CREATION` for validation only. TLC is free text with a 3-character minimum; `tlc_source_reference` and `tlc_source_gln` live in the `kdes` JSON; the `tlc_assigned` rule is a presence check, nothing models who may assign a TLC. Input-TLC normalization gap: the JSON events path promotes `input_traceability_lot_codes` into `kdes.input_lot_codes` untrimmed (`services/shared/canonical_event.py`), while `_create_transformation_links` in `services/shared/canonical_persistence/writer.py` strips input TLCs before the source-event lookup and the `input_tlc` insert, so a whitespace-bearing input TLC can miss its source event or be stored under a different value; CSV intake strips on the way in; output TLCs are inserted unchanged | Q5 (founder signs the seven CTE/KDE maps; SEL-591) | fold into SEL-591; propose new story S6 (P1) for the label drift and the input-TLC trimming fix (canonical TLC linkage is not covered until it lands) | +| 3 | Multi-channel ingestion: CSV, XLSX, API, webhook, EPCIS 2.0, supplier portal | partial | CSV and XLSX upload live; JSON events API live; EPCIS intake preview; portal links PROD; token-scoped correction sessions NON-PROD; email intake NON-PROD | `services/ingestion/app/csv_templates.py` (CSV and, through `services/ingestion/app/shared/xlsx_rows.py`, XLSX); `services/ingestion/app/webhook_router_v2/` (JSON events with HMAC and idempotency); `services/ingestion/app/epcis/router.py` (JSON, XML, batch, validate); `services/ingestion/app/supplier_portal.py` (expiring links, no account needed); `services/ingestion/app/remediation/supplier_router.py` (hashed token exchanged for a 30-minute session); `services/ingestion/app/email_ingest/` (Postmark inbound, no provisioning UI) | R1, R2, R4; P1 F2 (SEL-439 conformance checklist, SEL-429 identifier validation) | already covered | +| 4 | SFTP and EDI 856 intake | partial (code exists) | registry off for both; `edi` is on the PROD list until SEL-592 lands; SFTP is a connector class with no scheduled poll | `services/ingestion/app/edi_ingestion/routes.py` accepts an 856 transaction set on one route and multi-set interchanges on a second, and persists shipping CTEs through the shared ingest path; `services/shared/external_connectors/csv_sftp.py` polls a directory only when the NON-PROD `integration` router triggers a manual sync, and `asyncssh` is absent from `requirements.in`; `server/router_policy.py` still lists `edi` | section 7 non-goal ("EDI and SFTP intake"); R7, Q6, SEL-592; SEL-443 | deferred, conflicts with 2026-10-27 decision (section 9, deferred list) | +| 5 | Preservation and hashing of original source artifacts | partial | PROD for CSV and XLSX bytes; email-attachment bytes NON-PROD (email intake is off the production allowlist, row 3); EPCIS hash only; EDI none | CSV and XLSX uploads, and email attachments on the NON-PROD intake, keep the exact bytes with a SHA-256 in `fsma.manifest_blobs` (`alembic/versions/20260716_c7a1e5f2b9d4_v095_supplier_remediation_slice.py`), insert-only for the runtime role (`alembic/versions/20260928_c51d4898e0a2_v126_runtime_grants_csv_receipt_agent_worker.py`), linked to the hash-linked receipts ledger (`alembic/versions/20260513_c1d2e3f4a5b6_v079_ingestion_run_receipts.py`). EPCIS keeps a document SHA-256 and status, not the body (`services/shared/epcis_receipts.py`, `alembic/versions/20261024_a3f7ce361b0e_v137_epcis_document_receipts.py`). EDI keeps no file. `services/ingestion/app/s3_utils.py` writes regulatory documents, not supplier artifacts. Object storage was decided as Cloudflare R2 in `docs/decisions/2026-05-12-object-storage.md` and is not wired for artifacts | PRD section 4.4 invariants (hash chain, append-only receipts) | propose new story, P2 (EPCIS byte retention parity; artifacts to object storage) | +| 6 | Product, facility, supplier, lot, GTIN, GLN and SSCC normalization | partial | validators PROD; identity resolution NON-PROD and never called from ingest | Check-digit validators in `services/shared/fsma_validation.py`; stricter ones in `services/ingestion/app/gs1_identifiers.py`, whose only caller is `services/ingestion/app/labels/gs1_128.py`. Canonical entities, aliases, fuzzy match, merge and review queue in `services/shared/identity_resolution/`, exposed by `services/ingestion/app/identity_router.py` (not on the allowlist); `auto_register_from_event` has no caller, so ingest never resolves identities | P1 SEL-429 (identifier validation); P2 SEL-443 (GS1 roadmap) | fold identifiers into SEL-429; propose new story, P2 (identity resolution from ingest, review queue, no automatic merge) | +| 7 | Deterministic validation and a blocking commit gate | partial | rules PROD, blocking off by default; Workbench gate advisory; admin bulk-upload commit blocks; remediation release blocks (NON-PROD) | `REQUIRED_KDES_BY_CTE` in `services/ingestion/app/webhook_models.py` rejects single events (a copy lives in `services/shared/supplier_data_quality.py`). 37 seeded rules (26 critical, 11 warning) in `services/shared/rules/seeds.py` with `rule_version`, effective and retired dates; `RULES_ENGINE_ENFORCE` in `services/shared/rules/enforcement.py` defaults to `off` and is honored by three call sites (`docs/runbooks/RULES_ENGINE_ENFORCE_ROLLOUT.md`). `POST /api/v1/inflow-workbench/commit-gate` in `services/ingestion/app/inflow_workbench.py` returns a decision and blocks no write. The admin bulk-upload commit (`services/admin/app/bulk_upload/`) proceeds only from status `validated` | R1; Q4 and SEL-590 (switch to `cte_only` before the pilot's first export) | already covered | +| 8 | Exception and supplier-remediation workflows | implemented, part NON-PROD | exception queue PROD; remediation not on the allowlist | `services/ingestion/app/exception_router.py` (list, assign, resolve, waive, comments); `services/ingestion/app/remediation/` (manifests, cases, correction links, outbox) | R4 and SEL-593 (promote under the Promotion Rule) | already covered | +| 9 | Append-only corrections and revalidation | partial | correction patches chained with a trigger; the canonical event table is mutable; revalidation NON-PROD | `fsma.correction_patches` is hash-chained per case and trigger-protected (`services/ingestion/app/remediation/corrections.py`, v095). The canonical writer supports `supersedes_event_id` (`services/shared/canonical_persistence/writer.py`) but no public route writes an amendment; `services/ingestion/app/canonical_router.py` only reads history. `revalidate_record` lives in `services/ingestion/app/remediation/service.py` and is reached only through the supplier session. `fsma.traceability_events` has no append-only trigger and the runtime role holds UPDATE on it (`alembic/versions/20260803_b71d9e4c2a60_v111_runtime_role_grants.py`); the writer sets `status='superseded'` and the rules engine writes verdict columns. The PRD's section 4.4 names the append-only tables and this one is not among them | R4; Q2 and SEL-589 (canonical contract, read freeze on legacy tables) | propose new story S4 (P1); P2 amendment API | +| 10 | One-to-many and many-to-many transformation genealogy | partial | PROD (PostgreSQL) | `fsma.transformation_links` (v057, RLS policy v068) stores input TLC to output TLC per transformation event. The writer in `services/shared/canonical_persistence/writer.py` links many inputs to the single output TLC of one `transformation` or `initial_packing` event; one output per event, so one-to-many needs separate events. A failed link insert logs a warning and the event persists. Repack and relabel have no handling on the canonical path; a fixture-driven engine exists in `services/shared/recall_reconstruction/`. Test: `services/shared/tests/test_transformation_links_initial_packing.py` | unlisted | propose new story, P2 (design for) | +| 11 | Forward and backward trace | implemented, with cap gaps | PROD on PostgreSQL; Neo4j trace NON-PROD | `GET /trace/forward/{tlc}` and `/trace/backward/{tlc}` in `services/ingestion/app/canonical_router.py`: breadth-first, `max_depth` default 5 (maximum 20), result cap 10,000; `truncated` is set by the result cap only, reaching `max_depth` is not reported. The FDA export lineage walk in `services/shared/cte_persistence/queries.py` follows links without regard to direction (so it includes sibling lots) and does not flag its depth limit. The Neo4j tracer reports `truncated` at `TRACE_ROW_LIMIT` 5,000 (SEL-515) | R2 and SEL-412 (trace view, F7) | fold the depth-cap flag into SEL-412 (S1); propose new story S2 (P1) for lineage direction | +| 12 | FDA sortable spreadsheet export | implemented | PROD | `services/ingestion/app/fda_export/router.py` under `/api/v1/fda/`: `export`, `export/all`, `export/v2`, `export/recall`, `export/verify`, Merkle root and proof. KDE coverage gate at 80 percent in `services/ingestion/app/fda_export/coverage.py`; an `allow_incomplete` bypass still writes the export audit row (`log_export`; the export is refused when that write fails) but the bypass itself reaches only a warning log, and neither the audit row nor the package records it (S7 in section 9 adds it to the package); `/export/all` refuses windows over 90 days with HTTP 400. A second route, `GET /v1/fsma/audit/spreadsheet`, exists in `services/compliance/app/routes.py` | R1 (sortable spreadsheet inside the 24-hour records-request window, verified with `/export/verify`) | already covered | +| 13 | EPCIS 2.0 and canonical JSON exports | partial | EPCIS export preview, JSON-LD only, no XML; canonical JSON is a paginated read API | `GET /api/v1/epcis/export` reads `fsma.cte_events` with `LIMIT 2000` in `services/ingestion/app/epcis/persistence.py` and the response carries `event_count` but no truncation flag. `services/ingestion/app/epcis_export.py` is NON-PROD. `/api/v1/records` in `services/ingestion/app/canonical_router.py` pages canonical events; no bulk dump | P1 F2 (SEL-439) | fold the stated limit into SEL-439 (S3); propose new story, P2 (bulk canonical export) | +| 14 | Complete evidence ZIP with hashes, manifests, provenance, policy versions and known gaps | partial | PROD | The FDA package ZIP (`_build_fda_package` in `services/ingestion/app/fda_export_service.py`, served by `services/ingestion/app/fda_export/router.py`) holds the CSV, a chain-verification JSON, `manifest.json` with per-file SHA-256 and a Merkle root, a completeness summary, a validation-errors log and a README; it holds no source artifacts and no rule versions. The request-workflow package (`services/shared/request_workflow/assembly.py`, `services/ingestion/app/request_workflow_router.py`) carries `rule_versions`, `gap_analysis`, exception, signoff and waiver state and a self-verifying `package_hash`, as JSON in the database rather than a ZIP. The remediation evidence ZIP is NON-PROD. Compliance snapshots (`services/compliance/app/snapshots.py`) are sealed text | R1 (export package verified) | propose new story S7 (P1); P2 source artifacts in the bundle | +| 15 | Timed records-request and trace drills | partial | request workflow PROD; mock-audit drill NON-PROD; public recall drill returns no data | `services/ingestion/app/request_workflow_router.py` (scope, collect, gap analysis, assemble, signoff, submit, amend, deadlines). `services/ingestion/app/mock_audit.py` runs a timed drill persisted to `fsma.mock_audit_drills` (v102) and is not on the allowlist. `POST /api/v1/proof/recall-drill` in `services/ingestion/app/proof_recall_router.py` always returns `data_available: false`; `frontend/src/app/dashboard/recall-drills/page.tsx` hard-codes "not available" | Q6 (recall drill hidden until verified) | already covered for the pilot; propose new story, P2 (promote the mock-audit drill under the Promotion Rule) | +| 16 | Regulatory-policy versioning and impact analysis | partial | per-rule versions PROD | Each rule in `services/shared/rules/seeds.py` carries `rule_version`, `effective_date` and `retired_date`; `fsma.rule_evaluations` stores the version used. There is no rule-pack grouping and no pack version beyond the remediation `RULESET_HASH`. Impact analysis is one flag, `rule_version_changed`, in `services/shared/request_workflow/assembly.py` (test `tests/test_e2e_stale_evaluations.py`). The phrase "policy version" appears nowhere in the repository | unlisted | propose new story, P2 | +| 17 | Tenant isolation, least privilege, scoped supplier sessions | implemented, part NON-PROD | RLS and roles PROD; correction sessions NON-PROD; auditor access is a normal principal | Tenant from the authenticated principal only (`services/shared/tenant_context.py`); row-level security enabled or policies created in 50 Alembic revisions; least-privilege grant matrix in `alembic/versions/20260803_b71d9e4c2a60_v111_runtime_role_grants.py`; portal links bound to one tenant (`services/ingestion/app/supplier_portal.py`); single-use 30-minute supplier sessions (`services/ingestion/app/remediation/tokens.py`); `services/ingestion/app/auditor_router.py` is read-only under `audit.read` with no time-boxed auditor session type | PRD section 4.4; R4 (two-tenant isolation tests) | already covered; propose new story, P2 (time-boxed auditor session) | +| 18 | Complete customer data portability and exit export | absent | per-user export only | `GET /v1/account/export` in `services/admin/app/data_export_routes.py` is a per-user personal-data export limited to one per day (test `services/admin/tests/test_data_export_1094.py`). Tenant-scoped exports exist for the audit log (`services/admin/app/audit_routes.py`) and for FDA records as CSV or XLSX (`services/admin/app/supplier_compliance_routes.py`), but no portability export of events, source artifacts, manifests and rule versions exists. `docs/DATA_PROCESSING_AGREEMENT.md` line 120 promises export in a machine-readable format | unlisted; touches R5 (pilot paper) | propose new story, P2, flagged for the founder in section 10 | + +## 4. Acceptance gates against existing tests + +| External gate | Closest existing tests | Coverage | Note | +| --- | --- | --- | --- | +| Multi-channel supplier ingest | `services/ingestion/tests/test_csv_templates.py`, `services/ingestion/tests/test_epcis_ingestion_api.py`, `services/ingestion/tests/test_supplier_portal_1342.py`, `services/ingestion/tests/test_webhook_hmac_and_replay.py` | partial | Each channel is tested alone; no single scenario runs one dataset through every channel | +| Receiving | `services/ingestion/tests/test_csv_templates_1342.py` (per-CTE templates) | partial | | +| Complex transformation | `services/shared/tests/test_transformation_links_initial_packing.py` | partial | No many-to-many or one-to-many case | +| Repack and relabel | `tests/recall_reconstruction/` | none on the canonical path | The fixture engine is not wired to ingest | +| Shipping | CSV shipping template tests in `services/ingestion/tests/test_csv_templates.py`; EDI tests exist but that router leaves the allowlist under SEL-592 | partial | | +| Forward and backward tracing | `services/ingestion/tests/test_canonical_router_1342.py`, `tests/shared/test_cte_traversal_live.py`, `tests/shared/test_canonical_trace_cap_1282.py`, `services/graph/tests/test_trace_truncated_flag_sel515.py` | partial | The depth-cap flag is untested because it does not exist | +| FDA records-request simulation | `services/ingestion/tests/test_request_workflow_router.py`, `services/ingestion/tests/test_request_workflow_router_1342.py`, `tests/test_request_workflow_unit.py`, `tests/test_e2e_fda_request.py`, `tests/test_e2e_deadline_breach.py`, `services/ingestion/tests/test_mock_audit_1342.py` (NON-PROD) | partial | | +| Bad-data drill | `services/shared/tests/test_rules_fuzz_1353.py`; Workbench tests under `services/ingestion/tests/` (`test_inflow_workbench*.py`) | partial | R1 acceptance in the PRD is the live version of this gate | +| Integration-continuity drill | none found | none | | +| Complete exit export | `services/admin/tests/test_data_export_1094.py` (per-user only) | none for a tenant export | | +| Regulatory-policy update | `tests/test_e2e_stale_evaluations.py` (`rule_version_changed` only) | partial | | +| Tenant-isolation attack tests | `tests/security/test_tenant_route_isolation_matrix.py`, `tests/security/test_tenant_isolation.py`, `services/ingestion/tests/test_epcis_tenant_isolation.py`, `tests/test_e2e_tenant_isolation.py`, `services/shared/tests/test_rls_enforcement_live.py`, `services/shared/tests/test_tenant_isolation.py` | full for PostgreSQL paths | | +| Evidence-bundle tamper detection | `services/ingestion/tests/test_proof_verify_router.py`, `services/ingestion/tests/test_fda_export_merkle_1342.py`, `services/ingestion/tests/test_fda_export_verification_1342.py`, `services/ingestion/tests/test_fda_export_v2_reproducibility.py`, `services/ingestion/tests/test_dr_chain_break_it.py`, `services/ingestion/tests/test_chain_verification_job_1342.py` | full for the current bundle | | +| AI-authority boundary | `services/ingestion/tests/test_csv_jev_header_mapping.py`, `services/ingestion/tests/test_csv_jev_file_cte.py`, `services/shared/tests/test_jev_header_mapping.py`, `services/shared/tests/test_jev_cte_classification.py` | partial | Waiver-approver provenance is untested (section 7) | + +## 5. Regulatory-accuracy corrections against repo copy + +The external PRD lists six corrections to common market inaccuracies. Each was checked against the repo on 2026-10-28 with the commands in Appendix A. + +| # | Correction | Hits | Finding | Disposition | +| --- | --- | --- | --- | --- | +| 1 | Use the final-rule CTE structure, not proposed-rule labels | `growing` and `creation` are proposed-rule labels and appear in `services/ingestion/app/webhook_models.py`, `services/ingestion/app/csv_templates.py`, `services/ingestion/app/epcis/validation.py`, `services/ingestion/app/epcis/normalization.py`, `services/shared/canonical_event.py` (mapping to `None`), `services/shared/cte_persistence/core.py`, `services/shared/schemas.py`, the seeds `alembic/sql/V036__fsma_204_regulatory_seed_data.sql` and `alembic/sql/V037__obligation_cte_rules.sql`, and `docs/specs/FSMA_204_MVP_SPEC.md` line 28 ("Creation" as a future CTE). Public copy in `frontend/src` carries no proposed-rule label; `frontend/src/__tests__/lib/cte-suggestion.test.ts` uses `growing` on purpose as a value outside the vocabulary. "First land-based receiver" in the NLP enum is a final-rule CTE spelled differently, not drift | The canonical table stores final-rule names; the drift is in secondary enums, validation shortcuts and one DB CHECK | fold into SEL-591 (Q5 signoff); story S6 | +| 2 | Records available within 24 hours of an FDA request is not the same as completing a recall in 24 hours | Correct: `frontend/src/app/fsma-204/page.tsx` line 282 cites 21 CFR 1.1455(c)(1); `frontend/src/app/page.tsx` line 275 states RegEngine is not a recall clock product. Conflated: `frontend/src/app/blog/fsma-204-cte-kde-guide/page.tsx` line 187 ("the 24-hour recall response requirement"); `frontend/src/app/blog/24-hour-rule/page.tsx` line 15 and `frontend/src/app/blog/page.tsx` line 93 ("records within 24 hours of a recall or outbreak", where the trigger is an FDA request). Internal shorthand "24-hour recall" in `services/graph/app/fsma/tracer.py`, `services/graph/app/fsma_metrics.py`, `services/compliance/app/fsma_spreadsheet.py`, `services/shared/fsma_plan_builder.py`, `docs/compliance/RTM.md` line 24 and `docs/runbooks/fda-audit-checklist.md` line 13 | Public pages are mostly right; three blog lines conflate the two | blog lines fold into SEL-406 (F9), story S8; internal docstrings P2 hygiene; the RTM row is audit evidence and gets its own PR | +| 3 | Paper and electronic records are both permitted | No copy says electronic records are mandatory. `frontend/src/app/compliance/traceability-plan/page.tsx` line 158 offers "Paper records" as a record system; `docs/research/deep-market-lab-2026-08-31/regulation.md` line 33 states the rule permits paper | consistent | none | +| 4 | 21 CFR Part 11 is not a universal FSMA 204 requirement | `frontend/src/app/dashboard/audit-log/page.tsx` line 490 says "designed to support 21 CFR Part 11-style record controls" (softened earlier per `frontend/COPY_REMEDIATION_PLAN.md` line 144); `docs/security/TRUST_CLAIMS_MATRIX.md` line 9 bans "Part 11 certified"; `frontend/src/test/security/audit-trail.test.ts` line 7 is a test comment, not copy | consistent | none; optional tidy of the test comment | +| 5 | FDA obligations stay separate from Walmart and other buyer programs | `frontend/src/app/supplier-compliance/page.tsx` lines 51, 98 and 196 separate retailer expectations from FDA and tell suppliers to confirm with their buyer. `services/shared/external_connectors/retailers.py` docstring says retailers "participate in FSMA 204" and "Walmart requires" CTE records, which blends a buyer program with the rule; the connector sits behind the NON-PROD `integration` router | public copy consistent; one internal docstring conflates | P2 docstring fix | +| 6 | Model the July 20, 2028 enforcement boundary accurately | Correct: `frontend/src/components/layout/marketing-footer.tsx` line 98 ("FDA enforcement not before July 20, 2028"), `frontend/src/app/blog/fsma-204-compliance-guide/page.tsx` line 48 (correction dated Oct 26, 2026, with the January 20, 2026 compliance date and the unfinalized extension), PRD section 1. Imprecise: "July 2028 deadline" in `frontend/src/app/blog/food-traceability-software/page.tsx` lines 59 and 332 and `frontend/src/app/blog/page.tsx` lines 58 and 109; "July 2028 FSMA 204 enforcement window" in `frontend/src/components/pricing/PricingPageClient.tsx` line 154. `HARD_DATE_RE` in `scripts/check_trust_safety_copy.py` catches only word orders where "deadline" or "enforcement begins" directly precedes the date, so these lines pass CI | mostly correct; four blog lines and one pricing line are imprecise | fold into SEL-406 (F9), story S8 | + +## 6. Architecture claims against the repo + +| External claim | Repo today | Source | +| --- | --- | --- | +| PostgreSQL holds authoritative tenant-scoped records | Partly. `fsma.traceability_events` is the canonical write target. The legacy tables are not read-only yet: the production EPCIS ingest path still calls `CTEPersistence.store_event()` in `services/ingestion/app/epcis/persistence.py`, which inserts into `fsma.cte_events` (`services/shared/cte_persistence/writes.py`), and `CANONICAL_OWNERSHIP.md` lists that dual-write as pending removal. Read-only retention is the Q2/SEL-589 target; the active legacy writer is a migration gap until the cutover lands | `CANONICAL_OWNERSHIP.md`; PRD Q2 and SEL-589 | +| Neo4j is a rebuildable genealogy and traversal index | Not today. `ENABLE_NEO4J_SYNC` is off by default; the sync worker script this repo once named has been deleted (its path is carried in `.docs-link-allowlist` for `ASYNC_PROCESSES.md`); `services/graph/scripts/` holds constraint, schema and framework-loading scripts, not a rebuild from PostgreSQL; `services/admin/app/graph_outbox.py` receives rows from the bulk-upload commit and erasure paths, but no non-test caller drains that outbox, so the rows accumulate. Graph routers are not in production | `docs/operations/DISASTER_RECOVERY.md` sections 4 and 6.4; `server/router_policy.py`; PRD Q3 (Neo4j probed only when graph is enabled) | +| Object storage holds immutable source artifacts and evidence bundles | Decided, not built as described. Cloudflare R2 was chosen on 2026-05-12; supplier artifacts live in PostgreSQL `fsma.manifest_blobs`; `services/ingestion/app/s3_utils.py` serves regulatory-document ingest; evidence ZIPs are built on request | `docs/decisions/2026-05-12-object-storage.md`; row 5 | +| FastAPI for ingest, validation, exceptions, policies, traces and exports | True; one consolidated app, `server.main:app` | `docs/PRODUCT_DESCRIPTION.md` scope and runtime | +| Next.js for internal, supplier, auditor and administrator workflows | True for internal, supplier portal and admin; auditor access is an API router with no dedicated UI | `frontend/src/app/`; `services/ingestion/app/auditor_router.py` | +| Redis and queues are non-authoritative | True, with one caveat: Redis gates readiness in strict deployed runtimes because authenticated ingest fails closed without it | PRD Q3 | + +## 7. AI-authority boundary against the repo + +The external summary says AI may suggest mappings, extract candidates, explain errors and summarize drills, and may not invent KDEs, approve exceptions, determine compliance or silently merge identities. + +- Jev header mapping and file-CTE suggestions are advisory by default and recorded as `field_mapping_suggestion` artifacts (`services/shared/jev/header_mapping.py`, `services/shared/jev/cte_classification.py`; `docs/architecture/decisions/ADR-005-jev-header-mapping-suggestions.md`). Two switches, `REGENGINE_JEV_APPLY_HEADER_MAPPINGS` and `REGENGINE_JEV_APPLY_FILE_CTE_TYPE`, are read from the environment in `services/shared/jev/settings.py` (default off) and consumed in `services/ingestion/app/csv_templates.py`. When either is on, suggestions at or above the `auto_apply` threshold of 0.90 in `services/shared/jev/routing.py` are applied to the ingested rows without per-item human confirmation. The external boundary ("suggest only") holds in the default configuration and is narrower than what the switches allow; `docs/PRODUCT_DESCRIPTION.md` already describes the switches as opt-in. +- LlamaIndex extraction of PDFs stages rows in the upload session with a review warning; writing them needs a separate `POST /commit` (`services/admin/app/bulk_upload/routes.py`; `docs/architecture/decisions/ADR-006-llamaindex-extract-supplier-pdfs.md`). Consistent. +- Identity merges happen only through `merge_entities` in `services/ingestion/app/identity_router.py` or review resolution; the matcher is lexical, and matches at or above 0.90 are queued as "likely" rather than merged. Consistent with "no silent merge". +- Exception waivers: `PATCH /{case_id}/waive` in `services/ingestion/app/exception_router.py` requires `exceptions.write`, but `waiver_approved_by` comes from the request body, not the authenticated principal. This is not an AI path. It does weaken "may not approve exceptions" as a control, because the recorded approver is caller-supplied. Disposition: fold into SEL-463 (signed review audit trail, F8); story S5. + +## 8. Exclusions check: "no silent graph limits, export skips, or data truncation" + +| Limit | Reported to the caller | Source | +| --- | --- | --- | +| `/trace/forward` and `/trace/backward` result cap 10,000 | yes, `truncated: true` | `services/ingestion/app/canonical_router.py`; `tests/shared/test_canonical_trace_cap_1282.py` | +| `/trace/*` depth cap (`max_depth` default 5, maximum 20) | no | same file; story S1 | +| FDA export lineage expansion depth 5 | no | `services/shared/cte_persistence/queries.py`; story S2 | +| FDA export lineage walk ignoring link direction | not a truncation; an over-inclusion of sibling lots | same file; story S2 | +| EPCIS export `LIMIT 2000` | no; the response carries `event_count` only | `services/ingestion/app/epcis/persistence.py`, `services/ingestion/app/epcis/router.py`; story S3 | +| `/export/all` window over 90 days | yes, HTTP 400 | `services/ingestion/app/fda_export/router.py` | +| FDA spreadsheet KDE coverage under 80 percent | yes, blocked unless `allow_incomplete`; the bypass is a structured warning log, not an audit row | `services/ingestion/app/fda_export/coverage.py` | +| Full export oversized | yes, HTTP 413 | `services/ingestion/app/fda_export/router.py` | +| Neo4j trace `TRACE_ROW_LIMIT` 5,000 | yes, `truncated` and `truncation_limit` | `services/graph/tests/test_trace_truncated_flag_sel515.py` (NON-PROD) | +| Neo4j path length cap at 20 relationships when `max_depth` over 10 | no | `services/graph/app/fsma/tracer.py` (NON-PROD) | + +The other external exclusions (no FDA approval or certification claims, no promise of legal compliance outcomes, no claim that RegEngine proves source data is true, no synthetic production metrics, no unmeasured ROI claims) already match `docs/security/TRUST_CLAIMS_MATRIX.md`, the trust page, and the real-data rule in PRD R2 and SEL-447. The exclusion of "dependence on Agent Middleware as the end-user product" matches PRD section 7 (agent tooling is outside the initiative). + +## 9. Proposed stories and deferred items + +Proposals for founder triage. Nothing here is filed in Linear. Epics and features are the ones in PRD Appendix A (E1 Ingest and preflight, E2 Supplier remediation, E3 Evidence and export, E4 Public site and lead capture, E5 Production stability). + +Format: story | epic and feature, or the SEL issue to fold into | priority | gap row. + +### P1 candidates (do not conflict with the 2026-10-27 decisions) + +- S1. `/trace/forward` and `/trace/backward` report reaching `max_depth` separately from the result cap | E3 F7, fold into SEL-412 | P1 | row 11 +- S2. The FDA export lineage walk follows link direction and flags its depth limit | E3 F7 | P1 | row 11 +- S3. The EPCIS export states its 2,000-row limit in the response and in the Preview copy | E1 F2, fold into SEL-439 | P1 | row 13 +- S4. Review the runtime UPDATE grant on `fsma.traceability_events`: add an append-only trigger with an explicit path for `status='superseded'` and verdict columns, or record the exception in `docs/engineering/SCHEMA_CHANGE_POLICY.md` next to SEL-589 | E3 | P1 | row 9 +- S5. Derive `waiver_approved_by` from the authenticated principal, not the request body | E3 F8, fold into SEL-463 | P1 | section 7 +- S6. Inventory and retire proposed-rule CTE labels in the secondary enums, the EPCIS validation shortcut and the v057 CHECK constraint during the Q5 signoff | E3, fold into SEL-591 | P1 | row 2 +- S7. The FDA package ZIP carries `rule_versions` and a known-gaps section, matching the request-workflow package | E3 | P1 | row 14 +- S8. Rewrite the "July 2028 deadline" and "24-hour recall" blog and pricing lines to the PRD section 1 wording | E4 F9, fold into SEL-406 | P1 | section 5 + +### P2 candidates (design for, do not build in this PRD window) + +- EPCIS document body retention with the same insert-only storage as CSV uploads | E1 | row 5 +- Supplier artifacts and evidence bundles in object storage per the 2026-05-12 R2 decision | E3 | row 5 +- Tenant-level exit export: events, artifacts, manifests and rule versions, in canonical JSON and CSV | E3 | row 18 (see section 10) +- Backend exemption scoping under 21 CFR 1.1305, replacing the frontend-only questionnaire | E1 | row 1 +- Multi-output transformation events and repack or relabel on the canonical path | E3 | row 10 +- Rule-pack version stamp and a tenant impact report when a pack changes | E3 | row 16 +- Identity resolution triggered from ingest, with the review queue and no automatic merge | E1 F3 | row 6 +- Bulk canonical JSON export | E3 | row 13 +- Amendment API over `supersedes_event_id` with revalidation outside the supplier session | E3 | row 9 +- Time-boxed auditor session type | E3 | row 17 +- Promote the mock-audit drill under the Promotion Rule in `PRODUCTION_SURFACE.md` | E3 | row 15 +- Neo4j rebuild job from PostgreSQL, parked with graph promotion | graph, parked under SEL-443 | section 6 +- Copy hygiene for the internal "24-hour recall" docstrings and the retailer connector docstring | E5 | section 5 + +### Deferred, conflicts with 2026-10-27 decision (no story proposed) + +- EDI 856 intake. PRD section 7 non-goal; R7, Q6 and SEL-592 remove `edi` from `PRODUCTION_ENABLED_ROUTERS` before the first pilot ingest. Repo fact: `services/ingestion/app/edi_ingestion/routes.py` exists, parses 856 (and other X12 sets on its document route) and persists shipping CTEs through the shared path; the registry marks EDI off; no prospect has asked (PRD section 7). +- SFTP intake. PRD section 7 non-goal; SEL-443. Repo fact: `services/shared/external_connectors/csv_sftp.py` is a connector class run only by a manual sync through the NON-PROD `integration` router; there is no scheduled poll and `asyncssh` is not in `requirements.in`; the registry marks SFTP off. +- If the founder reopens section 7, either path would have to pass the Promotion Rule in `PRODUCTION_SURFACE.md` (tenant-isolation test, schema or contract test, failure-mode test, documented ownership, a reason it belongs to the FSMA pipeline) and the copy registry would change in the same release. This document does not recommend that. + +### Out of scope under both documents + +QMS, EHS, HACCP, CAPA, sanitation, maintenance and plant execution; FDA approval or certification claims; a universal supplier marketplace; agent orchestration tooling as the product. + +## 10. Not assessed, and the open question + +Not assessable from the summary: the KDE-level field mapping the external PRD specifies per CTE, its UX and workflow screens, pricing, and the pass criteria behind each of the 14 gates. If the full text is added to the repository later, sections 3 to 5 can be rechecked against it. + +Open question for the founder: should the tenant exit export (row 18) move from P2 to P1? `docs/DATA_PROCESSING_AGREEMENT.md` line 120 promises export in a machine-readable format, but as the Art. 20 data-subject portability right (personal data of one person), not as a tenant-level export of FSMA records, and that file is an internal template that must not be sent externally without counsel review (its line 7). It does not by itself create a tenant exit-export obligation; R5 sends a DPA with the pilot agreement, and today only a per-user personal-data export exists. Options: keep P2 and state the limitation in the pilot agreement, or raise to P1 before R5 goes out. Decide from the executed pilot agreement or a tenant-export clause in it, not from the template. + +## 11. Adjacent doc debt found, not fixed here + +- `docs/product/PRODUCT_OVERVIEW.md` and `docs/product/PRODUCT_ROADMAP.md` are superseded for product scope by the 2026-10-27 PRD (its line 4) and carry no banner. Add banners when the PRD leaves draft status. +- `docs/README.md` Product section did not link either PRD; this change adds both. +- `docs/product/ROADMAP.md` pointed at a path that does not exist; this change repoints it to `docs/product/PRODUCT_ROADMAP.md`. +- `docs/compliance/RTM.md` line 24 titles a row "24-Hour Recall Response"; it is audit evidence under the link gate and should change in its own PR. +- `docs/specs/FSMA_204_MVP_SPEC.md` line 28 still lists "Creation" and "Depletion" as future CTEs; it is marked a legacy planning spec but is also listed as source of truth in `AGENTS.md`. Candidate for the Q5 signoff. +- `docs/PRODUCT_DESCRIPTION.md` capability matrix does not mention that the EPCIS export is capped at 2,000 rows (story S3). +- `docs/product/PRODUCT_PRD_2026-10.md` names two paths the link checker cannot resolve: the git-ignored sales/local directory and a shorthand for `services/ingestion/app/stripe_billing/routes.py` that omits the leading directories. No workflow runs `scripts/check_docs_links.py`; its unit test `tests/scripts/test_check_docs_links.py` covers only the default scope, which excludes `docs/product/`, so this is informational; fix the shorthand when the PRD is next edited. + +## Appendix A. Commands used for section 5 + +Run from the repository root on 2026-10-28, excluding `node_modules`, `venv`, `.next`, `.git`, `docs/archive` and `docs/_archive`. + +```bash +grep -rIn -i 'part 11' --include=*.md --include=*.tsx --include=*.ts --include=*.py . | grep -v -i subpart +grep -rIn -i 'walmart' --include=*.md --include=*.tsx --include=*.ts --include=*.py . +grep -rIn -E '2028' --include=*.md --include=*.tsx --include=*.ts frontend/src docs/product docs/PRODUCT_DESCRIPTION.md README.md +grep -rIn -i -E '24[- ]hour[s]?[^.]{0,60}recall|recall[^.]{0,60}24[- ]hour' --include=*.tsx --include=*.ts --include=*.md --include=*.py frontend/src docs services +grep -rIn -i -E '\b(growing|creating|creation|landing)\b' --include=*.py --include=*.sql --include=*.tsx --include=*.ts services alembic frontend/src docs/specs +grep -rIn -i -E 'paper records?' --include=*.tsx --include=*.ts --include=*.md frontend/src docs +grep -rIn -i 'nulogy' . +``` diff --git a/docs/product/PRODUCT_PRD_2026-10.md b/docs/product/PRODUCT_PRD_2026-10.md index 0b93527f8..3540227ff 100644 --- a/docs/product/PRODUCT_PRD_2026-10.md +++ b/docs/product/PRODUCT_PRD_2026-10.md @@ -3,6 +3,7 @@ Date: 2026-10-27. Status: draft v1 for founder review. Product status: Beta, pre-revenue. Owner: founder / product. Supersedes for product scope: `PRODUCT_OVERVIEW.md` and `PRODUCT_ROADMAP.md` (2026-04-30). Does not replace `PRODUCTION_REMEDIATION_PRD_2026-05-01.md`, which is a fix program; its six open questions are carried into section 9. Verification basis. Code and surface claims were checked against `server/main.py`, `server/router_policy.py`, `PRODUCTION_SURFACE.md`, `frontend/src/lib/capabilities.ts` and Linear project P-SEL-2 as of 2026-10-27. Market, customer, pilot and compliance-scope claims come from the repository documents cited inline (`POSITIONING.md`, the May 2026 go-to-market research, the proof checklist, `docs/specs/FSMA_204_MVP_SPEC.md`, the 2026-10-27 plan note) and are those documents' claims, not verified facts about customers. Jev answers and the Q1 dates are planning inputs. Any other claim is marked "not verified". +See also: `docs/product/FSMA_204_READINESS_GAP_ANALYSIS_2026-10-28.md`, a subordinate reconciliation of an external PRD summary dated 2026-10-28; it changes none of the decisions in section 9. ## 1. Problem statement diff --git a/docs/product/ROADMAP.md b/docs/product/ROADMAP.md index a61ec039a..dc64ca64d 100644 --- a/docs/product/ROADMAP.md +++ b/docs/product/ROADMAP.md @@ -6,7 +6,7 @@ The older multi-jurisdiction roadmap content that previously lived here is not p Use these files instead: -- `docs/PRODUCT_ROADMAP.md` +- `docs/product/PRODUCT_ROADMAP.md` - `docs/specs/FSMA_204_MVP_SPEC.md` - `docs/product/POSITIONING.md` - `docs/engineering/AI_ENGINEERING_STANDARDS.md`