diff --git a/.github/workflows/qa-pipeline.yml b/.github/workflows/qa-pipeline.yml index 5c9f9b264..0aafaf60a 100644 --- a/.github/workflows/qa-pipeline.yml +++ b/.github/workflows/qa-pipeline.yml @@ -43,9 +43,6 @@ jobs: # checks use only Node built-ins; keep the existing job/check name for # consumers without repeating the dependency install and unit suite. - - name: FSMA 204 compliance checks - run: node qa/fsma-lite-check.js - - name: FSMA 204 CTE count copy check (7, never 6) run: node qa/fsma-cte-count-check.js @@ -70,15 +67,9 @@ jobs: with: node-version-file: .nvmrc - - name: Full ingestion-to-export flow - run: node qa/full-flow.js - - name: Bad data rejection run: node qa/bad-data.js - - name: Export artifact validation - run: node qa/export-validate.js - # ── Stage 3: AI Analysis ─────────────────────────────── # Advisory-only: copy, pricing, and credibility warnings for human # follow-up. Its result is intentionally NOT in decision.needs, so a diff --git a/.gitignore b/.gitignore index 5a28737a2..dad73ecbb 100644 --- a/.gitignore +++ b/.gitignore @@ -85,12 +85,11 @@ secrets.json # Artifacts that should never be tracked *.zip -# ...except the public proof/sample bundles the frontend serves. These are +# ...except the public proof bundles the frontend serves. These are # intentionally tracked fixtures (the audit-bundle pair backs the public # /api/v1/proof/verify demo, including the deliberately tampered one), and # the bare *.zip rule above was silently covering them (#2873). !frontend/public/proof/*.zip -!frontend/public/samples/*.zip login_resp.json frontend/build_log.txt frontend/build_output.txt diff --git a/.secrets.baseline b/.secrets.baseline index 38bb04d34..6638ef571 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -2162,348 +2162,6 @@ "is_secret": false } ], - "frontend/public/samples/sample_chain_verification.json": [ - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "b03e7ca89182d9affd88ded6fb9eb6347417e446", - "is_verified": false, - "line_number": 9 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "2818c1177db08a2b179769e8e23322ed6c57c213", - "is_verified": false, - "line_number": 15 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "b4aa62da0e2dfe0d2c5151c3531f501d677992dc", - "is_verified": false, - "line_number": 23 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "a7c47fb08160ee110b058fa6479061556100c387", - "is_verified": false, - "line_number": 24 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "7500610854ad7efbe52b67944cd235541483200c", - "is_verified": false, - "line_number": 31 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "14c91ff72e5d5dd4ca4903849461da13e3def2ec", - "is_verified": false, - "line_number": 32 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "de2b003be06f5e88de30b4af259792ee4f75ecd2", - "is_verified": false, - "line_number": 39 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "7a01d7e8628467837b2d0d7f873ac0ae405c115f", - "is_verified": false, - "line_number": 40 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "2652722375d0b0b824542fb98f65a60835b6d82a", - "is_verified": false, - "line_number": 47 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "1e91cb8ed2934ea1e361195c24027cc824f0ebd0", - "is_verified": false, - "line_number": 48 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "77541cd6fce0590784ef7fa6674b83d64e0e7953", - "is_verified": false, - "line_number": 55 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "374b86c55dafbdc10431415d1280d4e304dc62e0", - "is_verified": false, - "line_number": 56 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "b6817e2e5b876431166d2972c464c3427eaa2710", - "is_verified": false, - "line_number": 63 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "731918cc22bb6b820fb666c07aac661b6b3ac0b3", - "is_verified": false, - "line_number": 64 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "3d006e5b523512a8960af7c2057f1fb68e039b18", - "is_verified": false, - "line_number": 71 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "40f81f7c5d609d0dbffde6a6e9f1b71ece1dac19", - "is_verified": false, - "line_number": 72 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "6f8ad6f2c0c1befc64ff3f8870aeba5589374b4d", - "is_verified": false, - "line_number": 79 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "32e8a8bce5e7f39ea20a8c5114846c9ec681b23d", - "is_verified": false, - "line_number": 80 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "0a4988099f362f887180bf5fbac2ae9c920e260b", - "is_verified": false, - "line_number": 87 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "00a2b4ca2c5dde1a07946db9bf7bbf77ddb46302", - "is_verified": false, - "line_number": 88 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "c62bb43176a3891caee998b376e7a42a6c15b6de", - "is_verified": false, - "line_number": 95 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "21764570e14232c0cf7661adae27ff40f3f87b86", - "is_verified": false, - "line_number": 96 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_chain_verification.json", - "hashed_secret": "aa48fdb42b1a1165bbdc878ccd1f6c4574d02cbd", - "is_verified": false, - "line_number": 103 - } - ], - "frontend/public/samples/sample_epcis_2.0.json": [ - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "2818c1177db08a2b179769e8e23322ed6c57c213", - "is_verified": false, - "line_number": 24 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "b4aa62da0e2dfe0d2c5151c3531f501d677992dc", - "is_verified": false, - "line_number": 43 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "a7c47fb08160ee110b058fa6479061556100c387", - "is_verified": false, - "line_number": 44 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "7500610854ad7efbe52b67944cd235541483200c", - "is_verified": false, - "line_number": 62 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "14c91ff72e5d5dd4ca4903849461da13e3def2ec", - "is_verified": false, - "line_number": 63 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "de2b003be06f5e88de30b4af259792ee4f75ecd2", - "is_verified": false, - "line_number": 81 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "7a01d7e8628467837b2d0d7f873ac0ae405c115f", - "is_verified": false, - "line_number": 82 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "2652722375d0b0b824542fb98f65a60835b6d82a", - "is_verified": false, - "line_number": 100 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "1e91cb8ed2934ea1e361195c24027cc824f0ebd0", - "is_verified": false, - "line_number": 101 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "77541cd6fce0590784ef7fa6674b83d64e0e7953", - "is_verified": false, - "line_number": 119 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "374b86c55dafbdc10431415d1280d4e304dc62e0", - "is_verified": false, - "line_number": 120 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "b6817e2e5b876431166d2972c464c3427eaa2710", - "is_verified": false, - "line_number": 138 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "731918cc22bb6b820fb666c07aac661b6b3ac0b3", - "is_verified": false, - "line_number": 139 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "3d006e5b523512a8960af7c2057f1fb68e039b18", - "is_verified": false, - "line_number": 157 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "40f81f7c5d609d0dbffde6a6e9f1b71ece1dac19", - "is_verified": false, - "line_number": 158 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "6f8ad6f2c0c1befc64ff3f8870aeba5589374b4d", - "is_verified": false, - "line_number": 176 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "32e8a8bce5e7f39ea20a8c5114846c9ec681b23d", - "is_verified": false, - "line_number": 177 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "0a4988099f362f887180bf5fbac2ae9c920e260b", - "is_verified": false, - "line_number": 195 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "00a2b4ca2c5dde1a07946db9bf7bbf77ddb46302", - "is_verified": false, - "line_number": 196 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "c62bb43176a3891caee998b376e7a42a6c15b6de", - "is_verified": false, - "line_number": 214 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "21764570e14232c0cf7661adae27ff40f3f87b86", - "is_verified": false, - "line_number": 215 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "aa48fdb42b1a1165bbdc878ccd1f6c4574d02cbd", - "is_verified": false, - "line_number": 233 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_epcis_2.0.json", - "hashed_secret": "b03e7ca89182d9affd88ded6fb9eb6347417e446", - "is_verified": false, - "line_number": 234 - } - ], - "frontend/public/samples/sample_manifest.json": [ - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_manifest.json", - "hashed_secret": "b03e7ca89182d9affd88ded6fb9eb6347417e446", - "is_verified": false, - "line_number": 27 - }, - { - "type": "Hex High Entropy String", - "filename": "frontend/public/samples/sample_manifest.json", - "hashed_secret": "c26716d55190fc65601e165b63b530be01e2ac6d", - "is_verified": false, - "line_number": 30 - } - ], "frontend/src/__tests__/api/admin.test.ts": [ { "type": "Secret Keyword", diff --git a/docs/PRODUCT_DESCRIPTION.md b/docs/PRODUCT_DESCRIPTION.md index 397f94bf7..d1f7df63d 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. +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. ## Capability and availability matrix diff --git a/docs/archive/DEPLOY_CHECKLIST_SECURITY_HARDENING.md b/docs/archive/DEPLOY_CHECKLIST_SECURITY_HARDENING.md index 841d67dc1..1f8be5d6d 100644 --- a/docs/archive/DEPLOY_CHECKLIST_SECURITY_HARDENING.md +++ b/docs/archive/DEPLOY_CHECKLIST_SECURITY_HARDENING.md @@ -270,13 +270,10 @@ kubectl rollout status deployment/ -n regengine - [ ] **5.1** Run full QA pipeline against production ```bash # Stage 1: Fast Gate - node qa/fsma-lite-check.js --env=production node qa/tenant-test.js --env=production # Stage 2: System Simulation - node qa/full-flow.js --env=production node qa/bad-data.js --env=production - node qa/export-validate.js --env=production # Stage 3: AI Analysis node qa/ai-analysis.js --env=production diff --git a/docs/engineering/TRACEABILITY_FILE_INTAKE.md b/docs/engineering/TRACEABILITY_FILE_INTAKE.md index 922f80018..26db7d97c 100644 --- a/docs/engineering/TRACEABILITY_FILE_INTAKE.md +++ b/docs/engineering/TRACEABILITY_FILE_INTAKE.md @@ -93,8 +93,7 @@ tab does not pass `mode`. Validate-only calls `POST /api/v1/epcis/validate` once per JSON event and does not ingest. XML has no validate-only path; the tab tells the operator -to ingest the XML file directly. A sample document is linked from the tab -at `/samples/sample_epcis_2.0.json`. +to ingest the XML file directly. Empty documents, empty arrays, and non-JSON text fail in the browser. No request is sent. diff --git a/frontend/public/samples/regengine_sample_export.zip b/frontend/public/samples/regengine_sample_export.zip deleted file mode 100644 index 50b2936c8..000000000 Binary files a/frontend/public/samples/regengine_sample_export.zip and /dev/null differ diff --git a/frontend/public/samples/sample_chain_verification.json b/frontend/public/samples/sample_chain_verification.json deleted file mode 100644 index 2585fb022..000000000 --- a/frontend/public/samples/sample_chain_verification.json +++ /dev/null @@ -1,109 +0,0 @@ -{ - "verification_report": { - "generated_at": "2026-03-19T05:30:08.007979Z", - "lot_code": "ROM-2026-0312", - "product": "Romaine Lettuce Hearts 12ct", - "chain_length": 12, - "chain_valid": true, - "algorithm": "SHA-256", - "final_merkle_hash": "c16ea4a1aa3b0f127e42d72d4174306eb6474cc02de931fefe3693bd7c1f734d", - "records": [ - { - "sequence": 1, - "cte_type": "harvesting", - "facility": "Green Valley Farm", - "payload_sha256": "de23337e6ffd8a9fbdd6e973e4c342aa3a2589c1356b5ec65026e283127d183b", - "merkle_hash": "de23337e6ffd8a9fbdd6e973e4c342aa3a2589c1356b5ec65026e283127d183b", - "valid": true - }, - { - "sequence": 2, - "cte_type": "cooling", - "facility": "Valley Cooler Co-op", - "payload_sha256": "8409cca746e2caa7dde1eee70d147f516b77d2f3f45834f80d265da35e9973a9", - "merkle_hash": "95203a7bb202eaa8f3b4255dd73857401d36616e3af41757b0255076974d3f89", - "valid": true - }, - { - "sequence": 3, - "cte_type": "initial_packing", - "facility": "Valley Fresh Packhouse", - "payload_sha256": "d12f7525fa2548c0b7e97441e0839caaa3a7599afa088b31d173d69bf318a618", - "merkle_hash": "910eb11db7a915775b08b7c0340184e513f023e3fd46b2effeafd7eff055b97a", - "valid": true - }, - { - "sequence": 4, - "cte_type": "shipping", - "facility": "Valley Fresh Packhouse", - "payload_sha256": "86727ffc1c38bd1802f56e2b43c5bdd5130cddb8a68f29bced6af8375380285d", - "merkle_hash": "ce4561b848c356ad73bc5ac45e4ffbfa148816e0733e79a8144dc6e3196ef8f8", - "valid": true - }, - { - "sequence": 5, - "cte_type": "receiving", - "facility": "Metro Distribution Center", - "payload_sha256": "2a77c388476de3f2e4b592a9d6aba04bfea3d279b6a8342db6a6acd9515cbc8e", - "merkle_hash": "4aab599f4c15838a3ea347a469bf037ecb9e313dd3f8c0c99658759f9a225bec", - "valid": true - }, - { - "sequence": 6, - "cte_type": "shipping", - "facility": "Metro Distribution Center", - "payload_sha256": "fad3e1ea14b8749bacc23c2ddd68395ac240878a29fe70b972e3aaee7e766d66", - "merkle_hash": "79ef6f14b2c5cb02366596de5b1e8db05b3f18b34186a87c1f10179687105ce0", - "valid": true - }, - { - "sequence": 7, - "cte_type": "receiving", - "facility": "FreshWay Cross-Dock", - "payload_sha256": "bd4f8b4579664c1753e60a5dc19770d93820638ad323404ae6acefc46c38e6e6", - "merkle_hash": "fb15f5401a50bf7f76561154f0bd24a98c57e24e0c2d9b429cdad3deb9d3ad94", - "valid": true - }, - { - "sequence": 8, - "cte_type": "shipping", - "facility": "FreshWay Cross-Dock", - "payload_sha256": "cbc92d3377df758d82fab198e118601c5b5bb650fc881f7959dca5744f823d66", - "merkle_hash": "b54762dfc7629eed1f43462b9c45fbb30531ebae8952b9d8ad3b55811640847a", - "valid": true - }, - { - "sequence": 9, - "cte_type": "receiving", - "facility": "Kroger DC #7218", - "payload_sha256": "04a23f1f705bb0e9d222075c8930cd37643da24e6756e0c37f30ade2060e7cd4", - "merkle_hash": "9fa0cd7f955e748f8b9f9c3d94496dfd3ab05ebe52c8e36dcb704c36206ae402", - "valid": true - }, - { - "sequence": 10, - "cte_type": "transformation", - "facility": "Kroger DC #7218", - "payload_sha256": "b638aa6a9c796b723bcf61fcdd8262d52c1c4c488cafe73ba526a87e8c624689", - "merkle_hash": "0b933c9cf3a6fca61cfc2a48198c1499090abed1bbb3fbf02dc4d5400d636479", - "valid": true - }, - { - "sequence": 11, - "cte_type": "shipping", - "facility": "Kroger DC #7218", - "payload_sha256": "7b60eff1ba9fc45d81622feb44311289232decb4f35cde0e0943ee226db0a908", - "merkle_hash": "2c9d3b80223aa7a2e9069982ca6f0b014cf8a9a06b4452da45ab3bfa6d0efb72", - "valid": true - }, - { - "sequence": 12, - "cte_type": "receiving", - "facility": "Kroger Store #3301", - "payload_sha256": "a24e62d8b6a3667cf4006762d289048121c549f7770a5fa9282e793993df4495", - "merkle_hash": "c16ea4a1aa3b0f127e42d72d4174306eb6474cc02de931fefe3693bd7c1f734d", - "valid": true - } - ] - } -} \ No newline at end of file diff --git a/frontend/public/samples/sample_epcis_2.0.json b/frontend/public/samples/sample_epcis_2.0.json deleted file mode 100644 index 17f8da7fd..000000000 --- a/frontend/public/samples/sample_epcis_2.0.json +++ /dev/null @@ -1,246 +0,0 @@ -{ - "@context": [ - "https://ref.gs1.org/standards/epcis/2.0.0/epcis-context.jsonld" - ], - "type": "EPCISDocument", - "schemaVersion": "2.0", - "creationDate": "2026-03-19T05:30:08.005388Z", - "epcisBody": { - "eventList": [ - { - "type": "ObjectEvent", - "eventTime": "2026-03-01T06:00:00Z", - "eventTimeZoneOffset": "-08:00", - "action": "OBSERVE", - "bizStep": "urn:epcglobal:cbv:bizstep:harvesting", - "bizLocation": { - "id": "urn:epc:id:sgln:0614141000012" - }, - "extension": { - "fsma:traceabilityLotCode": "ROM-2026-0312", - "fsma:productDescription": "Romaine Lettuce Hearts 12ct", - "fsma:quantity": 500, - "fsma:unitOfMeasure": "cases", - "regengine:payloadSHA256": "de23337e6ffd8a9fbdd6e973e4c342aa3a2589c1356b5ec65026e283127d183b", - "regengine:merkleHash": "de23337e6ffd8a9fbdd6e973e4c342aa3a2589c1356b5ec65026e283127d183b", - "regengine:sequenceNumber": 1 - } - }, - { - "type": "ObjectEvent", - "eventTime": "2026-03-01T08:00:00Z", - "eventTimeZoneOffset": "-08:00", - "action": "OBSERVE", - "bizStep": "urn:epcglobal:cbv:bizstep:cooling", - "bizLocation": { - "id": "urn:epc:id:sgln:0614141000029" - }, - "extension": { - "fsma:traceabilityLotCode": "ROM-2026-0312", - "fsma:productDescription": "Romaine Lettuce Hearts 12ct", - "fsma:quantity": 500, - "fsma:unitOfMeasure": "cases", - "regengine:payloadSHA256": "8409cca746e2caa7dde1eee70d147f516b77d2f3f45834f80d265da35e9973a9", - "regengine:merkleHash": "95203a7bb202eaa8f3b4255dd73857401d36616e3af41757b0255076974d3f89", - "regengine:sequenceNumber": 2 - } - }, - { - "type": "ObjectEvent", - "eventTime": "2026-03-01T12:00:00Z", - "eventTimeZoneOffset": "-08:00", - "action": "OBSERVE", - "bizStep": "urn:epcglobal:cbv:bizstep:initial_packing", - "bizLocation": { - "id": "urn:epc:id:sgln:0614141000036" - }, - "extension": { - "fsma:traceabilityLotCode": "ROM-2026-0312", - "fsma:productDescription": "Romaine Lettuce Hearts 12ct", - "fsma:quantity": 480, - "fsma:unitOfMeasure": "cases", - "regengine:payloadSHA256": "d12f7525fa2548c0b7e97441e0839caaa3a7599afa088b31d173d69bf318a618", - "regengine:merkleHash": "910eb11db7a915775b08b7c0340184e513f023e3fd46b2effeafd7eff055b97a", - "regengine:sequenceNumber": 3 - } - }, - { - "type": "ObjectEvent", - "eventTime": "2026-03-01T14:00:00Z", - "eventTimeZoneOffset": "-08:00", - "action": "OBSERVE", - "bizStep": "urn:epcglobal:cbv:bizstep:shipping", - "bizLocation": { - "id": "urn:epc:id:sgln:0614141000036" - }, - "extension": { - "fsma:traceabilityLotCode": "ROM-2026-0312", - "fsma:productDescription": "Romaine Lettuce Hearts 12ct", - "fsma:quantity": 480, - "fsma:unitOfMeasure": "cases", - "regengine:payloadSHA256": "86727ffc1c38bd1802f56e2b43c5bdd5130cddb8a68f29bced6af8375380285d", - "regengine:merkleHash": "ce4561b848c356ad73bc5ac45e4ffbfa148816e0733e79a8144dc6e3196ef8f8", - "regengine:sequenceNumber": 4 - } - }, - { - "type": "ObjectEvent", - "eventTime": "2026-03-02T00:00:00Z", - "eventTimeZoneOffset": "-08:00", - "action": "OBSERVE", - "bizStep": "urn:epcglobal:cbv:bizstep:receiving", - "bizLocation": { - "id": "urn:epc:id:sgln:0614141000043" - }, - "extension": { - "fsma:traceabilityLotCode": "ROM-2026-0312", - "fsma:productDescription": "Romaine Lettuce Hearts 12ct", - "fsma:quantity": 460, - "fsma:unitOfMeasure": "cases", - "regengine:payloadSHA256": "2a77c388476de3f2e4b592a9d6aba04bfea3d279b6a8342db6a6acd9515cbc8e", - "regengine:merkleHash": "4aab599f4c15838a3ea347a469bf037ecb9e313dd3f8c0c99658759f9a225bec", - "regengine:sequenceNumber": 5 - } - }, - { - "type": "ObjectEvent", - "eventTime": "2026-03-02T02:00:00Z", - "eventTimeZoneOffset": "-08:00", - "action": "OBSERVE", - "bizStep": "urn:epcglobal:cbv:bizstep:shipping", - "bizLocation": { - "id": "urn:epc:id:sgln:0614141000043" - }, - "extension": { - "fsma:traceabilityLotCode": "ROM-2026-0312", - "fsma:productDescription": "Romaine Lettuce Hearts 12ct", - "fsma:quantity": 460, - "fsma:unitOfMeasure": "cases", - "regengine:payloadSHA256": "fad3e1ea14b8749bacc23c2ddd68395ac240878a29fe70b972e3aaee7e766d66", - "regengine:merkleHash": "79ef6f14b2c5cb02366596de5b1e8db05b3f18b34186a87c1f10179687105ce0", - "regengine:sequenceNumber": 6 - } - }, - { - "type": "ObjectEvent", - "eventTime": "2026-03-02T06:00:00Z", - "eventTimeZoneOffset": "-08:00", - "action": "OBSERVE", - "bizStep": "urn:epcglobal:cbv:bizstep:receiving", - "bizLocation": { - "id": "urn:epc:id:sgln:0614141000005" - }, - "extension": { - "fsma:traceabilityLotCode": "ROM-2026-0312", - "fsma:productDescription": "Romaine Lettuce Hearts 12ct", - "fsma:quantity": 440, - "fsma:unitOfMeasure": "cases", - "regengine:payloadSHA256": "bd4f8b4579664c1753e60a5dc19770d93820638ad323404ae6acefc46c38e6e6", - "regengine:merkleHash": "fb15f5401a50bf7f76561154f0bd24a98c57e24e0c2d9b429cdad3deb9d3ad94", - "regengine:sequenceNumber": 7 - } - }, - { - "type": "ObjectEvent", - "eventTime": "2026-03-02T08:00:00Z", - "eventTimeZoneOffset": "-08:00", - "action": "OBSERVE", - "bizStep": "urn:epcglobal:cbv:bizstep:shipping", - "bizLocation": { - "id": "urn:epc:id:sgln:0614141000005" - }, - "extension": { - "fsma:traceabilityLotCode": "ROM-2026-0312", - "fsma:productDescription": "Romaine Lettuce Hearts 12ct", - "fsma:quantity": 440, - "fsma:unitOfMeasure": "cases", - "regengine:payloadSHA256": "cbc92d3377df758d82fab198e118601c5b5bb650fc881f7959dca5744f823d66", - "regengine:merkleHash": "b54762dfc7629eed1f43462b9c45fbb30531ebae8952b9d8ad3b55811640847a", - "regengine:sequenceNumber": 8 - } - }, - { - "type": "ObjectEvent", - "eventTime": "2026-03-02T14:00:00Z", - "eventTimeZoneOffset": "-08:00", - "action": "OBSERVE", - "bizStep": "urn:epcglobal:cbv:bizstep:receiving", - "bizLocation": { - "id": "urn:epc:id:sgln:0614141000050" - }, - "extension": { - "fsma:traceabilityLotCode": "ROM-2026-0312", - "fsma:productDescription": "Romaine Lettuce Hearts 12ct", - "fsma:quantity": 420, - "fsma:unitOfMeasure": "cases", - "regengine:payloadSHA256": "04a23f1f705bb0e9d222075c8930cd37643da24e6756e0c37f30ade2060e7cd4", - "regengine:merkleHash": "9fa0cd7f955e748f8b9f9c3d94496dfd3ab05ebe52c8e36dcb704c36206ae402", - "regengine:sequenceNumber": 9 - } - }, - { - "type": "ObjectEvent", - "eventTime": "2026-03-02T18:00:00Z", - "eventTimeZoneOffset": "-08:00", - "action": "OBSERVE", - "bizStep": "urn:epcglobal:cbv:bizstep:transformation", - "bizLocation": { - "id": "urn:epc:id:sgln:0614141000050" - }, - "extension": { - "fsma:traceabilityLotCode": "ROM-2026-0312", - "fsma:productDescription": "Romaine Lettuce Hearts 12ct", - "fsma:quantity": 400, - "fsma:unitOfMeasure": "cases", - "regengine:payloadSHA256": "b638aa6a9c796b723bcf61fcdd8262d52c1c4c488cafe73ba526a87e8c624689", - "regengine:merkleHash": "0b933c9cf3a6fca61cfc2a48198c1499090abed1bbb3fbf02dc4d5400d636479", - "regengine:sequenceNumber": 10 - } - }, - { - "type": "ObjectEvent", - "eventTime": "2026-03-03T04:00:00Z", - "eventTimeZoneOffset": "-08:00", - "action": "OBSERVE", - "bizStep": "urn:epcglobal:cbv:bizstep:shipping", - "bizLocation": { - "id": "urn:epc:id:sgln:0614141000050" - }, - "extension": { - "fsma:traceabilityLotCode": "ROM-2026-0312", - "fsma:productDescription": "Romaine Lettuce Hearts 12ct", - "fsma:quantity": 400, - "fsma:unitOfMeasure": "cases", - "regengine:payloadSHA256": "7b60eff1ba9fc45d81622feb44311289232decb4f35cde0e0943ee226db0a908", - "regengine:merkleHash": "2c9d3b80223aa7a2e9069982ca6f0b014cf8a9a06b4452da45ab3bfa6d0efb72", - "regengine:sequenceNumber": 11 - } - }, - { - "type": "ObjectEvent", - "eventTime": "2026-03-03T08:00:00Z", - "eventTimeZoneOffset": "-08:00", - "action": "OBSERVE", - "bizStep": "urn:epcglobal:cbv:bizstep:receiving", - "bizLocation": { - "id": "urn:epc:id:sgln:0614141000067" - }, - "extension": { - "fsma:traceabilityLotCode": "ROM-2026-0312", - "fsma:productDescription": "Romaine Lettuce Hearts 12ct", - "fsma:quantity": 380, - "fsma:unitOfMeasure": "cases", - "regengine:payloadSHA256": "a24e62d8b6a3667cf4006762d289048121c549f7770a5fa9282e793993df4495", - "regengine:merkleHash": "c16ea4a1aa3b0f127e42d72d4174306eb6474cc02de931fefe3693bd7c1f734d", - "regengine:sequenceNumber": 12 - } - } - ] - }, - "regengine:integrity": { - "chainLength": 12, - "finalMerkleHash": "c16ea4a1aa3b0f127e42d72d4174306eb6474cc02de931fefe3693bd7c1f734d", - "algorithm": "SHA-256", - "generatedAt": "2026-03-19T05:30:08.005388Z" - } -} diff --git a/frontend/public/samples/sample_fda_export.csv b/frontend/public/samples/sample_fda_export.csv deleted file mode 100644 index c85fc106c..000000000 --- a/frontend/public/samples/sample_fda_export.csv +++ /dev/null @@ -1,13 +0,0 @@ -Traceability Lot Code,CTE Type,Event Time,Product Description,Product Code (GTIN),Facility Name,Facility GLN,Location,Quantity,Unit,Ship To,Ship To GLN,Carrier,Payload SHA-256,Merkle Hash,Sequence # -ROM-2026-0312,harvesting,2026-03-01T06:00:00Z,Romaine Lettuce Hearts 12ct,00614141000012,Green Valley Farm,0614141000012,"Salinas, CA",500,cases,,,,de23337e6ffd8a9fbdd6e973e4c342aa3a2589c1356b5ec65026e283127d183b,de23337e6ffd8a9fbdd6e973e4c342aa3a2589c1356b5ec65026e283127d183b,1 -ROM-2026-0312,cooling,2026-03-01T08:00:00Z,Romaine Lettuce Hearts 12ct,00614141000012,Valley Cooler Co-op,0614141000029,"Salinas, CA",500,cases,,,,8409cca746e2caa7dde1eee70d147f516b77d2f3f45834f80d265da35e9973a9,95203a7bb202eaa8f3b4255dd73857401d36616e3af41757b0255076974d3f89,2 -ROM-2026-0312,initial_packing,2026-03-01T12:00:00Z,Romaine Lettuce Hearts 12ct,00614141000012,Valley Fresh Packhouse,0614141000036,"Salinas, CA",480,cases,,,,d12f7525fa2548c0b7e97441e0839caaa3a7599afa088b31d173d69bf318a618,910eb11db7a915775b08b7c0340184e513f023e3fd46b2effeafd7eff055b97a,3 -ROM-2026-0312,shipping,2026-03-01T14:00:00Z,Romaine Lettuce Hearts 12ct,00614141000012,Valley Fresh Packhouse,0614141000036,"Salinas, CA",480,cases,Metro Distribution Center,0614141000043,Cold Express Logistics,86727ffc1c38bd1802f56e2b43c5bdd5130cddb8a68f29bced6af8375380285d,ce4561b848c356ad73bc5ac45e4ffbfa148816e0733e79a8144dc6e3196ef8f8,4 -ROM-2026-0312,receiving,2026-03-02T00:00:00Z,Romaine Lettuce Hearts 12ct,00614141000012,Metro Distribution Center,0614141000043,"Los Angeles, CA",460,cases,,,,2a77c388476de3f2e4b592a9d6aba04bfea3d279b6a8342db6a6acd9515cbc8e,4aab599f4c15838a3ea347a469bf037ecb9e313dd3f8c0c99658759f9a225bec,5 -ROM-2026-0312,shipping,2026-03-02T02:00:00Z,Romaine Lettuce Hearts 12ct,00614141000012,Metro Distribution Center,0614141000043,"Los Angeles, CA",460,cases,FreshWay Cross-Dock,0614141000005,FreshLine Transport,fad3e1ea14b8749bacc23c2ddd68395ac240878a29fe70b972e3aaee7e766d66,79ef6f14b2c5cb02366596de5b1e8db05b3f18b34186a87c1f10179687105ce0,6 -ROM-2026-0312,receiving,2026-03-02T06:00:00Z,Romaine Lettuce Hearts 12ct,00614141000012,FreshWay Cross-Dock,0614141000005,"Riverside, CA",440,cases,,,,bd4f8b4579664c1753e60a5dc19770d93820638ad323404ae6acefc46c38e6e6,fb15f5401a50bf7f76561154f0bd24a98c57e24e0c2d9b429cdad3deb9d3ad94,7 -ROM-2026-0312,shipping,2026-03-02T08:00:00Z,Romaine Lettuce Hearts 12ct,00614141000012,FreshWay Cross-Dock,0614141000005,"Riverside, CA",440,cases,Kroger DC #7218,0614141000050,Arctic Haul Inc,cbc92d3377df758d82fab198e118601c5b5bb650fc881f7959dca5744f823d66,b54762dfc7629eed1f43462b9c45fbb30531ebae8952b9d8ad3b55811640847a,8 -ROM-2026-0312,receiving,2026-03-02T14:00:00Z,Romaine Lettuce Hearts 12ct,00614141000012,Kroger DC #7218,0614141000050,"Phoenix, AZ",420,cases,,,,04a23f1f705bb0e9d222075c8930cd37643da24e6756e0c37f30ade2060e7cd4,9fa0cd7f955e748f8b9f9c3d94496dfd3ab05ebe52c8e36dcb704c36206ae402,9 -ROM-2026-0312,transformation,2026-03-02T18:00:00Z,Romaine Lettuce Hearts 12ct,00614141000012,Kroger DC #7218,0614141000050,"Phoenix, AZ",400,cases,,,,b638aa6a9c796b723bcf61fcdd8262d52c1c4c488cafe73ba526a87e8c624689,0b933c9cf3a6fca61cfc2a48198c1499090abed1bbb3fbf02dc4d5400d636479,10 -ROM-2026-0312,shipping,2026-03-03T04:00:00Z,Romaine Lettuce Hearts 12ct,00614141000012,Kroger DC #7218,0614141000050,"Phoenix, AZ",400,cases,Kroger Store #3301,0614141000067,TempGuard Shipping,7b60eff1ba9fc45d81622feb44311289232decb4f35cde0e0943ee226db0a908,2c9d3b80223aa7a2e9069982ca6f0b014cf8a9a06b4452da45ab3bfa6d0efb72,11 -ROM-2026-0312,receiving,2026-03-03T08:00:00Z,Romaine Lettuce Hearts 12ct,00614141000012,Kroger Store #3301,0614141000067,"Scottsdale, AZ",380,cases,,,,a24e62d8b6a3667cf4006762d289048121c549f7770a5fa9282e793993df4495,c16ea4a1aa3b0f127e42d72d4174306eb6474cc02de931fefe3693bd7c1f734d,12 diff --git a/frontend/public/samples/sample_manifest.json b/frontend/public/samples/sample_manifest.json deleted file mode 100644 index e364ce33c..000000000 --- a/frontend/public/samples/sample_manifest.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "manifest": { - "generated_at": "2026-03-19T05:30:08.008871Z", - "generator": "RegEngine v1.0", - "lot_code": "ROM-2026-0312", - "product": "Romaine Lettuce Hearts 12ct", - "files": [ - { - "name": "sample_epcis_2.0.json", - "format": "GS1 EPCIS 2.0 JSON-LD", - "records": 12 - }, - { - "name": "sample_fda_export.csv", - "format": "FDA Sortable Spreadsheet", - "records": 12 - }, - { - "name": "sample_chain_verification.json", - "format": "Chain Verification Report", - "records": 12 - } - ], - "integrity": { - "chain_length": 12, - "chain_valid": true, - "final_merkle_hash": "c16ea4a1aa3b0f127e42d72d4174306eb6474cc02de931fefe3693bd7c1f734d" - }, - "retention_notice": "RegEngine recommends maintaining customer-controlled off-platform archives from week one. This export package is designed to be portable and independently verifiable.", - "manifest_sha256": "96e6531b2c0146a43c9c6ac4e6e60f30604bbbab5be78ed554c2f25831876082" - } -} \ No newline at end of file diff --git a/frontend/public/samples/sandbox_sample_ftl.csv b/frontend/public/samples/sandbox_sample_ftl.csv deleted file mode 100644 index ad98aa051..000000000 --- a/frontend/public/samples/sandbox_sample_ftl.csv +++ /dev/null @@ -1,9 +0,0 @@ -cte_type,traceability_lot_code,product_description,quantity,unit_of_measure,location_name,location_gln,timestamp,ship_from_location,ship_to_location,ship_from_gln,ship_to_gln,reference_document,harvest_date,harvester_business_name,harvest_location_ref,field_name,cooling_date,temperature_celsius,packing_date,ship_date,receive_date,receiving_location,immediate_previous_source,tlc_source_reference,transformation_date,input_tlcs -harvesting,SV-20260115-001,Romaine Lettuce,500,cases,Sunny Valley Farms,1234567890128,2026-01-15T06:30:00Z,,,,,HRV-88421,2026-01-15,Sunny Valley Farms,FARM-SV-04,Block 12 North,,,,,,,,Sunny Valley Farms,, -cooling,SV-20260115-001,Romaine Lettuce,500,cases,Sunny Valley Farms,1234567890128,2026-01-15T09:10:00Z,,,,,COOL-88421,2026-01-15,Sunny Valley Farms,FARM-SV-04,Block 12 North,2026-01-15,1.5,,,,,,Sunny Valley Farms,, -initial_packing,SV-20260115-001,Romaine Lettuce,500,cases,Sunny Valley Farms,1234567890128,2026-01-15T11:45:00Z,,,,,PACK-88421,2026-01-15,Sunny Valley Farms,FARM-SV-04,Block 12 North,2026-01-15,1.5,2026-01-15,,,,,Sunny Valley Farms,, -shipping,SV-20260115-001,Romaine Lettuce,500,cases,Sunny Valley Farms,1234567890128,2026-01-15T14:00:00Z,Sunny Valley Farms,Fresh Cut Co,1234567890128,2345678901234,BOL-44190,2026-01-15,Sunny Valley Farms,FARM-SV-04,Block 12 North,2026-01-15,1.5,2026-01-15,2026-01-15,,,,Sunny Valley Farms,, -receiving,SV-20260115-001,Romaine Lettuce,500,cases,Fresh Cut Co,2345678901234,2026-01-16T08:00:00Z,Sunny Valley Farms,Fresh Cut Co,1234567890128,2345678901234,BOL-44190,,,,,,,,,2026-01-16,Fresh Cut Co Dock 3,Sunny Valley Farms,Sunny Valley Farms,, -transformation,FC-20260116-A,Chopped Romaine,250,cases,Fresh Cut Co,2345678901234,2026-01-16T10:30:00Z,,,,,,,,,,,,,,,,,Fresh Cut Co,2026-01-16, -shipping,FC-20260116-A,Chopped Romaine,250,cases,Fresh Cut Co,2345678901234,2026-01-16T16:20:00Z,Fresh Cut Co,Metro Grocery DC,2345678901234,3456789012341,BOL-44207,,,,,,,,2026-01-16,,,,,, -receiving,FC-20260116-A,Chopped Romaine,250,cases,Metro Grocery DC,3456789012341,2026-01-17T07:15:00Z,Fresh Cut Co,Metro Grocery DC,2345678901234,3456789012341,BOL-44207,,,,,,,,,2026-01-17,Metro Grocery DC Bay 7,Fresh Cut Co,Fresh Cut Co,, diff --git a/frontend/src/__tests__/pages/ingest-epcis.test.tsx b/frontend/src/__tests__/pages/ingest-epcis.test.tsx index e29e05102..04750adbb 100644 --- a/frontend/src/__tests__/pages/ingest-epcis.test.tsx +++ b/frontend/src/__tests__/pages/ingest-epcis.test.tsx @@ -2,7 +2,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import IngestPage from '@/app/ingest/page'; -import sampleEpcisDocument from '../../../public/samples/sample_epcis_2.0.json'; + +// Data-free placeholder: the tests only check passthrough and event count. +const epcisDocument = { epcisBody: { eventList: Array.from({ length: 2 }, () => ({})) } }; const mocks = vi.hoisted(() => ({ ingestURL: vi.fn(), @@ -41,6 +43,72 @@ vi.mock('@/app/ingest/NormalizedDocumentViewer', () => ({ // array vs .xml), so no event field values are needed. Real data only in tests. const SINGLE_EVENT: Record = {}; +// What _validate_epcis (services/ingestion/app/epcis/validation.py) really +// returns for the {} placeholder, in order: each missing required field +// (type, eventTime, action, bizStep), then the unsupported type, then the +// missing lot code. Every {} event is invalid, so the batch path rejects each +// one with HTTPException(400, detail={"errors": [...]}) (persistence.py). +const REQUIRED_FIELDS = ['type', 'eventTime', 'action', 'bizStep']; +const BACKEND_EVENT_ERRORS = [ + ...REQUIRED_FIELDS.map((field) => `Missing required EPCIS field '${field}'`), + 'Unsupported EPCIS event type', + 'Missing traceability lot code (fsma:traceabilityLotCode or cbvmda:lotNumber)', +]; +const BACKEND_VALIDATION_STATUS = 400; +// page.tsx and epcis-upload.ts both join an error list with ': '. +const JOINED_EVENT_ERRORS = BACKEND_EVENT_ERRORS.join(': '); + +// Axios-style rejection, as the api client surfaces any non-2xx response. +function axiosRejection(status: number, data: unknown) { + return Object.assign(new Error(`Request failed with status code ${status}`), { + response: { status, data }, + }); +} + +// An all-failed batch is a 400 rejection in both modes (router.py +// create_epcis_events_batch). The frontend sends no ?mode, so the deployment +// default applies: atomic, per _batch_transactional() in persistence.py. +// +// Atomic: _ingest_batch_events_db_atomic raises HTTPException(400) with this +// detail; each error keeps its index, status_code and detail. +const ATOMIC_BATCH_MESSAGE = + 'One or more events failed pre-DB validation. No events were persisted. ' + + 'Use ?mode=partial to opt in to per-event processing with HTTP 207 semantics.'; +function atomicBatchRejection(events: unknown[]) { + return axiosRejection(BACKEND_VALIDATION_STATUS, { + detail: { + error: 'batch_validation_failed', + mode: 'atomic', + message: ATOMIC_BATCH_MESSAGE, + errors: events.map((_, index) => ({ + index, + status_code: BACKEND_VALIDATION_STATUS, + detail: { errors: BACKEND_EVENT_ERRORS }, + })), + }, + }); +} + +// Partial: the router returns the payload itself (no detail wrapper) with +// status 400. results only holds successes, so it is empty; each failure is +// {index, detail} with no status_code. +function partialBatchRejection(events: unknown[]) { + return axiosRejection(BACKEND_VALIDATION_STATUS, { + total: events.length, + created: 0, + failed: events.length, + results: [], + errors: events.map((_, index) => ({ index, detail: { errors: BACKEND_EVENT_ERRORS } })), + mode: 'partial', + }); +} + +function expectEveryEventRejected(count: number) { + for (let index = 0; index < count; index += 1) { + expect(screen.getByText(`Event ${index + 1}: ${JOINED_EVENT_ERRORS}`)).toBeInTheDocument(); + } +} + const EPCIS_XML = ` @@ -66,7 +134,7 @@ describe('EPCIS 2.0 import on /ingest', () => { mocks.storeApiKey.mockResolvedValue(undefined); }); - it('offers an EPCIS 2.0 tab with a JSON/XML chooser and the sample file link', async () => { + it('offers an EPCIS 2.0 tab with a JSON/XML chooser and no sample file link', async () => { await openEpcisTab(); expect(screen.getByText('CSV uploads skip bad rows. EPCIS files are all or nothing: if any event fails, nothing is saved.')).toBeInTheDocument(); expect(document.getElementById('epcis-file-upload')).toHaveAttribute( @@ -74,24 +142,16 @@ describe('EPCIS 2.0 import on /ingest', () => { '.json,application/json,.xml,application/xml,text/xml', ); expect(screen.getByText('JSON or XML files up to 5MB')).toBeInTheDocument(); - const sampleLink = screen.getByRole('link', { name: /download a sample epcis file/i }); - expect(sampleLink).toHaveAttribute('href', '/samples/sample_epcis_2.0.json'); + expect(screen.queryByRole('link', { name: /download a sample epcis file/i })).not.toBeInTheDocument(); }); - it('sends the sample EPCIS JSON document to the batch endpoint once with all events', async () => { - const eventList = (sampleEpcisDocument as { epcisBody: { eventList: unknown[] } }).epcisBody.eventList; + it('sends an EPCIS JSON document to the batch endpoint once with all events', async () => { + const eventList = (epcisDocument as { epcisBody: { eventList: unknown[] } }).epcisBody.eventList; expect(eventList.length).toBeGreaterThan(1); - mocks.ingestEpcisBatch.mockResolvedValue({ - total: eventList.length, - created: eventList.length, - failed: 0, - results: [], - errors: [], - mode: 'partial', - }); + mocks.ingestEpcisBatch.mockRejectedValue(atomicBatchRejection(eventList)); await openEpcisTab(); - uploadEpcisFile('sample_epcis_2.0.json', JSON.stringify(sampleEpcisDocument), 'application/json'); + uploadEpcisFile('events.json', JSON.stringify(epcisDocument), 'application/json'); await userEvent.click(screen.getByRole('button', { name: /upload & process/i })); expect(mocks.ingestEpcisBatch).toHaveBeenCalledTimes(1); @@ -102,23 +162,18 @@ describe('EPCIS 2.0 import on /ingest', () => { expect(mocks.ingestEpcisEvent).not.toHaveBeenCalled(); expect(mocks.ingestEpcisXml).not.toHaveBeenCalled(); expect(mocks.validateEpcis).not.toHaveBeenCalled(); - expect(await screen.findByText('EPCIS processed')).toBeInTheDocument(); - expect( - screen.getByText(`${eventList.length} of ${eventList.length} events accepted; 0 rejected.`), - ).toBeInTheDocument(); - expect(mocks.refresh).toHaveBeenCalledTimes(1); + // Atomic (default) rejection: the page shows detail.message only. The + // per-event errors carry no msg/message key, so detailToMessage drops them. + expect(await screen.findByText('Ingestion Failed')).toBeInTheDocument(); + expect(screen.getByText(ATOMIC_BATCH_MESSAGE)).toBeInTheDocument(); + expect(screen.queryByText(/^Event \d+:/)).not.toBeInTheDocument(); + expect(mocks.refresh).not.toHaveBeenCalled(); }); it('sends a bare JSON array of events to the batch endpoint', async () => { const events = [{ ...SINGLE_EVENT }, { ...SINGLE_EVENT }]; - mocks.ingestEpcisBatch.mockResolvedValue({ - total: 2, - created: 1, - failed: 1, - results: [{ index: 1, status_code: 400, detail: 'Missing bizLocation' }], - errors: [{ index: 1, detail: 'Missing bizLocation' }], - mode: 'partial', - }); + const rejection = partialBatchRejection(events); + mocks.ingestEpcisBatch.mockRejectedValue(rejection); await openEpcisTab(); uploadEpcisFile('events.json', JSON.stringify(events), 'application/json'); @@ -129,13 +184,18 @@ describe('EPCIS 2.0 import on /ingest', () => { apiKey: 'cookie-managed', // pragma: allowlist secret events, }); - expect(await screen.findByText('EPCIS partially accepted')).toBeInTheDocument(); - expect(screen.getByText('1 of 2 events accepted; 1 rejected.')).toBeInTheDocument(); - expect(screen.getByText('Event 2: Missing bizLocation')).toBeInTheDocument(); + // Partial rejection: the body has no detail key, so the page falls back + // to the error message and shows no per-event errors. + expect(await screen.findByText('Ingestion Failed')).toBeInTheDocument(); + expect(screen.getByText(rejection.message)).toBeInTheDocument(); + expect(screen.queryByText(/^Event \d+:/)).not.toBeInTheDocument(); + expect(mocks.refresh).not.toHaveBeenCalled(); }); it('sends a single-event JSON to the single-event endpoint', async () => { - mocks.ingestEpcisEvent.mockResolvedValue({ status: 201 }); + mocks.ingestEpcisEvent.mockRejectedValue( + axiosRejection(BACKEND_VALIDATION_STATUS, { detail: { errors: BACKEND_EVENT_ERRORS } }), + ); await openEpcisTab(); uploadEpcisFile('event.json', JSON.stringify(SINGLE_EVENT), 'application/json'); @@ -148,20 +208,16 @@ describe('EPCIS 2.0 import on /ingest', () => { }); expect(mocks.ingestEpcisBatch).not.toHaveBeenCalled(); expect(mocks.ingestEpcisXml).not.toHaveBeenCalled(); - expect(await screen.findByText('EPCIS processed')).toBeInTheDocument(); - expect(screen.getByText('1 of 1 events accepted; 0 rejected.')).toBeInTheDocument(); + expect(await screen.findByText('Ingestion Failed')).toBeInTheDocument(); + expect(screen.getByText(JOINED_EVENT_ERRORS)).toBeInTheDocument(); + expect(mocks.refresh).not.toHaveBeenCalled(); }); it('sends an XML file to the XML endpoint untouched', async () => { - mocks.ingestEpcisXml.mockResolvedValue({ - total: 1, - created: 1, - failed: 0, - results: [], - errors: [], - format: 'EPCIS_XML_2.0', - mode: 'partial', - }); + // EPCIS_XML carries an empty EventList: router.py raises + // HTTPException(422, 'No EPCIS events found in XML document'). + const xmlDetail = 'No EPCIS events found in XML document'; + mocks.ingestEpcisXml.mockRejectedValue(axiosRejection(422, { detail: xmlDetail })); await openEpcisTab(); uploadEpcisFile('events.xml', EPCIS_XML, 'application/xml'); @@ -174,11 +230,13 @@ describe('EPCIS 2.0 import on /ingest', () => { }); expect(mocks.ingestEpcisBatch).not.toHaveBeenCalled(); expect(mocks.ingestEpcisEvent).not.toHaveBeenCalled(); - expect(await screen.findByText('EPCIS processed')).toBeInTheDocument(); + expect(await screen.findByText('Ingestion Failed')).toBeInTheDocument(); + expect(screen.getByText(xmlDetail)).toBeInTheDocument(); + expect(mocks.refresh).not.toHaveBeenCalled(); }); it('validate-only calls the validate endpoint and never ingests', async () => { - mocks.validateEpcis.mockResolvedValue({ valid: true, errors: [], normalized_cte: null }); + mocks.validateEpcis.mockResolvedValue({ valid: false, errors: BACKEND_EVENT_ERRORS, normalized_cte: null }); await openEpcisTab(); uploadEpcisFile('event.json', JSON.stringify(SINGLE_EVENT), 'application/json'); @@ -193,27 +251,26 @@ describe('EPCIS 2.0 import on /ingest', () => { expect(mocks.ingestEpcisEvent).not.toHaveBeenCalled(); expect(mocks.ingestEpcisXml).not.toHaveBeenCalled(); expect(mocks.ingestFile).not.toHaveBeenCalled(); - expect(await screen.findByText('All events valid')).toBeInTheDocument(); - expect(screen.getByText('1 of 1 events valid.')).toBeInTheDocument(); + expect(await screen.findByText('Validation found issues')).toBeInTheDocument(); + expect(screen.getByText(`0 of ${[SINGLE_EVENT].length} events valid.`)).toBeInTheDocument(); + expectEveryEventRejected([SINGLE_EVENT].length); }); it('validate-only reports per-event failures for a batch document', async () => { const events = [{ ...SINGLE_EVENT }, { ...SINGLE_EVENT }]; - mocks.validateEpcis - .mockResolvedValueOnce({ valid: true, errors: [], normalized_cte: null }) - .mockResolvedValueOnce({ valid: false, errors: ['Missing bizLocation'], normalized_cte: null }); + mocks.validateEpcis.mockResolvedValue({ valid: false, errors: BACKEND_EVENT_ERRORS, normalized_cte: null }); await openEpcisTab(); uploadEpcisFile('events.json', JSON.stringify(events), 'application/json'); await userEvent.click(screen.getByRole('button', { name: /^validate only$/i })); - expect(mocks.validateEpcis).toHaveBeenCalledTimes(2); + expect(mocks.validateEpcis).toHaveBeenCalledTimes(events.length); expect(mocks.ingestEpcisBatch).not.toHaveBeenCalled(); expect(mocks.ingestEpcisEvent).not.toHaveBeenCalled(); expect(mocks.ingestEpcisXml).not.toHaveBeenCalled(); expect(await screen.findByText('Validation found issues')).toBeInTheDocument(); - expect(screen.getByText('1 of 2 events valid.')).toBeInTheDocument(); - expect(screen.getByText('Event 2: Missing bizLocation')).toBeInTheDocument(); + expect(screen.getByText(`0 of ${events.length} events valid.`)).toBeInTheDocument(); + expectEveryEventRejected(events.length); }); it('shows a parse error and makes no request for a non-JSON .json file', async () => { @@ -234,7 +291,7 @@ describe('EPCIS 2.0 import on /ingest', () => { mocks.ingestEpcisBatch.mockRejectedValue(new Error('Batch ingest failed')); await openEpcisTab(); - uploadEpcisFile('sample_epcis_2.0.json', JSON.stringify(sampleEpcisDocument), 'application/json'); + uploadEpcisFile('events.json', JSON.stringify(epcisDocument), 'application/json'); await userEvent.click(screen.getByRole('button', { name: /upload & process/i })); expect(await screen.findByText('Ingestion Failed')).toBeInTheDocument(); diff --git a/frontend/src/app/ingest/page.tsx b/frontend/src/app/ingest/page.tsx index 83261482d..d18e238ee 100644 --- a/frontend/src/app/ingest/page.tsx +++ b/frontend/src/app/ingest/page.tsx @@ -712,13 +712,6 @@ export default function IngestPage() {

CSV uploads skip bad rows. EPCIS files are all or nothing: if any event fails, nothing is saved.

-
- -
diff --git a/qa/README.md b/qa/README.md index 398af22a1..8c378b478 100644 --- a/qa/README.md +++ b/qa/README.md @@ -6,12 +6,7 @@ These checks **block merge**. No exceptions, no softening. | Rule | Check | Script | |---|---|---| -| Merkle chain integrity | Every hash cryptographically verified against its predecessor | `fsma-lite-check.js` | -| FSMA 204 CTE count | Must reference 7 CTEs, never 6 | `fsma-lite-check.js` | -| FSMA 204 CTE count (standalone) | Fails if fsma-204/page.tsx says 6 CTEs or doesn't state 7 | `fsma-cte-count-check.js` | -| KDE completeness | All required fields present on every event | `fsma-lite-check.js` | -| Cross-format consistency | JSON, CSV, verification report, and manifest must agree on every hash | `full-flow.js` | -| Export artifact presence | All 5 files exist, valid format, correct record counts | `export-validate.js` | +| FSMA 204 CTE count | Fails if fsma-204/page.tsx says 6 CTEs or doesn't state 7 | `fsma-cte-count-check.js` | | No hardcoded credentials | No API keys, JWTs, or AWS keys in source | `frontend-security-scan.js` | | Bad data rejection | Invalid CTEs blocked, empty lot codes rejected | `bad-data.js` | @@ -21,9 +16,6 @@ These are the canonical "known bad" cases. If any of these ever passes, the pipe | Case | What breaks | Expected result | |---|---|---| -| Tamper any Merkle hash | Chain verification | `fsma-lite-check.js` exits 1, `full-flow.js` exits 1 | -| Change "7 CTEs" to "6 CTEs" in fsma-204/page.tsx | Regulatory copy check | `fsma-lite-check.js` exits 1 | -| Delete any sample artifact file | Export validation | `export-validate.js` exits 1 | | Add hardcoded API key to source | Credential scan | `frontend-security-scan.js` exits 1 | | Submit row with empty lot code | Bad data validator | `bad-data.js` exits 1 | @@ -50,11 +42,8 @@ decision → aggregates fast-gate + system-sim, blocks deploy on failu ## Running Locally ```bash -node qa/fsma-lite-check.js # 31 checks node qa/frontend-security-scan.js # 15 checks -node qa/full-flow.js # 68 checks node qa/bad-data.js # 18 checks -node qa/export-validate.js # 25 checks node qa/ai-analysis.js # 7 checks + warnings node qa/decision.js # requires FAST_GATE_RESULT and SYSTEM_SIM_RESULT env vars ``` diff --git a/qa/decision.js b/qa/decision.js index db1ecdd88..c77658a02 100644 --- a/qa/decision.js +++ b/qa/decision.js @@ -29,11 +29,8 @@ if (allPassed) { console.log('\u2705 DEPLOY: All checks passed.'); console.log(''); console.log(' Pipeline summary:'); - console.log(' \u2500 FSMA 204 compliance: verified'); console.log(' \u2500 Tenant isolation: verified'); - console.log(' \u2500 Full flow simulation: passed'); console.log(' \u2500 Bad data rejection: passed'); - console.log(' \u2500 Export artifacts: validated'); console.log(''); process.exit(0); } else if (anyFailed) { @@ -47,11 +44,8 @@ if (allPassed) { console.error(''); console.error(' Fix the failing checks before deploying.'); console.error(' Run individual check scripts locally:'); - console.error(' node qa/fsma-lite-check.js'); console.error(' node qa/frontend-security-scan.js'); - console.error(' node qa/full-flow.js'); console.error(' node qa/bad-data.js'); - console.error(' node qa/export-validate.js'); console.error(''); process.exit(1); } else if (anySkipped) { diff --git a/qa/export-validate.js b/qa/export-validate.js deleted file mode 100644 index cc66b7772..000000000 --- a/qa/export-validate.js +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env node -/** - * Export Artifact Validation - * Verifies all downloadable export files are: - * - Present and non-empty - * - Valid JSON/CSV - * - Cross-consistent (same hashes, same record counts) - * - Zip bundle contains all expected files - */ - -const fs = require('fs'); -const path = require('path'); - -const SAMPLES = path.join(__dirname, '..', 'frontend', 'public', 'samples'); -let passed = 0; -let failed = 0; - -function check(name, condition, detail) { - if (condition) { console.log(` \u2713 ${name}`); passed++; } - else { console.error(` \u2717 ${name}: ${detail}`); failed++; } -} - -console.log('\n=== Export Artifact Validation ===\n'); - -// ── 1. File Existence & Size ──────────────────────────── -console.log('1. File Presence'); - -const expectedFiles = [ - 'sample_epcis_2.0.json', - 'sample_fda_export.csv', - 'sample_chain_verification.json', - 'sample_manifest.json', - 'regengine_sample_export.zip', -]; - -for (const f of expectedFiles) { - const fp = path.join(SAMPLES, f); - const exists = fs.existsSync(fp); - check(`${f} exists`, exists, 'File not found'); - if (exists) { - const size = fs.statSync(fp).size; - check(`${f} is non-empty (${size} bytes)`, size > 100, `Only ${size} bytes`); - } -} - -// ── 2. JSON Validity ──────────────────────────────────── -console.log('\n2. JSON Validity'); - -const jsonFiles = ['sample_epcis_2.0.json', 'sample_chain_verification.json', 'sample_manifest.json']; -for (const f of jsonFiles) { - try { - JSON.parse(fs.readFileSync(path.join(SAMPLES, f), 'utf8')); - check(`${f} is valid JSON`, true, ''); - } catch (e) { - check(`${f} is valid JSON`, false, e.message); - } -} - -// ── 3. CSV Structure ──────────────────────────────────── -console.log('\n3. CSV Structure'); - -const csv = fs.readFileSync(path.join(SAMPLES, 'sample_fda_export.csv'), 'utf8'); -const csvLines = csv.trim().split('\n'); -check('CSV has header + 12 data rows', csvLines.length === 13, `Got ${csvLines.length} lines`); -check('CSV header has 16 columns', csvLines[0].split(',').length === 16, `Got ${csvLines[0].split(',').length}`); - -// ── 4. Record Count Consistency ───────────────────────── -console.log('\n4. Record Counts'); - -const epcis = JSON.parse(fs.readFileSync(path.join(SAMPLES, 'sample_epcis_2.0.json'), 'utf8')); -const verification = JSON.parse(fs.readFileSync(path.join(SAMPLES, 'sample_chain_verification.json'), 'utf8')); -const manifest = JSON.parse(fs.readFileSync(path.join(SAMPLES, 'sample_manifest.json'), 'utf8')); - -const counts = { - epcis: epcis.epcisBody.eventList.length, - csv: csvLines.length - 1, - verification: verification.verification_report.records.length, - integrityBlock: epcis['regengine:integrity'].chainLength, - manifest: manifest.manifest.integrity.chain_length, -}; - -check('All sources agree on 12 records', - Object.values(counts).every(c => c === 12), - `Counts: ${JSON.stringify(counts)}` -); - -// ── 5. Facility Consistency ───────────────────────────── -console.log('\n5. Facility Consistency'); - -const epcisGLNs = new Set(epcis.epcisBody.eventList.map(e => e.bizLocation.id.split(':').pop())); -const verFacilities = new Set(verification.verification_report.records.map(r => r.facility)); - -check('EPCIS has 7 unique GLNs', epcisGLNs.size === 7, `Got ${epcisGLNs.size}`); -check('Verification has 7 unique facilities', verFacilities.size === 7, `Got ${verFacilities.size}`); - -// ── 6. Manifest Integrity ─────────────────────────────── -console.log('\n6. Manifest'); - -check('Manifest lists 3 files', manifest.manifest.files.length === 3, `Got ${manifest.manifest.files.length}`); -check('Manifest has sha256', typeof manifest.manifest.manifest_sha256 === 'string' && manifest.manifest.manifest_sha256.length === 64, 'Missing or invalid'); -check('Manifest chain_valid is true', manifest.manifest.integrity.chain_valid === true, 'Not valid'); -check('Manifest has retention notice', manifest.manifest.retention_notice?.length > 20, 'Missing'); - -// ── 7. Zip Bundle ─────────────────────────────────────── -console.log('\n7. Zip Bundle'); - -const zipPath = path.join(SAMPLES, 'regengine_sample_export.zip'); -if (fs.existsSync(zipPath)) { - const zipSize = fs.statSync(zipPath).size; - check('Zip is > 1KB', zipSize > 1000, `Only ${zipSize} bytes`); - check('Zip is < 1MB (reasonable size)', zipSize < 1000000, `${zipSize} bytes is too large`); - // Check zip magic bytes - const buf = Buffer.alloc(4); - const fd = fs.openSync(zipPath, 'r'); - fs.readSync(fd, buf, 0, 4, 0); - fs.closeSync(fd); - check('Zip has valid magic bytes (PK)', buf[0] === 0x50 && buf[1] === 0x4B, 'Not a valid zip file'); -} - -// ── Summary ───────────────────────────────────────────── -console.log(`\n${'='.repeat(50)}`); -console.log(`Export Validation: ${passed} passed, ${failed} failed`); -console.log(`${'='.repeat(50)}\n`); - -if (failed > 0) process.exit(1); diff --git a/qa/fsma-lite-check.js b/qa/fsma-lite-check.js deleted file mode 100644 index 9bb0c9211..000000000 --- a/qa/fsma-lite-check.js +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env node -/** - * FSMA 204 Compliance Lite Check - * Validates sample export data against FDA FSMA 204 requirements: - * - 7 CTE types recognized - * - Required KDEs present per event - * - Lot codes follow TLC format - * - Hash chain integrity - * - Export format compliance (EPCIS 2.0) - */ - -const fs = require('fs'); -const crypto = require('crypto'); -const path = require('path'); - -const SAMPLES = path.join(__dirname, '..', 'frontend', 'public', 'samples'); - -const VALID_CTES = [ - 'harvesting', 'cooling', 'initial_packing', - 'first_land_based_receiving', - 'shipping', 'receiving', 'transformation' -]; - -const REQUIRED_EXTENSION_FIELDS = [ - 'fsma:traceabilityLotCode', - 'fsma:productDescription', - 'fsma:quantity', - 'fsma:unitOfMeasure', - 'regengine:payloadSHA256', - 'regengine:merkleHash', - 'regengine:sequenceNumber' -]; - -let passed = 0; -let failed = 0; - -function check(name, condition, detail) { - if (condition) { - console.log(` \u2713 ${name}`); - passed++; - } else { - console.error(` \u2717 ${name}: ${detail}`); - failed++; - } -} - -// ── 1. Load sample EPCIS ──────────────────────────────── -console.log('\n=== FSMA 204 Compliance Checks ===\n'); -console.log('1. EPCIS 2.0 Structure'); - -const epcisPath = path.join(SAMPLES, 'sample_epcis_2.0.json'); -check('EPCIS file exists', fs.existsSync(epcisPath), 'Missing sample_epcis_2.0.json'); - -const epcis = JSON.parse(fs.readFileSync(epcisPath, 'utf8')); -check('Has @context', Array.isArray(epcis['@context']), 'Missing @context array'); -check('Schema version is 2.0', epcis.schemaVersion === '2.0', `Got ${epcis.schemaVersion}`); -check('Type is EPCISDocument', epcis.type === 'EPCISDocument', `Got ${epcis.type}`); -check('Has eventList', Array.isArray(epcis.epcisBody?.eventList), 'Missing epcisBody.eventList'); - -const events = epcis.epcisBody.eventList; - -// ── 2. CTE Validation ─────────────────────────────────── -console.log('\n2. Critical Tracking Events'); - -const cteTypes = events.map(e => e.bizStep.replace('urn:epcglobal:cbv:bizstep:', '')); -const uniqueCtes = [...new Set(cteTypes)]; - -check('All events use valid CTE types', - cteTypes.every(c => VALID_CTES.includes(c)), - `Invalid CTEs: ${cteTypes.filter(c => !VALID_CTES.includes(c)).join(', ')}` -); - -check('At least 3 distinct CTE types used', - uniqueCtes.length >= 3, - `Only ${uniqueCtes.length} types: ${uniqueCtes.join(', ')}` -); - -// ── 3. KDE Completeness ───────────────────────────────── -console.log('\n3. Key Data Elements'); - -events.forEach((evt, i) => { - const ext = evt.extension || {}; - const missing = REQUIRED_EXTENSION_FIELDS.filter(f => !(f in ext)); - check(`Event ${i + 1} has all required KDEs`, - missing.length === 0, - `Missing: ${missing.join(', ')}` - ); -}); - -// ── 4. Lot Code Format ────────────────────────────────── -console.log('\n4. Traceability Lot Codes'); - -const lotCodes = events.map(e => e.extension['fsma:traceabilityLotCode']); -check('All events have a TLC', lotCodes.every(Boolean), 'Some events missing TLC'); -check('TLC is consistent across chain', - new Set(lotCodes).size === 1, - `Multiple TLCs: ${[...new Set(lotCodes)].join(', ')}` -); -check('TLC is non-empty string', - lotCodes[0] && lotCodes[0].length > 3, - `TLC too short: "${lotCodes[0]}"` -); - -// ── 5. Hash Chain Integrity ───────────────────────────── -console.log('\n5. Merkle Hash Chain'); - -const hashes = events.map(e => ({ - payload: e.extension['regengine:payloadSHA256'], - merkle: e.extension['regengine:merkleHash'], - seq: e.extension['regengine:sequenceNumber'] -})); - -check('First event: merkle == payload (chain root)', - hashes[0].merkle === hashes[0].payload, - `Root mismatch: ${hashes[0].merkle.slice(0, 16)} != ${hashes[0].payload.slice(0, 16)}` -); - -check('All hashes are 64-char hex strings', - hashes.every(h => /^[0-9a-f]{64}$/.test(h.payload) && /^[0-9a-f]{64}$/.test(h.merkle)), - 'Some hashes are not valid SHA-256' -); - -// Verify Merkle chain linkage -let chainValid = true; -for (let i = 1; i < hashes.length; i++) { - const expected = crypto.createHash('sha256') - .update(hashes[i - 1].merkle + hashes[i].payload) - .digest('hex'); - if (expected !== hashes[i].merkle) { - chainValid = false; - console.error(` Chain break at seq ${i + 1}: expected ${expected.slice(0, 16)}..., got ${hashes[i].merkle.slice(0, 16)}...`); - } -} -check('Merkle chain is cryptographically valid', chainValid, 'Chain verification failed'); - -// Verify final hash matches integrity block -const finalHash = epcis['regengine:integrity']?.finalMerkleHash; -check('Final hash matches integrity block', - finalHash === hashes[hashes.length - 1].merkle, - `Integrity: ${finalHash?.slice(0, 16)} vs chain: ${hashes[hashes.length - 1].merkle.slice(0, 16)}` -); - -check('Chain length matches event count', - epcis['regengine:integrity']?.chainLength === events.length, - `Integrity says ${epcis['regengine:integrity']?.chainLength}, events: ${events.length}` -); - -// ── 6. Sequence Ordering ──────────────────────────────── -console.log('\n6. Event Sequencing'); - -const seqs = events.map(e => e.extension['regengine:sequenceNumber']); -check('Sequences start at 1', seqs[0] === 1, `Starts at ${seqs[0]}`); -check('Sequences are contiguous', - seqs.every((s, i) => s === i + 1), - `Gap in sequence: ${seqs.join(',')}` -); - -// ── 7. Site Copy Check ────────────────────────────────── -console.log('\n7. Regulatory Copy Consistency'); - -const fsmaPage = path.join(__dirname, '..', 'frontend', 'src', 'app', 'fsma-204', 'page.tsx'); -if (fs.existsSync(fsmaPage)) { - const content = fs.readFileSync(fsmaPage, 'utf8'); - check('FSMA guide does NOT say "6 Critical Tracking Events"', - !content.includes('6 Critical Tracking Events'), - 'Found "6 Critical Tracking Events" — must be 7' - ); - check('FSMA guide says "7 Critical Tracking Events"', - content.includes('7 Critical Tracking Events'), - 'Missing "7 Critical Tracking Events"' - ); -} else { - console.log(' (skipped — fsma-204/page.tsx not found)'); -} - -// ── Summary ───────────────────────────────────────────── -console.log(`\n${'='.repeat(50)}`); -console.log(`FSMA 204 Checks: ${passed} passed, ${failed} failed`); -console.log(`${'='.repeat(50)}\n`); - -if (failed > 0) { - process.exit(1); -} diff --git a/qa/full-flow.js b/qa/full-flow.js deleted file mode 100644 index d3a18ca38..000000000 --- a/qa/full-flow.js +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env node -/** - * Full Flow Simulation - * Simulates the ingestion-to-export pipeline offline using sample data: - * 1. Parse CSV input - * 2. Validate fields - * 3. Compute SHA-256 hashes - * 4. Build Merkle chain - * 5. Generate EPCIS 2.0 output - * 6. Verify output matches expected - */ - -const fs = require('fs'); -const crypto = require('crypto'); -const path = require('path'); - -const SAMPLES = path.join(__dirname, '..', 'frontend', 'public', 'samples'); -let passed = 0; -let failed = 0; - -function check(name, condition, detail) { - if (condition) { console.log(` \u2713 ${name}`); passed++; } - else { console.error(` \u2717 ${name}: ${detail}`); failed++; } -} - -console.log('\n=== Full Flow Simulation ===\n'); - -// ── Step 1: Parse CSV ─────────────────────────────────── -console.log('1. CSV Ingestion'); - -const csvPath = path.join(SAMPLES, 'sample_fda_export.csv'); -const csvContent = fs.readFileSync(csvPath, 'utf8'); -const lines = csvContent.trim().replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n'); -const headers = lines[0].split(',').map(h => h.trim()); -const rows = lines.slice(1).map(line => { - const values = []; - let current = ''; - let inQuotes = false; - for (const char of line) { - if (char === '"') { inQuotes = !inQuotes; } - else if (char === ',' && !inQuotes) { values.push(current); current = ''; } - else { current += char; } - } - values.push(current); - const row = {}; - headers.forEach((h, i) => row[h] = values[i] || ''); - return row; -}); - -check('CSV has header row', headers.length >= 10, `Only ${headers.length} columns`); -check('CSV has 12 data rows', rows.length === 12, `Got ${rows.length} rows`); -check('CSV has Merkle Hash column', headers.includes('Merkle Hash'), 'Missing Merkle Hash'); -check('CSV has Sequence # column', headers.includes('Sequence #'), 'Missing Sequence #'); - -// ── Step 2: Validate Fields ───────────────────────────── -console.log('\n2. Field Validation'); - -rows.forEach((row, i) => { - const seq = parseInt(row['Sequence #']); - check(`Row ${seq}: has lot code`, row['Traceability Lot Code']?.length > 0, 'Empty TLC'); - check(`Row ${seq}: has facility`, row['Facility Name']?.length > 0, 'Empty facility'); - check(`Row ${seq}: has valid CTE`, row['CTE Type']?.length > 0, 'Empty CTE type'); - check(`Row ${seq}: has quantity`, parseInt(row['Quantity']) > 0, `Qty: ${row['Quantity']}`); -}); - -// ── Step 3: Verify Hashes ─────────────────────────────── -console.log('\n3. Hash Verification'); - -const csvHashes = rows.map(r => ({ - payload: r['Payload SHA-256'], - merkle: r['Merkle Hash'], - seq: parseInt(r['Sequence #']) -})); - -check('First row: merkle == payload (root)', - csvHashes[0].merkle === csvHashes[0].payload, - 'Root hash mismatch' -); - -let csvChainValid = true; -for (let i = 1; i < csvHashes.length; i++) { - const expected = crypto.createHash('sha256') - .update(csvHashes[i - 1].merkle + csvHashes[i].payload) - .digest('hex'); - if (expected !== csvHashes[i].merkle) { - csvChainValid = false; - console.error(` Break at seq ${csvHashes[i].seq}`); - } -} -check('CSV Merkle chain is valid', csvChainValid, 'Chain broken'); - -// ── Step 4: Cross-validate CSV ↔ JSON ─────────────────── -console.log('\n4. Cross-Format Validation'); - -const epcis = JSON.parse(fs.readFileSync(path.join(SAMPLES, 'sample_epcis_2.0.json'), 'utf8')); -const jsonEvents = epcis.epcisBody.eventList; - -check('JSON and CSV have same event count', - jsonEvents.length === rows.length, - `JSON: ${jsonEvents.length}, CSV: ${rows.length}` -); - -jsonEvents.forEach((evt, i) => { - const csvRow = rows[i]; - const jsonMerkle = evt.extension['regengine:merkleHash']; - const csvMerkle = csvRow['Merkle Hash']; - check(`Seq ${i + 1}: JSON merkle == CSV merkle`, - jsonMerkle === csvMerkle, - `JSON: ${jsonMerkle.slice(0, 16)} vs CSV: ${csvMerkle.slice(0, 16)}` - ); -}); - -// ── Step 5: Verify Final Hash Propagation ──────────────── -console.log('\n5. Final Hash Propagation'); - -const verification = JSON.parse( - fs.readFileSync(path.join(SAMPLES, 'sample_chain_verification.json'), 'utf8') -); -const manifest = JSON.parse( - fs.readFileSync(path.join(SAMPLES, 'sample_manifest.json'), 'utf8') -); - -const finalHashes = { - epcis: epcis['regengine:integrity']?.finalMerkleHash, - verification: verification.verification_report?.final_merkle_hash, - manifest: manifest.manifest?.integrity?.final_merkle_hash, - csvLast: csvHashes[csvHashes.length - 1].merkle, -}; - -check('All 4 artifacts agree on final hash', - new Set(Object.values(finalHashes)).size === 1, - `Mismatch: ${JSON.stringify(finalHashes, null, 2)}` -); - -// ── Summary ───────────────────────────────────────────── -console.log(`\n${'='.repeat(50)}`); -console.log(`Full Flow: ${passed} passed, ${failed} failed`); -console.log(`${'='.repeat(50)}\n`); - -if (failed > 0) process.exit(1); diff --git a/qa/summary.js b/qa/summary.js index 4dd2e39ee..1b7065738 100644 --- a/qa/summary.js +++ b/qa/summary.js @@ -10,11 +10,8 @@ const fs = require('fs'); const path = require('path'); const SCRIPTS = [ - { name: 'compliance', script: 'fsma-lite-check.js', category: 'compliance' }, { name: 'frontend-security', script: 'frontend-security-scan.js', category: 'security' }, - { name: 'full-flow', script: 'full-flow.js', category: 'pipeline' }, { name: 'bad-data', script: 'bad-data.js', category: 'pipeline' }, - { name: 'export', script: 'export-validate.js', category: 'pipeline' }, { name: 'ai-analysis', script: 'ai-analysis.js', category: 'trust' }, ];