From aa7fb5e8df60b68227e23cac395d4d677149a81a Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:32:42 -0700 Subject: [PATCH 1/5] fix(ingestion): replace synthetic recall drill with honest empty result Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> --- services/ingestion/app/proof_recall_router.py | 169 ++++-------------- .../tests/test_proof_recall_router.py | 58 +++--- 2 files changed, 69 insertions(+), 158 deletions(-) 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..f10076ee4 100644 --- a/services/ingestion/tests/test_proof_recall_router.py +++ b/services/ingestion/tests/test_proof_recall_router.py @@ -12,11 +12,14 @@ - 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 uuid import sys from itertools import count @@ -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 From db3f401109f16f0161c2b6abb870abd5f750fd8d Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:35:47 -0700 Subject: [PATCH 2/5] style(ingestion): sort imports in proof recall router test Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> --- services/ingestion/tests/test_proof_recall_router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/ingestion/tests/test_proof_recall_router.py b/services/ingestion/tests/test_proof_recall_router.py index f10076ee4..7300b1f8c 100644 --- a/services/ingestion/tests/test_proof_recall_router.py +++ b/services/ingestion/tests/test_proof_recall_router.py @@ -19,8 +19,8 @@ from __future__ import annotations import os -import uuid import sys +import uuid from itertools import count import app.proof_recall_router as proof_recall_module From c455b18b6ef3e816bc256390d19b04864704be0c Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:37:57 -0700 Subject: [PATCH 3/5] fix(proof): render honest empty state on recall-drill page, drop fake drill data Remove DEFAULT_DRILL_STEPS/DEFAULT_DRILL_RESPONSE fallbacks, fake lot, supplier names, trace graph and sample CSV link. Show the API message when data_available is false; never derive a trace time. Update test to be data-free and correct stale synthetic-drill mentions in docs. Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> --- PRODUCTION_SURFACE.md | 4 +- docs/PRODUCT_DESCRIPTION.md | 4 +- docs/security/TRUST_CLAIMS_MATRIX.md | 2 +- .../pages/proof-recall-drill.test.tsx | 91 +++-- frontend/src/app/case-studies/page.tsx | 4 +- frontend/src/app/proof/recall-drill/page.tsx | 341 ++++-------------- 6 files changed, 144 insertions(+), 302 deletions(-) diff --git a/PRODUCTION_SURFACE.md b/PRODUCTION_SURFACE.md index 4577b0128..30b37437b 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 7666cb8e4..c3ceb416c 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. -Public synthetic artifacts, 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. +Public synthetic artifacts, a browser verifier, 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. The public recall-drill endpoint currently returns no drill data; it reports that honestly rather than showing a sample drill. ## Capability and availability matrix @@ -29,7 +29,7 @@ Public synthetic artifacts, a browser verifier, a downloadable Python bundle ver | 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), [recall drill](https://regengine.co/proof/recall-drill), [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/frontend/src/__tests__/pages/proof-recall-drill.test.tsx b/frontend/src/__tests__/pages/proof-recall-drill.test.tsx index 2b0f65497..f2a63bd5b 100644 --- a/frontend/src/__tests__/pages/proof-recall-drill.test.tsx +++ b/frontend/src/__tests__/pages/proof-recall-drill.test.tsx @@ -1,49 +1,76 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import RecallDrillPage from '@/app/proof/recall-drill/page'; -describe('public proof recall drill page', () => { - beforeEach(() => { - vi.stubGlobal( - 'fetch', - vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ - steps: [ - { - name: 'Identifying from API', - duration_ms: 100, - status: 'ok', - detail: 'Endpoint loaded the frozen public dataset.', - }, - ], - time_to_trace_seconds: 0.1, - output_csv_url: '/samples/sample_recall_drill.csv', - lot_code: 'SV-20240115-001', - fda_24h_mandate_seconds: 86400, - timestamp: '2026-05-19T00:00:00Z', - }), - }), - ); - }); +const uid = () => crypto.randomUUID(); + +function stubFetch(body: unknown, ok = true) { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok, status: ok ? 200 : 503, json: async () => body })); +} +describe('public proof recall drill page', () => { afterEach(() => { vi.unstubAllGlobals(); }); - it('starts the public backend drill endpoint and renders returned steps', async () => { + it('renders the API message as an empty state when data_available is false', async () => { + const message = uid(); + stubFetch({ + data_available: false, + steps: [], + time_to_trace_seconds: null, + output_csv_url: null, + lot_code: null, + fda_24h_mandate_seconds: 86400, + message, + timestamp: new Date().toISOString(), + }); const user = userEvent.setup(); render(); await user.click(screen.getByRole('button', { name: /start drill/i })); - expect(fetch).toHaveBeenCalledWith('/api/ingestion/api/v1/proof/recall-drill', { - method: 'POST', + expect(fetch).toHaveBeenCalledWith('/api/ingestion/api/v1/proof/recall-drill', { method: 'POST' }); + expect(await screen.findByTestId('recall-drill-empty')).toHaveTextContent(message); + expect(screen.queryByText(/Drill log/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/Time to trace/i)).not.toBeInTheDocument(); + expect(screen.queryByRole('link', { name: /download recall csv/i })).not.toBeInTheDocument(); + expect(document.body.textContent).not.toMatch(/samples\//); + }); + + it('renders only steps the API returned when data is available', async () => { + const stepName = uid(); + const stepDetail = uid(); + stubFetch({ + data_available: true, + steps: [{ name: stepName, duration_ms: 0, status: 'ok', detail: stepDetail }], + time_to_trace_seconds: null, + output_csv_url: null, + lot_code: null, + fda_24h_mandate_seconds: 86400, + message: uid(), + timestamp: new Date().toISOString(), }); - expect(await screen.findByText(/Identifying from API/)).toBeInTheDocument(); - expect( - screen.getByText(/Endpoint loaded the frozen public dataset/i), - ).toBeInTheDocument(); + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('button', { name: /start drill/i })); + + expect(await screen.findByText(stepName)).toBeInTheDocument(); + expect(await screen.findByText(stepDetail)).toBeInTheDocument(); + // No trace time is invented when the API reports none. + expect(screen.queryByText(/Time to trace/i)).not.toBeInTheDocument(); + }); + + it('shows an error and no fallback drill when the endpoint fails', async () => { + stubFetch({}, false); + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('button', { name: /start drill/i })); + + expect(await screen.findByRole('alert')).toBeInTheDocument(); + expect(screen.queryByText(/Drill log/i)).not.toBeInTheDocument(); }); }); diff --git a/frontend/src/app/case-studies/page.tsx b/frontend/src/app/case-studies/page.tsx index 324e1e589..85f96d49e 100644 --- a/frontend/src/app/case-studies/page.tsx +++ b/frontend/src/app/case-studies/page.tsx @@ -103,8 +103,8 @@ export default function CaseStudy() { Product validation · synthetic data modeled on industry patterns. Not a named-customer case study.

- These metrics are reproducible. Try the same recall drill at{' '} - /proof/recall-drill. + The public recall-drill endpoint at{' '} + /proof/recall-drill currently returns no drill data and does not reproduce these metrics.

We ran a synthetic validation workflow modeled on leafy-greens supply chains (from Salinas Valley diff --git a/frontend/src/app/proof/recall-drill/page.tsx b/frontend/src/app/proof/recall-drill/page.tsx index 4e21e1a29..6a26fa543 100644 --- a/frontend/src/app/proof/recall-drill/page.tsx +++ b/frontend/src/app/proof/recall-drill/page.tsx @@ -1,25 +1,14 @@ 'use client'; import { useState, useRef, useEffect } from 'react'; -import { - AlertTriangle, - Play, - Download, - CheckCircle2, - Clock, - ArrowRight, - RotateCcw, - Lock, -} from 'lucide-react'; +import { Play, Download, CheckCircle2, Clock, RotateCcw, Lock, Inbox } from 'lucide-react'; import { Container } from '@/components/layout/container'; import { PageHeader } from '@/components/layout/page-header'; -import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Callout } from '@/components/ui/callout'; import { Card, CardHeader } from '@/components/ui/card'; import { StatTile } from '@/components/ui/stat-tile'; -const RECALL_CSV_URL = '/samples/sample_recall_drill.csv'; const PROOF_RECALL_ENDPOINT = '/api/ingestion/api/v1/proof/recall-drill'; type DrillStep = { @@ -36,90 +25,18 @@ type RecallDrillApiStep = { detail?: string | null; }; -type RecallDrillResponse = { +export type RecallDrillResponse = { + data_available: boolean; steps: RecallDrillApiStep[]; - time_to_trace_seconds: number; - output_csv_url: string; - lot_code: string; + time_to_trace_seconds: number | null; + output_csv_url: string | null; + lot_code: string | null; fda_24h_mandate_seconds: number; + message: string; timestamp: string; }; -const DEFAULT_DRILL_STEPS: DrillStep[] = [ - { - label: 'Identifying source lot', - detailLines: [ - 'Loaded contaminated lot: SV-20240115-001 (Romaine Lettuce, 500 cases)', - 'Origin: Sunny Valley Farms (GLN 1234567890128)', - ], - durationMs: 1200, - status: 'ok', - }, - { - label: 'Tracing forward through 7 CTEs', - detailLines: [ - '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 transformation node', - ], - durationMs: 2400, - status: 'ok', - }, - { - label: 'Walking downstream graph edges', - detailLines: [ - 'Visited 11 events across 5 facilities', - 'Reached 3 receivers: Metro Foods, Valley Wholesale, SuperMart #42', - ], - durationMs: 1800, - status: 'ok', - }, - { - label: 'Generating FDA-oriented sortable recall CSV from sample data', - detailLines: [ - 'Wrote 21 CFR §§1.1325 to 1.1350 Key Data Elements for each downstream lot', - 'Hash-anchored each record (SHA-256 chain integrity preserved)', - ], - durationMs: 1600, - status: 'ok', - }, -]; - -/* Final trace nodes used in the result panel diagram. */ -type TraceNode = { - id: string; - facility: string; - cte: string; - lot: string; - qty: string; -}; - -const TRACE_NODES: TraceNode[] = [ - { id: 'h', facility: 'Sunny Valley Farms', cte: 'HARVESTING', lot: 'SV-20240115-001', qty: '500 cs' }, - { id: 's1', facility: 'Sunny Valley Farms', cte: 'SHIPPING', lot: 'SV-20240115-001', qty: '500 cs' }, - { id: 'r1', facility: 'Fresh Cut Co', cte: 'RECEIVING', lot: 'SV-20240115-001', qty: '500 cs' }, - { id: 't', facility: 'Fresh Cut Co', cte: 'TRANSFORMATION', lot: 'FC-20240116-A / -B', qty: '250 + 250 cs' }, - { id: 's2', facility: 'Fresh Cut Co', cte: 'SHIPPING', lot: 'FC-20240116-A', qty: '100 cs' }, - { id: 'r2', facility: 'Metro Foods', cte: 'RECEIVING', lot: 'FC-20240116-A', qty: '100 cs' }, - { id: 's3', facility: 'Metro Foods', cte: 'SHIPPING', lot: 'FC-20240116-A', qty: '50 cs' }, - { id: 'r3', facility: 'SuperMart #42', cte: 'RECEIVING', lot: 'FC-20240116-A', qty: '50 cs' }, -]; - -const DEFAULT_DRILL_RESPONSE: RecallDrillResponse = { - steps: DEFAULT_DRILL_STEPS.map((step) => ({ - name: step.label, - duration_ms: step.durationMs, - status: step.status, - detail: step.detailLines.join(' '), - })), - time_to_trace_seconds: DEFAULT_DRILL_STEPS.reduce((acc, s) => acc + s.durationMs, 0) / 1000, - output_csv_url: RECALL_CSV_URL, - lot_code: 'SV-20240115-001', - fda_24h_mandate_seconds: 86400, - timestamp: '', -}; - -type DrillState = 'idle' | 'loading' | 'running' | 'done'; +type DrillState = 'idle' | 'loading' | 'running' | 'done' | 'empty'; function normalizeStep(step: RecallDrillApiStep): DrillStep { return { @@ -130,15 +47,8 @@ function normalizeStep(step: RecallDrillApiStep): DrillStep { }; } -function normalizeDrill(data: RecallDrillResponse): { - response: RecallDrillResponse; - steps: DrillStep[]; -} { - const steps = data.steps.map(normalizeStep).filter((step) => step.durationMs >= 0); - return { - response: data, - steps: steps.length > 0 ? steps : DEFAULT_DRILL_STEPS, - }; +function normalizeSteps(data: RecallDrillResponse): DrillStep[] { + return (data.steps ?? []).map(normalizeStep).filter((step) => step.durationMs >= 0); } function formatSeconds(value: number): string { @@ -149,8 +59,8 @@ export default function RecallDrillPage() { const [state, setState] = useState('idle'); const [activeStep, setActiveStep] = useState(-1); const [elapsedMs, setElapsedMs] = useState(0); - const [drill, setDrill] = useState(DEFAULT_DRILL_RESPONSE); - const [drillSteps, setDrillSteps] = useState(DEFAULT_DRILL_STEPS); + const [drill, setDrill] = useState(null); + const [drillSteps, setDrillSteps] = useState([]); const [errorMessage, setErrorMessage] = useState(null); const startedAtRef = useRef(null); const tickRef = useRef | null>(null); @@ -219,15 +129,17 @@ export default function RecallDrillPage() { } const payload = (await response.json()) as RecallDrillResponse; - const normalized = normalizeDrill(payload); - setDrill(normalized.response); - setDrillSteps(normalized.steps); - runAnimation(normalized.steps); + const steps = normalizeSteps(payload); + setDrill(payload); + setDrillSteps(steps); + if (!payload.data_available || steps.length === 0) { + setState('empty'); + return; + } + runAnimation(steps); } catch { setState('idle'); - setErrorMessage( - 'Could not start the public recall-drill endpoint. Try again shortly.', - ); + setErrorMessage('Could not reach the public recall-drill endpoint. Try again shortly.'); } } @@ -238,38 +150,28 @@ export default function RecallDrillPage() { setActiveStep(-1); setElapsedMs(0); setErrorMessage(null); + setDrill(null); + setDrillSteps([]); } const elapsedS = (elapsedMs / 1000).toFixed(1); - const totalDurationS = drill.time_to_trace_seconds || ( - drillSteps.reduce((acc, s) => acc + s.durationMs, 0) / 1000 - ); - const finalSeconds = state === 'done' ? totalDurationS : Number(elapsedS); - const outputCsvUrl = drill.output_csv_url || RECALL_CSV_URL; - const fdaMandateSeconds = drill.fda_24h_mandate_seconds || 86400; - const finalSecondsLabel = formatSeconds(finalSeconds); - const totalDurationLabel = formatSeconds(totalDurationS); - const marginMultiple = finalSeconds > 0 ? Math.round(fdaMandateSeconds / finalSeconds) : 0; + // Only show a trace time the API actually reported; never derive one. + const traceSeconds = drill?.time_to_trace_seconds ?? null; + const fdaMandateSeconds = drill?.fda_24h_mandate_seconds || 86400; + const marginMultiple = + traceSeconds != null && traceSeconds > 0 ? Math.round(fdaMandateSeconds / traceSeconds) : null; return (

- {/* ─── Hero ────────────────────────────────── */} -