From a9804a7f7385b8f7933c39b1e1524cd388bf10de Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:37:23 -0700 Subject: [PATCH 1/5] fix(tools): replace fake tool data with empty states --- .../src/app/proof/transformation/page.tsx | 273 ++++++------ .../components/DrillSimulatorClient.tsx | 108 +---- .../drill-simulator/lib/record-validator.ts | 73 +--- .../components/AnomalySimulator.tsx | 392 ------------------ .../components/UnifiedDashboardClient.tsx | 47 +-- .../components/KnowledgeGraph.tsx | 63 +-- .../__tests__/KnowledgeGraph.test.ts | 181 +++----- .../components/SOPGeneratorClient.tsx | 8 +- frontend/src/app/why-regengine/page.tsx | 14 +- 9 files changed, 251 insertions(+), 908 deletions(-) delete mode 100644 frontend/src/app/tools/fsma-unified/components/AnomalySimulator.tsx diff --git a/frontend/src/app/proof/transformation/page.tsx b/frontend/src/app/proof/transformation/page.tsx index 5d75c480e..2c2d3446c 100644 --- a/frontend/src/app/proof/transformation/page.tsx +++ b/frontend/src/app/proof/transformation/page.tsx @@ -10,33 +10,33 @@ import { Card } from "@/components/ui/card"; export const metadata: Metadata = { title: "Proof: CSV → CTE → FDA row | RegEngine", description: - "Watch a real supplier CSV become an FDA-oriented sortable record. Three CTEs, three transformations, real production code, synthetic data.", + "How a supplier CSV becomes an FDA-oriented sortable record. Three CTEs, three transformations, real production code.", openGraph: { title: "How a supplier CSV becomes an FDA-oriented sortable record | RegEngine", description: - "Three CTEs, three transformations. Real production code, synthetic data.", + "Three CTEs, three transformations. Real production code.", url: "https://regengine.co/proof/transformation", type: "website", }, }; // --------------------------------------------------------------------------- -// Data: hand-curated, faithful to production shapes +// Structural walkthrough — no records, only shapes and field mappings // --------------------------------------------------------------------------- -// CSV rows are direct copies from sample_data/0X_*.csv on main. -// Canonical JSON matches the TraceabilityEvent shape from -// services/shared/canonical_event.py. -// FDA rows match the column spec in -// services/ingestion/app/fda_export_service.py (FDA_COLUMNS, _NAMED_KDE_COLUMNS). -// PII columns shown unredacted here for demonstration; in a real export -// they pass through _redact_location_value when include_pii=False (#1219). +// CSV headers match the column spec the ingestion pipeline accepts. +// Canonical JSON shows the TraceabilityEvent shape from +// services/shared/canonical_event.py with field-level placeholders, not values. +// FDA rows name the column spec in +// services/ingestion/app/fda_export_service.py (FDA_COLUMNS, _NAMED_KDE_COLUMNS) +// and note where each column's value comes from. PII columns pass through +// _redact_location_value when include_pii=False (#1219). type Step = { cte: string; title: string; subtitle: string; csvHeader: string; - csvRows: string[]; + csvNote: string; canonical: string; canonicalAnnotation: string; fdaRow: Array<[string, string]>; @@ -48,62 +48,53 @@ const STEPS: Step[] = [ cte: "§1.1330", title: "Harvesting", subtitle: - "Origination event. Defines the Traceability Lot Code that follows romaine through every downstream CTE.", + "Origination event. Defines the Traceability Lot Code that follows the product through every downstream CTE.", csvHeader: "traceability_lot_code,product_description,quantity,unit_of_measure,harvest_date,location_name,location_gln,field_id,harvester_name,reference_document", - csvRows: [ - "LOT-ROM-2026-0312A,Organic Romaine Hearts,5000,lbs,2026-03-12,Salinas Valley Farms,0061414100014,FIELD-SVF-7,Salinas Valley Farms LLC,HARV-SVF-2026-0312-001", - "LOT-ROM-2026-0312B,Chopped Romaine Mix,3200,lbs,2026-03-12,Salinas Valley Farms,0061414100014,FIELD-SVF-12,Salinas Valley Farms LLC,HARV-SVF-2026-0312-002", - "LOT-ROM-2026-0311C,Romaine Hearts 3-Pack,4100,lbs,2026-03-11,Pacific Coast Growers,0061414100021,FIELD-PCG-3,Pacific Coast Growers Inc,HARV-SVF-2026-0312-003", - ], + csvNote: + "One row per harvesting event. Your values populate these columns; nothing is pre-filled.", canonical: `{ - "event_id": "8f3c2e6a-4d11-4a0b-9b2e-5c0a1f4a2c10", - "tenant_id": "9d2b1c4f-7e88-4f12-9c1a-6f0e2d3a8b71", + "event_id": "", + "tenant_id": "", "schema_version": 1, "source_system": "csv", "event_type": "harvesting", - "event_timestamp": "2026-03-12T00:00:00+00:00", - "traceability_lot_code": "LOT-ROM-2026-0312A", - "product_reference": "Organic Romaine Hearts", - "quantity": 5000, - "unit_of_measure": "lbs", - "from_facility_reference": "0061414100014", + "event_timestamp": "", + "traceability_lot_code": "", + "product_reference": "", + "quantity": , + "unit_of_measure": "", + "from_facility_reference": "", "kdes": { - "harvest_date": "2026-03-12", - "growing_area_name": "FIELD-SVF-7", - "harvester_name": "Salinas Valley Farms LLC", - "reference_document_number": "HARV-SVF-2026-0312-001", - "location_name": "Salinas Valley Farms" + "harvest_date": "", + "growing_area_name": "", + "harvester_name": "", + "reference_document_number": "", + "location_name": "" }, "confidence_score": 1.0, "status": "active", - "sha256_hash": "9c4e1b7a2f8d5630ae914c2b0f7e6a18c3d5f9b2e0a7d4c1f8b3e6a9d2c5f0b7", - "chain_hash": "f0a2b5c8d1e4f7a0b3c6d9e2f5a8b1c4d7e0f3a6b9c2d5e8f1a4b7c0d3e6f9a2" + "sha256_hash": "", + "chain_hash": "" }`, canonicalAnnotation: "KDE validation against §1.1330 (a)(1) through (a)(7). Hash chained to previous event.", fdaRow: [ - ["Traceability Lot Code (TLC)", "LOT-ROM-2026-0312A"], - ["Product Description", "Organic Romaine Hearts"], - ["Quantity", "5000"], - ["Unit of Measure", "lbs"], + ["Traceability Lot Code (TLC)", "← traceability_lot_code"], + ["Product Description", "← product_description"], + ["Quantity", "← quantity"], + ["Unit of Measure", "← unit_of_measure"], ["Event Type (CTE)", "harvesting"], - ["Event Date", "2026-03-12"], - ["Event Time", "00:00:00 UTC"], - ["Location GLN", "0061414100014"], - ["Location Name", "Salinas Valley Farms"], - ["Harvest Date", "2026-03-12"], - ["Growing Area", "FIELD-SVF-7"], - ["Reference Document Number", "HARV-SVF-2026-0312-001"], - ["Source Document", "01_harvesting.csv"], - [ - "Record Hash (SHA-256)", - "9c4e1b7a2f8d5630ae914c2b0f7e6a18c3d5f9b2e0a7d4c1f8b3e6a9d2c5f0b7", - ], - [ - "Chain Hash", - "f0a2b5c8d1e4f7a0b3c6d9e2f5a8b1c4d7e0f3a6b9c2d5e8f1a4b7c0d3e6f9a2", - ], + ["Event Date", "← harvest_date"], + ["Event Time", "from event_timestamp"], + ["Location GLN", "← location_gln"], + ["Location Name", "← location_name"], + ["Harvest Date", "← harvest_date"], + ["Growing Area", "← field_id"], + ["Reference Document Number", "← reference_document"], + ["Source Document", "your uploaded file name"], + ["Record Hash (SHA-256)", "computed from the canonical record"], + ["Chain Hash", "chains this record to the previous one"], ], fdaAnnotation: "Mapped to FDA harvesting columns (TLC, Harvest Date, Growing Area). Cells pass through sanitize_cell to neutralize spreadsheet-formula injection (#1081).", @@ -115,61 +106,52 @@ const STEPS: Step[] = [ "Pre-shipment temperature event. Carries the same TLC. Adds the cooling KDE the FDA requires for produce.", csvHeader: "traceability_lot_code,product_description,quantity,unit_of_measure,cooling_date,location_name,location_gln,temperature_celsius,reference_document", - csvRows: [ - "LOT-ROM-2026-0312A,Organic Romaine Hearts,5000,lbs,2026-03-12,Salinas Valley Cooler #1,0061414100014,1.8,COOL-SVF-2026-0312-001", - "LOT-ROM-2026-0312B,Chopped Romaine Mix,3200,lbs,,Salinas Valley Cooler #1,0061414100014,2.3,COOL-SVF-2026-0312-002", - "LOT-ROM-2026-0311C,Romaine Hearts 3-Pack,4100,lbs,2026-03-11,Pacific Coast Cold Storage,0061414100021,2.0,COOL-SVF-2026-0312-003", - ], + csvNote: + "One row per cooling event. The TLC column links each row back to its harvesting event.", canonical: `{ - "event_id": "1a7d4e8b-2c93-4f67-a510-3b8e4c9f0a25", - "tenant_id": "9d2b1c4f-7e88-4f12-9c1a-6f0e2d3a8b71", + "event_id": "", + "tenant_id": "", "schema_version": 1, "source_system": "csv", "event_type": "cooling", - "event_timestamp": "2026-03-12T00:00:00+00:00", - "traceability_lot_code": "LOT-ROM-2026-0312A", - "product_reference": "Organic Romaine Hearts", - "quantity": 5000, - "unit_of_measure": "lbs", - "from_facility_reference": "0061414100014", + "event_timestamp": "", + "traceability_lot_code": "", + "product_reference": "", + "quantity": , + "unit_of_measure": "", + "from_facility_reference": "", "kdes": { - "cooling_date": "2026-03-12", - "temperature": 35.24, - "location_name": "Salinas Valley Cooler #1", - "reference_document_number": "COOL-SVF-2026-0312-001" + "cooling_date": "", + "temperature": "", + "location_name": "", + "reference_document_number": "" }, "confidence_score": 1.0, "status": "active", - "sha256_hash": "2e8c5a1b4d7f0936e2a5b8c1d4e7f0a3b6c9d2e5f8a1b4c7d0e3f6a9b2c5d8e1", - "chain_hash": "a4b7c0d3e6f9a2b5c8d1e4f7a0b3c6d9e2f5a8b1c4d7e0f3a6b9c2d5e8f1a4b7" + "sha256_hash": "", + "chain_hash": "" }`, canonicalAnnotation: - "Celsius normalized to Fahrenheit (1.8°C → 35.24°F). TLC linkage proves continuity from harvest.", + "Celsius normalized to Fahrenheit. TLC linkage proves continuity from harvest.", fdaRow: [ - ["Traceability Lot Code (TLC)", "LOT-ROM-2026-0312A"], - ["Product Description", "Organic Romaine Hearts"], - ["Quantity", "5000"], - ["Unit of Measure", "lbs"], + ["Traceability Lot Code (TLC)", "← traceability_lot_code"], + ["Product Description", "← product_description"], + ["Quantity", "← quantity"], + ["Unit of Measure", "← unit_of_measure"], ["Event Type (CTE)", "cooling"], - ["Event Date", "2026-03-12"], - ["Event Time", "00:00:00 UTC"], - ["Location GLN", "0061414100014"], - ["Location Name", "Salinas Valley Cooler #1"], - ["Cooling Date", "2026-03-12"], - ["Temperature (°F)", "35.24"], - ["Reference Document Number", "COOL-SVF-2026-0312-001"], - ["Source Document", "02_cooling.csv"], - [ - "Record Hash (SHA-256)", - "2e8c5a1b4d7f0936e2a5b8c1d4e7f0a3b6c9d2e5f8a1b4c7d0e3f6a9b2c5d8e1", - ], - [ - "Chain Hash", - "a4b7c0d3e6f9a2b5c8d1e4f7a0b3c6d9e2f5a8b1c4d7e0f3a6b9c2d5e8f1a4b7", - ], + ["Event Date", "← cooling_date"], + ["Event Time", "from event_timestamp"], + ["Location GLN", "← location_gln"], + ["Location Name", "← location_name"], + ["Cooling Date", "← cooling_date"], + ["Temperature (°F)", "← temperature_celsius, normalized"], + ["Reference Document Number", "← reference_document"], + ["Source Document", "your uploaded file name"], + ["Record Hash (SHA-256)", "computed from the canonical record"], + ["Chain Hash", "chains this record to the previous one"], ], fdaAnnotation: - "Mapped to FDA Cooling Date and Temperature columns. Same TLC links this row to the harvesting row above for sortable recall scope.", + "Mapped to FDA Cooling Date and Temperature columns. The shared TLC links this row to the harvesting row above for sortable recall scope.", }, { cte: "§1.1340", @@ -178,65 +160,56 @@ const STEPS: Step[] = [ "Transfer event. Adds Ship-From / Ship-To facilities with GLN identifiers, the FDA's expected join keys.", csvHeader: "traceability_lot_code,product_description,quantity,unit_of_measure,ship_date,ship_from_location,ship_to_location,ship_from_gln,ship_to_gln,carrier_name,po_number,reference_document", - csvRows: [ - "LOT-ROM-2026-0312A-PK,Organic Romaine Hearts 3ct,800,cases,2026-03-14,FreshCut Processing,MegaMart Distribution Center,0061414100038,0061414100045,FedEx Freight,PO-MM-2026-7201,BOL-CFS-2026-0313-001", - "LOT-ROM-2026-0312A-PK,Organic Romaine Hearts 3ct,800,cases,2026-03-14,FreshCut Processing,Premier Grocery Warehouse,0061414100038,0061414100052,Cold Express Logistics,PO-PG-2026-3388,BOL-CFS-2026-0313-002", - "LOT-ROM-2026-0312B-PK,Chopped Romaine 12oz Bag,2800,bags,2026-03-14,FreshCut Processing,MegaMart Distribution Center,0061414100038,0061414100045,FedEx Freight,PO-MM-2026-7202,BOL-CFS-2026-0313-003", - ], + csvNote: + "One row per shipment. Ship-from and ship-to GLNs are the join keys an auditor uses to follow the lot.", canonical: `{ - "event_id": "5b2f8c1e-7d40-4a9b-8e15-2c7a0f4b9d63", - "tenant_id": "9d2b1c4f-7e88-4f12-9c1a-6f0e2d3a8b71", + "event_id": "", + "tenant_id": "", "schema_version": 1, "source_system": "csv", "event_type": "shipping", - "event_timestamp": "2026-03-14T00:00:00+00:00", - "traceability_lot_code": "LOT-ROM-2026-0312A-PK", - "product_reference": "Organic Romaine Hearts 3ct", - "quantity": 800, - "unit_of_measure": "cases", - "from_facility_reference": "0061414100038", - "to_facility_reference": "0061414100045", + "event_timestamp": "", + "traceability_lot_code": "", + "product_reference": "", + "quantity": , + "unit_of_measure": "", + "from_facility_reference": "", + "to_facility_reference": "", "kdes": { - "ship_date": "2026-03-14", - "ship_from_gln": "0061414100038", - "ship_from_location": "FreshCut Processing", - "ship_to_gln": "0061414100045", - "ship_to_location": "MegaMart Distribution Center", - "carrier": "FedEx Freight", - "po_number": "PO-MM-2026-7201", - "reference_document_number": "BOL-CFS-2026-0313-001" + "ship_date": "", + "ship_from_gln": "", + "ship_from_location": "", + "ship_to_gln": "", + "ship_to_location": "", + "carrier": "", + "po_number": "", + "reference_document_number": "" }, "confidence_score": 1.0, "status": "active", - "sha256_hash": "7f3a0c5d8e1b4f7a0c3d6e9f2a5b8c1d4e7f0a3b6c9d2e5f8a1b4c7d0e3f6a90", - "chain_hash": "b8c1d4e7f0a3b6c9d2e5f8a1b4c7d0e3f6a9b2c5d8e1f4a7b0c3d6e9f2a5b8c1" + "sha256_hash": "", + "chain_hash": "" }`, canonicalAnnotation: "Both Ship-From and Ship-To GLNs preserved as structured KDEs. Carrier and PO travel in the kdes map.", fdaRow: [ - ["Traceability Lot Code (TLC)", "LOT-ROM-2026-0312A-PK"], - ["Product Description", "Organic Romaine Hearts 3ct"], - ["Quantity", "800"], - ["Unit of Measure", "cases"], + ["Traceability Lot Code (TLC)", "← traceability_lot_code"], + ["Product Description", "← product_description"], + ["Quantity", "← quantity"], + ["Unit of Measure", "← unit_of_measure"], ["Event Type (CTE)", "shipping"], - ["Event Date", "2026-03-14"], - ["Event Time", "00:00:00 UTC"], - ["Ship From GLN", "0061414100038"], - ["Ship From Name", "FreshCut Processing"], - ["Ship To GLN", "0061414100045"], - ["Ship To Name", "MegaMart Distribution Center"], - ["Ship Date", "2026-03-14"], - ["Carrier", "FedEx Freight"], - ["Reference Document Number", "BOL-CFS-2026-0313-001"], - ["Source Document", "04_shipping.csv"], - [ - "Record Hash (SHA-256)", - "7f3a0c5d8e1b4f7a0c3d6e9f2a5b8c1d4e7f0a3b6c9d2e5f8a1b4c7d0e3f6a90", - ], - [ - "Chain Hash", - "b8c1d4e7f0a3b6c9d2e5f8a1b4c7d0e3f6a9b2c5d8e1f4a7b0c3d6e9f2a5b8c1", - ], + ["Event Date", "← ship_date"], + ["Event Time", "from event_timestamp"], + ["Ship From GLN", "← ship_from_gln"], + ["Ship From Name", "← ship_from_location"], + ["Ship To GLN", "← ship_to_gln"], + ["Ship To Name", "← ship_to_location"], + ["Ship Date", "← ship_date"], + ["Carrier", "← carrier_name"], + ["Reference Document Number", "← reference_document"], + ["Source Document", "your uploaded file name"], + ["Record Hash (SHA-256)", "computed from the canonical record"], + ["Chain Hash", "chains this record to the previous one"], ], fdaAnnotation: "Mapped to FDA Ship From / Ship To GLN + Name columns (auditor join keys). Facility names redact to [REDACTED] in the default export (#1219); GLNs always emit.", @@ -280,9 +253,8 @@ function StepBlock({ step, index }: { step: Step; index: number }) {
             {step.csvHeader}
-            {"\n"}
-            {step.csvRows.join("\n")}
           
+

{step.csvNote}

@@ -330,13 +302,13 @@ export default function TransformationProofPage() { className="mb-0 mt-4 border-b-0 pb-0" />

- Three CTEs, three transformations. Real production code, synthetic - data. Every row you see comes from{" "} + Three CTEs, three transformations, running on real production code. + Each step shows the CSV column spec, the canonical event shape from{" "} - sample_data/ - {" "} - and matches the canonical event shape and FDA spreadsheet column - spec used in production. + services/shared/canonical_event.py + + , and the FDA spreadsheet column mapping used in production. Your + records supply the values; the pipeline supplies the structure.

@@ -352,13 +324,14 @@ export default function TransformationProofPage() { Verify this for yourself.

- Download the same sample CSVs, run the canonical pipeline, and - verify the SHA-256 + chain hashes with our open verifier. + Upload your own supplier CSV, run it through the canonical + pipeline, and verify the SHA-256 and chain hashes with our open + verifier.

diff --git a/frontend/src/app/tools/drill-simulator/components/DrillSimulatorClient.tsx b/frontend/src/app/tools/drill-simulator/components/DrillSimulatorClient.tsx index fec259e65..ff4b66155 100644 --- a/frontend/src/app/tools/drill-simulator/components/DrillSimulatorClient.tsx +++ b/frontend/src/app/tools/drill-simulator/components/DrillSimulatorClient.tsx @@ -8,14 +8,14 @@ import { Button } from '@/components/ui/button'; import { Badge, type BadgeTone } from '@/components/ui/badge'; import { Callout } from '@/components/ui/callout'; import { - Play, CheckCircle2, AlertTriangle, FileText, + Play, CheckCircle2, AlertTriangle, ArrowRight, Clock, Loader2, Network, Upload, Download, ChevronDown, ChevronRight, Timer, Shield, Zap, TrendingUp, RotateCcw, } from 'lucide-react'; import { toast } from '@/components/ui/use-toast'; import { - parseCSV, validateRecords, generateSampleCSV, getScoreColor, + parseCSV, validateRecords, generateCsvTemplate, getScoreColor, getSeverityColor, getSeverityBadgeColor, type ValidationResult, } from '../lib/record-validator'; @@ -25,7 +25,6 @@ import { trackToolRunStarted, trackToolRunCompleted } from '@/lib/conversion-eve type Phase = 'scenario' | 'upload' | 'analyzing' | 'results' | 'report'; interface DrillState { - selectedScenario: string | null; uploadedFile: File | null; validationResult: ValidationResult | null; phase: Phase; @@ -33,30 +32,6 @@ interface DrillState { uploadTime: number | null; } -const scenarios = [ - { - id: 'produce', - title: 'Fresh Produce Supplier', - description: 'Multi-state leafy greens distributor with 500+ retail customers', - complexity: 'High', - recordCount: '2,000+', - }, - { - id: 'dairy', - title: 'Dairy Processing Facility', - description: 'Fluid milk processor with complex ingredients and co-packers', - complexity: 'Medium', - recordCount: '800+', - }, - { - id: 'supplements', - title: 'Supplement Manufacturer', - description: 'Multi-ingredient dietary supplements with international suppliers', - complexity: 'Very High', - recordCount: '5,000+', - }, -]; - const analysisStages = [ { name: 'Parsing records...', duration: 500 }, { name: 'Mapping CTEs and KDEs...', duration: 800 }, @@ -67,7 +42,6 @@ const analysisStages = [ export function DrillSimulatorClient() { const [state, setState] = useState({ - selectedScenario: null, uploadedFile: null, validationResult: null, phase: 'scenario', @@ -104,10 +78,9 @@ export function DrillSimulatorClient() { return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; }; - const handleScenarioSelect = (scenarioId: string, drillStartTime: number) => { + const handleStartDrill = (drillStartTime: number) => { setState({ ...state, - selectedScenario: scenarioId, phase: 'upload', drillStartTime, uploadTime: null, @@ -209,20 +182,13 @@ export function DrillSimulatorClient() { } }; - const handleLoadSampleData = async () => { - const csv = generateSampleCSV(); - const blob = new Blob([csv], { type: 'text/csv' }); - const file = new File([blob], 'sample-records.csv', { type: 'text/csv' }); - setState({ ...state, uploadedFile: file }); - }; - - const handleDownloadSample = () => { - const csv = generateSampleCSV(); + const handleDownloadTemplate = () => { + const csv = generateCsvTemplate(); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = 'sample-traceability-records.csv'; + a.download = 'traceability-records-template.csv'; a.click(); URL.revokeObjectURL(url); }; @@ -231,14 +197,10 @@ export function DrillSimulatorClient() { if (!state.validationResult) return; const result = state.validationResult; - const scenarioName = - scenarios.find((s) => s.id === state.selectedScenario)?.title || - 'Drill'; const report = `FDA RECALL DRILL SIMULATOR: FULL REPORT Generated: ${new Date().toISOString()} -SCENARIO: ${scenarioName} RECORDS ANALYZED: ${result.totalRecords} OVERALL SCORE: ${result.scores.overallScore.toFixed(1)}/100 (${result.scores.grade}) @@ -297,7 +259,6 @@ STATUS: ${ const handleReset = () => { setState({ - selectedScenario: null, uploadedFile: null, validationResult: null, phase: 'scenario', @@ -341,42 +302,21 @@ STATUS: ${

-
- {scenarios.map((scenario) => ( - - ))} -
-
-
@@ -404,15 +344,6 @@ STATUS: ${ - -

- Scenario: {scenarios.find((s) => s.id === state.selectedScenario)?.title} -

-

- {scenarios.find((s) => s.id === state.selectedScenario)?.description} -

-
-
Analyze My Records -
); @@ -585,7 +509,7 @@ STATUS: ${ {gapSummary}

- Self-check on synthetic drill data. Not a compliance verdict. + Self-check on the records you uploaded. Not a compliance verdict.

@@ -663,7 +587,7 @@ STATUS: ${ {gapSummary}

- Self-check on synthetic drill data. Not a compliance verdict. + Self-check on the records you uploaded. Not a compliance verdict.

@@ -892,7 +816,7 @@ STATUS: ${ headline="Get Your Drill Findings" subheadline="Unlock detailed findings, gap analysis, and a downloadable self-check report." ctaText="Unlock Findings" - toolContext={{ toolInputs: { score: result.scores.overallScore, grade: result.scores.grade, scenario: state.selectedScenario } }} + toolContext={{ toolInputs: { score: result.scores.overallScore, grade: result.scores.grade } }} teaser={teaser} onUnlock={() => {}} > diff --git a/frontend/src/app/tools/drill-simulator/lib/record-validator.ts b/frontend/src/app/tools/drill-simulator/lib/record-validator.ts index fa8625adf..2ebecb61e 100644 --- a/frontend/src/app/tools/drill-simulator/lib/record-validator.ts +++ b/frontend/src/app/tools/drill-simulator/lib/record-validator.ts @@ -902,72 +902,13 @@ function calculateScores( // UTILITY FUNCTIONS // ============================================================================ -export function generateSampleCSV(): string { - const lines = [ - 'Event Type,Product,Lot Code,GTIN,Facility GLN,Source GLN,Destination GLN,Timestamp,Quantity,Unit of Measure,Reference Document', - ]; - - // Harvest/Create - lines.push( - 'Create,Romaine Lettuce: Head,LOT-2024-001,0123456789012,1234567890128,1234567890128,3456789012345,2024-01-15T08:30:00Z,500,cases,FARM-001-DOC' - ); - - // First transformation - lines.push( - 'Transform,Romaine Lettuce: Packaged,LOT-2024-001,0123456789029,3456789012345,,3456789012345,2024-01-15T12:00:00Z,480,cases,PKG-2024-001' - ); - - // Ship from packer - lines.push( - 'Ship,Romaine Lettuce: Packaged,LOT-2024-001,0123456789029,3456789012345,3456789012345,5678901234567,2024-01-16T14:00:00Z,480,cases,BOL-2024-001' - ); - - // Receive at distributor - lines.push( - 'Receive,Romaine Lettuce: Packaged,LOT-2024-001,0123456789029,5678901234567,3456789012345,5678901234567,2024-01-17T08:30:00Z,475,cases,RCV-2024-001' - ); - - // Ship from distributor - lines.push( - 'Ship,Romaine Lettuce: Packaged,LOT-2024-001,0123456789029,5678901234567,5678901234567,7890123456789,2024-01-18T10:00:00Z,450,cases,BOL-2024-002' - ); - - // Receive at retailer - lines.push( - 'Receive,Romaine Lettuce: Packaged,LOT-2024-001,0123456789029,7890123456789,5678901234567,7890123456789,2024-01-18T15:30:00Z,450,cases,RCV-2024-002' - ); - - // Different lot - intentionally missing some KDEs - lines.push('Create,Spinach: Bulk,LOT-2024-002,,1234567890135,1234567890135,3456789012352,2024-01-15T07:00:00Z,800,cases,'); - - lines.push('Ship,Spinach: Bulk,LOT-2024-002,,3456789012352,3456789012352,5678901234574,2024-01-16T16:00:00Z,800,,BOL-2024-003'); - - lines.push('Receive,Spinach: Bulk,LOT-2024-002,,5678901234574,3456789012352,5678901234574,2024-01-17T09:00:00Z,800,cases,'); - - // Third lot with time gap - lines.push( - 'Create,Tomatoes: Vine Ripe,LOT-2024-003,0123456789036,1234567890142,1234567890142,3456789012359,2024-01-10T06:00:00Z,1200,cases,FARM-003' - ); - - lines.push( - 'Ship,Tomatoes: Vine Ripe,LOT-2024-003,0123456789036,3456789012359,3456789012359,5678901234581,2024-01-10T14:00:00Z,1200,cases,BOL-2024-004' - ); - - // Large gap here - 3 days - lines.push( - 'Receive,Tomatoes: Vine Ripe,LOT-2024-003,0123456789036,5678901234581,3456789012359,5678901234581,2024-01-13T10:30:00Z,1150,cases,RCV-2024-003' - ); - - // Fourth lot with unmatched ship (no receive) - lines.push( - 'Create,Cucumbers,LOT-2024-004,0123456789043,1234567890159,1234567890159,3456789012366,2024-01-14T08:00:00Z,600,cases,FARM-004' - ); - - lines.push( - 'Ship,Cucumbers,LOT-2024-004,0123456789043,3456789012366,3456789012366,5678901234598,2024-01-15T12:00:00Z,600,cases,BOL-2024-005' - ); - - return lines.join('\n'); +/** + * Blank CSV template: the header row only, matching the column spec the + * validator checks (see COLUMN_MAP and REQUIRED_KDES above). No data rows — + * users fill it with their own records. + */ +export function generateCsvTemplate(): string { + return 'Event Type,Product,Lot Code,GTIN,Facility GLN,Source GLN,Destination GLN,Timestamp,Quantity,Unit of Measure,Reference Document'; } /** diff --git a/frontend/src/app/tools/fsma-unified/components/AnomalySimulator.tsx b/frontend/src/app/tools/fsma-unified/components/AnomalySimulator.tsx deleted file mode 100644 index 7bea17323..000000000 --- a/frontend/src/app/tools/fsma-unified/components/AnomalySimulator.tsx +++ /dev/null @@ -1,392 +0,0 @@ -'use client'; - -import React, { useEffect, useMemo, useRef, useState } from "react"; -import { Card, CardContent } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { EmptyState } from "@/components/ui/empty-state"; -import { AlertTriangle, Loader2, RefreshCw, Sparkles } from "lucide-react"; -import { trackToolRunStarted, trackToolRunCompleted } from "@/lib/conversion-events"; - -// ── Lazy-loaded recharts ── -type RechartsModule = typeof import("recharts"); - -function useRecharts() { - const [mod, setMod] = useState(null); - useEffect(() => { - let cancelled = false; - import("recharts").then(m => { if (!cancelled) setMod(m); }); - return () => { cancelled = true; }; - }, []); - return mod; -} - -// ── Types ── -type RNG = () => number; -function mulberry32(seed: number): RNG { - let a = seed >>> 0; - return function () { - a |= 0; a = (a + 0x6d2b79f5) | 0; - let t = Math.imul(a ^ (a >>> 15), 1 | a); - t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; - }; -} -function clamp(n: number, min: number, max: number) { return Math.max(min, Math.min(max, n)); } -function mean(arr: number[]) { return arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0; } -function stddev(arr: number[]) { - if (arr.length < 2) return 0; - const m = mean(arr); - return Math.sqrt(arr.reduce((a, b) => a + (b - m) ** 2, 0) / (arr.length - 1)); -} -function fmt(n: number, d = 2) { return Number.isFinite(n) ? n.toFixed(d) : "N/A"; } -function isoHourLabel(ts: number) { return new Date(ts).toISOString().slice(0, 13) + ":00"; } - -type TempPoint = { t: number; hour: string; tempF: number; injectedAnomaly: boolean; dayIdx: number; hourOfDay: number }; -type LotFlowPoint = { id: string; supplierId: string; supplierName: string; lot: string; dayIdx: number; qty: number; tempExposureHours: number; transitHours: number; tempOnArrival: number; flagged: boolean }; -type Supplier = { id: string; name: string; region: string }; -type AlgoMode = "ensemble" | "statistical" | "rule" | "pattern"; -type DetectionResult = { flaggedHours: Set; explanations: Map }; -type Confusion = { tp: number; fp: number; tn: number; fn: number; precision: number; recall: number; f1: number }; -type DailyAggregate = { day: number; label: string; min: number; max: number; avg: number; anomalyCount: number; maxScore: number }; - -// ── Synthetic Data Generator ── -function generateSyntheticColdChain(seed = 204, days = 90) { - const rng = mulberry32(seed); - const start = Date.UTC(2026, 0, 1); - const totalHours = days * 24; - const anomalyDays = new Set(); - while (anomalyDays.size < 6) anomalyDays.add(Math.floor(rng() * days)); - - const points: TempPoint[] = []; - for (let h = 0; h < totalHours; h++) { - const t = start + h * 3600_000; - const dayIdx = Math.floor(h / 24); - const hourOfDay = h % 24; - const diurnal = Math.sin((2 * Math.PI * hourOfDay) / 24) * 0.7; - const noise = (rng() - 0.5) * 0.9; - let tempF = 36.5 + diurnal + noise; - let injected = false; - if (anomalyDays.has(dayIdx)) { - const startExc = 10 + Math.floor(rng() * 6); - const dur = 5 + Math.floor(rng() * 3); - if (hourOfDay >= startExc && hourOfDay < startExc + dur) { tempF = 42 + (rng() - 0.5) * 1.2; injected = true; } - } - points.push({ t, hour: isoHourLabel(t), tempF: Number(tempF.toFixed(2)), injectedAnomaly: injected, dayIdx, hourOfDay }); - } - - const suppliers: Supplier[] = [ - { id: "sup-a", name: "Arroyo Farms", region: "CA" }, { id: "sup-b", name: "Delta Leaf Co.", region: "AZ" }, - { id: "sup-c", name: "North Basin Produce", region: "NV" }, { id: "sup-d", name: "Canyon Ridge Growers", region: "CA" }, - { id: "sup-e", name: "Mesa Fresh Logistics", region: "AZ" }, { id: "sup-f", name: "Pacific Valley Supply", region: "CA" }, - ]; - - const lots: LotFlowPoint[] = []; - for (let i = 0; i < 200; i++) { - const s = suppliers[Math.floor(rng() * suppliers.length)]; - const dayIdx = Math.floor(rng() * days); - const qty = Math.round(80 + rng() * 920); - const exposure = anomalyDays.has(dayIdx) ? 2 + Math.floor(rng() * 6) : Math.floor(rng() * 2); - const transitHours = 2 + Math.floor(rng() * 48); - const isAnomalous = anomalyDays.has(dayIdx) || (transitHours > 36 && rng() > 0.5); - const tempOnArrival = isAnomalous ? 38 + Math.floor(rng() * 20) : 34 + Math.floor(rng() * 4); - lots.push({ id: `lf-${i}`, supplierId: s.id, supplierName: s.name, lot: `RML-${String(dayIdx).padStart(3, "0")}-${Math.floor(100 + rng() * 900)}`, dayIdx, qty, tempExposureHours: exposure, transitHours, tempOnArrival, flagged: false }); - } - return { points, lots, suppliers, anomalyDays, start, days }; -} - -// ── Detection Algorithms ── -function detectAnomalies(series: TempPoint[], mode: AlgoMode, s01: number): DetectionResult { - const temps = series.map(p => p.tempF); - const m = mean(temps), sd = stddev(temps); - const zThresh = 3.0 - s01 * 1.2; - const consHours = Math.round(4 - s01 * 2); - const driftThresh = 1.6 - s01 * 0.8; - const explain = new Map(); - - const statFlags = new Set(); - if (sd > 0) series.forEach(p => { const z = Math.abs((p.tempF - m) / sd); if (z >= zThresh) { statFlags.add(p.hour); explain.set(p.hour, `Statistical (Z-score): |(${fmt(p.tempF)} − ${fmt(m)}) / ${fmt(sd)}| = ${fmt(z)} ≥ ${fmt(zThresh)}.`); } }); - - const ruleFlags = new Set(); - let run = 0; - for (let i = 0; i < series.length; i++) { - if (series[i].tempF > 40) run++; else run = 0; - if (run >= consHours) for (let k = i - (consHours - 1); k <= i; k++) { ruleFlags.add(series[k].hour); if (!explain.has(series[k].hour)) explain.set(series[k].hour, `Rule-based: ${fmt(series[k].tempF)}°F > 40°F for ≥ ${consHours} consecutive hours.`); } - } - - const patternFlags = new Set(); - for (let i = 192; i < series.length; i++) { - const mw = mean(series.slice(i - 24, i).map(p => p.tempF)); - const mb = mean(series.slice(i - 192, i - 24).map(p => p.tempF)); - if (mw - mb >= driftThresh) { patternFlags.add(series[i - 1].hour); if (!explain.has(series[i - 1].hour)) explain.set(series[i - 1].hour, `Pattern drift: 24h mean (${fmt(mw)}°F) − 7d baseline (${fmt(mb)}°F) = ${fmt(mw - mb)}°F ≥ ${fmt(driftThresh)}°F.`); } - } - - const combine = (a: Set, b: Set) => { const o = new Set(a); b.forEach(x => o.add(x)); return o; }; - let out: Set; - if (mode === "statistical") out = statFlags; - else if (mode === "rule") out = ruleFlags; - else if (mode === "pattern") out = patternFlags; - else out = combine(combine(statFlags, ruleFlags), patternFlags); - - out.forEach(h => { const p = series.find(x => x.hour === h); if (p && p.tempF > 40) { const prior = explain.get(h) || ""; explain.set(h, `${prior} Regulatory: cold-chain deviations are a hazard-control concern under 21 CFR Part 117.`); } }); - return { flaggedHours: out, explanations: explain }; -} - -function confusionMatrix(series: TempPoint[], flagged: Set): Confusion { - let tp = 0, fp = 0, tn = 0, fn = 0; - for (const p of series) { const pred = flagged.has(p.hour); if (pred && p.injectedAnomaly) tp++; else if (pred) fp++; else if (p.injectedAnomaly) fn++; else tn++; } - const precision = tp + fp === 0 ? 0 : tp / (tp + fp); - const recall = tp + fn === 0 ? 0 : tp / (tp + fn); - const f1 = precision + recall === 0 ? 0 : (2 * precision * recall) / (precision + recall); - return { tp, fp, tn, fn, precision, recall, f1 }; -} - -function costRec(c: Confusion, fpC: number, fnC: number) { - const eFp = c.fp * fpC, eFn = c.fn * fnC; - if (eFn > eFp * 2) return { headline: "Bias toward sensitivity", detail: "False negatives dominate cost. Increase sensitivity." }; - if (eFp > eFn * 2) return { headline: "Bias toward specificity", detail: "False positives dominate cost. Reduce sensitivity." }; - return { headline: "Balanced tradeoff", detail: "Costs in same band. Focus on data quality." }; -} - -function aggregateDaily(points: TempPoint[], flagged: Set): DailyAggregate[] { - const days: Record = {}; - points.forEach(p => { if (!days[p.dayIdx]) days[p.dayIdx] = { temps: [], anomalies: 0 }; days[p.dayIdx].temps.push(p.tempF); if (flagged.has(p.hour)) days[p.dayIdx].anomalies++; }); - return Object.entries(days).map(([d, v]) => ({ day: parseInt(d), label: `Day ${parseInt(d) + 1}`, min: Math.round(Math.min(...v.temps) * 10) / 10, max: Math.round(Math.max(...v.temps) * 10) / 10, avg: Math.round(mean(v.temps) * 10) / 10, anomalyCount: v.anomalies, maxScore: v.anomalies > 0 ? Math.min(1, v.anomalies / 8) : 0 })); -} - -export function AnomalyDetectionSimulator() { - const recharts = useRecharts(); - const [seed, setSeed] = useState(204); - const [mode, setMode] = useState("ensemble"); - const [sensitivity, setSensitivity] = useState(55); - const [tab, setTab] = useState<"stream" | "daily" | "lots" | "suppliers" | "eval">("stream"); - const [selectedHour, setSelectedHour] = useState(null); - const [fpCost, setFpCost] = useState(800); - const [fnCost, setFnCost] = useState(50000); - const reseedPendingRef = useRef(false); - - const dataset = useMemo(() => generateSyntheticColdChain(seed, 90), [seed]); - useEffect(() => { - if (reseedPendingRef.current) { - reseedPendingRef.current = false; - trackToolRunCompleted('fsma-unified', 'success'); - } - }, [dataset]); - const det = useMemo(() => detectAnomalies(dataset.points, mode, sensitivity / 100), [dataset.points, mode, sensitivity]); - const conf = useMemo(() => confusionMatrix(dataset.points, det.flaggedHours), [dataset.points, det.flaggedHours]); - const rec = useMemo(() => costRec(conf, fpCost, fnCost), [conf, fpCost, fnCost]); - const dailyData = useMemo(() => aggregateDaily(dataset.points, det.flaggedHours), [dataset.points, det.flaggedHours]); - - const streamData = useMemo(() => dataset.points.map((p, idx) => ({ idx, hour: p.hour, tempF: p.tempF, flagged: det.flaggedHours.has(p.hour) ? 1 : 0 })), [dataset.points, det.flaggedHours]); - - const lotsData = useMemo(() => { - const dayHasFlag = new Set(); - dataset.points.forEach((p, i) => { if (det.flaggedHours.has(p.hour)) dayHasFlag.add(Math.floor(i / 24)); }); - return dataset.lots.map(l => ({ ...l, supplier: l.supplierName, flagged: dayHasFlag.has(l.dayIdx) || l.tempExposureHours >= 4 })); - }, [dataset, det.flaggedHours]); - - const normalLots = useMemo(() => lotsData.filter(l => !l.flagged), [lotsData]); - const flaggedLots = useMemo(() => lotsData.filter(l => l.flagged), [lotsData]); - - const supplierMatrix = useMemo(() => { - const m = new Map(); - dataset.suppliers.forEach(s => m.set(s.id, { name: s.name, region: s.region, lots: 0, flagged: 0, exp: 0, transit: 0, temp: 0 })); - lotsData.forEach(l => { const e = m.get(l.supplierId); if (!e) return; e.lots++; if (l.flagged) e.flagged++; e.exp += l.tempExposureHours; e.transit += l.transitHours; e.temp += l.tempOnArrival; }); - return Array.from(m.entries()).map(([id, v]) => { - const rate = v.lots ? v.flagged / v.lots : 0; - return { id, supplier: v.name, region: v.region, lots: v.lots, flaggedLots: v.flagged, flaggedRate: Number((rate * 100).toFixed(1)), avgTransit: Number((v.lots ? v.transit / v.lots : 0).toFixed(1)), avgTemp: Number((v.lots ? v.temp / v.lots : 0).toFixed(1)), score: Number(clamp(rate * 70 + ((v.lots ? v.exp / v.lots : 0) / 6) * 30, 0, 100).toFixed(1)) }; - }).sort((a, b) => b.score - a.score); - }, [dataset.suppliers, lotsData]); - - const topFlags = useMemo(() => dataset.points.filter(p => det.flaggedHours.has(p.hour)).slice(-12).reverse(), [dataset.points, det.flaggedHours]); - // Chart marks read the token layer so they follow the theme (SPEC §1.6). - const sevFill = (s: number) => (s > 0.4 ? "var(--re-danger)" : s > 0.1 ? "var(--re-warning)" : "var(--re-success)"); - const sevOpacity = (s: number) => (s > 0.7 ? 1 : s > 0.4 ? 0.8 : s > 0.1 ? 0.55 : 0.25); - - if (!recharts) { - return ( -
-
- ); - } - - const { - LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, - ScatterChart, Scatter, ZAxis, Legend, BarChart, Bar, Cell, ComposedChart, Area, ReferenceLine, - } = recharts; - - return ( -
-
-
-
-
- - -
-
- - -
-
- -
Higher = more flags.
-
- setSensitivity(Number(e.target.value))} - min={0} max={100} step={1} - /> -
{sensitivity}/100
-
-
-
-
-
Precision
{fmt(conf.precision * 100, 1)}%
-
Recall
{fmt(conf.recall * 100, 1)}%
-
F1
{fmt(conf.f1 * 100, 1)}%
-
-
-
- -
-
- setTab(v as "stream" | "daily" | "lots" | "suppliers" | "eval")}> - - Temp stream - Daily overview - Lot scatter - Supplier risk - Evaluation - - - -
- - - n === "tempF" ? [`${fmt(Number(v ?? 0))} °F`, "Temp"] : [String(v ?? ""), String(n)]} labelFormatter={(l) => streamData[Number(l)]?.hour || String(l)} /> - - -
-
-
Recent flags
- {topFlags.length === 0 ? : topFlags.map(p => ( - - ))} -
-
Explainability
- {selectedHour ?
{selectedHour}
{det.explanations.get(selectedHour) || "No explanation."}
:
Click a flag to view explanation.
} -
-
-
- - -
- - - - - - ) => { const { cx, cy, payload } = props as { cx?: number; cy?: number; payload?: { anomalyCount?: number; day?: number } }; return (payload?.anomalyCount ?? 0) > 0 ? : ; }} name="Max" /> - - -
-
- - {dailyData.map((d, i) => )} - -
-
- - -
- - - - - - }> }) => { if (!payload?.[0]) return null; const d = payload[0].payload as { lot?: string; supplier?: string; transitHours?: number; tempOnArrival?: number }; return
{d.lot}
{d.supplier}
Transit: {d.transitHours}h · {d.tempOnArrival}°F
; }) as never} /> - -
-
-
- - -
-
- - {supplierMatrix.map((s, i) => 40 ? "var(--re-danger)" : s.score > 20 ? "var(--re-warning)" : "var(--re-success)"} />)} - -
-
- SupplierFlag%Score - {supplierMatrix.map(r =>
{r.supplier}
{r.region} · {r.lots} lots · {r.avgTemp}°F avg
{r.flaggedRate}%{r.score}
)}
-
-
-
- - -
-
-
True positives
{conf.tp}
-
False positives
{conf.fp}
-
False negatives
{conf.fn}
-
True negatives
{conf.tn}
-
-
-
-
-
-
-
-
    -
  • • Multi-algorithm detection
  • • Evaluation harness
  • • Explainability per event
  • • Cost-based tuning lens
  • • Daily aggregation
  • -
-
-
-
-
- ); -} diff --git a/frontend/src/app/tools/fsma-unified/components/UnifiedDashboardClient.tsx b/frontend/src/app/tools/fsma-unified/components/UnifiedDashboardClient.tsx index 6706c07dd..66c8b5de4 100644 --- a/frontend/src/app/tools/fsma-unified/components/UnifiedDashboardClient.tsx +++ b/frontend/src/app/tools/fsma-unified/components/UnifiedDashboardClient.tsx @@ -1,50 +1,23 @@ 'use client'; import React from "react"; -import { motion } from "framer-motion"; -import dynamic from "next/dynamic"; - -const AnomalyDetectionSimulator = dynamic( - () => import("./AnomalySimulator").then(mod => ({ default: mod.AnomalyDetectionSimulator })), - { - ssr: false, - loading: () => ( -
- Loading simulator... -
- ), - } -); +import { Thermometer } from "lucide-react"; import { FreeToolPageShell } from "@/components/layout/FreeToolPageShell"; -import { LeadGate } from "@/components/lead-gate/LeadGate"; +import { EmptyState } from "@/components/ui/empty-state"; export function UnifiedDashboardClient() { return ( - -
🌡️
-

Cold Chain Anomaly Detection

-

Ensemble, statistical, rule-based, and pattern-drift algorithms with a full evaluation harness, lot scatter, and supplier risk matrix.

-
- } - > - - - - + ); } diff --git a/frontend/src/app/tools/knowledge-graph/components/KnowledgeGraph.tsx b/frontend/src/app/tools/knowledge-graph/components/KnowledgeGraph.tsx index 9df292e65..e1bf45183 100644 --- a/frontend/src/app/tools/knowledge-graph/components/KnowledgeGraph.tsx +++ b/frontend/src/app/tools/knowledge-graph/components/KnowledgeGraph.tsx @@ -15,15 +15,6 @@ import { import { GitBranch, Search } from "lucide-react"; // ── Utilities ── -function mulberry32(seed: number) { - let a = seed >>> 0; - return function () { - a |= 0; a = (a + 0x6d2b79f5) | 0; - let t = Math.imul(a ^ (a >>> 15), 1 | a); - t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; - }; -} function clamp(n: number, min: number, max: number) { return Math.max(min, Math.min(max, n)); } // ── Types ── @@ -63,36 +54,6 @@ function hexToRgba(hex: string, a: number) { function nodeR(t: NodeType) { return t === "Facility" ? 20 : t === "Supplier" || t === "Product" ? 18 : t === "Location" ? 14 : 16; } -function seededScenario(seed = 204) { - const rng = mulberry32(seed); const j = () => (rng() - 0.5) * 30; - const nodes: GraphNode[] = [ - { id: "sup-1", type: "Supplier", label: "Arroyo Farms", x: 120 + j(), y: 140 + j(), meta: { region: "CA" } }, - { id: "loc-1", type: "Location", label: "Salinas, CA", x: 110 + j(), y: 260 + j() }, - { id: "fac-1", type: "Facility", label: "Cooler • Farm Pack", x: 280 + j(), y: 160 + j() }, - { id: "cte-1", type: "CTE", label: "Harvest / Cooling", x: 280 + j(), y: 260 + j() }, - { id: "prod-1", type: "Product", label: "Romaine Lettuce", x: 460 + j(), y: 140 + j() }, - { id: "lot-1", type: "Lot", label: "TLC: RML-021-744", x: 460 + j(), y: 260 + j() }, - { id: "fac-2", type: "Facility", label: "Processor • Wash/Trim", x: 640 + j(), y: 150 + j() }, - { id: "cte-2", type: "CTE", label: "Transform", x: 640 + j(), y: 260 + j() }, - { id: "lot-2", type: "Lot", label: "TLC: RML-021-744-A", x: 800 + j(), y: 260 + j() }, - { id: "fac-3", type: "Facility", label: "DC • West", x: 960 + j(), y: 150 + j() }, - { id: "cte-3", type: "CTE", label: "Ship to DC", x: 960 + j(), y: 260 + j() }, - { id: "fac-4", type: "Facility", label: "Retail • Pasadena", x: 1120 + j(), y: 150 + j() }, - { id: "cte-4", type: "CTE", label: "Receive / Sell", x: 1120 + j(), y: 260 + j() }, - ]; - const edges: GraphEdge[] = [ - { id: "e-1", type: "LocatedAt", from: "sup-1", to: "loc-1" }, { id: "e-2", type: "Supplies", from: "sup-1", to: "fac-1" }, - { id: "e-3", type: "TracedFrom", from: "cte-1", to: "fac-1" }, { id: "e-4", type: "Contains", from: "prod-1", to: "lot-1" }, - { id: "e-5", type: "TracedFrom", from: "cte-1", to: "lot-1" }, { id: "e-6", type: "ShipsTo", from: "fac-1", to: "fac-2" }, - { id: "e-7", type: "TracedFrom", from: "cte-2", to: "fac-2" }, { id: "e-8", type: "Transforms", from: "lot-1", to: "lot-2" }, - { id: "e-9", type: "TracedFrom", from: "cte-2", to: "lot-2" }, { id: "e-10", type: "ShipsTo", from: "fac-2", to: "fac-3" }, - { id: "e-11", type: "TracedFrom", from: "cte-3", to: "fac-3" }, { id: "e-12", type: "ShipsTo", from: "fac-3", to: "fac-4" }, - { id: "e-13", type: "TracedFrom", from: "cte-4", to: "fac-4" }, { id: "e-14", type: "TracedFrom", from: "cte-3", to: "lot-2" }, - { id: "e-15", type: "TracedFrom", from: "cte-4", to: "lot-2" }, - ]; - return { nodes, edges }; -} - function traceNodes(startId: string, edges: GraphEdge[], mode: TraceMode, depth: number) { const out = new Map(), inn = new Map(); edges.forEach(e => { if (!out.has(e.from)) out.set(e.from, []); if (!inn.has(e.to)) inn.set(e.to, []); out.get(e.from)!.push(e.to); inn.get(e.to)!.push(e.from); }); @@ -115,11 +76,8 @@ function useAnimFrame(cb: () => void, on: boolean) { } export function SupplyChainKnowledgeGraphBuilder() { - const [seed] = useState(204); - const scenario = useMemo(() => seededScenario(seed), [seed]); - const [nodes, setNodes] = useState(scenario.nodes); - const [edges, setEdges] = useState(scenario.edges); - useEffect(() => { setNodes(scenario.nodes); setEdges(scenario.edges); }, [scenario]); + const [nodes, setNodes] = useState([]); + const [edges, setEdges] = useState([]); const canvasRef = useRef(null); const containerRef = useRef(null); @@ -247,6 +205,23 @@ export function SupplyChainKnowledgeGraphBuilder() { const onDown = (e: React.MouseEvent) => { const w = toW(e.clientX, e.clientY); const h = hitNode(w.x, w.y); if (h) { setSelEdge(null); setSelNode(h); setDragId(h); const n = nodes.find(x => x.id === h); if (n) setDragOff({ x: w.x - n.x, y: w.y - n.y }); } else { setSelNode(null); setSelEdge(null); setPanning(true); setLastPan({ x: e.clientX, y: e.clientY }); } }; const onMove = (e: React.MouseEvent) => { if (dragId && dragOff) { const w = toW(e.clientX, e.clientY); setNodes(p => p.map(n => n.id === dragId ? { ...n, x: w.x - dragOff.x, y: w.y - dragOff.y } : n)); } else if (panning && lastPan) { setVp(v => ({ ...v, panX: v.panX + e.clientX - lastPan.x, panY: v.panY + e.clientY - lastPan.y })); setLastPan({ x: e.clientX, y: e.clientY }); } }; const onUp = () => { setDragId(null); setDragOff(null); setPanning(false); setLastPan(null); }; + + if (nodes.length === 0) { + return ( +
+
+
Knowledge Graph Builder
+
FSMA 204-native traceability graph.
+
+ +
+ ); + } + return (
diff --git a/frontend/src/app/tools/knowledge-graph/components/__tests__/KnowledgeGraph.test.ts b/frontend/src/app/tools/knowledge-graph/components/__tests__/KnowledgeGraph.test.ts index 086ca3437..3427811ed 100644 --- a/frontend/src/app/tools/knowledge-graph/components/__tests__/KnowledgeGraph.test.ts +++ b/frontend/src/app/tools/knowledge-graph/components/__tests__/KnowledgeGraph.test.ts @@ -1,57 +1,26 @@ import { describe, it, expect } from 'vitest'; - -// We map out the internal functional logic from the KnowledgeGraph component -// For testing purposes without exporting them directly from the TSX file, -// we duplicate the pure functions here to ensure the logic holds up to our standards. - -// ── Types ── -type NodeType = "Supplier" | "Facility" | "Product" | "Lot" | "CTE" | "Location"; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// The graph component must ship with no hardcoded supply-chain records and +// must show an empty state when no data is loaded. These tests assert that +// invariant directly against the component source, plus keep coverage of the +// pure trace logic with an abstract structure (no invented records). + +const componentPath = join( + dirname(fileURLToPath(import.meta.url)), + '..', + 'KnowledgeGraph.tsx' +); +const source = readFileSync(componentPath, 'utf8'); + +// ── Types (mirrored from KnowledgeGraph.tsx) ── type EdgeType = "Supplies" | "Contains" | "Transforms" | "ShipsTo" | "TracedFrom" | "LocatedAt"; -type GraphNode = { id: string; type: NodeType; label: string; meta?: Record; x: number; y: number }; type GraphEdge = { id: string; type: EdgeType; from: string; to: string; meta?: Record }; type TraceMode = "forward" | "backward" | "bidirectional"; -// ── Pure Functions from KnowledgeGraph.tsx ── -function mulberry32(seed: number) { - let a = seed >>> 0; - return function () { - a |= 0; a = (a + 0x6d2b79f5) | 0; - let t = Math.imul(a ^ (a >>> 15), 1 | a); - t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; - }; -} - -function seededScenario(seed = 204) { - const rng = mulberry32(seed); const j = () => (rng() - 0.5) * 30; - const nodes: GraphNode[] = [ - { id: "sup-1", type: "Supplier", label: "Arroyo Farms", x: 120 + j(), y: 140 + j(), meta: { region: "CA" } }, - { id: "loc-1", type: "Location", label: "Salinas, CA", x: 110 + j(), y: 260 + j() }, - { id: "fac-1", type: "Facility", label: "Cooler • Farm Pack", x: 280 + j(), y: 160 + j() }, - { id: "cte-1", type: "CTE", label: "Harvest / Cooling", x: 280 + j(), y: 260 + j() }, - { id: "prod-1", type: "Product", label: "Romaine Lettuce", x: 460 + j(), y: 140 + j() }, - { id: "lot-1", type: "Lot", label: "TLC: RML-021-744", x: 460 + j(), y: 260 + j() }, - { id: "fac-2", type: "Facility", label: "Processor • Wash/Trim", x: 640 + j(), y: 150 + j() }, - { id: "cte-2", type: "CTE", label: "Transform", x: 640 + j(), y: 260 + j() }, - { id: "lot-2", type: "Lot", label: "TLC: RML-021-744-A", x: 800 + j(), y: 260 + j() }, - { id: "fac-3", type: "Facility", label: "DC • West", x: 960 + j(), y: 150 + j() }, - { id: "cte-3", type: "CTE", label: "Ship to DC", x: 960 + j(), y: 260 + j() }, - { id: "fac-4", type: "Facility", label: "Retail • Pasadena", x: 1120 + j(), y: 150 + j() }, - { id: "cte-4", type: "CTE", label: "Receive / Sell", x: 1120 + j(), y: 260 + j() }, - ]; - const edges: GraphEdge[] = [ - { id: "e-1", type: "LocatedAt", from: "sup-1", to: "loc-1" }, { id: "e-2", type: "Supplies", from: "sup-1", to: "fac-1" }, - { id: "e-3", type: "TracedFrom", from: "cte-1", to: "fac-1" }, { id: "e-4", type: "Contains", from: "prod-1", to: "lot-1" }, - { id: "e-5", type: "TracedFrom", from: "cte-1", to: "lot-1" }, { id: "e-6", type: "ShipsTo", from: "fac-1", to: "fac-2" }, - { id: "e-7", type: "TracedFrom", from: "cte-2", to: "fac-2" }, { id: "e-8", type: "Transforms", from: "lot-1", to: "lot-2" }, - { id: "e-9", type: "TracedFrom", from: "cte-2", to: "lot-2" }, { id: "e-10", type: "ShipsTo", from: "fac-2", to: "fac-3" }, - { id: "e-11", type: "TracedFrom", from: "cte-3", to: "fac-3" }, { id: "e-12", type: "ShipsTo", from: "fac-3", to: "fac-4" }, - { id: "e-13", type: "TracedFrom", from: "cte-4", to: "fac-4" }, { id: "e-14", type: "TracedFrom", from: "cte-3", to: "lot-2" }, - { id: "e-15", type: "TracedFrom", from: "cte-4", to: "lot-2" }, - ]; - return { nodes, edges }; -} - +// ── Pure function mirrored from KnowledgeGraph.tsx ── function traceNodes(startId: string, edges: GraphEdge[], mode: TraceMode, depth: number) { const out = new Map(), inn = new Map(); edges.forEach(e => { if (!out.has(e.from)) out.set(e.from, []); if (!inn.has(e.to)) inn.set(e.to, []); out.get(e.from)!.push(e.to); inn.get(e.to)!.push(e.from); }); @@ -68,81 +37,63 @@ function traceNodes(startId: string, edges: GraphEdge[], mode: TraceMode, depth: return { nodes: visited, edges: usedEdges }; } -describe('Supply Chain Knowledge Graph Logistics', () => { - - describe('seededScenario determinism', () => { - it('should generate identical graphs for the same seed', () => { - const graphA = seededScenario(204); - const graphB = seededScenario(204); - expect(graphA.nodes[0].x).toBeCloseTo(graphB.nodes[0].x); - expect(graphA.nodes[0].y).toBeCloseTo(graphB.nodes[0].y); - expect(graphA.edges.length).toBe(graphB.edges.length); - }); - - it('should generate different spatial layouts for different seeds', () => { - const graphA = seededScenario(204); - const graphB = seededScenario(999); - expect(graphA.nodes[0].x).not.toBe(graphB.nodes[0].x); - }); +describe('KnowledgeGraph data policy', () => { + it('ships no hardcoded graph scenario or seeded generator', () => { + expect(source).not.toContain('seededScenario'); + expect(source).not.toContain('mulberry32'); }); - describe('traceNodes BFS pathfinding', () => { - const { nodes, edges } = seededScenario(); - - it('should trace FORWARD downstream accurately', () => { - // Tracing forward from Supplier (sup-1) should hit Cooler (fac-1) at depth 1 - const trace = traceNodes('sup-1', edges, 'forward', 1); - expect(trace.nodes.has('sup-1')).toBe(true); - expect(trace.nodes.has('fac-1')).toBe(true); - expect(trace.nodes.has('loc-1')).toBe(true); - - // Should not hit depth 2 yet - expect(trace.nodes.has('fac-2')).toBe(false); + it('contains no invented supply-chain records (companies, lots, locations)', () => { + expect(source).not.toMatch(/Arroyo|Salinas|Pasadena|RML-\d|TLC:/); + }); - // Edges e-1 and e-2 should be used - expect(trace.edges.has('e-1')).toBe(true); - expect(trace.edges.has('e-2')).toBe(true); - }); + it('initializes with empty nodes and edges', () => { + expect(source).toContain('useState([])'); + expect(source).toContain('useState([])'); + }); - it('should trace BACKWARD upstream accurately', () => { - // Trace backward from DC (fac-3) should hit Processor (fac-2) at depth 1 - const trace = traceNodes('fac-3', edges, 'backward', 1); - expect(trace.nodes.has('fac-3')).toBe(true); - expect(trace.nodes.has('fac-2')).toBe(true); - expect(trace.nodes.has('cte-3')).toBe(true); + it('renders an empty state when no graph data is loaded', () => { + expect(source).toContain('EmptyState'); + expect(source).toContain('nodes.length === 0'); + }); +}); - // Should not see forward edges - expect(trace.nodes.has('fac-4')).toBe(false); - }); +describe('traceNodes BFS pathfinding', () => { + // Abstract structural graph: identifiers only, no records. + const edges: GraphEdge[] = [ + { id: 'e1', type: 'ShipsTo', from: 'a', to: 'b' }, + { id: 'e2', type: 'ShipsTo', from: 'b', to: 'c' }, + ]; - it('should trace BIDIRECTIONALLY across the full graph deeply', () => { - // Selecting a middle node like Processor (fac-2) and going depth 5 should capture almost everything - const trace = traceNodes('fac-2', edges, 'bidirectional', 5); - expect(trace.nodes.has('fac-2')).toBe(true); // Self - expect(trace.nodes.has('fac-1')).toBe(true); // Upstream Cooler - expect(trace.nodes.has('fac-4')).toBe(true); // Downstream Retail - expect(trace.nodes.has('sup-1')).toBe(true); // Original Supplier + it('traces forward across edges', () => { + const trace = traceNodes('a', edges, 'forward', 2); + expect(trace.nodes.has('a')).toBe(true); + expect(trace.nodes.has('b')).toBe(true); + expect(trace.nodes.has('c')).toBe(true); + expect(trace.edges.has('e1')).toBe(true); + expect(trace.edges.has('e2')).toBe(true); + }); - // Should identify significant edges - expect(trace.edges.has('e-6')).toBe(true); - expect(trace.edges.has('e-12')).toBe(true); - }); + it('traces backward across edges', () => { + const trace = traceNodes('c', edges, 'backward', 2); + expect(trace.nodes.has('a')).toBe(true); + expect(trace.nodes.has('b')).toBe(true); + expect(trace.nodes.has('c')).toBe(true); + }); - it('should strictly respect depth parameters', () => { - // Depth 0 should only contain the starting node - const trace0 = traceNodes('lot-1', edges, 'bidirectional', 0); - expect(trace0.nodes.size).toBe(1); - expect(trace0.edges.size).toBe(0); + it('respects depth limits', () => { + const trace0 = traceNodes('a', edges, 'bidirectional', 0); + expect(trace0.nodes.size).toBe(1); + expect(trace0.edges.size).toBe(0); - // Depth 1 from Lot 1 - const trace1 = traceNodes('lot-1', edges, 'bidirectional', 1); - expect(trace1.nodes.has('lot-1')).toBe(true); - expect(trace1.nodes.has('prod-1')).toBe(true); // From 'Contains' - expect(trace1.nodes.has('cte-1')).toBe(true); // From 'TracedFrom' - expect(trace1.nodes.has('lot-2')).toBe(true); // From 'Transforms' + const trace1 = traceNodes('a', edges, 'forward', 1); + expect(trace1.nodes.has('b')).toBe(true); + expect(trace1.nodes.has('c')).toBe(false); + }); - // Should NOT have deeply nested items - expect(trace1.nodes.has('fac-2')).toBe(false); // Depth 2 away from CTE-1/CTE-2 - }); + it('handles an empty graph', () => { + const trace = traceNodes('a', [], 'bidirectional', 3); + expect(trace.nodes.size).toBe(1); + expect(trace.edges.size).toBe(0); }); }); diff --git a/frontend/src/app/tools/sop-generator/components/SOPGeneratorClient.tsx b/frontend/src/app/tools/sop-generator/components/SOPGeneratorClient.tsx index 4f7e9fb8e..800c9deb9 100644 --- a/frontend/src/app/tools/sop-generator/components/SOPGeneratorClient.tsx +++ b/frontend/src/app/tools/sop-generator/components/SOPGeneratorClient.tsx @@ -145,7 +145,7 @@ ${retailers.length > 0 ? `## Retailer Requirements (informational)\n${retailers.
- +
@@ -161,11 +161,11 @@ ${retailers.length > 0 ? `## Retailer Requirements (informational)\n${retailers.
- +
- +
@@ -190,7 +190,6 @@ ${retailers.length > 0 ? `## Retailer Requirements (informational)\n${retailers. updated[i] = e.target.value; setProducts(updated); }} - placeholder="e.g. Roma Tomatoes, Romaine Lettuce" className="rounded-xl" /> {products.length > 1 && ( @@ -221,7 +220,6 @@ ${retailers.length > 0 ? `## Retailer Requirements (informational)\n${retailers. updated[i] = e.target.value; setFacilities(updated); }} - placeholder="e.g. Valley Fresh Farms, Salinas CA" className="rounded-xl" /> {facilities.length > 1 && ( diff --git a/frontend/src/app/why-regengine/page.tsx b/frontend/src/app/why-regengine/page.tsx index 665ab68d5..74ef4bf22 100644 --- a/frontend/src/app/why-regengine/page.tsx +++ b/frontend/src/app/why-regengine/page.tsx @@ -415,16 +415,16 @@ export default function WhyRegEnginePage() { Content-Type: application/json { - "traceability_lot_code": "LOT-2026-001", - "product": "Organic spinach", - "supplier_gln": "5012345678909", - "received_date": "2026-04-05", - "location_gln": "5012345678909" + "traceability_lot_code": "", + "product": "", + "supplier_gln": "<13-digit GLN>", + "received_date": "", + "location_gln": "<13-digit GLN>" } HTTP 201 { - "cte_id": "cte_abc123def456", + "cte_id": "", "status": "validated", "missing_kdes": [] }`} @@ -608,7 +608,7 @@ HTTP 201 {` - 2026-04-05T... + ObjectEvent From 2484a62a0976d660075f2d2e5c242ff74fc87083 Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:45:45 -0700 Subject: [PATCH 2/5] fix(tools): honest empty-state copy for cold-chain and knowledge graph --- .../components/UnifiedDashboardClient.tsx | 15 ++++++++++++--- .../knowledge-graph/components/KnowledgeGraph.tsx | 10 +++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/tools/fsma-unified/components/UnifiedDashboardClient.tsx b/frontend/src/app/tools/fsma-unified/components/UnifiedDashboardClient.tsx index 66c8b5de4..3d216680d 100644 --- a/frontend/src/app/tools/fsma-unified/components/UnifiedDashboardClient.tsx +++ b/frontend/src/app/tools/fsma-unified/components/UnifiedDashboardClient.tsx @@ -1,22 +1,31 @@ 'use client'; import React from "react"; +import Link from "next/link"; import { Thermometer } from "lucide-react"; import { FreeToolPageShell } from "@/components/layout/FreeToolPageShell"; import { EmptyState } from "@/components/ui/empty-state"; +import { Button } from "@/components/ui/button"; export function UnifiedDashboardClient() { return ( + + Open the Data Import Hub + + + } /> ); diff --git a/frontend/src/app/tools/knowledge-graph/components/KnowledgeGraph.tsx b/frontend/src/app/tools/knowledge-graph/components/KnowledgeGraph.tsx index e1bf45183..f67335c1c 100644 --- a/frontend/src/app/tools/knowledge-graph/components/KnowledgeGraph.tsx +++ b/frontend/src/app/tools/knowledge-graph/components/KnowledgeGraph.tsx @@ -1,6 +1,7 @@ 'use client'; import React, { useEffect, useMemo, useRef, useState } from "react"; +import Link from "next/link"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/ui/empty-state"; @@ -216,7 +217,14 @@ export function SupplyChainKnowledgeGraphBuilder() { + + Import traceability data + + + } />
); From 12430858039a70a0281bd9bd9389faae54dda156 Mon Sep 17 00:00:00 2001 From: Christopher Sellers <1@christophersellers.com> Date: Fri, 23 Oct 2026 22:18:25 -0700 Subject: [PATCH 3/5] fix: remove stray brace in record-validator after main merge Signed-off-by: Christopher Sellers <1@christophersellers.com> --- frontend/src/app/tools/drill-simulator/lib/record-validator.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/app/tools/drill-simulator/lib/record-validator.ts b/frontend/src/app/tools/drill-simulator/lib/record-validator.ts index b98b18007..65921da05 100644 --- a/frontend/src/app/tools/drill-simulator/lib/record-validator.ts +++ b/frontend/src/app/tools/drill-simulator/lib/record-validator.ts @@ -909,7 +909,6 @@ export const CSV_TEMPLATE_HEADERS = export function generateCsvTemplate(): string { return `${CSV_TEMPLATE_HEADERS}\n`; } -} /** * Design-system colour helpers. From 39e11e6926371b396d3ce25d6e0eea258e0f8cc9 Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Sat, 24 Oct 2026 20:38:58 -0700 Subject: [PATCH 4/5] fix(drill-simulator): restore Load Sample Data and generateSampleCSV from main Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> --- .../components/DrillSimulatorClient.tsx | 16 ++++- .../drill-simulator/lib/record-validator.ts | 68 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/tools/drill-simulator/components/DrillSimulatorClient.tsx b/frontend/src/app/tools/drill-simulator/components/DrillSimulatorClient.tsx index 8943b4b85..af62762f8 100644 --- a/frontend/src/app/tools/drill-simulator/components/DrillSimulatorClient.tsx +++ b/frontend/src/app/tools/drill-simulator/components/DrillSimulatorClient.tsx @@ -15,7 +15,7 @@ import { } from 'lucide-react'; import { toast } from '@/components/ui/use-toast'; import { - parseCSV, validateRecords, generateCsvTemplate, getScoreColor, + parseCSV, validateRecords, generateSampleCSV, generateCsvTemplate, getScoreColor, getSeverityColor, getSeverityBadgeColor, type ValidationResult, } from '../lib/record-validator'; @@ -182,6 +182,13 @@ export function DrillSimulatorClient() { } }; + const handleLoadSampleData = async () => { + const csv = generateSampleCSV(); + const blob = new Blob([csv], { type: 'text/csv' }); + const file = new File([blob], 'sample-records.csv', { type: 'text/csv' }); + setState({ ...state, uploadedFile: file }); + }; + const handleDownloadTemplate = () => { const csv = generateCsvTemplate(); const blob = new Blob([csv], { type: 'text/csv' }); @@ -417,6 +424,13 @@ STATUS: ${ > Analyze My Records +
); diff --git a/frontend/src/app/tools/drill-simulator/lib/record-validator.ts b/frontend/src/app/tools/drill-simulator/lib/record-validator.ts index 65921da05..31658811c 100644 --- a/frontend/src/app/tools/drill-simulator/lib/record-validator.ts +++ b/frontend/src/app/tools/drill-simulator/lib/record-validator.ts @@ -910,6 +910,74 @@ export function generateCsvTemplate(): string { return `${CSV_TEMPLATE_HEADERS}\n`; } +export function generateSampleCSV(): string { + const lines = [ + 'Event Type,Product,Lot Code,GTIN,Facility GLN,Source GLN,Destination GLN,Timestamp,Quantity,Unit of Measure,Reference Document', + ]; + + // Harvest/Create + lines.push( + 'Create,Romaine Lettuce: Head,LOT-2024-001,0123456789012,1234567890128,1234567890128,3456789012345,2024-01-15T08:30:00Z,500,cases,FARM-001-DOC' + ); + + // First transformation + lines.push( + 'Transform,Romaine Lettuce: Packaged,LOT-2024-001,0123456789029,3456789012345,,3456789012345,2024-01-15T12:00:00Z,480,cases,PKG-2024-001' + ); + + // Ship from packer + lines.push( + 'Ship,Romaine Lettuce: Packaged,LOT-2024-001,0123456789029,3456789012345,3456789012345,5678901234567,2024-01-16T14:00:00Z,480,cases,BOL-2024-001' + ); + + // Receive at distributor + lines.push( + 'Receive,Romaine Lettuce: Packaged,LOT-2024-001,0123456789029,5678901234567,3456789012345,5678901234567,2024-01-17T08:30:00Z,475,cases,RCV-2024-001' + ); + + // Ship from distributor + lines.push( + 'Ship,Romaine Lettuce: Packaged,LOT-2024-001,0123456789029,5678901234567,5678901234567,7890123456789,2024-01-18T10:00:00Z,450,cases,BOL-2024-002' + ); + + // Receive at retailer + lines.push( + 'Receive,Romaine Lettuce: Packaged,LOT-2024-001,0123456789029,7890123456789,5678901234567,7890123456789,2024-01-18T15:30:00Z,450,cases,RCV-2024-002' + ); + + // Different lot - intentionally missing some KDEs + lines.push('Create,Spinach: Bulk,LOT-2024-002,,1234567890135,1234567890135,3456789012352,2024-01-15T07:00:00Z,800,cases,'); + + lines.push('Ship,Spinach: Bulk,LOT-2024-002,,3456789012352,3456789012352,5678901234574,2024-01-16T16:00:00Z,800,,BOL-2024-003'); + + lines.push('Receive,Spinach: Bulk,LOT-2024-002,,5678901234574,3456789012352,5678901234574,2024-01-17T09:00:00Z,800,cases,'); + + // Third lot with time gap + lines.push( + 'Create,Tomatoes: Vine Ripe,LOT-2024-003,0123456789036,1234567890142,1234567890142,3456789012359,2024-01-10T06:00:00Z,1200,cases,FARM-003' + ); + + lines.push( + 'Ship,Tomatoes: Vine Ripe,LOT-2024-003,0123456789036,3456789012359,3456789012359,5678901234581,2024-01-10T14:00:00Z,1200,cases,BOL-2024-004' + ); + + // Large gap here - 3 days + lines.push( + 'Receive,Tomatoes: Vine Ripe,LOT-2024-003,0123456789036,5678901234581,3456789012359,5678901234581,2024-01-13T10:30:00Z,1150,cases,RCV-2024-003' + ); + + // Fourth lot with unmatched ship (no receive) + lines.push( + 'Create,Cucumbers,LOT-2024-004,0123456789043,1234567890159,1234567890159,3456789012366,2024-01-14T08:00:00Z,600,cases,FARM-004' + ); + + lines.push( + 'Ship,Cucumbers,LOT-2024-004,0123456789043,3456789012366,3456789012366,5678901234598,2024-01-15T12:00:00Z,600,cases,BOL-2024-005' + ); + + return lines.join('\n'); +} + /** * Design-system colour helpers. * From e053dd2f38c806a5c3f4d536bf446f947b7a502e Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Sat, 24 Oct 2026 20:49:48 -0700 Subject: [PATCH 5/5] fix(drill-simulator): say records analyzed in self-check note Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> --- .../tools/drill-simulator/components/DrillSimulatorClient.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/tools/drill-simulator/components/DrillSimulatorClient.tsx b/frontend/src/app/tools/drill-simulator/components/DrillSimulatorClient.tsx index af62762f8..1b8987f29 100644 --- a/frontend/src/app/tools/drill-simulator/components/DrillSimulatorClient.tsx +++ b/frontend/src/app/tools/drill-simulator/components/DrillSimulatorClient.tsx @@ -523,7 +523,7 @@ STATUS: ${
{gapSummary}

- Self-check on the records you uploaded. Not a compliance verdict. + Self-check on the records analyzed. Not a compliance verdict.

@@ -601,7 +601,7 @@ STATUS: ${ {gapSummary}

- Self-check on the records you uploaded. Not a compliance verdict. + Self-check on the records analyzed. Not a compliance verdict.