diff --git a/PRODUCTION_SURFACE.md b/PRODUCTION_SURFACE.md index cbd3b109c..98bad9be5 100644 --- a/PRODUCTION_SURFACE.md +++ b/PRODUCTION_SURFACE.md @@ -23,7 +23,7 @@ These surfaces are part of the core production spine when their corresponding ro token-scoped supplier correction session (`remediation`; compliance-critical, see "Boot-critical routers" below) - FDA export and export monitoring -- Public proof endpoints: export-bundle verification and the recall drill +- Public proof endpoints: export-bundle verification and the recall-drill endpoint (currently returns no drill data) (`proof_verify`, `proof_recall`; tenantless and unauthenticated, see below) - Tenant, user, API-key, supplier, and audit-log administration - Billing routes required for customer subscription management @@ -85,7 +85,7 @@ so `ENABLE_EXPERIMENTAL_ROUTERS` does not gate them: Public export-bundle hash-chain/Merkle verifier. No auth, no tenant context. Rate limit 10 req/min/IP. - `proof_recall` — `POST /api/v1/proof/recall-drill` (`server/main.py:654`). - Public deterministic recall drill over a frozen synthetic dataset. No auth, + Public recall-drill endpoint. It reads no customer data and currently returns an honest empty result (`data_available: false`, no steps, no lot, no trace time, no CSV) with an explanatory message. No auth, no tenant context. Rate limit 5 req/min/IP. Both carry an in-module justification docstring arguing they belong to the diff --git a/docs/PRODUCT_DESCRIPTION.md b/docs/PRODUCT_DESCRIPTION.md index d1f7df63d..6f4f38df4 100644 --- a/docs/PRODUCT_DESCRIPTION.md +++ b/docs/PRODUCT_DESCRIPTION.md @@ -14,7 +14,7 @@ Accepted events are stored with tenant boundaries in PostgreSQL. The platform ev Optional Jev assistance suggests mappings for unfamiliar CSV columns and a file's CTE when none is declared. Upload screens expose suggestions for operator review. Automatic application defaults off, requests use headers without row samples by default, and model suggestions do not replace deterministic validation. Preview and evaluation tools let operators inspect suggestions before enabling automatic application. -A browser verifier, a downloadable Python bundle verifier, and a reproducible recall drill let reviewers inspect the evidence format. Successful hash or Merkle verification establishes artifact integrity; it does not establish source-data accuracy or regulatory approval. +A browser verifier and a downloadable Python bundle verifier let reviewers inspect the evidence format. Successful hash or Merkle verification establishes artifact integrity; it does not establish source-data accuracy or regulatory approval. ## Capability and availability matrix @@ -29,7 +29,7 @@ A browser verifier, a downloadable Python bundle verifier, and a reproducible re | Tenant-scoped records and tamper-evident chains | [Tenant authority](../services/shared/tenant_context.py), [canonical persistence](../services/shared/canonical_persistence/writer.py), [canonical chain verification](../services/shared/canonical_persistence/verification.py), [admin audit integrity](../services/admin/app/audit_integrity.py). See [persistence cutover](persistence/dual-write-cutover.md). | | FDA-oriented exports and request-response packages | Release-enabled: [FDA export](../services/ingestion/app/fda_export/router.py), [request workflow](../services/ingestion/app/request_workflow_router.py), [auditor API](../services/ingestion/app/auditor_router.py). | | Readiness assessments and recall reporting | Release-enabled: [readiness](../services/ingestion/app/readiness_router.py), [recall reports](../services/ingestion/app/recall_report.py). | -| Public proof and synthetic recall drill | [Proof page](https://regengine.co/proof), [browser verifier](https://regengine.co/verify), [recall drill](https://regengine.co/proof/recall-drill), [verifier source](../frontend/public/sdk/verify_chain.py). Synthetic demonstration data. | +| Public proof and recall-drill endpoint | [Proof page](https://regengine.co/proof), [browser verifier](https://regengine.co/verify), [verifier source](../frontend/public/sdk/verify_chain.py). Synthetic verification artifacts; the recall-drill endpoint currently returns no drill data. | | Supplier manifest remediation and correction links | Implemented in [remediation](../services/ingestion/app/remediation/) and the [buyer dashboard](../frontend/src/app/remediation/page.tsx); excluded from the production router allowlist. Notification delivery also requires recipient/provider configuration. | | EPCIS JSON-LD export and broader recall simulations | Implemented in [EPCIS export](../services/ingestion/app/epcis_export.py) and [simulations](../services/ingestion/app/recall_simulations.py); these routers are experimental and excluded from the production allowlist. Public sample EPCIS downloads are a separate proof surface. | | Inbound email CSV ingestion | [Postmark integration](../services/ingestion/app/email_ingest/); excluded from the production allowlist, with no self-service provisioning UI. See [rollout notes](engineering/EMAIL_INGESTION.md). | diff --git a/docs/security/TRUST_CLAIMS_MATRIX.md b/docs/security/TRUST_CLAIMS_MATRIX.md index 685bbebf4..41de2ae50 100644 --- a/docs/security/TRUST_CLAIMS_MATRIX.md +++ b/docs/security/TRUST_CLAIMS_MATRIX.md @@ -28,7 +28,7 @@ Do not use these phrases in public or procurement copy without owner approval an | COM-001 | Design-partner pricing and rate commitments are contract-controlled. | Review each use | Public / Contract | Pricing, order forms, procurement | Pricing page and executed terms | Founder / Legal | `frontend/src/components/pricing/PricingPageClient.tsx`, `frontend/src/app/pricing/page.tsx` | 2026-06-24 | "Rate controlled by your executed design-partner terms." | "Locked forever", "no increases ever" unless order form says so | Before pricing-page release | | COM-002 | Competitor comparisons are qualitative unless individually sourced. | Review each use | Public | Pricing, sales decks | Pricing page | Product / Legal | `frontend/src/app/pricing/page.tsx`, `frontend/src/components/pricing/PricingPageClient.tsx` | 2026-06-24 | "Validate competitor capabilities directly before procurement decisions." | Checkmarks or unsupported feature claims without citations | Before pricing-page release | | INT-001 | Integration status taxonomy is conservative and status labels are source-controlled. | Approved | Public | Trust, integrations, dashboard | Customer readiness registry | Product / Engineering | `frontend/src/lib/customer-readiness.ts`, `frontend/src/app/trust/page.tsx`, `frontend/src/app/integrations/page.tsx` | 2026-06-24 | "Implemented", "Supported", "Pilot", "Custom Scoped", "Contract-only." | "Native Bidirectional" unless truly supported | Monthly and before registry additions | -| MET-001 | Synthetic benchmark metrics are not customer ROI, SLA, or production-performance commitments. | Review each use | Public | Case studies, proof pages, sales decks | Case-study methodology and claims matrix | Product / Legal | `frontend/src/app/case-studies/page.tsx`, `frontend/src/app/proof/recall-drill/page.tsx` | 2026-06-24 | "Synthetic validation workflow modeled on leafy-greens supply chains; not customer ROI, SLA, or production-performance commitments." | "No projections", "customer-proven savings", "guaranteed recall time" without executed evidence | Before publishing metrics | +| MET-001 | Synthetic benchmark metrics are not customer ROI, SLA, or production-performance commitments. | Review each use | Public | Case studies, proof pages, sales decks | Case-study methodology and claims matrix | Product / Legal | `frontend/src/app/case-studies/page.tsx` | 2026-06-24 | "Synthetic validation workflow modeled on leafy-greens supply chains; not customer ROI, SLA, or production-performance commitments." | "No projections", "customer-proven savings", "guaranteed recall time" without executed evidence | Before publishing metrics | | AVAIL-001 | Guided onboarding and self-serve language must reflect design-partner availability. | Review each use | Public / Contract | Pricing, case studies, onboarding pages | Trust Center maturity banner and pricing page | Product / Legal | `frontend/src/app/pricing/page.tsx`, `frontend/src/components/pricing/PricingPageClient.tsx`, `frontend/src/app/case-studies/page.tsx` | 2026-06-24 | "Guided onboarding is available for approved design-partner workspaces; timeline, support, and commercial terms are confirmed during scoping." | "Self-serve in minutes", "no contract negotiation", "public checkout available" unless billing is aligned | Before pricing-page release | | EXT-001 | External market, retailer, and recall-cost claims require citations or conservative caveats. | Review each use | Public | Case studies, blogs, sales decks | Source citation or buyer validation | Product / Legal | `frontend/src/app/case-studies/page.tsx`, `frontend/src/app/blog/` | 2026-06-24 | "Retailer requirements vary and should be confirmed directly with each buyer." | "Walmart/Kroger require..." without current citation and qualification | Monthly and before sales use | | BILL-001 | Public price, CTA, checkout, and order-form consistency must be verified before self-serve claims. | Review each use | Public / Contract | Pricing, checkout, terms | Billing config and executed order form | Product / Finance / Legal | `frontend/src/app/pricing/page.tsx`, `frontend/src/components/billing/PricingCheckoutButton.tsx`, `services/ingestion/app/stripe_billing/plans.py` | 2026-06-24 | "Pricing shown is indicative; executed agreement controls final plan, billing period, and checkout availability." | "Start plan" / direct checkout claims when plan IDs or billing terms are not aligned | Before billing/pricing releases | diff --git a/services/ingestion/app/proof_recall_router.py b/services/ingestion/app/proof_recall_router.py index aec0d4c26..d25a26e53 100644 --- a/services/ingestion/app/proof_recall_router.py +++ b/services/ingestion/app/proof_recall_router.py @@ -1,48 +1,32 @@ """Public recall-drill endpoint. -Why this belongs in the FSMA pipeline (production-surface promotion rule -per ``PRODUCTION_SURFACE.md``): - -The core production contract is:: - - ingest -> canonicalize -> validate -> persist evidence -> export - -This router adds a public, tenantless surface that demonstrates the -*recall-response* capability built on top of the export stage. It is -the FSMA pipeline's public proof surface for recall capability: a -prospect, regulator, or trade partner can run a deterministic recall -drill against a frozen synthetic dataset and observe the wall-clock -trace time vs. the FDA 24-hour mandate, without an account, without -tenant context, and without reading any tenant data. - Endpoint: ``POST /api/v1/proof/recall-drill`` -- No auth (public). Rate limit: 5 requests per minute per IP via the - shared slowapi ``limiter`` (``shared.rate_limit``). -- Operates *only* against a frozen synthetic dataset baked into this - module. The endpoint MUST NEVER read real tenant data: static - analysis of this module should show zero imports of - ``shared.database``, ``CTEPersistence``, or any tenant-scoped - client. -- Output is deterministic — every call returns the same step list, - ``time_to_trace_seconds`` (sum of step durations), ``lot_code``, - and ``output_csv_url``. -- The pre-generated recall CSV referenced by ``output_csv_url`` lives - in the frontend's public assets (``/samples/sample_recall_drill.csv``) - and was produced from the synthetic 7-CTE dataset. +This is a public, tenantless, unauthenticated surface (rate limit: 5 +requests per minute per IP via the shared slowapi ``limiter``). It MUST +NEVER read tenant data: this module imports no ``shared.database``, +``CTEPersistence`` or other tenant-scoped client. + +Because it cannot read tenant data, it has no real recall to trace. It +previously returned a frozen synthetic drill (an invented lot, invented +facilities, invented step durations and a pre-generated sample CSV). +That was fake data presented as a proof, so it was removed. The endpoint +now returns an honest empty result: no steps, no lot code, no trace time +and no CSV, with ``data_available: false``. The only populated value is +the FDA 24-hour records-production window from 21 CFR 1.1455(c)(3), +which is a regulatory constant, not data. Returned JSON shape:: { - "steps": [ - {"name": str, "duration_ms": int, - "status": "ok"|"warning"|"error", "detail": str?}, - ... - ], - "time_to_trace_seconds": float, - "output_csv_url": str, - "lot_code": str, - "fda_24h_mandate_seconds": 86400 + "data_available": false, + "steps": [], + "time_to_trace_seconds": null, + "output_csv_url": null, + "lot_code": null, + "fda_24h_mandate_seconds": 86400, + "message": str, + "timestamp": str } """ @@ -61,65 +45,12 @@ router = APIRouter(prefix="/api/v1/proof", tags=["Public Proof"]) - -# --------------------------------------------------------------------------- -# Frozen synthetic drill -- deterministic by construction -# --------------------------------------------------------------------------- -# This corresponds to the public CSV at -# ``frontend/public/samples/sample_recall_drill.csv`` -- a known-contaminated -# romaine lot harvested at Sunny Valley Farms, traced forward through the -# 7 CTEs (harvesting, cooling, initial packing, shipping, receiving, -# transformation, shipping) into Metro Foods, Valley Wholesale, and -# SuperMart #42. -FROZEN_LOT_CODE = "SV-20240115-001" -OUTPUT_CSV_URL = "/samples/sample_recall_drill.csv" +# 21 CFR 1.1455(c)(3): records must be provided to FDA within 24 hours. FDA_24H_MANDATE_SECONDS = 86400 -# Steps mirror the synthetic frontend script so a UI consuming this -# endpoint produces the same per-step animation. Durations sum to -# 7.0 seconds — well inside the 24h FDA mandate. -_FROZEN_STEPS: tuple[dict, ...] = ( - { - "name": "Identifying source lot", - "duration_ms": 1200, - "status": "ok", - "detail": ( - "Loaded contaminated lot SV-20240115-001 (Romaine Lettuce, " - "500 cases) from Sunny Valley Farms (GLN 1234567890128)." - ), - }, - { - "name": "Tracing forward through 7 CTEs", - "duration_ms": 2400, - "status": "ok", - "detail": ( - "Walked CTE-1 HARVESTING -> CTE-2 COOLING -> CTE-3 " - "INITIAL_PACKING -> CTE-4 SHIPPING -> CTE-5 RECEIVING -> " - "CTE-6 TRANSFORMATION -> CTE-7 SHIPPING. Resolved 2 child " - "lots (FC-20240116-A, FC-20240116-B) at the transformation " - "node." - ), - }, - { - "name": "Walking downstream graph edges", - "duration_ms": 1800, - "status": "ok", - "detail": ( - "Visited 11 events across 5 facilities. Reached 3 " - "downstream receivers: Metro Foods, Valley Wholesale, " - "SuperMart #42." - ), - }, - { - "name": "Generating FDA-oriented recall CSV", - "duration_ms": 1600, - "status": "ok", - "detail": ( - "Wrote 21 CFR §§1.1325–1.1350 Key Data Elements for each downstream " - "lot. Hash-anchored each record (SHA-256 chain integrity " - "preserved)." - ), - }, +NO_DATA_MESSAGE = ( + "No recall drill data is available on this public endpoint. " + "Run a recall trace against your own records after signing in." ) @@ -134,11 +65,13 @@ class DrillStep(BaseModel): class RecallDrillResponse(BaseModel): - steps: list[DrillStep] - time_to_trace_seconds: float - output_csv_url: str - lot_code: str + data_available: bool = False + steps: list[DrillStep] = Field(default_factory=list) + time_to_trace_seconds: Optional[float] = None + output_csv_url: Optional[str] = None + lot_code: Optional[str] = None fda_24h_mandate_seconds: int = FDA_24H_MANDATE_SECONDS + message: str = NO_DATA_MESSAGE timestamp: str @@ -157,22 +90,8 @@ def _hash_ip(client_host: Optional[str]) -> str: def _build_response() -> RecallDrillResponse: - """Build the deterministic drill response. - - Pure function of module constants — no I/O, no tenant lookups. - Two calls return identical ``steps`` / ``time_to_trace_seconds`` - (only ``timestamp`` differs). - """ - steps = [DrillStep(**s) for s in _FROZEN_STEPS] - total_ms = sum(s.duration_ms for s in steps) - return RecallDrillResponse( - steps=steps, - time_to_trace_seconds=round(total_ms / 1000.0, 3), - output_csv_url=OUTPUT_CSV_URL, - lot_code=FROZEN_LOT_CODE, - fda_24h_mandate_seconds=FDA_24H_MANDATE_SECONDS, - timestamp=_now_iso(), - ) + """Build the honest empty response. No I/O, no tenant lookups.""" + return RecallDrillResponse(timestamp=_now_iso()) # --------------------------------------------------------------------------- @@ -181,30 +100,20 @@ def _build_response() -> RecallDrillResponse: @router.post( "/recall-drill", response_model=RecallDrillResponse, - summary="Run a deterministic recall drill against a frozen synthetic dataset", + summary="Public recall drill: returns an honest empty result (no tenant data is read)", ) @limiter.limit("5/minute") async def run_recall_drill(request: Request) -> RecallDrillResponse: - """Run a public recall drill against the frozen synthetic dataset. - - Takes no body. Returns a deterministic step list plus the - wall-clock ``time_to_trace_seconds`` for the synthetic drill, the - FDA 24-hour response target for client-side comparison, and the - URL of a pre-generated FDA-oriented recall CSV. + """Return an honest empty recall-drill result. - This endpoint MUST NOT read any tenant data: it operates entirely - against module-level constants derived from - ``frontend/public/samples/sample_recall_drill.csv``. No database, - queue, or tenant store is touched. + Takes no body and reads no tenant data, so there is nothing real to + trace: ``steps`` is empty, ``lot_code``, ``time_to_trace_seconds`` + and ``output_csv_url`` are null, and ``data_available`` is false. """ ip_hash = _hash_ip(request.client.host if request.client else None) response = _build_response() logger.info( "proof_recall_drill_call", - extra={ - "ip_hash": ip_hash, - "lot_code": response.lot_code, - "time_to_trace_seconds": response.time_to_trace_seconds, - }, + extra={"ip_hash": ip_hash, "data_available": response.data_available}, ) return response diff --git a/services/ingestion/tests/test_proof_recall_router.py b/services/ingestion/tests/test_proof_recall_router.py index ec60d415f..7300b1f8c 100644 --- a/services/ingestion/tests/test_proof_recall_router.py +++ b/services/ingestion/tests/test_proof_recall_router.py @@ -12,12 +12,15 @@ - Failure modes: the 6th request inside a one-minute window returns 429 (rate-limit triggers), and the response is deterministic across successive successful calls. +- Real data only: the endpoint returns an honest empty result (no + steps, no lot code, no trace time, no CSV) instead of a synthetic drill. """ from __future__ import annotations import os import sys +import uuid from itertools import count import app.proof_recall_router as proof_recall_module @@ -28,8 +31,6 @@ from slowapi.util import get_remote_address FDA_24H_MANDATE_SECONDS = proof_recall_module.FDA_24H_MANDATE_SECONDS -FROZEN_LOT_CODE = proof_recall_module.FROZEN_LOT_CODE -OUTPUT_CSV_URL = proof_recall_module.OUTPUT_CSV_URL proof_recall_router = proof_recall_module.router limiter = proof_recall_module.limiter _client_counter = count(1) @@ -107,8 +108,8 @@ def test_endpoint_accepts_no_tenant_header_and_works(self, client: TestClient): resp = client.post("/api/v1/proof/recall-drill") assert resp.status_code == 200, resp.text data = resp.json() - assert data["lot_code"] == FROZEN_LOT_CODE - assert data["output_csv_url"] == OUTPUT_CSV_URL + assert data["lot_code"] is None + assert data["output_csv_url"] is None def test_module_does_not_import_tenant_scoped_clients(self): """Defense-in-depth: the recall-drill module must not import @@ -139,7 +140,7 @@ def test_tenant_id_header_is_ignored(self, client: TestClient): resp_a = client.post("/api/v1/proof/recall-drill") resp_b = client.post( "/api/v1/proof/recall-drill", - headers={"X-Tenant-ID": "00000000-0000-0000-0000-000000000000"}, + headers={"X-Tenant-ID": str(uuid.uuid4())}, ) assert resp_a.status_code == 200 assert resp_b.status_code == 200 @@ -167,33 +168,20 @@ def test_response_shape_matches_spec(self, client: TestClient): "output_csv_url", "lot_code", "fda_24h_mandate_seconds", + "data_available", + "message", } # Constants. assert data["fda_24h_mandate_seconds"] == FDA_24H_MANDATE_SECONDS - assert data["lot_code"] == FROZEN_LOT_CODE - assert data["output_csv_url"] == OUTPUT_CSV_URL - - # time_to_trace_seconds must be a positive number well under - # the FDA 24h mandate (otherwise the demo is misleading). - assert isinstance(data["time_to_trace_seconds"], (int, float)) - assert 0 < data["time_to_trace_seconds"] < FDA_24H_MANDATE_SECONDS - - # Steps shape. - steps = data["steps"] - assert isinstance(steps, list) and len(steps) >= 1 - valid_status = {"ok", "warning", "error"} - total_ms = 0 - for s in steps: - assert set(s.keys()) >= {"name", "duration_ms", "status"} - assert isinstance(s["name"], str) and s["name"] - assert isinstance(s["duration_ms"], int) and s["duration_ms"] >= 0 - assert s["status"] in valid_status - total_ms += s["duration_ms"] - - # time_to_trace_seconds must equal the sum of step durations - # (deterministic by construction). - assert abs(data["time_to_trace_seconds"] - total_ms / 1000.0) < 1e-6 + assert data["lot_code"] is None + assert data["output_csv_url"] is None + + # Honest empty result: no invented drill, steps, timings or CSV. + assert data["data_available"] is False + assert data["steps"] == [] + assert data["time_to_trace_seconds"] is None + assert isinstance(data["message"], str) and data["message"] def test_get_method_not_allowed(self, client: TestClient): """GET on a POST-only route returns 405.""" @@ -245,3 +233,17 @@ def test_response_is_deterministic_across_calls(self, client: TestClient): a.pop("timestamp", None) b.pop("timestamp", None) assert a == b + + +# --------------------------------------------------------------------------- +# Real data only — no synthetic drill content is served +# --------------------------------------------------------------------------- +class TestNoSyntheticData: + def test_module_has_no_frozen_dataset(self): + for name in ("FROZEN_LOT_CODE", "OUTPUT_CSV_URL", "_FROZEN_STEPS"): + assert not hasattr(proof_recall_module, name), name + + def test_response_references_no_sample_assets(self, client: TestClient): + resp = client.post("/api/v1/proof/recall-drill") + assert resp.status_code == 200, resp.text + assert "/samples/" not in resp.text