diff --git a/frontend/src/app/compliance/page.tsx b/frontend/src/app/compliance/page.tsx
index 7dc5b5266..38177035e 100644
--- a/frontend/src/app/compliance/page.tsx
+++ b/frontend/src/app/compliance/page.tsx
@@ -257,7 +257,7 @@ export default function CompliancePage() {
FSMA 204
- EPCIS 2.0 export
+ EPCIS 2.0 export (Preview)GS1FDA CTEsKDEs
diff --git a/frontend/src/app/dashboard/compliance/page.tsx b/frontend/src/app/dashboard/compliance/page.tsx
index be965d0f8..0ee189b69 100644
--- a/frontend/src/app/dashboard/compliance/page.tsx
+++ b/frontend/src/app/dashboard/compliance/page.tsx
@@ -33,7 +33,7 @@ import { useAuth } from '@/lib/auth-context';
import { useTenant } from '@/lib/tenant-context';
import { fetchComplianceScore, fetchWorkbenchReadinessSummary } from '@/lib/api-hooks';
import { ErrorState } from '@/components/ui/error-state';
-import { experimentalFeaturesEnabled, filterExperimentalLinks } from '@/lib/experimental-features';
+import { filterExperimentalLinks } from '@/lib/experimental-features';
import { NO_DATA_YET } from '@/lib/empty-values';
/* ── Types matching ComplianceScoreResponse from backend ── */
@@ -127,9 +127,9 @@ const STATUS_CONFIG = {
const COMPLIANCE_SETUP_STEPS = [
{
title: 'Load traceability records',
- detail: 'Import a CSV, scan labels, or run Inflow Lab so the score has CTE and KDE evidence to evaluate.',
- href: '/dashboard/inflow-lab',
- action: 'Open Inflow Lab',
+ detail: 'Import a CSV or scan labels so the score has CTE and KDE evidence to evaluate.',
+ href: '/ingest',
+ action: 'Import records',
icon: Upload,
},
{
@@ -141,7 +141,7 @@ const COMPLIANCE_SETUP_STEPS = [
},
{
title: 'Preview export readiness',
- detail: 'Once records exist, configure FDA and EPCIS export jobs for audit-response workflows.',
+ detail: 'Once records exist, configure FDA and EPCIS (Preview) export jobs for audit-response workflows.',
href: '/dashboard/export-jobs',
action: 'Set up exports',
icon: Download,
@@ -439,13 +439,6 @@ export default function ComplianceDashboardPage() {
If this workspace is new, there may not be enough records yet. You can keep this page open, or jump directly to the setup paths below.
- {experimentalFeaturesEnabled() && (
-
-
-
- )}
- {experimentalFeaturesEnabled() && (
-
-
-
- )}
@@ -596,7 +582,7 @@ export default function ComplianceDashboardPage() {
@@ -616,7 +602,7 @@ export default function ComplianceDashboardPage() {
diff --git a/frontend/src/app/dashboard/export-jobs/page.tsx b/frontend/src/app/dashboard/export-jobs/page.tsx
index d4a54ad8a..8ede51ad7 100644
--- a/frontend/src/app/dashboard/export-jobs/page.tsx
+++ b/frontend/src/app/dashboard/export-jobs/page.tsx
@@ -69,9 +69,9 @@ export default function ExportJobsPage() {
const exportPrereqs = filterExperimentalLinks([
{
title: 'Connect source data',
- detail: 'Use Inflow Lab or import tools to build the record set export jobs will package once scheduling ships.',
- href: '/dashboard/inflow-lab',
- action: 'Open Inflow Lab',
+ detail: 'Use the import tools to build the record set export jobs will package once scheduling ships.',
+ href: '/ingest',
+ action: 'Import records',
icon: Database,
},
{
@@ -182,7 +182,7 @@ export default function ExportJobsPage() {
className={`${fieldControlClasses} h-11`}
>
-
+
@@ -250,7 +250,7 @@ export default function ExportJobsPage() {
- Pick the route by intent so simulated connector traffic stays separate from customer imports and FDA export work.
+ Pick the route by intent: import customer files, review mappings, then generate FDA exports when records are ready.
@@ -612,7 +575,7 @@ export default function DashboardIntegrationsPage() {
Validate-only preview
diff --git a/frontend/src/app/dashboard/layout.tsx b/frontend/src/app/dashboard/layout.tsx
index be73786f2..34e24721b 100644
--- a/frontend/src/app/dashboard/layout.tsx
+++ b/frontend/src/app/dashboard/layout.tsx
@@ -46,7 +46,6 @@ import {
Fingerprint,
ClipboardList,
Route,
- FlaskConical,
Bot,
ShieldAlert,
Code2,
@@ -84,7 +83,6 @@ interface CollapsibleSection {
const TOP_ITEMS: NavItem[] = [
{ label: 'Overview', href: '/dashboard', icon: Activity },
{ label: 'Trace', href: '/trace', icon: Route },
- { label: 'Inflow Lab', href: '/dashboard/inflow-lab', icon: FlaskConical, experimentalPage: true },
{ label: 'Compliance', href: '/dashboard/compliance', icon: BarChart3 },
{ label: 'Alerts', href: '/dashboard/alerts', icon: Bell, experimentalPage: true },
];
@@ -566,9 +564,9 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
{demoMode && (
- Sandbox mode: using sample data for demonstration.
+ No data yet. This workspace has no live records connected.
- Connect real data to exit sample mode
+ Connect real data to get started
diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx
index eb6a1286c..b6360288c 100644
--- a/frontend/src/app/dashboard/page.tsx
+++ b/frontend/src/app/dashboard/page.tsx
@@ -160,11 +160,11 @@ export default function DashboardPage() {
rows.push({
id: "workbench-fixes",
state: "needs-correction",
- item: `${fixes} unresolved ${fixes === 1 ? "fix" : "fixes"} in the Inflow Workbench`,
+ item: `${fixes} unresolved ${fixes === 1 ? "fix" : "fixes"} in imported records`,
source: workbenchReadiness?.source && workbenchReadiness.source !== "none"
? workbenchReadiness.source
- : "Inflow Workbench",
- href: "/dashboard/inflow-lab",
+ : "Import readiness",
+ href: "/ingest",
});
}
@@ -224,8 +224,8 @@ export default function DashboardPage() {
const fixes = workbenchReadiness?.unresolved_fix_count ?? 0;
if (fixes > 0) {
actions.push({
- label: `Resolve ${fixes} open ${fixes === 1 ? "fix" : "fixes"} in the workbench`,
- href: "/dashboard/inflow-lab",
+ label: `Resolve ${fixes} open ${fixes === 1 ? "fix" : "fixes"} in imported records`,
+ href: "/ingest",
cta: "Open",
});
}
@@ -260,16 +260,6 @@ export default function DashboardPage() {
Import data
}
- primaryAction={
- experimentalFeaturesEnabled() ? (
-
-
- Open Inflow Lab
-
-
-
- ) : undefined
- }
/>
-
-
- {report?.demo_disclaimer ??
- 'Sample scenario only. Live recall evidence appears after data and export workflows are connected for this workspace.'}
-
-
@@ -555,7 +395,7 @@ export default function RecallReportPage() {
Prioritized Actions
- {isDemo ? 'Address these to improve recall readiness' : 'Personalized recommendations based on your traceability data'}
+ Personalized recommendations based on your traceability data
diff --git a/frontend/src/app/dashboard/receiving/page.tsx b/frontend/src/app/dashboard/receiving/page.tsx
index 0fcd19d80..5b5ed7fd0 100644
--- a/frontend/src/app/dashboard/receiving/page.tsx
+++ b/frontend/src/app/dashboard/receiving/page.tsx
@@ -210,7 +210,7 @@ export default function ReceivingDockPage() {
quantity: item.quantity,
unit_of_measure: 'cases',
timestamp: item.scannedAt || new Date().toISOString(),
- location_name: `Receiving Dock — ${session.supplier}`,
+ location_name: `Receiving Dock, ${session.supplier}`,
kdes: {
gtin: item.gtin || undefined,
lot_code: item.lotCode || undefined,
diff --git a/frontend/src/app/developers/page.tsx b/frontend/src/app/developers/page.tsx
index e096f7a1c..f56674f15 100644
--- a/frontend/src/app/developers/page.tsx
+++ b/frontend/src/app/developers/page.tsx
@@ -43,7 +43,7 @@ const capabilities = [
{
Icon: FileSpreadsheet,
title: 'Records Export',
- description: 'Generate FDA-oriented sortable spreadsheet records and EPCIS 2.0 event exports where the enabled workflow and source data support them.',
+ description: 'Generate FDA-oriented sortable spreadsheet records and EPCIS 2.0 (Preview) event exports where the enabled workflow and source data support them.',
},
{
Icon: BarChart3,
@@ -60,7 +60,7 @@ const capabilities = [
/* ── SDKs (planned, not yet released) ── */
/* ── Quick-start curl snippet ── */
-/* Rendered from one payload: src/lib/api-samples/ingest-receiving.json. */
+/* Rendered from the shared request-body skeleton in src/lib/api-samples/ingest-samples.ts. */
const curlSnippet = `${INGEST_ENV_SETUP}
${INGEST_CURL_SAMPLE}`;
diff --git a/frontend/src/app/docs/api/ApiReferenceClient.tsx b/frontend/src/app/docs/api/ApiReferenceClient.tsx
index 09ddf689b..49d0a6870 100644
--- a/frontend/src/app/docs/api/ApiReferenceClient.tsx
+++ b/frontend/src/app/docs/api/ApiReferenceClient.tsx
@@ -96,14 +96,14 @@ const API_SERVICES: { name: string; baseUrl: string; icon: React.ComponentType<{
response: JSON.stringify({
tenant_id: "",
events: [{
- event_id: "0b9c2f4e-6d1a-4c53-9a7e-5f3d8e21c7ab",
+ event_id: "",
event_type: "receiving",
- traceability_lot_code: "TEST-00012345678901-LOT-001",
- product_reference: "Romaine Lettuce",
- quantity: 500.0,
- unit_of_measure: "cases",
- from_facility_reference: "Distribution Center #4",
- to_facility_reference: "Distribution Center #4",
+ traceability_lot_code: "",
+ product_reference: "",
+ quantity: "",
+ unit_of_measure: "",
+ from_facility_reference: "",
+ to_facility_reference: "",
event_timestamp: "",
source_system: "webhook_api",
status: "active",
@@ -120,7 +120,7 @@ const API_SERVICES: { name: string; baseUrl: string; icon: React.ComponentType<{
description: 'Full record detail: the normalized fields, kdes, provenance_metadata, raw_payload, rule_evaluations, exception_cases, and the amendment_chain when the record amends an earlier one.',
auth: true,
headers: ['X-RegEngine-API-Key'],
- response: '{ "event_id": "0b9c2f4e-6d1a-4c53-9a7e-5f3d8e21c7ab", "event_type": "receiving", "kdes": {...}, "provenance_metadata": {...}, "raw_payload": {...}, "rule_evaluations": [...], "exception_cases": [...] }',
+ response: '{ "event_id": "", "event_type": "receiving", "kdes": {...}, "provenance_metadata": {...}, "raw_payload": {...}, "rule_evaluations": [...], "exception_cases": [...] }',
},
{
method: 'GET',
@@ -128,7 +128,7 @@ const API_SERVICES: { name: string; baseUrl: string; icon: React.ComponentType<{
description: 'Amendment chain for a record: every correction with who, when, and why',
auth: true,
headers: ['X-RegEngine-API-Key'],
- response: '{ "event_id": "0b9c2f4e-6d1a-4c53-9a7e-5f3d8e21c7ab", "amendment_chain": [...] }',
+ response: '{ "event_id": "", "amendment_chain": [...] }',
},
],
},
@@ -146,12 +146,12 @@ const API_SERVICES: { name: string; baseUrl: string; icon: React.ComponentType<{
headers: ['X-RegEngine-API-Key'],
params: ['tenant_id={your-workspace-uuid}'],
response: JSON.stringify({
- seed_tlc: "TEST-00012345678901-LOT-001",
+ seed_tlc: "",
tenant_id: "",
traversal_depth: 5,
node_count: 1,
edge_count: 0,
- nodes: [{ tlc: "TEST-00012345678901-LOT-001", is_seed: true, role: "seed", event_count: 1 }],
+ nodes: [{ tlc: "", is_seed: true, role: "seed", event_count: 1 }],
edges: [],
total_events: 1
}, null, 2),
@@ -162,7 +162,7 @@ const API_SERVICES: { name: string; baseUrl: string; icon: React.ComponentType<{
description: 'Generate an FDA-oriented export for a traceability lot code (the 24-hour traceability request format). Requires the fda.export scope. format is "package" (a ZIP bundle, the default), "csv", or "pdf".',
auth: true,
headers: ['X-RegEngine-API-Key'],
- params: ['tenant_id={your-workspace-uuid}', 'tlc=TEST-00012345678901-LOT-001', 'format=csv'],
+ params: ['tenant_id={your-workspace-uuid}', 'tlc={the-lot-code}', 'format=csv'],
response: 'A file download: text/csv, application/pdf, or application/zip, depending on format',
},
{
@@ -180,7 +180,7 @@ const API_SERVICES: { name: string; baseUrl: string; icon: React.ComponentType<{
description: 'Recall-filtered FDA export: only events matching the product, location, lot code, or event type you filter on. Requires the fda.export scope.',
auth: true,
headers: ['X-RegEngine-API-Key'],
- params: ['tenant_id={your-workspace-uuid}', 'tlc=TEST-00012345678901-LOT-001'],
+ params: ['tenant_id={your-workspace-uuid}', 'tlc={the-lot-code}'],
response: 'A file download (CSV or ZIP package) scoped to the filters',
},
{
@@ -214,7 +214,7 @@ const API_SERVICES: { name: string; baseUrl: string; icon: React.ComponentType<{
exports: [{
id: "",
export_type: "",
- query_tlc: "TEST-00012345678901-LOT-001",
+ query_tlc: "",
record_count: 1,
export_hash: "",
generated_by: "",
diff --git a/frontend/src/app/docs/changelog/page.tsx b/frontend/src/app/docs/changelog/page.tsx
index c68568f6d..7143ad24d 100644
--- a/frontend/src/app/docs/changelog/page.tsx
+++ b/frontend/src/app/docs/changelog/page.tsx
@@ -83,7 +83,7 @@ export default function ChangelogPage() {
Verification
-
Offline verifier now validates the current audit bundle structure, including EPCIS package contents
+
Offline verifier now validates the current audit bundle structure, including EPCIS (Preview) package contents
Tampered proof-bundle sample added so evaluators can see failure detection instead of only the happy path
Root Docker deploy context and admin proxy routing aligned with the consolidated production surface
diff --git a/frontend/src/app/docs/connectors/inflow-lab/opengraph-image.tsx b/frontend/src/app/docs/connectors/inflow-lab/opengraph-image.tsx
deleted file mode 100644
index 49bed226c..000000000
--- a/frontend/src/app/docs/connectors/inflow-lab/opengraph-image.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { renderOgCard, OG_SIZE, OG_CONTENT_TYPE } from '@/lib/og-image';
-
-export const runtime = 'edge';
-export const alt = 'The Inflow Lab connector';
-export const size = OG_SIZE;
-export const contentType = OG_CONTENT_TYPE;
-
-export default function Image() {
- return renderOgCard({ eyebrow: 'Documentation', title: 'The Inflow Lab connector' });
-}
diff --git a/frontend/src/app/docs/connectors/inflow-lab/page.tsx b/frontend/src/app/docs/connectors/inflow-lab/page.tsx
index eeb4d9fb7..3594cf9b4 100644
--- a/frontend/src/app/docs/connectors/inflow-lab/page.tsx
+++ b/frontend/src/app/docs/connectors/inflow-lab/page.tsx
@@ -1,176 +1,5 @@
-import type { Metadata } from "next";
-import Link from "next/link";
-import { ArrowLeft, ArrowRight, Check } from "lucide-react";
-import { INGEST_ENV_SETUP, INGEST_INFLOW_LAB_CURL_SAMPLE } from "@/lib/api-samples/ingest-samples";
-import { PageHeader } from "@/components/layout/page-header";
-import { Card } from "@/components/ui/card";
-import { CodeBlock } from "@/components/ui/code-block";
-
-export const metadata: Metadata = {
- title: "Inflow Lab Connector | RegEngine",
- description:
- "Use Inflow Lab to distinguish sandbox diagnosis, mock feed validation, authenticated ingest, and production evidence boundaries.",
- openGraph: {
- title: 'Inflow Lab Connector',
- description: 'Use Inflow Lab to distinguish sandbox diagnosis, mock feed validation, authenticated ingest, and production evidence boundaries.',
- url: 'https://regengine.co/docs/connectors/inflow-lab',
- type: 'website',
- },
-};
-
-const localRun = `git clone https://github.com/regengine/inflow-lab.git
-cd inflow-lab
-cp .env.example .env
-# Set RE_API_BASE_URL, RE_API_KEY, and RE_TENANT_ID
-python -m pytest`;
-
-const statusFacts = [
- ["Public slug", "inflow-lab"],
- ["Backend id", "inflow_lab"],
- ["Repository", "regengine/inflow-lab"],
- ["CI contract", "Authenticated ingest"],
-];
-
-const livePath = [
- ["Sandbox/mock", "Diagnoses source shape without creating production evidence"],
- ["Authenticated ingest", "POST /api/v1/webhooks/ingest"],
- ["Alias resolution", "inflow-lab resolves to inflow_lab"],
- ["Persisted records", "Tenant-scoped records become eligible for evidence export"],
-];
-
-const connectionSteps = [
- "Inflow Lab can diagnose CSV shape and generate RegEngine-shaped FSMA 204 CTE payloads.",
- "Mock and sandbox runs stay separate from production ingestion and are not evidence.",
- "Only authenticated webhook ingest creates persisted tenant records that can be used for production evidence export.",
-];
-
-/** Inline identifier — SPEC §2.3. */
-const CODE =
- "rounded-sm bg-re-surface-inset px-1.5 py-0.5 font-mono text-[0.8125rem] text-re-text-primary";
+import { notFound } from "next/navigation";
export default function InflowLabDocsPage() {
- return (
- <>
-
-
- Back to integrations
-
- }
- eyebrow="Pilot connector"
- title="Inflow Lab"
- description="Inflow Lab is RegEngine's FSMA 204 boundary workspace for sandbox diagnosis, mock feed validation, and authenticated ingest checks. Mock data never becomes production evidence; evidence export starts from authenticated, persisted tenant records."
- />
-
-
- Use source: "inflow-lab" so dashboard and audit surfaces can distinguish test traffic from generic webhook submissions. Backend routes resolve that public slug to canonical connector id inflow_lab. Production evidence still requires authenticated persisted records. RegEngine issues the API key and signing secret on request; the Quickstart explains the signature and the test workspace.
-
-
-
Environment
-
-
POST /api/v1/webhooks/ingest
-
-
-
-
- {/* Local and CI usage */}
-
-
Local and CI usage
-
- The simulator lives in the RegEngine GitHub organization. Run it locally for contract validation; the RegEngine workflow pins the repo and runs authenticated ingest validation in CI.
-
-
-
Local setup
-
-
-
-
- {/* Operational notes */}
-
-
Operational notes
-
-
Inflow Lab is a simulator and developer tool, not a production vendor data source.
-
Sandbox and mock records are for diagnosis, mapping, and feed monitoring setup. They are not production evidence.
-
Cross-repo contract CI should pin the simulator commit that RegEngine expects, then run an authenticated ingest assertion against the local service.
-
-
-
-
- Open API docs
-
-
-
- >
- );
+ notFound();
}
diff --git a/frontend/src/app/docs/fsma-204/page.tsx b/frontend/src/app/docs/fsma-204/page.tsx
index 0d6c2880d..0a2d01ec4 100644
--- a/frontend/src/app/docs/fsma-204/page.tsx
+++ b/frontend/src/app/docs/fsma-204/page.tsx
@@ -61,7 +61,7 @@ const RELATED_ENDPOINTS = [
const RECALL_SNIPPET = `curl -G https://regengine.co/api/v1/fda/export/recall \\
-H "X-RegEngine-API-Key: $REGENGINE_API_KEY" \\
-d "tenant_id=YOUR_WORKSPACE_ID" \\
- -d "tlc=TEST-00012345678901-LOT-001" \\
+ -d "tlc=" \\
-o fda_response.csv`;
/** A labelled code well — SPEC §5.1: code sits one surface step below the page. */
@@ -214,7 +214,7 @@ export default function FSMA204GuidePage() {
\\
-H "X-RegEngine-API-Key: $REGENGINE_API_KEY" \\
-d "tenant_id=YOUR_WORKSPACE_ID"`}
/>
diff --git a/frontend/src/app/docs/page.tsx b/frontend/src/app/docs/page.tsx
index 902a669fe..89c851785 100644
--- a/frontend/src/app/docs/page.tsx
+++ b/frontend/src/app/docs/page.tsx
@@ -81,7 +81,6 @@ const subPages = [
{ title: 'Authentication', href: '/docs/authentication', icon: Key, description: 'The key header, scopes, and what a key can reach' },
{ title: 'API Reference', href: '/docs/api', icon: Code2, description: 'Full endpoint reference with schemas' },
{ title: 'Outbound webhooks (planned)', href: '/docs/webhooks', icon: Webhook, description: 'Notifications from RegEngine. To send events in, use the JSON events API (POST /webhooks/ingest).' },
- { title: 'Inflow Lab', href: '/docs/connectors/inflow-lab', icon: Terminal, description: 'Preflight, readiness scoring, fix queue, and commit gates' },
{ title: 'Error Codes', href: '/docs/errors', icon: AlertTriangle, description: 'Error responses and troubleshooting' },
{ title: 'Rate Limits', href: '/docs/rate-limits', icon: Clock, description: 'What to do when you get a 429' },
{ title: 'SDKs', href: '/docs/sdks', icon: Package, description: 'Client libraries are planned. Use REST and the OpenAPI spec today.' },
@@ -97,7 +96,7 @@ const quickStartSteps = [
const SECTION_LABEL = 'mb-5 font-mono text-label uppercase text-re-text-muted';
/* ─── Code Samples ─────────────────────────────────────────── */
-/* Rendered from one payload: src/lib/api-samples/ingest-receiving.json. */
+/* Rendered from the shared request-body skeleton in src/lib/api-samples/ingest-samples.ts. */
const codeTabs = [
{ label: 'cURL', lang: 'bash', code: INGEST_CURL_SAMPLE },
diff --git a/frontend/src/app/docs/quickstart/page.tsx b/frontend/src/app/docs/quickstart/page.tsx
index a03217cfc..e00cebbed 100644
--- a/frontend/src/app/docs/quickstart/page.tsx
+++ b/frontend/src/app/docs/quickstart/page.tsx
@@ -222,7 +222,7 @@ export default function QuickstartPage() {
Filter the records endpoint by lot code:
" \\
-H "X-RegEngine-API-Key: $REGENGINE_API_KEY"`}
language="bash"
/>
@@ -231,12 +231,12 @@ export default function QuickstartPage() {
"tenant_id": "",
"events": [
{
- "event_id": "0b9c2f4e-6d1a-4c53-9a7e-5f3d8e21c7ab",
+ "event_id": "",
"event_type": "receiving",
- "traceability_lot_code": "TEST-00012345678901-LOT-001",
- "product_reference": "Romaine Lettuce",
- "quantity": 500.0,
- "unit_of_measure": "cases",
+ "traceability_lot_code": "",
+ "product_reference": "",
+ "quantity": "",
+ "unit_of_measure": "",
"event_timestamp": "",
"source_system": "webhook_api",
"status": "active"
diff --git a/frontend/src/app/docs/walkthrough/page.tsx b/frontend/src/app/docs/walkthrough/page.tsx
index 9e8b1aa87..e0ae2838f 100644
--- a/frontend/src/app/docs/walkthrough/page.tsx
+++ b/frontend/src/app/docs/walkthrough/page.tsx
@@ -5,13 +5,7 @@ import { filterExperimentalToolLinks } from '@/lib/experimental-features';
import { PageHeader } from '@/components/layout/page-header';
import { Callout } from '@/components/ui/callout';
import { CodeBlock } from '@/components/ui/code-block';
-import {
- INGEST_WALKTHROUGH_RECEIVING_CURL_SAMPLE,
- INGEST_WALKTHROUGH_SHIPPING_CURL_SAMPLE,
- INGEST_WALKTHROUGH_TRANSFORMATION_CURL_SAMPLE,
- WALKTHROUGH_BAGGED_LOT,
- WALKTHROUGH_RECEIVED_LOT,
-} from '@/lib/api-samples/ingest-samples';
+import { INGEST_CURL_SAMPLE } from '@/lib/api-samples/ingest-samples';
export const metadata: Metadata = {
title: 'End-to-End Walkthrough | RegEngine',
@@ -65,28 +59,30 @@ export default function WalkthroughPage() {
You should have completed the Quickstart and have:
$REGENGINE_API_KEY and $REGENGINE_SIGNING_SECRET in your shell
-
A test workspace. Accepted events are permanent and can be billed, so every lot below starts with TEST-
+
A test workspace. Accepted events are permanent and can be billed, so keep the TEST- prefix on your lot codes
A working curl with openssl, and ~15 minutes
- The scenario: you're a fresh-cut produce processor. One supplier sent you a romaine
- lot; you transform it; you ship it to a distributor. Three events in one supplier-to-shipping
- chain. We'll post them, list them, and export them.
+ The flow: one receiving event, one transformation event, one shipping event —
+ three events in a single supplier-to-shipping chain. Fill each request-body
+ skeleton with your own lot codes, locations, and quantities, then list the
+ events and export them.
{/* Step 1 */}
- You receive lot {WALKTHROUGH_RECEIVED_LOT} from
- Supplier A. This is a Receiving CTE, the starting anchor for the chain.
+ Post a Receiving CTE for a lot that arrived at your facility — the starting anchor for
+ the chain. Fill the skeleton's {''},{' '}
+ immediate_previous_source, and the other fields with your own values.
- You wash and bag the lot. That's a Transformation CTE. The input lot is the
- Receiving lot from Step 1; the output lot is new.
+ Post a Transformation CTE for the lot you process. Set cte_type to{' '}
+ transformation and point kdes.input_lot_codes{' '}
+ at the Receiving lot from Step 1; the output lot is new.
-
+
Same shape as Step 1. The new event chains to the receiving event via{' '}
@@ -136,9 +133,11 @@ export default function WalkthroughPage() {
{/* Step 3 */}
- The bagged lot ships to a distributor. This closes the chain for this lot.
+ Post a Shipping CTE when the lot leaves for its next destination. Set{' '}
+ cte_type to shipping with the same lot code.
+ This closes the chain for the lot.
-
+
{/* Step 4 */}
@@ -148,7 +147,7 @@ export default function WalkthroughPage() {
/dashboard/compliance. You should see:
-
3 events in your workspace, with lot codes that start {WALKTHROUGH_RECEIVED_LOT}
+
3 events in your workspace, with the lot codes you posted
The traceability lot code visible in the events list
A compliance score (early-stage; will improve as more lots flow through)
{filterExperimentalToolLinks([
{ title: 'Try a Recall Drill', desc: 'Simulate an FDA recall request against the data you just posted', href: '/dashboard/recall-drills' },
- { title: 'CSV Bulk Ingest', desc: 'Upload a real supplier CSV through Inflow Workbench preflight', href: '/tools/inflow-lab' },
- { title: 'EPCIS intake (Preview)', desc: 'Send records by CSV upload or the JSON events API. EPCIS 2.0 is in Preview on the Ingest page.', href: '/docs/connectors/inflow-lab' },
+ { title: 'CSV Bulk Ingest', desc: 'Upload a real supplier CSV through the Data Import Hub', href: '/tools/data-import' },
+ { title: 'EPCIS intake (Preview)', desc: 'Send records by CSV upload or the JSON events API. EPCIS 2.0 is in Preview on the Ingest page.', href: '/ingest' },
]).map(({ title, desc, href }) => (
-
API & EPCIS 2.0
+
API & EPCIS 2.0 (Preview)
For enterprise systems (SAP, Oracle, NetSuite). Push events directly to our endpoints via the JSON events API; GS1 EPCIS 2.0 XML/JSON upload is in Preview.
Ingestion request completed
@@ -933,23 +913,6 @@ export default function IngestPage() {
Use URL ingestion for public documents, File Upload for CSV traceability events, or EPCIS 2.0 (Preview) for GS1 EPCIS JSON and XML files.
- {experimentalFeaturesEnabled() && (
-
-
-
- Inflow Lab
-
-
- Use the lab for FSMA event simulation and event replay so test traffic stays tagged.
-
Know which sources are ready, guided, or custom scoped.
- A compact view of available intake modes, guided workflows, and custom-scoped implementation paths. Named vendor and retailer workflows require scope confirmation and evidence review before being treated as production connectors. Send records by CSV upload or the JSON events API. EPCIS 2.0 is in Preview. Inflow Lab is the simulator path for JSON events API demos and contract CI.
+ A compact view of available intake modes, guided workflows, and custom-scoped implementation paths. Named vendor and retailer workflows require scope confirmation and evidence review before being treated as production connectors. Send records by CSV upload or the JSON events API. EPCIS 2.0 is in Preview.
-
-
- Open Inflow docs
-
-
-
@@ -135,60 +111,7 @@ export default function IntegrationsPage() {
diff --git a/frontend/src/app/pricing/page.tsx b/frontend/src/app/pricing/page.tsx
index ca172ad1f..a9b01c76e 100644
--- a/frontend/src/app/pricing/page.tsx
+++ b/frontend/src/app/pricing/page.tsx
@@ -1,20 +1,24 @@
import type { Metadata } from 'next';
+import Link from 'next/link';
import { PricingPageClient } from '@/components/pricing/PricingPageClient';
import type { PricingComparisonRow, PricingFaqItem } from '@/components/pricing/PricingPageClient';
+import { Container } from '@/components/layout/container';
+import { Button } from '@/components/ui/button';
export const metadata: Metadata = {
title: 'FSMA 204 Pricing | RegEngine',
- description: 'FSMA 204 traceability readiness from $425/mo. Transparent Founding rate pricing with onboarding terms controlled by your agreement.',
+ description: 'FSMA 204 traceability readiness with transparent Founding rate pricing. Onboarding terms are controlled by your agreement.',
openGraph: {
title: 'FSMA 204 Pricing | RegEngine',
- description: 'FSMA 204 traceability readiness from $425/mo. Transparent Founding rate pricing with onboarding terms controlled by your agreement.',
+ description: 'FSMA 204 traceability readiness with transparent Founding rate pricing. Onboarding terms are controlled by your agreement.',
url: 'https://regengine.co/pricing',
type: 'website',
},
};
// Cache pricing for 1 hour server-side so the page stays fast even when
-// the billing service is slow. Falls back to hardcoded data on error.
+// the billing service is slow. When billing is unreachable the page renders
+// an honest unavailable state instead of hardcoded prices.
export const revalidate = 3600;
interface BillingPlan {
@@ -53,21 +57,19 @@ async function fetchBillingPlans(): Promise {
if (!Array.isArray(plans) || plans.length === 0) return null;
return plans;
} catch {
- // Billing service unreachable — graceful degradation to hardcoded data.
+ // Billing service unreachable: caller renders the unavailable state.
return null;
}
}
+// Static tier copy (names, feature lists, CTAs). Prices come only from the
+// billing service; nothing here may carry a number.
const PRICING_TIERS = [
{
id: 'base',
name: 'Base',
icon: 'zap' as const,
description: '1 facility, getting started',
- gaMonthly: 999,
- gaAnnual: 849,
- partnerMonthly: 499,
- partnerAnnual: 425,
highlighted: false,
cta: 'Apply for Base',
features: [
@@ -86,10 +88,6 @@ const PRICING_TIERS = [
name: 'Standard',
icon: 'rocket' as const,
description: '2 to 3 facilities, scaling up',
- gaMonthly: 1299,
- gaAnnual: 1099,
- partnerMonthly: 649,
- partnerAnnual: 549,
highlighted: true,
cta: 'Apply for Standard',
features: [
@@ -98,7 +96,7 @@ const PRICING_TIERS = [
'Everything in Base',
'Multi-facility operations',
'Retailer-specific readiness benchmarks',
- 'FDA-oriented sortable export + EPCIS 2.0 export',
+ 'FDA-oriented sortable export + EPCIS 2.0 export (Preview)',
'Priority email support',
],
},
@@ -107,10 +105,6 @@ const PRICING_TIERS = [
name: 'Premium',
icon: 'crown' as const,
description: '4+ facilities, full coverage',
- gaMonthly: 1499,
- gaAnnual: 1275,
- partnerMonthly: 749,
- partnerAnnual: 639,
highlighted: false,
cta: 'Apply for Premium',
features: [
@@ -148,26 +142,61 @@ const FAQ: PricingFaqItem[] = [
];
export default async function PricingPage() {
- // Attempt to load live pricing from the billing service.
- // On failure (service down, network error, bad payload) we silently fall
- // back to the statically-defined PRICING_TIERS above.
+ // Load live pricing from the billing service. When it is unreachable or
+ // returns no usable plans, show an honest unavailable state rather than
+ // any hardcoded figures.
const livePlans = await fetchBillingPlans();
- const pricingTiers = livePlans
- ? PRICING_TIERS.map((tier) => {
- const live = livePlans.find((p) => p.id === tier.id);
- if (!live) return tier;
- return {
- ...tier,
- ...(live.ga_monthly !== undefined && { gaMonthly: live.ga_monthly }),
- ...(live.ga_annual !== undefined && { gaAnnual: live.ga_annual }),
- ...(live.partner_monthly !== undefined && { partnerMonthly: live.partner_monthly }),
- ...(live.partner_annual !== undefined && { partnerAnnual: live.partner_annual }),
- ...(live.description !== undefined && { description: live.description }),
- ...(Array.isArray(live.features) && live.features.length > 0 && { features: live.features }),
- };
- })
- : PRICING_TIERS;
+ if (!livePlans) {
+ return (
+
+
+
+ Pricing is temporarily unavailable
+
+
+ We could not load current plan pricing from our billing service, so no figures are shown here.
+ Plan rates are confirmed in your executed order form or design-partner agreement.
+
+
+
+
+ Apply as a founding design partner
+
+
+
+
+ Contact us
+
+
+
- {/* ─── Hero ────────────────────────────────── */}
-
-
-
- Live Recall Drill • Synthetic Data
-
- }
- title="Run a recall drill on synthetic supply-chain data."
- className="mb-0 border-b-0 pb-0"
- />
-
- Click Start drill and RegEngine
- traces the contaminated lot through the applicable CTEs and produces an FDA-oriented sortable
- recall CSV in under 60 seconds. Synthetic data, illustrative only.
-
-
-
-
- All data is synthetic. The drill runs on a frozen public dataset separate
- from any customer data. Run as many times as you like; results are
- deterministic.
-
-
-
- Load Sample Dataset
-
- Push a synthetic Romaine Lettuce supply chain (TLC1001) through the full pipeline with 9 CTE events from farm to retailer.
-
-
- Review each Critical Tracking Event (CTE) and Key Data Element (KDE) to see why a supplier lot may fail before production export.
-
-
-
- setIsTutorialOpen(true)}
- >
-
- How to use
-
-
-
-
- Connections
-
-
- fileInputRef.current?.click()}
- >
-
- Upload CSV
-
-
-
-
-
-
- Sandbox preview only.
-
- Production FDA-oriented exports stay sealed and review-ready. Nothing here writes to them.
-
-
- What runs in sandbox →
-
-
No single root cause. These need lot-by-lot triage.
-
- Each lot has a different combination of missing KDEs. A bulk fix would either over-match or under-match. Recommend triaging individually, starting with the largest case counts.
-
-
- onSortChange("cases")}
- >
- Sort by case count
-
- onSortChange("recency")}
- >
- Sort by recency
-
-
-
- Export to CSV
-
-
-
- {/* Verdict. Absent until there is real data to report on. */}
- {hasData && isBlocked && lot && (
-
-
-
- {nodes.filter((node) => node.state === "missing").length === 1
- ? "One record never arrived."
- : `${nodes.filter((node) => node.state === "missing").length} records never arrived.`}
-
-
- Blocked now
-
-
-
- {`${lot.product} lot ${lot.code.slice(-6)} is blocked because ${missingEvidence} never reached the evidence chain, so the export cannot be certified.`}
-
-
-
-
-
Missing
-
{missingEvidence}
-
-
-
Pattern
-
{patternLabel}
-
-
-
- Lots affected
-
-
- {lotsAffected && lotsAffected > 1
- ? `${lotsAffected} lots, same pattern`
- : "This lot only"}
-
- {`${lot.product} lot ${lot.code.slice(-6)} has all five CTEs with complete KDE evidence. To watch a defect get caught, delete the receiving row from your CSV and run the trace again.`}
-
- Test supplier traceability data before production import. This lab uses mock or uploaded test records only; formal production records start after signed-in import.
-
-
-
-
- Start guided test run
-
-
-
- How to use
-
-
-
-
-
- Last inbound record
- {lastReceivedLabel}
-
-
- Readiness score
- {readinessScore}/100
-
-
- Validation outcome
- {validationOutcome}
-
-
- Test completeness
- {exportReadinessLabel}
-
-
-
-
- )}
-
-
-
-
-
-
-
- Boundary active
- {activeDataBoundary}
-
-
- {boundaryStateDetail} The lab checks Critical Tracking Event (CTE) handoffs and required Key Data Element (KDE) fields, but it cannot create formal production records. Your operation remains responsible for compliance.
-
- {isStandalone
- ? "Review mock-generated records, trace lots, and preview test filters without mixing simulator data into production imports."
- : "Validate FSMA 204 records, resolve exceptions, prove traceability, and keep formal production records separate from test data."}
-
- Test submissions are routed through the Inflow Lab gateway and remain outside production ingestion.
-
-
-
-
-
-
-
Traceability Readiness Score
-
-
{readinessScore}/100
-
- Blends KDE completeness, CTE lifecycle coverage, delivery quality, sandbox pass rate, and connection health.
-
-
-
-
-
- {exceptionCount ? (
-
- ) : (
-
- )}
-
Validation outcome
-
-
{validationOutcome}
-
- Ready: {exportReadyCount}. Warning: {warningLots.length}. Blocked: {blockedLots.length}. Records need CTE coverage, KDE completeness, lineage, and posted delivery before export.
-
-
-
-
-
-
-
Test completeness
-
-
{exportReadinessLabel}
-
- Test completeness uses the selected lot. Formal production records come later from signed in imports.
-
-
-
-
-
-
-
-
-
Lot readiness
-
Select a lot to inspect the traceability path and required FSMA 204 events.
- Uses the public stateless sandbox evaluator. Data is parsed, normalized, and checked against FSMA 204 rules without being stored or promoted to production ingestion.
-
- Scenarios are reusable preflight inputs for demos, supplier onboarding, implementation training, and contract regression checks before rules or mappings change.
-
-
-
- )}
-
- {activeTab === "Suppliers" && (
-
-
-
Supplier readiness
-
- Supplier state uses sample validation, mapping progress, repeated fixes, and export-ready lots to show who is blocking readiness.
-
- {isStandalone ? "Mock scenario loaded" : "No inbound records loaded"}
-
-
- {isStandalone
- ? "Run the pipeline to generate FSMA 204 traceability records."
- : "Load test data to validate FSMA 204 records before production ingestion."}
-
- CTE coverage {lot.completeCount} of {lot.totalCount}; delivery {lot.readiness.deliveryLabel}; last seen at {lot.lastLocation}
-
- {lot.missingCtes.length > 0 && (
-
Missing KDE evidence: {lot.missingCtes.join(", ")}
- )}
-
- ))}
-
-
- )}
-
- {activeTab === "Lineage" && (
-
-
-
-
-
Lineage for {selectedLot}
-
- CTE coverage {selectedCompleteCount} of {selectedLineage.length}; delivery {selectedReadiness.deliveryLabel}.
-
-
-
-
- {!selectedIsExportReady && (
-
- {selectedReadiness.missingCtes.length > 0
- ? `Capture missing KDE evidence for ${selectedReadiness.missingCtes.join(", ")} before this lot is counted as test complete.`
- : "Resolve delivery status before this lot is counted as test complete."}
-
- );
-}
diff --git a/frontend/src/app/tools/inflow-lab/components/InflowLabTutorial.tsx b/frontend/src/app/tools/inflow-lab/components/InflowLabTutorial.tsx
deleted file mode 100644
index f7ab76b9e..000000000
--- a/frontend/src/app/tools/inflow-lab/components/InflowLabTutorial.tsx
+++ /dev/null
@@ -1,224 +0,0 @@
-"use client";
-
-import { useCallback, useSyncExternalStore } from "react";
-import { ChevronRight, GraduationCap, X } from "lucide-react";
-
-import type { TutorialStep } from "@/components/ui/tutorial";
-
-// One key for the tutorial's own "don't show again" checkbox, a separate key
-// for the banner so dismissing the banner never suppresses the walkthrough
-// itself (it stays reachable from the header's "How to use" button).
-export const INFLOW_LAB_TUTORIAL_STORAGE_KEY = "inflow-lab-dashboard-tutorial";
-export const INFLOW_LAB_TUTORIAL_BANNER_KEY = "inflow-lab-dashboard-tutorial-banner-dismissed";
-// The standalone /tools/inflow-lab client carries its own tour with its own
-// dismissal state — finishing one surface's tour must not hide the other's.
-export const INFLOW_LAB_CLIENT_TUTORIAL_STORAGE_KEY = "inflow-lab-client-tutorial";
-export const INFLOW_LAB_CLIENT_TUTORIAL_BANNER_KEY = "inflow-lab-client-tutorial-banner-dismissed";
-
-// Step-by-step operating guide for the dashboard Inflow Lab tab. Targets are
-// stable anchors on the page: data-tutorial markers, the funnel test id, and
-// the selected-lot panel marker the tests already rely on.
-//
-// A builder rather than a constant: the "run your first test" step carries a
-// real action button wired to the page's guided proof, so the walkthrough can
-// evaluate the user's own rows and fill the panels its later steps describe.
-export function buildInflowLabTutorialSteps({
- runGuidedProof,
-}: {
- runGuidedProof: () => void;
-}): TutorialStep[] {
- return [
- {
- target: '[data-testid="inflow-evidence-chain"]',
- title: "What Inflow Lab is",
- content:
- "A sandbox for supplier traceability data. This is one lot's chain of custody, harvest through receiving. A solid link is a record we have; a broken one is a record that never arrived. The gate at the end is what a missing link costs you. Nothing here writes to production exports.",
- position: "bottom",
- },
- {
- target: '[data-tutorial="inflow-load-data"]',
- title: "Bring supplier data in",
- content:
- "Upload CSV takes your own supplier rows and opens them in the data editor below, where you can also paste rows directly. Nothing is preloaded: until you add data, the panels show No data yet.",
- position: "bottom",
- },
- {
- target: '[data-tutorial="inflow-guided-proof"]',
- title: "Run your first test",
- content:
- "This evaluates the rows you uploaded or pasted, end to end. The highlighted button is live, or use the one right here. The chain, pipeline, score, and lot panels fill with the results. With no rows loaded, it opens the data editor instead.",
- position: "bottom",
- action: "Run trace",
- onAction: runGuidedProof,
- },
- {
- target: '[data-testid="inflow-funnel"]',
- title: "Read the pipeline funnel",
- content:
- "Lots flow Loaded → Validated → Passing → Preview-ready → Production import. Wherever the numbers drop is where your data is losing evidence.",
- position: "top",
- },
- {
- target: '[data-tutorial="inflow-readiness"]',
- title: "Check the Data Preflight Score",
- content:
- "Five factors roll up into one readiness score. The “biggest gap” badge marks the factor holding you back. That is always the next thing to fix.",
- position: "right",
- },
- {
- target: '[data-tutorial="inflow-exceptions"]',
- title: "Clear exceptions by pattern",
- content:
- "Blocked lots are grouped by shared root cause: handoff, source, or mixed. Fix the pattern once (for example, map a missing handoff field) and the whole group unblocks. This list stays empty until your data brings real exceptions.",
- position: "top",
- },
- {
- target: "[data-selected-lot-panel]",
- title: "Trace one lot",
- content:
- "Click any lot code to see its five-step lifecycle and which CTE evidence is missing. When an evaluation looks right, Save test run in the data editor persists it to the Inflow Workbench (write access required). Reopen this guide any time from “How to use” in the header.",
- position: "left",
- },
- ];
-}
-
-// Operating guide for the standalone /tools/inflow-lab workbench (also used
-// by its embedded dashboard variant). Anchors are stable regardless of tab
-// state; the demo step carries a live action so the tour can produce the data
-// its later steps describe.
-export function buildInflowLabClientTutorialSteps({
- runDemo,
- isStandalone,
-}: {
- runDemo: () => void;
- isStandalone: boolean;
-}): TutorialStep[] {
- return [
- {
- target: 'section[aria-label="Inflow Lab environment boundary"]',
- title: "What Inflow Lab is",
- content:
- "A mock environment for supplier traceability data. Everything here uses test or uploaded records only. Formal production records start after signed-in import, so you can experiment freely.",
- position: "bottom",
- },
- {
- target: '[data-tutorial="lab-primary-actions"]',
- title: "Run your first demo",
- content: isStandalone
- ? "Load mock scenario fills the lab with a believable supplier feed; Run pipeline processes it end to end. The button here does both in one go and jumps to the event log when it finishes."
- : "Start guided test run loads a mock supplier feed if the lab is empty, processes it end to end, and jumps to the record log when it finishes.",
- position: "bottom",
- action: "Run the demo",
- onAction: runDemo,
- },
- {
- target: '[data-tutorial="lab-tabs"]',
- title: "Find your way around",
- content:
- "Each tab is a workspace: Data feeder tests your own CSV before import, Fix queue groups blocking defects, Lots and Lineage trace individual lot codes, and the log tab shows every record the pipeline produced.",
- position: "bottom",
- },
- {
- target: '[data-tutorial="lab-workspace"]',
- title: "Read the results",
- content:
- "Pipeline results land here: connection status, lot counts, and delivery outcomes on the overview, with the full detail one tab away. Reopen this guide any time from “How to use” in the header.",
- position: "top",
- },
- ];
-}
-
-// The dismissed flag lives in localStorage, which React must treat as an
-// external store: reading it during render would break SSR, and reading it in
-// an effect would cascade an extra render. useSyncExternalStore handles both —
-// the server snapshot reports "dismissed" so the server never renders a banner
-// the client might immediately remove.
-const bannerListeners = new Set<() => void>();
-
-function subscribeToBanner(onStoreChange: () => void) {
- bannerListeners.add(onStoreChange);
- return () => {
- bannerListeners.delete(onStoreChange);
- };
-}
-
-function readBannerDismissed(key: string): boolean {
- try {
- return window.localStorage.getItem(key) === "true";
- } catch {
- return false;
- }
-}
-
-function bannerDismissedOnServer() {
- return true;
-}
-
-// Completing (or explicitly skipping) the walkthrough is the banner's job
-// done — calling this retires it without requiring a separate dismissal.
-export function dismissInflowLabTutorialBanner(key: string = INFLOW_LAB_TUTORIAL_BANNER_KEY) {
- try {
- window.localStorage.setItem(key, "true");
- } catch {
- /* private mode — banner simply reappears next visit */
- }
- bannerListeners.forEach((listener) => listener());
-}
-
-export function InflowLabTutorialBanner({
- onStart,
- dismissKey = INFLOW_LAB_TUTORIAL_BANNER_KEY,
- description = "A guided walkthrough: load supplier data, run a sandbox test, read the readiness score, and clear blocked lots.",
-}: {
- onStart: () => void;
- dismissKey?: string;
- description?: string;
-}) {
- const readDismissed = useCallback(() => readBannerDismissed(dismissKey), [dismissKey]);
- const dismissed = useSyncExternalStore(
- subscribeToBanner,
- readDismissed,
- bannerDismissedOnServer,
- );
-
- const dismiss = useCallback(() => {
- dismissInflowLabTutorialBanner(dismissKey);
- }, [dismissKey]);
-
- if (dismissed) return null;
-
- return (
-
-
-
-
-
-
-
- New to Inflow Lab? Learn to operate it step by step
-
- {description}
-
-
- Start walkthrough
-
-
-
-
-
-
-
- );
-}
diff --git a/frontend/src/app/tools/inflow-lab/lib/exception-grouping.ts b/frontend/src/app/tools/inflow-lab/lib/exception-grouping.ts
deleted file mode 100644
index c2d034f69..000000000
--- a/frontend/src/app/tools/inflow-lab/lib/exception-grouping.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-export type ExceptionPatternId = "handoff" | "source" | "mixed";
-
-export type LotForGrouping = {
- lotCode: string;
- product: string;
- missingCtes: string[];
- readiness: {
- state: "ready" | "warning" | "blocked";
- deliveryLabel: string;
- exportReady: boolean;
- };
- events?: number;
- cases?: number;
- supplier?: string;
-};
-
-export type ExceptionGroup = {
- id: ExceptionPatternId;
- label: string;
- description: string;
- owner: string;
- bulkActionable: boolean;
- lots: TLot[];
-};
-
-const HANDOFF_LABELS = new Set(["Initial packing", "Shipping", "DC receiving"]);
-const SOURCE_LABELS = new Set(["Harvesting", "Cooling"]);
-
-export function classifyExceptionLot(lot: LotForGrouping): ExceptionPatternId {
- const missing = lot.missingCtes;
- const hasHandoff = missing.some((cte) => HANDOFF_LABELS.has(cte));
- const hasSource = missing.some((cte) => SOURCE_LABELS.has(cte));
-
- if (hasHandoff && hasSource) return "mixed";
- if (hasHandoff && !hasSource) return "handoff";
- if (hasSource && !hasHandoff) return "source";
- return "mixed";
-}
-
-export function groupExceptionsByPattern(
- lots: TLot[]
-): ExceptionGroup[] {
- const handoff: TLot[] = [];
- const source: TLot[] = [];
- const mixed: TLot[] = [];
-
- for (const lot of lots) {
- const pattern = classifyExceptionLot(lot);
- if (pattern === "handoff") handoff.push(lot);
- else if (pattern === "source") source.push(lot);
- else mixed.push(lot);
- }
-
- return [
- {
- id: "handoff",
- label: "Missing handoff evidence",
- description: "Handoff fields absent for initial packing, shipping, and DC receiving.",
- owner: "Shipping / receiving source",
- bulkActionable: handoff.length > 0,
- lots: handoff,
- },
- {
- id: "source",
- label: "Missing source evidence",
- description: "Harvesting and cooling KDEs absent at the source.",
- owner: "Supplier data owner",
- bulkActionable: source.length > 0,
- lots: source,
- },
- {
- id: "mixed",
- label: "Mixed / multi-cause",
- description: "Combinations across handoff and source. Treat individually.",
- owner: "Mixed",
- bulkActionable: false,
- lots: mixed,
- },
- ];
-}
-
-export type SourceSupplierBreakdown = {
- harvestSuppliers: { supplier: string; lotCount: number }[];
- coolingSuppliers: { supplier: string; lotCount: number }[];
- overlapCount: number;
-};
-
-export function summarizeSourceSuppliers(
- sourceLots: LotForGrouping[]
-): SourceSupplierBreakdown {
- const harvestBySupplier = new Map();
- const coolingBySupplier = new Map();
- let overlap = 0;
-
- for (const lot of sourceLots) {
- const supplier = lot.supplier ?? "Unknown supplier";
- const missingHarvest = lot.missingCtes.includes("Harvesting");
- const missingCooling = lot.missingCtes.includes("Cooling");
- if (missingHarvest) {
- harvestBySupplier.set(supplier, (harvestBySupplier.get(supplier) ?? 0) + 1);
- }
- if (missingCooling) {
- coolingBySupplier.set(supplier, (coolingBySupplier.get(supplier) ?? 0) + 1);
- }
- if (missingHarvest && missingCooling) {
- overlap += 1;
- }
- }
-
- return {
- harvestSuppliers: Array.from(harvestBySupplier, ([supplier, lotCount]) => ({
- supplier,
- lotCount,
- })),
- coolingSuppliers: Array.from(coolingBySupplier, ([supplier, lotCount]) => ({
- supplier,
- lotCount,
- })),
- overlapCount: overlap,
- };
-}
diff --git a/frontend/src/app/tools/inflow-lab/opengraph-image.tsx b/frontend/src/app/tools/inflow-lab/opengraph-image.tsx
deleted file mode 100644
index 604282c15..000000000
--- a/frontend/src/app/tools/inflow-lab/opengraph-image.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { renderOgCard, OG_SIZE, OG_CONTENT_TYPE } from '@/lib/og-image';
-
-export const runtime = 'edge';
-export const alt = 'Preflight supplier data before it becomes evidence';
-export const size = OG_SIZE;
-export const contentType = OG_CONTENT_TYPE;
-
-export default function Image() {
- return renderOgCard({ eyebrow: 'Inflow Lab', title: 'Preflight supplier data before it becomes evidence' });
-}
diff --git a/frontend/src/app/tools/inflow-lab/page.tsx b/frontend/src/app/tools/inflow-lab/page.tsx
index a9a4c7688..143179663 100644
--- a/frontend/src/app/tools/inflow-lab/page.tsx
+++ b/frontend/src/app/tools/inflow-lab/page.tsx
@@ -1,29 +1,5 @@
-import { Metadata } from "next";
-import { InflowLabClient } from "./components/InflowLabClient";
-import { FeatureNotEnabled } from "@/components/experimental/FeatureNotEnabled";
-import { isExperimentalToolEnabled } from "@/lib/experimental-features";
-
-export const metadata: Metadata = {
- title: "RegEngine Inflow Lab | Supplier Data Preflight Workbench",
- description:
- "Preflight supplier traceability data, validate FSMA 204 Critical Tracking Event (CTE) and Key Data Element (KDE) coverage, generate fix queues, replay scenarios, and gate production evidence commits.",
- openGraph: {
- title: "RegEngine Inflow Lab | Supplier Data Preflight Workbench",
- description: "Inflow prepares supplier data before the Engine commits validated, tenant-scoped FSMA 204 evidence.",
- type: "website",
- url: "https://www.regengine.co/tools/inflow-lab",
- },
-};
+import { notFound } from "next/navigation";
export default function InflowLabPage() {
- if (!isExperimentalToolEnabled("inflow-lab")) {
- return (
-
- );
- }
- return ;
+ notFound();
}
diff --git a/frontend/src/app/trust/architecture/page.tsx b/frontend/src/app/trust/architecture/page.tsx
index 7d97cd8ef..68d875ab6 100644
--- a/frontend/src/app/trust/architecture/page.tsx
+++ b/frontend/src/app/trust/architecture/page.tsx
@@ -89,7 +89,7 @@ const DATA_FLOW_STEPS = [
{
step: '5',
title: 'Export',
- description: 'Compliance records may be exported in one or more supported formats such as EPCIS 2.0, FDA-oriented spreadsheets, or CSV, depending on the enabled workflow.',
+ description: 'Compliance records may be exported in one or more supported formats such as EPCIS 2.0 (Preview), FDA-oriented spreadsheets, or CSV, depending on the enabled workflow.',
},
];
@@ -144,7 +144,7 @@ const SYSTEM_FLOW = [
{
Icon: Workflow,
title: 'Evidence exports',
- detail: 'FDA-oriented spreadsheets, EPCIS/CSV packages, and manifests where configured',
+ detail: 'FDA-oriented spreadsheets, EPCIS (Preview) and CSV packages, and manifests where configured',
boundary: 'Customer review and archive',
},
];
diff --git a/frontend/src/app/trust/page.tsx b/frontend/src/app/trust/page.tsx
index 623086758..f201255d4 100644
--- a/frontend/src/app/trust/page.tsx
+++ b/frontend/src/app/trust/page.tsx
@@ -138,7 +138,7 @@ const POSITIONING = [
},
{
title: 'Full data portability',
- detail: 'Supported workflows can export CTE events, KDE fields, lot records, and supplier relationships in EPCIS 2.0 JSON-LD, FDA-oriented sortable spreadsheet, or CSV.',
+ detail: 'Supported workflows can export CTE events, KDE fields, lot records, and supplier relationships in EPCIS 2.0 JSON-LD (Preview), FDA-oriented sortable spreadsheet, or CSV.',
},
{
title: 'Tenant isolation',
@@ -209,7 +209,7 @@ const EXPORT_FORMAT_GROUPS = [
{
title: 'Export formats',
items: [
- 'GS1 EPCIS 2.0 JSON-LD (industry standard)',
+ 'GS1 EPCIS 2.0 JSON-LD (Preview, industry standard)',
'FDA-oriented sortable spreadsheet aligned to 21 CFR 1.1455 where applicable',
'CSV with all CTE/KDE fields',
'Audit evidence bundle with manifest hashing',
@@ -374,7 +374,7 @@ export default function TrustCenterPage() {
diff --git a/frontend/src/app/trust/retention/page.tsx b/frontend/src/app/trust/retention/page.tsx
index 298d1a22c..408be8d74 100644
--- a/frontend/src/app/trust/retention/page.tsx
+++ b/frontend/src/app/trust/retention/page.tsx
@@ -56,7 +56,7 @@ const RETENTION_TIMELINE = [
];
const EXPORT_FORMATS = [
{
- format: 'EPCIS 2.0 JSON-LD',
+ format: 'EPCIS 2.0 JSON-LD (Preview)',
standard: 'GS1 industry standard',
useCase: 'Interoperable with other FSMA 204 platforms, trading partner systems, and FDA submissions',
icon: FileCheck,
@@ -96,7 +96,7 @@ const ARCHIVE_NOTES = [
},
{
title: 'No vendor lock-in',
- body: 'Exports are intended to use industry-standard formats such as EPCIS 2.0, FDA spreadsheet layouts, and CSV. Format coverage depends on the workflow enabled for your account.',
+ body: 'Exports are intended to use industry-standard formats such as EPCIS 2.0 (Preview), FDA spreadsheet layouts, and CSV. Format coverage depends on the workflow enabled for your account.',
},
];
diff --git a/frontend/src/app/walkthrough/page.tsx b/frontend/src/app/walkthrough/page.tsx
index a9d2525fb..9c9aa4939 100644
--- a/frontend/src/app/walkthrough/page.tsx
+++ b/frontend/src/app/walkthrough/page.tsx
@@ -94,7 +94,7 @@ const TIMELINE = [
packageDetails: [
{ label: 'Package', value: 'Immutable once assembled' },
{ label: 'SHA-256 manifest', value: 'Included with the package' },
- { label: 'Format', value: 'FDA 21 CFR 1.1455 sortable spreadsheet + EPCIS 2.0 JSON-LD' },
+ { label: 'Format', value: 'FDA 21 CFR 1.1455 sortable spreadsheet + EPCIS 2.0 JSON-LD (Preview)' },
],
},
{
diff --git a/frontend/src/app/why-regengine/page.tsx b/frontend/src/app/why-regengine/page.tsx
index 665ab68d5..7d6bd3587 100644
--- a/frontend/src/app/why-regengine/page.tsx
+++ b/frontend/src/app/why-regengine/page.tsx
@@ -70,7 +70,7 @@ const COMPARISON_DATA: ComparisonRow[] = [
repositrak: 'Verify with vendor',
},
{
- feature: 'EPCIS 2.0 export',
+ feature: 'EPCIS 2.0 export (Preview)',
regengine: true,
trustwell: 'Verify with vendor',
safetychain: 'Verify with vendor',
@@ -158,9 +158,9 @@ const DIFFERENTIATORS = [
},
{
icon: Workflow,
- title: 'EPCIS 2.0 export',
+ title: 'EPCIS 2.0 export (Preview)',
description:
- "EPCIS 2.0 export for trading-partner interoperability. Verify each enabled workflow against your retailer and buyer requirements.",
+ "EPCIS 2.0 (Preview) export for trading-partner interoperability. Verify each enabled workflow against your retailer and buyer requirements.",
detail: 'We started on EPCIS 2.0; verify EPCIS coverage with each vendor for your stack.',
},
{
@@ -582,17 +582,17 @@ HTTP 201
- EPCIS 2.0 export
+ EPCIS 2.0 export (Preview)
- EPCIS 2.0 can support trading-partner interoperability. RegEngine treats EPCIS 2.0 as a supported schema where the enabled workflow and source data allow it.
+ EPCIS 2.0 (Preview) can support trading-partner interoperability. RegEngine treats EPCIS 2.0 as a preview schema where the enabled workflow and source data allow it.
Verify EPCIS coverage with each vendor; some platforms layer EPCIS over legacy schemas, which can introduce conversion edges.
{[
- 'EPCIS 2.0 XML + JSON serialization',
+ 'EPCIS 2.0 XML + JSON serialization (Preview)',
'Traceability event export support',
'No third-party conversion needed',
'Designed for buyer and trading-partner interoperability checks',
diff --git a/frontend/src/components/control-plane/demo-banner.tsx b/frontend/src/components/control-plane/demo-banner.tsx
index 42cca6b3a..02c1e2625 100644
--- a/frontend/src/components/control-plane/demo-banner.tsx
+++ b/frontend/src/components/control-plane/demo-banner.tsx
@@ -1,5 +1,6 @@
'use client';
+import Link from 'next/link';
import { Callout } from '@/components/ui/callout';
export function DemoBanner({ visible }: { visible: boolean }) {
@@ -7,8 +8,11 @@ export function DemoBanner({ visible }: { visible: boolean }) {
return (
- Sample Data: Backend unavailable.
- Data will appear once services are connected.
+ No data yet: nothing
+ is connected for this view yet.{' '}
+
+ Connect real data on the Ingest page
+
);
diff --git a/frontend/src/components/dashboard/breadcrumb.tsx b/frontend/src/components/dashboard/breadcrumb.tsx
index afdfa11c1..d377f1f10 100644
--- a/frontend/src/components/dashboard/breadcrumb.tsx
+++ b/frontend/src/components/dashboard/breadcrumb.tsx
@@ -16,7 +16,6 @@ const LABEL_MAP: Record = {
'audit-log': 'Audit Log',
'export-jobs': 'Scheduled Exports',
heartbeat: 'Daily Heartbeat',
- 'inflow-lab': 'Inflow Lab',
integrations: 'Integrations',
issues: 'Issues',
notifications: 'Notification Preferences',
diff --git a/frontend/src/components/dashboard/metrics-overview-widget.tsx b/frontend/src/components/dashboard/metrics-overview-widget.tsx
index 1e3f1e394..700b06393 100644
--- a/frontend/src/components/dashboard/metrics-overview-widget.tsx
+++ b/frontend/src/components/dashboard/metrics-overview-widget.tsx
@@ -109,7 +109,7 @@ export function MetricsOverviewWidget({
emptyCaption={
servicesDown
? "Services unreachable"
- : "No saved Inflow Workbench run yet"
+ : "No saved import readiness run yet"
}
status={
score > 0 ? (
@@ -165,7 +165,7 @@ export function MetricsOverviewWidget({
empty={typeof openFixes !== "number"}
emptyLabel={readinessFailed ? "Couldn't load" : EMPTY_LABEL}
emptyAction={readinessFailed ? undefined : }
- emptyCaption="From the Inflow Workbench run"
+ emptyCaption="From the import readiness run"
/>
- Base plan includes 500 CTEs/month. Standard and Premium are unlimited. Need more on Base? Add CTEs at $0.002 each.{' '}
+ Base plan includes 500 CTEs/month. Standard and Premium are unlimited. Need more on Base? Additional CTE pricing is set in your agreement.{' '}
See Terms for full details.
@@ -194,7 +194,7 @@ export function PricingPageClient({
RegEngine is priced around facility operations, not a long enterprise procurement cycle.
- A GMA/FMI study found that the average major food recall costs companies over $10 million in lost product, logistics, and brand damage. RegEngine's Founding rate starts at $425/mo (billed annually).
+ A GMA/FMI study found that the average major food recall costs companies over $10 million in lost product, logistics, and brand damage. RegEngine's Founding rate is documented in your executed design-partner agreement.
@@ -251,7 +251,7 @@ export function PricingPageClient({
Become a founding design partner
- The Founding rate starts at $425/mo, billed annually. Apply now and get the product shaped around real supplier data.
+ Founding rates are documented in the executed design-partner agreement. Apply now and get the product shaped around real supplier data.
diff --git a/frontend/src/components/ui/tutorial.tsx b/frontend/src/components/ui/tutorial.tsx
index d2e19371d..997c8d317 100644
--- a/frontend/src/components/ui/tutorial.tsx
+++ b/frontend/src/components/ui/tutorial.tsx
@@ -256,8 +256,8 @@ export function Tutorial({ steps, isOpen, onClose, onComplete, storageKey }: Tut
: [{ key: 'full', style: { top: 0, left: 0, right: 0, bottom: 0 } }];
// Portaled to : the tour mounts inside arbitrary page containers, and
- // page-scoped CSS (e.g. the inflow-lab shim styling every direct-child div)
- // or transformed ancestors would otherwise re-style the fixed overlay.
+ // page-scoped CSS (e.g. a page shim styling every direct-child div) or
+ // transformed ancestors would otherwise re-style the fixed overlay.
return createPortal(
<>
{backdropPanels.map((panel) => (
diff --git a/frontend/src/lib/api-samples/ingest-inflow-lab.json b/frontend/src/lib/api-samples/ingest-inflow-lab.json
deleted file mode 100644
index 6686f79a2..000000000
--- a/frontend/src/lib/api-samples/ingest-inflow-lab.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "source": "inflow-lab",
- "events": [
- {
- "cte_type": "shipping",
- "traceability_lot_code": "TEST-ROM-0427-A",
- "product_description": "Romaine Lettuce",
- "quantity": 48,
- "unit_of_measure": "cases",
- "location_name": "Inflow Lab Test DC",
- "timestamp": "{{EVENT_TIMESTAMP}}",
- "kdes": {
- "ship_date": "{{EVENT_DATE}}",
- "ship_from_location": "Test Grower",
- "ship_to_location": "Test Retailer",
- "reference_document": "TEST-BOL-INFLOW-001",
- "tlc_source_reference": "TEST-GROWER-001",
- "carrier": "Inflow Lab Carrier"
- }
- }
- ]
-}
diff --git a/frontend/src/lib/api-samples/ingest-receiving.json b/frontend/src/lib/api-samples/ingest-receiving.json
deleted file mode 100644
index da7b63dd6..000000000
--- a/frontend/src/lib/api-samples/ingest-receiving.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "source": "erp",
- "events": [
- {
- "cte_type": "receiving",
- "traceability_lot_code": "TEST-00012345678901-LOT-001",
- "product_description": "Romaine Lettuce",
- "quantity": 500,
- "unit_of_measure": "cases",
- "location_name": "Distribution Center #4",
- "timestamp": "{{EVENT_TIMESTAMP}}",
- "kdes": {
- "receive_date": "{{EVENT_DATE}}",
- "receiving_location": "Distribution Center #4",
- "immediate_previous_source": "GreenLeaf Farms LLC",
- "reference_document": "TEST-ASN-00123",
- "tlc_source_reference": "TEST-GLF-001"
- }
- }
- ]
-}
diff --git a/frontend/src/lib/api-samples/ingest-samples.ts b/frontend/src/lib/api-samples/ingest-samples.ts
index 09e310c9e..8277935a8 100644
--- a/frontend/src/lib/api-samples/ingest-samples.ts
+++ b/frontend/src/lib/api-samples/ingest-samples.ts
@@ -1,21 +1,15 @@
import { API_BASE } from '@/lib/api-base';
-import ingestInflowLab from './ingest-inflow-lab.json';
-import ingestReceiving from './ingest-receiving.json';
-import ingestWalkthroughReceiving from './ingest-walkthrough-receiving.json';
-import ingestWalkthroughShipping from './ingest-walkthrough-shipping.json';
-import ingestWalkthroughTransformation from './ingest-walkthrough-transformation.json';
/**
- * Code samples for `POST /webhooks/ingest`, all rendered from the payloads in
- * this directory (`ingest-*.json`).
+ * Code samples for `POST /webhooks/ingest`.
*
- * Each JSON file keeps two placeholders instead of dates, so no page can ship a
- * timestamp that goes stale (events older than 90 days are rejected). Each
- * sample below swaps them for a time expression in its own language.
- * `tests/docs/test_ingest_samples_validate.py` fills the same placeholders with
- * "now minus 1 hour" and runs every `ingest-*.json` through the backend's real
- * ingest checks. A page that shows an ingest call renders it from here instead
- * of carrying its own payload.
+ * No sample carries an example record: a lot code, supplier, or quantity
+ * printed in the docs reads as real data, and an accepted lot is permanent.
+ * The body below is a skeleton of angle-bracket field descriptions to fill
+ * with your own records. `{{EVENT_TIMESTAMP}}` / `{{EVENT_DATE}}` stay
+ * placeholders so no page can ship a timestamp that goes stale (events older
+ * than 90 days are rejected); each sample swaps them for a clock expression
+ * in its own language.
*/
const TIMESTAMP_PLACEHOLDER = '"{{EVENT_TIMESTAMP}}"';
const DATE_PLACEHOLDER = '"{{EVENT_DATE}}"';
@@ -26,8 +20,31 @@ export const INGEST_ENV_SETUP = [
'export REGENGINE_SIGNING_SECRET="..." # from RegEngine', // pragma: allowlist secret
].join('\n');
-/** A sample payload as text (the receiving sample by default), with each quoted placeholder replaced by `timestamp` / `date` verbatim. */
-export function ingestSamplePayload(timestamp: string, date: string, payload: object = ingestReceiving): string {
+/** Request-body skeleton: field names are real, every value is a `<...>` description to replace with your own record. */
+const INGEST_BODY_SKELETON = {
+ source: '',
+ events: [
+ {
+ cte_type: 'receiving',
+ traceability_lot_code: '',
+ product_description: '',
+ quantity: '',
+ unit_of_measure: '',
+ location_name: '',
+ timestamp: '{{EVENT_TIMESTAMP}}',
+ kdes: {
+ receive_date: '{{EVENT_DATE}}',
+ receiving_location: '',
+ immediate_previous_source: '',
+ reference_document: '',
+ tlc_source_reference: '',
+ },
+ },
+ ],
+};
+
+/** The skeleton as text, with each quoted placeholder replaced by `timestamp` / `date` verbatim. */
+export function ingestSamplePayload(timestamp: string, date: string, payload: object = INGEST_BODY_SKELETON): string {
return JSON.stringify(payload, null, 2)
.split(TIMESTAMP_PLACEHOLDER)
.join(timestamp)
@@ -56,17 +73,7 @@ curl -X POST ${API_BASE}/webhooks/ingest \\
--data-binary "$BODY"`;
}
-export const INGEST_CURL_SAMPLE = signedCurl(ingestReceiving);
-
-/** Same recipe, other payloads: the three walkthrough steps (their own TEST-...-WALK lots) and the Inflow Lab connector page. */
-export const INGEST_WALKTHROUGH_RECEIVING_CURL_SAMPLE = signedCurl(ingestWalkthroughReceiving);
-export const INGEST_WALKTHROUGH_TRANSFORMATION_CURL_SAMPLE = signedCurl(ingestWalkthroughTransformation);
-export const INGEST_WALKTHROUGH_SHIPPING_CURL_SAMPLE = signedCurl(ingestWalkthroughShipping);
-export const INGEST_INFLOW_LAB_CURL_SAMPLE = signedCurl(ingestInflowLab);
-
-/** The walkthrough's receiving and shipping lot codes, for the prose around the samples. */
-export const WALKTHROUGH_RECEIVED_LOT = ingestWalkthroughReceiving.events[0].traceability_lot_code;
-export const WALKTHROUGH_BAGGED_LOT = ingestWalkthroughShipping.events[0].traceability_lot_code;
+export const INGEST_CURL_SAMPLE = signedCurl(INGEST_BODY_SKELETON);
export const INGEST_PYTHON_SAMPLE = `import hashlib
import hmac
@@ -120,22 +127,22 @@ const resp = await fetch("${API_BASE}/webhooks/ingest", {
});
console.log(resp.status, await resp.json());`;
-/** What a 200 looks like when the event is accepted. Hashes are shortened. */
+/** What a 200 looks like when the event is accepted. Values are `<...>` descriptions of what the API returns, not example records. */
export const INGEST_RESPONSE_SAMPLE = `{
"accepted": 1,
"rejected": 0,
"total": 1,
"events": [
{
- "traceability_lot_code": "${ingestReceiving.events[0].traceability_lot_code}",
+ "traceability_lot_code": "",
"cte_type": "receiving",
"status": "accepted",
- "event_id": "0b9c2f4e-6d1a-4c53-9a7e-5f3d8e21c7ab",
- "sha256_hash": "a3f2b891c4d5e6f7...",
- "chain_hash": "7f6e5d4c3b2a1908...",
+ "event_id": "",
+ "sha256_hash": "",
+ "chain_hash": "",
"errors": []
}
],
- "ingestion_timestamp": "2026-10-03T14:05:12.481203+00:00",
+ "ingestion_timestamp": "",
"ingestion_run_id": null
}`;
diff --git a/frontend/src/lib/api-samples/ingest-walkthrough-receiving.json b/frontend/src/lib/api-samples/ingest-walkthrough-receiving.json
deleted file mode 100644
index 0c0b690af..000000000
--- a/frontend/src/lib/api-samples/ingest-walkthrough-receiving.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "source": "manual-walkthrough",
- "events": [
- {
- "cte_type": "receiving",
- "traceability_lot_code": "TEST-00012345678901-WALK-001",
- "product_description": "Romaine Lettuce, whole heads",
- "quantity": 500,
- "unit_of_measure": "cases",
- "location_gln": "0012345600005",
- "location_name": "DC-4 Receiving Dock",
- "timestamp": "{{EVENT_TIMESTAMP}}",
- "kdes": {
- "receive_date": "{{EVENT_DATE}}",
- "receiving_location": "DC-4 Receiving Dock",
- "immediate_previous_source": "Supplier A",
- "reference_document": "TEST-ASN-00456",
- "tlc_source_reference": "TEST-SUPA-001"
- }
- }
- ]
-}
diff --git a/frontend/src/lib/api-samples/ingest-walkthrough-shipping.json b/frontend/src/lib/api-samples/ingest-walkthrough-shipping.json
deleted file mode 100644
index a3a22914d..000000000
--- a/frontend/src/lib/api-samples/ingest-walkthrough-shipping.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "source": "manual-walkthrough",
- "events": [
- {
- "cte_type": "shipping",
- "traceability_lot_code": "TEST-00012345678901-WALK-001-BAGGED",
- "product_description": "Romaine, washed + bagged, 10oz consumer pack",
- "quantity": 4800,
- "unit_of_measure": "units",
- "location_gln": "0012345600005",
- "location_name": "DC-4 Outbound",
- "timestamp": "{{EVENT_TIMESTAMP}}",
- "kdes": {
- "ship_date": "{{EVENT_DATE}}",
- "ship_from_location": "DC-4 Outbound",
- "ship_to_location": "Distributor B",
- "reference_document": "TEST-BOL-00789",
- "tlc_source_reference": "TEST-DC4-BAGGED-001"
- }
- }
- ]
-}
diff --git a/frontend/src/lib/api-samples/ingest-walkthrough-transformation.json b/frontend/src/lib/api-samples/ingest-walkthrough-transformation.json
deleted file mode 100644
index 79fa00cb3..000000000
--- a/frontend/src/lib/api-samples/ingest-walkthrough-transformation.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "source": "manual-walkthrough",
- "events": [
- {
- "cte_type": "transformation",
- "traceability_lot_code": "TEST-00012345678901-WALK-001-BAGGED",
- "product_description": "Romaine, washed + bagged, 10oz consumer pack",
- "quantity": 4800,
- "unit_of_measure": "units",
- "location_gln": "0012345600005",
- "location_name": "DC-4 Processing Line 2",
- "timestamp": "{{EVENT_TIMESTAMP}}",
- "kdes": {
- "transformation_date": "{{EVENT_DATE}}",
- "reference_document": "TEST-WO-00456",
- "input_lot_codes": ["TEST-00012345678901-WALK-001"],
- "inputs": [
- {
- "traceability_lot_code": "TEST-00012345678901-WALK-001",
- "quantity": 480,
- "unit_of_measure": "cases"
- }
- ],
- "transformation_type": "wash_and_bag"
- }
- }
- ]
-}
diff --git a/frontend/src/lib/customer-readiness.test.ts b/frontend/src/lib/customer-readiness.test.ts
index 5ff446b58..e851ad480 100644
--- a/frontend/src/lib/customer-readiness.test.ts
+++ b/frontend/src/lib/customer-readiness.test.ts
@@ -22,4 +22,12 @@ describe('customer readiness registry', () => {
it('exposes at least one public diligence artifact', () => {
expect(TRUST_ARTIFACTS.some((artifact) => artifact.access === 'public')).toBe(true);
});
+
+ it('never renders SFTP in display text for the csv-sftp key or csv_sftp_import label', () => {
+ expect(DELIVERY_MODE_LABELS.csv_sftp_import).not.toMatch(/sftp/i);
+ const entry = CAPABILITY_REGISTRY.find((item) => item.id === 'csv-sftp');
+ expect(entry).toBeDefined();
+ expect(entry?.name).not.toMatch(/sftp/i);
+ expect(entry?.customer_copy).not.toMatch(/sftp/i);
+ });
});
diff --git a/frontend/src/lib/customer-readiness.ts b/frontend/src/lib/customer-readiness.ts
index f3db557a3..cd4f2e25a 100644
--- a/frontend/src/lib/customer-readiness.ts
+++ b/frontend/src/lib/customer-readiness.ts
@@ -245,7 +245,7 @@ export const CAPABILITY_REGISTRY: CapabilityRegistryEntry[] = [
status: 'export_supported',
delivery_mode: 'export_only',
integration_type: 'one_way_export',
- customer_copy: 'RegEngine can prepare EPCIS-oriented export packages for retailer submission workflows.',
+ customer_copy: 'RegEngine can prepare EPCIS (Preview) export packages for retailer submission workflows.',
evidence_url: '/trust',
},
{
@@ -298,24 +298,14 @@ export const CAPABILITY_REGISTRY: CapabilityRegistryEntry[] = [
customer_copy: 'Submit CTE events as JSON to POST /api/v1/webhooks/ingest for validation and downstream normalization (one-way inbound).',
evidence_url: '/docs/api',
},
- {
- id: 'inflow-lab',
- name: 'Inflow Lab',
- category: 'developer_api',
- status: 'pilot',
- delivery_mode: 'native_api',
- integration_type: 'api_based_custom',
- customer_copy: 'RegEngine-owned FSMA 204 simulator that posts events to the JSON events API for demos, contract tests, and developer validation.',
- evidence_url: '/docs/connectors/inflow-lab',
- },
{
id: 'epcis',
- name: 'GS1 EPCIS 2.0',
+ name: 'GS1 EPCIS 2.0 (Preview)',
category: 'developer_api',
status: 'export_supported',
delivery_mode: 'export_only',
integration_type: 'one_way_export',
- customer_copy: 'JSON-LD EPCIS export is supported. EPCIS ingest is in Preview. Default deployments use PostgreSQL-backed task processing; legacy Kafka-compatible signing is opt-in.',
+ customer_copy: 'EPCIS export and ingest are in Preview. Default deployments use PostgreSQL-backed task processing; legacy Kafka-compatible signing is opt-in.',
evidence_url: '/developers',
},
{
@@ -447,7 +437,7 @@ export const RECALL_DRILL_RUNS: RecallDrillRun[] = [
dateRange: '2026-02-01 to 2026-02-28',
status: 'completed',
elapsed: '18m 24s',
- artifacts: ['FDA package', 'EPCIS export', 'drill report'],
+ artifacts: ['FDA package', 'EPCIS export (Preview)', 'drill report'],
warnings: [],
},
{
diff --git a/frontend/src/lib/doc-pages.ts b/frontend/src/lib/doc-pages.ts
index d86761fee..f44b595b6 100644
--- a/frontend/src/lib/doc-pages.ts
+++ b/frontend/src/lib/doc-pages.ts
@@ -12,7 +12,6 @@ export const DOC_PAGES: DocPage[] = [
{ title: 'Authentication', href: '/docs/authentication' },
{ title: 'API Reference', href: '/docs/api' },
{ title: 'Outbound webhooks (planned)', href: '/docs/webhooks' },
- { title: 'Inflow Lab', href: '/docs/connectors/inflow-lab' },
{ title: 'Error Codes', href: '/docs/errors' },
{ title: 'Rate Limits', href: '/docs/rate-limits' },
{ title: 'SDKs', href: '/docs/sdks' },
diff --git a/frontend/src/lib/inflow-error.ts b/frontend/src/lib/inflow-error.ts
deleted file mode 100644
index e9a73d9ef..000000000
--- a/frontend/src/lib/inflow-error.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export function inflowErrorMessage(status: number, payload: Record): string {
- if (typeof payload.error === 'string' && payload.error) return payload.error;
- if (typeof payload.detail === 'string' && payload.detail) return payload.detail;
- if (status === 404) return 'Inflow Lab is unavailable on this deployment. Core backend health does not include the simulator.';
- return `Inflow Lab request failed: ${status}`;
-}
diff --git a/frontend/src/lib/marketing-analytics-routes.ts b/frontend/src/lib/marketing-analytics-routes.ts
index 41c2579e7..8120505d5 100644
--- a/frontend/src/lib/marketing-analytics-routes.ts
+++ b/frontend/src/lib/marketing-analytics-routes.ts
@@ -22,8 +22,7 @@ const PUBLIC_MARKETING_SECTIONS = new Set([
]);
const PUBLIC_TOOL_SLUGS = new Set([
'anomaly-simulator', 'ask', 'cte-mapper', 'data-import', 'drill-simulator',
- 'export', 'fsma-unified', 'ftl-checker', 'inflow-lab',
- 'inflow-lab-dashboard-preview', 'kde-checker', 'knowledge-graph',
+ 'export', 'fsma-unified', 'ftl-checker', 'kde-checker', 'knowledge-graph',
'label-scanner', 'notice-validator', 'obligation-scanner',
'readiness-assessment', 'recall-readiness', 'records-request-checklist',
'retailer-readiness', 'roi-calculator', 'scan', 'self-reported-checklist',
diff --git a/frontend/tests/visual/routes.json b/frontend/tests/visual/routes.json
index 37edfd881..c396aa1ac 100644
--- a/frontend/tests/visual/routes.json
+++ b/frontend/tests/visual/routes.json
@@ -1 +1 @@
-{"_generated":"2026-10-23 from /tmp/re-all-routes.txt (155 non-empty lines) crosschecked against frontend/src/app/**/page.{tsx,jsx}; reconciled 2026-10-23 with the designer crawl (81 routes 200-no-redirect on production): 4 routes moved public->expectRedirectOr404 (/design-system, /tools/anomaly-simulator, /trust-center, /verticals/food-safety)","_note":"Single replaceable route data file for the visual suite. Public vs auth determined by frontend/src/proxy.ts requireAppAuth + AUTHENTICATED_APP_ROUTE_PREFIXES in frontend/src/lib/app-routes.ts + GATED_DEV_ROUTES (/developer/portal) + next.config.js redirects(). Signed-out behavior only.","authOnly":["/admin","/admin/api-console","/api-keys","/audit","/compliance/labels","/compliance/profile","/compliance/snapshots","/compliance/status","/compliance/traceability-plan","/controls","/dashboard","/dashboard/agent-reviews","/dashboard/alerts","/dashboard/audit-log","/dashboard/compliance","/dashboard/export-jobs","/dashboard/heartbeat","/dashboard/inflow-lab","/dashboard/integrations","/dashboard/issues","/dashboard/notifications","/dashboard/products","/dashboard/qa-health","/dashboard/recall-drills","/dashboard/recall-report","/dashboard/recall-response","/dashboard/receiving","/dashboard/scan","/dashboard/settings","/dashboard/suppliers","/dashboard/team","/developer/portal","/developer/portal/keys","/exceptions","/fsma","/fsma/assessment","/fsma/field-capture","/fsma/integration","/identity","/ingest","/ingest/curation","/onboarding","/onboarding/bulk-upload","/onboarding/setup","/onboarding/setup/facility","/onboarding/setup/ftl-check","/onboarding/setup/welcome","/records","/remediation","/requests","/review","/rules","/settings/notifications","/settings/profile","/settings/security","/settings/support","/sysadmin","/trace"],"baseURLEnv":"VISUAL_BASE_URL","excludedReason":{"/admin":"auth-gated: /admin prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/admin/api-console":"auth-gated: /admin prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/alpha":"redirect(): src/app/alpha/page.tsx redirects to /contact; asserted in expectRedirectOr404","/api-keys":"auth-gated transitively: bare redirect() to gated /developer/portal/keys (see GATED_DEV_ROUTES comment in proxy.ts)","/audit":"auth-gated: /audit prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/auth/sso/mock":"excluded entirely: mock SSO fixture page (CHROMELESS_EXACT_ROUTES); must never be screenshotted or visited by monitoring","/compliance":"redirect source: next.config.js 308 /compliance -> /dashboard/compliance; also auth-gated (/compliance prefix); asserted in expectRedirectOr404","/compliance/labels":"auth-gated: /compliance prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/compliance/profile":"auth-gated: /compliance prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/compliance/snapshots":"auth-gated: /compliance prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/compliance/status":"auth-gated: /compliance prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/compliance/traceability-plan":"auth-gated: /compliance prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/controls":"auth-gated: /controls prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/agent-reviews":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/alerts":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/audit-log":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/compliance":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/export-jobs":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/heartbeat":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/inflow-lab":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/integrations":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/issues":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/notifications":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/products":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/qa-health":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/recall-drills":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/recall-report":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/recall-response":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/receiving":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/scan":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/settings":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/suppliers":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/team":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/demo":"demo surface: asserted in expectRedirectOr404 (no baseline). /demo is removed by #3543: src/app/demo/page.tsx is deleted, so the expectRedirectOr404 assertion now passes","/design-system":"404 on production: src/app/design-system/page.tsx calls notFound() when NODE_ENV=production (Internal, development only); absent from the designer 200-no-redirect crawl; asserted in expectRedirectOr404","/developer/portal":"auth-gated: GATED_DEV_ROUTES in proxy.ts; signed-out proxy redirects to /login","/developer/portal/keys":"auth-gated: GATED_DEV_ROUTES in proxy.ts; signed-out proxy redirects to /login","/exceptions":"auth-gated: /exceptions prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/fsma":"auth-gated: /fsma prefix in AUTHENTICATED_APP_ROUTE_PREFIXES (exact + slash match only; does NOT cover /fsma-204); signed-out proxy redirects to /login","/fsma-204/[cte]":"dynamic [cte] param: no fixed URL to baseline; needs a param allowlist before it can be covered","/fsma/assessment":"auth-gated: /fsma prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/fsma/field-capture":"auth-gated: /fsma prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/fsma/integration":"auth-gated: /fsma prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/identity":"auth-gated: /identity prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/ingest":"auth-gated: /ingest prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/ingest/curation":"auth-gated: /ingest prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/ingest/results/[id]":"dynamic [id] param: no fixed URL to baseline; also auth-gated (/ingest prefix)","/onboarding":"auth-gated: /onboarding prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/onboarding/bulk-upload":"auth-gated: /onboarding prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/onboarding/setup":"auth-gated: /onboarding prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/onboarding/setup/facility":"auth-gated: /onboarding prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/onboarding/setup/ftl-check":"auth-gated: /onboarding prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/onboarding/setup/welcome":"auth-gated: /onboarding prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/portal/[portalId]":"dynamic [portalId] param: no fixed URL to baseline","/records":"auth-gated: /records prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/remediation":"auth-gated: /remediation prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/requests":"auth-gated: /requests prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/requests/[id]":"dynamic [id] param: no fixed URL to baseline; also auth-gated (/requests prefix)","/retailer-readiness":"redirect source: next.config.js 308 /retailer-readiness -> /buyer-programs; page.tsx exists but the redirect wins; asserted in expectRedirectOr404","/review":"auth-gated: /review prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/rules":"auth-gated: /rules prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/sandbox":"deleted in #3557: src/app/sandbox/ no longer exists, so the route is expected to 404; asserted in expectRedirectOr404","/settings":"redirect source: next.config.js 308 /settings -> /dashboard/settings; also auth-gated (/settings prefix); asserted in expectRedirectOr404","/settings/notifications":"auth-gated: /settings prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/settings/profile":"auth-gated: /settings prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/settings/security":"auth-gated: /settings prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/settings/support":"auth-gated: /settings prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/settings/users":"redirect source: next.config.js 308 /settings/users -> /dashboard/settings/users; has NO page.tsx (see ROUTE_CROSSCHECK.md); also auth-gated; asserted in expectRedirectOr404","/sysadmin":"auth-gated: /sysadmin prefix in AUTHENTICATED_APP_ROUTE_PREFIXES (+ sysadmin-only check); signed-out proxy redirects to /login","/tools/anomaly-simulator":"redirect(): src/app/tools/anomaly-simulator/page.tsx permanentRedirects to /tools/drill-simulator (in public); excluded from the designer 200-no-redirect crawl as redirecting; asserted in expectRedirectOr404","/tools/readiness-assessment":"redirect source: next.config.js 308 /tools/readiness-assessment -> /tools/self-reported-checklist; asserted in expectRedirectOr404","/tools/recall-readiness":"redirect source: next.config.js 308 /tools/recall-readiness -> /tools/records-request-checklist; asserted in expectRedirectOr404","/tools/retailer-readiness":"redirect source: page.tsx redirects to /retailer-readiness, which 308s to /buyer-programs; asserted in expectRedirectOr404","/trace":"auth-gated: /trace prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/trust-center":"redirect(): src/app/trust-center/page.tsx redirects to /trust (in public); excluded from the designer 200-no-redirect crawl as redirecting; asserted in expectRedirectOr404","/verticals/food-safety":"redirect(): src/app/verticals/food-safety/page.tsx redirects to /fsma-204 (in public); excluded from the designer 200-no-redirect crawl as redirecting; asserted in expectRedirectOr404"},"expectRedirectOr404":["/demo","/sandbox","/alpha","/retailer-readiness","/tools/recall-readiness","/tools/readiness-assessment","/tools/retailer-readiness","/settings","/settings/users","/compliance","/design-system","/tools/anomaly-simulator","/trust-center","/verticals/food-safety"],"priority":["/","/pricing","/integrations","/developers","/trust","/why-regengine","/proof","/tools/scan","/login","/signup"],"public":["/","/about","/accept-invite","/auth/verify","/blog","/blog/24-hour-rule","/blog/fda-food-traceability-requirements","/blog/food-traceability-software","/blog/fsma-204-compliance-checklist-2026","/blog/fsma-204-compliance-guide","/blog/fsma-204-cte-kde-guide","/blog/fsma-204-mid-market-guide","/blog/fsma-204-traceability-lot-codes","/blog/regengine-vs-spreadsheets-traceability","/buyer-programs","/case-studies","/compare/foodlogiq","/compare/repositrak","/compare/tracegains","/contact","/developers","/developers/compliance-verticals","/docs","/docs/api","/docs/authentication","/docs/changelog","/docs/connectors/inflow-lab","/docs/errors","/docs/fsma-204","/docs/quickstart","/docs/rate-limits","/docs/sdks","/docs/walkthrough","/docs/webhooks","/dpa","/fsma-204","/integrations","/legal/vulnerability-disclosure","/login","/pricing","/privacy","/product","/proof","/proof/recall-drill","/proof/transformation","/security","/signup","/status","/supplier-compliance","/terms","/tools","/tools/ask","/tools/cte-mapper","/tools/data-import","/tools/drill-simulator","/tools/export","/tools/fsma-unified","/tools/ftl-checker","/tools/inflow-lab","/tools/inflow-lab-dashboard-preview","/tools/kde-checker","/tools/knowledge-graph","/tools/label-scanner","/tools/notice-validator","/tools/obligation-scanner","/tools/records-request-checklist","/tools/roi-calculator","/tools/scan","/tools/self-reported-checklist","/tools/sop-generator","/tools/tlc-validator","/trust","/trust/architecture","/trust/retention","/trust/support","/verify","/walkthrough","/why-regengine"],"skippedDynamic":["/fsma-204/[cte]","/ingest/results/[id]","/portal/[portalId]","/requests/[id]"]}
+{"_generated":"2026-10-23 from /tmp/re-all-routes.txt (155 non-empty lines) crosschecked against frontend/src/app/**/page.{tsx,jsx}; reconciled 2026-10-23 with the designer crawl (81 routes 200-no-redirect on production): 4 routes moved public->expectRedirectOr404 (/design-system, /tools/anomaly-simulator, /trust-center, /verticals/food-safety)","_note":"Single replaceable route data file for the visual suite. Public vs auth determined by frontend/src/proxy.ts requireAppAuth + AUTHENTICATED_APP_ROUTE_PREFIXES in frontend/src/lib/app-routes.ts + GATED_DEV_ROUTES (/developer/portal) + next.config.js redirects(). Signed-out behavior only.","authOnly":["/admin","/admin/api-console","/api-keys","/audit","/compliance/labels","/compliance/profile","/compliance/snapshots","/compliance/status","/compliance/traceability-plan","/controls","/dashboard","/dashboard/agent-reviews","/dashboard/alerts","/dashboard/audit-log","/dashboard/compliance","/dashboard/export-jobs","/dashboard/heartbeat","/dashboard/inflow-lab","/dashboard/integrations","/dashboard/issues","/dashboard/notifications","/dashboard/products","/dashboard/qa-health","/dashboard/recall-drills","/dashboard/recall-report","/dashboard/recall-response","/dashboard/receiving","/dashboard/scan","/dashboard/settings","/dashboard/suppliers","/dashboard/team","/developer/portal","/developer/portal/keys","/exceptions","/fsma","/fsma/assessment","/fsma/field-capture","/fsma/integration","/identity","/ingest","/ingest/curation","/onboarding","/onboarding/bulk-upload","/onboarding/setup","/onboarding/setup/facility","/onboarding/setup/ftl-check","/onboarding/setup/welcome","/records","/remediation","/requests","/review","/rules","/settings/notifications","/settings/profile","/settings/security","/settings/support","/sysadmin","/trace"],"baseURLEnv":"VISUAL_BASE_URL","excludedReason":{"/admin":"auth-gated: /admin prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/admin/api-console":"auth-gated: /admin prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/alpha":"redirect(): src/app/alpha/page.tsx redirects to /contact; asserted in expectRedirectOr404","/api-keys":"auth-gated transitively: bare redirect() to gated /developer/portal/keys (see GATED_DEV_ROUTES comment in proxy.ts)","/audit":"auth-gated: /audit prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/auth/sso/mock":"excluded entirely: mock SSO fixture page (CHROMELESS_EXACT_ROUTES); must never be screenshotted or visited by monitoring","/compliance":"redirect source: next.config.js 308 /compliance -> /dashboard/compliance; also auth-gated (/compliance prefix); asserted in expectRedirectOr404","/compliance/labels":"auth-gated: /compliance prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/compliance/profile":"auth-gated: /compliance prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/compliance/snapshots":"auth-gated: /compliance prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/compliance/status":"auth-gated: /compliance prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/compliance/traceability-plan":"auth-gated: /compliance prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/controls":"auth-gated: /controls prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/agent-reviews":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/alerts":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/audit-log":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/compliance":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/export-jobs":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/heartbeat":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/inflow-lab":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/integrations":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/issues":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/notifications":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/products":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/qa-health":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/recall-drills":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/recall-report":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/recall-response":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/receiving":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/scan":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/settings":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/suppliers":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/dashboard/team":"auth-gated: /dashboard prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/demo":"demo surface: asserted in expectRedirectOr404 (no baseline). /demo is removed by #3543: src/app/demo/page.tsx is deleted, so the expectRedirectOr404 assertion now passes","/design-system":"404 on production: src/app/design-system/page.tsx calls notFound() when NODE_ENV=production (Internal, development only); absent from the designer 200-no-redirect crawl; asserted in expectRedirectOr404","/developer/portal":"auth-gated: GATED_DEV_ROUTES in proxy.ts; signed-out proxy redirects to /login","/developer/portal/keys":"auth-gated: GATED_DEV_ROUTES in proxy.ts; signed-out proxy redirects to /login","/exceptions":"auth-gated: /exceptions prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/fsma":"auth-gated: /fsma prefix in AUTHENTICATED_APP_ROUTE_PREFIXES (exact + slash match only; does NOT cover /fsma-204); signed-out proxy redirects to /login","/fsma-204/[cte]":"dynamic [cte] param: no fixed URL to baseline; needs a param allowlist before it can be covered","/fsma/assessment":"auth-gated: /fsma prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/fsma/field-capture":"auth-gated: /fsma prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/fsma/integration":"auth-gated: /fsma prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/identity":"auth-gated: /identity prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/ingest":"auth-gated: /ingest prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/ingest/curation":"auth-gated: /ingest prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/ingest/results/[id]":"dynamic [id] param: no fixed URL to baseline; also auth-gated (/ingest prefix)","/onboarding":"auth-gated: /onboarding prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/onboarding/bulk-upload":"auth-gated: /onboarding prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/onboarding/setup":"auth-gated: /onboarding prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/onboarding/setup/facility":"auth-gated: /onboarding prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/onboarding/setup/ftl-check":"auth-gated: /onboarding prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/onboarding/setup/welcome":"auth-gated: /onboarding prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/portal/[portalId]":"dynamic [portalId] param: no fixed URL to baseline","/proof/recall-drill":"deleted in #3551: src/app/proof/recall-drill/page.tsx no longer exists (recall drill simulation removed), so the route is expected to 404; asserted in expectRedirectOr404","/records":"auth-gated: /records prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/remediation":"auth-gated: /remediation prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/requests":"auth-gated: /requests prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/requests/[id]":"dynamic [id] param: no fixed URL to baseline; also auth-gated (/requests prefix)","/retailer-readiness":"redirect source: next.config.js 308 /retailer-readiness -> /buyer-programs; page.tsx exists but the redirect wins; asserted in expectRedirectOr404","/review":"auth-gated: /review prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/rules":"auth-gated: /rules prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/sandbox":"deleted in #3557: src/app/sandbox/ no longer exists, so the route is expected to 404; asserted in expectRedirectOr404","/settings":"redirect source: next.config.js 308 /settings -> /dashboard/settings; also auth-gated (/settings prefix); asserted in expectRedirectOr404","/settings/notifications":"auth-gated: /settings prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/settings/profile":"auth-gated: /settings prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/settings/security":"auth-gated: /settings prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/settings/support":"auth-gated: /settings prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/settings/users":"redirect source: next.config.js 308 /settings/users -> /dashboard/settings/users; has NO page.tsx (see ROUTE_CROSSCHECK.md); also auth-gated; asserted in expectRedirectOr404","/sysadmin":"auth-gated: /sysadmin prefix in AUTHENTICATED_APP_ROUTE_PREFIXES (+ sysadmin-only check); signed-out proxy redirects to /login","/tools/anomaly-simulator":"redirect(): src/app/tools/anomaly-simulator/page.tsx permanentRedirects to /tools/drill-simulator (in public); excluded from the designer 200-no-redirect crawl as redirecting; asserted in expectRedirectOr404","/tools/readiness-assessment":"redirect source: next.config.js 308 /tools/readiness-assessment -> /tools/self-reported-checklist; asserted in expectRedirectOr404","/tools/recall-readiness":"redirect source: next.config.js 308 /tools/recall-readiness -> /tools/records-request-checklist; asserted in expectRedirectOr404","/tools/retailer-readiness":"redirect source: page.tsx redirects to /retailer-readiness, which 308s to /buyer-programs; asserted in expectRedirectOr404","/trace":"auth-gated: /trace prefix in AUTHENTICATED_APP_ROUTE_PREFIXES; signed-out proxy redirects to /login","/trust-center":"redirect(): src/app/trust-center/page.tsx redirects to /trust (in public); excluded from the designer 200-no-redirect crawl as redirecting; asserted in expectRedirectOr404","/verticals/food-safety":"redirect(): src/app/verticals/food-safety/page.tsx redirects to /fsma-204 (in public); excluded from the designer 200-no-redirect crawl as redirecting; asserted in expectRedirectOr404"},"expectRedirectOr404":["/demo","/sandbox","/alpha","/retailer-readiness","/tools/recall-readiness","/tools/readiness-assessment","/tools/retailer-readiness","/settings","/settings/users","/compliance","/design-system","/tools/anomaly-simulator","/trust-center","/verticals/food-safety","/proof/recall-drill"],"priority":["/","/pricing","/integrations","/developers","/trust","/why-regengine","/proof","/tools/scan","/login","/signup"],"public":["/","/about","/accept-invite","/auth/verify","/blog","/blog/24-hour-rule","/blog/fda-food-traceability-requirements","/blog/food-traceability-software","/blog/fsma-204-compliance-checklist-2026","/blog/fsma-204-compliance-guide","/blog/fsma-204-cte-kde-guide","/blog/fsma-204-mid-market-guide","/blog/fsma-204-traceability-lot-codes","/blog/regengine-vs-spreadsheets-traceability","/buyer-programs","/case-studies","/compare/foodlogiq","/compare/repositrak","/compare/tracegains","/contact","/developers","/developers/compliance-verticals","/docs","/docs/api","/docs/authentication","/docs/changelog","/docs/connectors/inflow-lab","/docs/errors","/docs/fsma-204","/docs/quickstart","/docs/rate-limits","/docs/sdks","/docs/walkthrough","/docs/webhooks","/dpa","/fsma-204","/integrations","/legal/vulnerability-disclosure","/login","/pricing","/privacy","/product","/proof","/proof/transformation","/security","/signup","/status","/supplier-compliance","/terms","/tools","/tools/ask","/tools/cte-mapper","/tools/data-import","/tools/drill-simulator","/tools/export","/tools/fsma-unified","/tools/ftl-checker","/tools/inflow-lab","/tools/inflow-lab-dashboard-preview","/tools/kde-checker","/tools/knowledge-graph","/tools/label-scanner","/tools/notice-validator","/tools/obligation-scanner","/tools/records-request-checklist","/tools/roi-calculator","/tools/scan","/tools/self-reported-checklist","/tools/sop-generator","/tools/tlc-validator","/trust","/trust/architecture","/trust/retention","/trust/support","/verify","/walkthrough","/why-regengine"],"skippedDynamic":["/fsma-204/[cte]","/ingest/results/[id]","/portal/[portalId]","/requests/[id]"]}
From 0b8ddf3d34b5a87ef0e4ce009ccd9bcf7c311f71 Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Sat, 24 Oct 2026 20:18:41 -0700
Subject: [PATCH 6/9] fix(ci): Engine Review Pass picks up real PASS markers
(#3577)
* fix(ci): Engine Review Pass reads issue comment markers and allows pi, deepseek, devops
Real PASS markers on #3553, #3547, #3530 and #3527 were ignored: pi was not an
allowed engine, issue comment markers were never read, and devops/ branches
failed as an invalid author engine. Also rerun on PR issue comments.
Signed-off-by: Christopher Sellers <1@christophersellers.com>
* ci(engine-review-pass): drop devops, add hermes, require exact full sha
Signed-off-by: Christopher Sellers <1@christophersellers.com>
---------
Signed-off-by: Christopher Sellers <1@christophersellers.com>
---
.github/workflows/engine-review-pass.yml | 51 +++++++++++++++++++++---
1 file changed, 46 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/engine-review-pass.yml b/.github/workflows/engine-review-pass.yml
index 87ddb7d42..e84025c1f 100644
--- a/.github/workflows/engine-review-pass.yml
+++ b/.github/workflows/engine-review-pass.yml
@@ -7,15 +7,18 @@ on:
types: [opened, synchronize, reopened]
pull_request_review:
types: [submitted, dismissed]
+ issue_comment:
+ types: [created, edited]
# Review bursts must not cancel in-progress runs.
concurrency:
- group: engine-review-pass-${{ github.event.pull_request.number }}
+ group: engine-review-pass-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: false
permissions:
statuses: write
pull-requests: read
+ issues: read
contents: read
jobs:
@@ -30,7 +33,20 @@ jobs:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
- const pr = context.payload.pull_request;
+ let pr = context.payload.pull_request;
+
+ if (!pr) {
+ if (!context.payload.issue?.pull_request) {
+ core.info('Issue comment is not on a pull request. Skipping.');
+ return;
+ }
+ const prResponse = await github.rest.pulls.get({
+ owner,
+ repo,
+ pull_number: context.payload.issue.number,
+ });
+ pr = prResponse.data;
+ }
if (!pr) {
core.setFailed('Could not determine pull request from event payload.');
@@ -50,6 +66,9 @@ jobs:
'grok',
'muse',
'kimi',
+ 'pi',
+ 'deepseek',
+ 'hermes',
'visual-designer',
'product-designer',
]);
@@ -107,7 +126,29 @@ jobs:
core.info(`Total reviews retrieved: ${reviews.length}`);
- const eligibleReviews = reviews.filter((r) => {
+ core.info(`Fetching comments for pull request #${prNumber}...`);
+
+ const comments = await github.paginate(github.rest.issues.listComments, {
+ owner,
+ repo,
+ issue_number: prNumber,
+ per_page: 100,
+ });
+
+ core.info(`Total issue comments retrieved: ${comments.length}`);
+
+ const normalizedComments = comments.map((c) => ({
+ id: c.id,
+ body: c.body,
+ submitted_at: c.created_at,
+ author_association: c.author_association,
+ user: c.user,
+ state: 'COMMENTED',
+ }));
+
+ const allReviews = reviews.concat(normalizedComments);
+
+ const eligibleReviews = allReviews.filter((r) => {
if (r.state === 'DISMISSED' || !r.submitted_at || !r.body) {
return false;
}
@@ -131,7 +172,7 @@ jobs:
});
// The marker must be alone on its own line.
- const markerRegex = /^REVIEW: (PASS|BLOCKED|FAIL) ([A-Za-z0-9._-]+) ([0-9a-f]{7,40})\s*$/;
+ const markerRegex = /^REVIEW: (PASS|BLOCKED|FAIL) ([A-Za-z0-9._-]+) ([0-9a-f]{40})\s*$/;
const headShaLower = headSha.toLowerCase();
const authorEngineLower = authorEngine.toLowerCase();
@@ -154,7 +195,7 @@ jobs:
continue;
}
- if (!headShaLower.startsWith(markerSha.toLowerCase())) {
+ if (markerSha.toLowerCase() !== headShaLower) {
core.info(`Ignoring marker for non-matching SHA ${markerSha} on head ${headSha}`);
continue;
}
From f97397996a14368f7b64458ab521e4c7178ea7db Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Sat, 24 Oct 2026 20:19:54 -0700
Subject: [PATCH 7/9] feat(copy): SEL-452 copy gate with capabilities registry
and baseline ratchet (#3549)
* feat(copy): SEL-452 copy gate with four-status capabilities registry and baseline ratchet
Signed-off-by: Christopher Sellers <1@christophersellers.com>
* ci(copy): copy-gate path filters include copy-gate.yml
Signed-off-by: Christopher Sellers <1@christophersellers.com>
* fix(copy): gate against main's baseline so a PR cannot loosen the ratchet
Signed-off-by: Christopher Sellers <1@christophersellers.com>
* fix(copy-gate): reject unparseable registry, fail on baseline lookup errors, diff pushes against event.before
* chore(copy-lint): refresh baseline after main removed sandbox copy (SEL-452)
Signed-off-by: Christopher Sellers <1@christophersellers.com>
* chore(copy-gate): refresh copy baseline after merging main (counts only dropped)
* fix(schema): include public.data_gap_events in expected schema snapshot
Regenerate docs/schema/expected_schema.json from a fresh alembic upgrade head so the RLS isolation suite schema parity check stops reporting EXTRA TABLE for the v135 data gap migration.
Co-authored-by: Cursor
---------
Signed-off-by: Christopher Sellers <1@christophersellers.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor
---
.github/workflows/copy-gate.yml | 58 ++++++
docs/schema/expected_schema.json | 49 +++++
frontend/package.json | 1 +
frontend/src/lib/capabilities.ts | 12 +-
scripts/copy-lint/baseline.json | 266 ++++++++++++++++++++++++++
scripts/copy-lint/check-copy.mjs | 84 +++++++-
scripts/copy-lint/check-copy.test.mjs | 137 ++++++++++---
scripts/workflow_inventory.txt | 1 +
8 files changed, 574 insertions(+), 34 deletions(-)
create mode 100644 .github/workflows/copy-gate.yml
create mode 100644 scripts/copy-lint/baseline.json
diff --git a/.github/workflows/copy-gate.yml b/.github/workflows/copy-gate.yml
new file mode 100644
index 000000000..ceaeaec75
--- /dev/null
+++ b/.github/workflows/copy-gate.yml
@@ -0,0 +1,58 @@
+name: Copy gate
+
+on:
+ pull_request:
+ paths:
+ - 'frontend/**'
+ - 'scripts/copy-lint/**'
+ - '.github/workflows/copy-gate.yml'
+ - 'scripts/copy-lint/baseline.json'
+ push:
+ branches: [main]
+ paths:
+ - 'frontend/**'
+ - 'scripts/copy-lint/**'
+ - '.github/workflows/copy-gate.yml'
+ - 'scripts/copy-lint/baseline.json'
+
+permissions:
+ contents: read
+
+jobs:
+ gate:
+ name: Copy gate
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+ - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
+ with:
+ node-version: '20'
+ - run: node --test scripts/copy-lint/check-copy.test.mjs
+ - name: Read base copy baseline
+ env:
+ BASE_REF: ${{ github.base_ref }}
+ BEFORE_SHA: ${{ github.event.before }}
+ run: |
+ if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
+ base_ref="origin/${BASE_REF}"
+ else
+ if [[ "$BEFORE_SHA" =~ ^0{40}$ ]]; then
+ exit 0
+ fi
+ base_ref="$BEFORE_SHA"
+ fi
+ base_commit=$(git rev-parse --verify "${base_ref}^{commit}") || exit 1
+ baseline_path="scripts/copy-lint/baseline.json"
+ if git cat-file -e "${base_commit}:${baseline_path}" 2>/dev/null; then
+ git show "${base_commit}:${baseline_path}" > "$RUNNER_TEMP/base-baseline.json" || exit 1
+ else
+ baseline_entry=$(git ls-tree -r --name-only "$base_commit" -- "$baseline_path") || exit 1
+ if [ -n "$baseline_entry" ]; then
+ echo "Unable to read base copy baseline" >&2
+ exit 1
+ fi
+ fi
+ - run: node scripts/copy-lint/check-copy.mjs . --gate --github --base-baseline "$RUNNER_TEMP/base-baseline.json"
diff --git a/docs/schema/expected_schema.json b/docs/schema/expected_schema.json
index 97739280e..e59024e33 100644
--- a/docs/schema/expected_schema.json
+++ b/docs/schema/expected_schema.json
@@ -5525,6 +5525,55 @@
}
]
},
+ "public.data_gap_events": {
+ "columns": {
+ "closed_at": {
+ "nullable": true,
+ "type": "timestamp"
+ },
+ "closing_record_hash": {
+ "nullable": true,
+ "type": "text"
+ },
+ "closing_record_id": {
+ "nullable": true,
+ "type": "uuid"
+ },
+ "cte_type": {
+ "nullable": false,
+ "type": "text"
+ },
+ "gap_key": {
+ "nullable": false,
+ "type": "text"
+ },
+ "gap_kind": {
+ "nullable": false,
+ "type": "text"
+ },
+ "id": {
+ "nullable": false,
+ "type": "uuid"
+ },
+ "lot_code": {
+ "nullable": false,
+ "type": "text"
+ },
+ "opened_at": {
+ "nullable": false,
+ "type": "timestamp"
+ },
+ "supplier_key": {
+ "nullable": false,
+ "type": "text"
+ },
+ "tenant_id": {
+ "nullable": false,
+ "type": "uuid"
+ }
+ },
+ "foreign_keys": []
+ },
"public.food_traceability_list": {
"columns": {
"applicable_ctes": {
diff --git a/frontend/package.json b/frontend/package.json
index 1089ebec2..272542906 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -31,6 +31,7 @@
"src/lib/capabilities.ts"
]
},
+ "//knip": "src/lib/capabilities.ts is read by scripts/copy-lint/check-copy.mjs (the SEL-452 copy gate registry), not imported by frontend code.",
"//": "Pinning policy (#569): next, eslint-config-next, @sentry/nextjs, and @supabase/supabase-js are pinned to exact versions (no ^ or ~ prefix) because breaking changes in these packages have caused production incidents. Bump them deliberately after testing. All other deps use ^ for minor/patch auto-updates.",
"dependencies": {
"@radix-ui/react-checkbox": "^1.4.1",
diff --git a/frontend/src/lib/capabilities.ts b/frontend/src/lib/capabilities.ts
index efa00ece8..af57ba1c3 100644
--- a/frontend/src/lib/capabilities.ts
+++ b/frontend/src/lib/capabilities.ts
@@ -2,5 +2,15 @@
export const capabilities = {
live: ['CSV upload', 'JSON events API'],
preview: ['EPCIS intake', 'EPCIS export'],
- neverShown: ['EDI', 'SFTP'],
+ planned: [],
+ off: ['EDI', 'SFTP'],
} as const;
+
+export type CapabilityStatus = keyof typeof capabilities;
+
+export function statusOf(name: string): CapabilityStatus | undefined {
+ for (const status of ['live', 'preview', 'planned', 'off'] as const) {
+ if ((capabilities[status] as readonly string[]).includes(name)) return status;
+ }
+ return undefined;
+}
diff --git a/scripts/copy-lint/baseline.json b/scripts/copy-lint/baseline.json
new file mode 100644
index 000000000..5aee75702
--- /dev/null
+++ b/scripts/copy-lint/baseline.json
@@ -0,0 +1,266 @@
+{
+ "frontend/src/app/auth/verify/page.tsx": {
+ "empty-render": 2
+ },
+ "frontend/src/app/blog/fda-food-traceability-requirements/page.tsx": {
+ "epcis-unlabeled": 1
+ },
+ "frontend/src/app/blog/food-traceability-software/page.tsx": {
+ "epcis-unlabeled": 1,
+ "sample-demo": 2
+ },
+ "frontend/src/app/blog/fsma-204-compliance-checklist-2026/page.tsx": {
+ "invented-entity": 2
+ },
+ "frontend/src/app/blog/fsma-204-compliance-guide/page.tsx": {
+ "epcis-unlabeled": 2
+ },
+ "frontend/src/app/blog/fsma-204-cte-kde-guide/page.tsx": {
+ "epcis-unlabeled": 6
+ },
+ "frontend/src/app/blog/regengine-vs-spreadsheets-traceability/page.tsx": {
+ "epcis-unlabeled": 1
+ },
+ "frontend/src/app/compliance/page.tsx": {
+ "epcis-unlabeled": 1
+ },
+ "frontend/src/app/contact/layout.tsx": {
+ "sample-demo": 2
+ },
+ "frontend/src/app/controls/page.tsx": {
+ "epcis-unlabeled": 2
+ },
+ "frontend/src/app/dashboard/compliance/page.tsx": {
+ "epcis-unlabeled": 2
+ },
+ "frontend/src/app/dashboard/export-jobs/page.tsx": {
+ "epcis-unlabeled": 4
+ },
+ "frontend/src/app/dashboard/integrations/page.tsx": {
+ "empty-render": 1,
+ "sample-demo": 3
+ },
+ "frontend/src/app/dashboard/layout.tsx": {
+ "sample-demo": 2,
+ "sandbox": 1
+ },
+ "frontend/src/app/dashboard/qa-health/page.tsx": {
+ "dash-placeholder": 1
+ },
+ "frontend/src/app/dashboard/recall-drills/page.tsx": {
+ "epcis-unlabeled": 1
+ },
+ "frontend/src/app/dashboard/recall-report/page.tsx": {
+ "dash-placeholder": 4,
+ "invented-entity": 12,
+ "sample-demo": 1
+ },
+ "frontend/src/app/dashboard/recall-response/page.tsx": {
+ "dash-placeholder": 1
+ },
+ "frontend/src/app/dashboard/receiving/page.tsx": {
+ "em-dash": 1
+ },
+ "frontend/src/app/dashboard/settings/users/page.tsx": {
+ "empty-render": 1
+ },
+ "frontend/src/app/design-system/page.tsx": {
+ "invented-entity": 1,
+ "sample-demo": 1,
+ "sandbox": 1
+ },
+ "frontend/src/app/developer/portal/page.tsx": {
+ "empty-render": 1
+ },
+ "frontend/src/app/developers/page.tsx": {
+ "epcis-unlabeled": 1
+ },
+ "frontend/src/app/docs/api/ApiReferenceClient.tsx": {
+ "hyphen-dash": 8
+ },
+ "frontend/src/app/docs/changelog/page.tsx": {
+ "epcis-unlabeled": 1,
+ "sample-demo": 1
+ },
+ "frontend/src/app/docs/connectors/inflow-lab/page.tsx": {
+ "sandbox": 6
+ },
+ "frontend/src/app/docs/quickstart/page.tsx": {
+ "sample-demo": 1
+ },
+ "frontend/src/app/docs/walkthrough/page.tsx": {
+ "sample-demo": 1
+ },
+ "frontend/src/app/exceptions/page.tsx": {
+ "dash-placeholder": 3,
+ "empty-render": 2
+ },
+ "frontend/src/app/fsma-204/[cte]/page.tsx": {
+ "invented-entity": 1
+ },
+ "frontend/src/app/fsma/page.tsx": {
+ "sample-demo": 1
+ },
+ "frontend/src/app/identity/page.tsx": {
+ "dash-placeholder": 2,
+ "empty-render": 1
+ },
+ "frontend/src/app/ingest/page.tsx": {
+ "epcis-unlabeled": 6,
+ "sample-demo": 1,
+ "sandbox": 3
+ },
+ "frontend/src/app/ingest/results/[id]/AnalysisResultClient.tsx": {
+ "sample-demo": 1
+ },
+ "frontend/src/app/ingest/sandbox-handoff.ts": {
+ "sandbox": 6
+ },
+ "frontend/src/app/integrations/page.tsx": {
+ "epcis-unlabeled": 1
+ },
+ "frontend/src/app/legal/vulnerability-disclosure/page.tsx": {
+ "sample-demo": 1
+ },
+ "frontend/src/app/onboarding/bulk-upload/page.tsx": {
+ "sample-demo": 1
+ },
+ "frontend/src/app/pricing/page.tsx": {
+ "epcis-unlabeled": 1
+ },
+ "frontend/src/app/product/page.tsx": {
+ "sandbox": 2
+ },
+ "frontend/src/app/proof/page.tsx": {
+ "epcis-unlabeled": 7,
+ "sample-demo": 5
+ },
+ "frontend/src/app/proof/recall-drill/page.tsx": {
+ "invented-entity": 6,
+ "sample-demo": 7
+ },
+ "frontend/src/app/records/page.tsx": {
+ "dash-placeholder": 3,
+ "empty-render": 1,
+ "epcis-unlabeled": 2
+ },
+ "frontend/src/app/requests/page.tsx": {
+ "em-dash": 1
+ },
+ "frontend/src/app/rules/page.tsx": {
+ "em-dash": 1,
+ "empty-render": 2
+ },
+ "frontend/src/app/security/page.tsx": {
+ "sample-demo": 1
+ },
+ "frontend/src/app/sysadmin/page.tsx": {
+ "dash-placeholder": 3
+ },
+ "frontend/src/app/tools/ask/page.tsx": {
+ "sample-demo": 6
+ },
+ "frontend/src/app/tools/data-import/components/DataImportClient.tsx": {
+ "sample-demo": 2
+ },
+ "frontend/src/app/tools/drill-simulator/components/DrillSimulatorClient.tsx": {
+ "sample-demo": 3
+ },
+ "frontend/src/app/tools/export/page.tsx": {
+ "empty-render": 2,
+ "sample-demo": 6
+ },
+ "frontend/src/app/tools/fsma-unified/components/UnifiedDashboardClient.tsx": {
+ "sample-demo": 2
+ },
+ "frontend/src/app/tools/inflow-lab/components/DashboardInflowLab.tsx": {
+ "invented-entity": 5,
+ "sandbox": 14
+ },
+ "frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx": {
+ "invented-entity": 15,
+ "sample-demo": 4,
+ "sandbox": 19
+ },
+ "frontend/src/app/tools/inflow-lab/components/InflowLabTutorial.tsx": {
+ "sample-demo": 2,
+ "sandbox": 2
+ },
+ "frontend/src/app/tools/notice-validator/page.tsx": {
+ "invented-entity": 1,
+ "sample-demo": 2
+ },
+ "frontend/src/app/tools/scan/page.tsx": {
+ "empty-render": 1
+ },
+ "frontend/src/app/tools/sop-generator/components/SOPGeneratorClient.tsx": {
+ "invented-entity": 2
+ },
+ "frontend/src/app/tools/tlc-validator/components/TLCValidatorClient.tsx": {
+ "sample-demo": 1
+ },
+ "frontend/src/app/trust/architecture/page.tsx": {
+ "epcis-unlabeled": 2
+ },
+ "frontend/src/app/trust/page.tsx": {
+ "epcis-unlabeled": 3
+ },
+ "frontend/src/app/trust/retention/page.tsx": {
+ "epcis-unlabeled": 2
+ },
+ "frontend/src/app/walkthrough/page.tsx": {
+ "epcis-unlabeled": 1
+ },
+ "frontend/src/app/why-regengine/page.tsx": {
+ "epcis-unlabeled": 7
+ },
+ "frontend/src/components/control-plane/demo-banner.tsx": {
+ "sample-demo": 1
+ },
+ "frontend/src/components/dashboard/compliance-status-widget.tsx": {
+ "empty-render": 1
+ },
+ "frontend/src/components/dashboard/metrics-overview-widget.tsx": {
+ "em-dash": 1,
+ "empty-render": 1
+ },
+ "frontend/src/components/fsma/FSMAToolShell.tsx": {
+ "sample-demo": 1
+ },
+ "frontend/src/components/layout/breadcrumbs.tsx": {
+ "empty-render": 1
+ },
+ "frontend/src/components/lead-gate/LeadGate.tsx": {
+ "empty-render": 1
+ },
+ "frontend/src/components/marketing/sandbox-grid/diagnosis.ts": {
+ "sandbox": 1
+ },
+ "frontend/src/components/mobile/FieldCaptureClient.tsx": {
+ "dash-placeholder": 5
+ },
+ "frontend/src/lib/capabilities.ts": {
+ "edi-sftp": 2
+ },
+ "frontend/src/lib/customer-readiness.ts": {
+ "epcis-unlabeled": 4
+ },
+ "frontend/src/lib/dev-compliance-snapshots.ts": {
+ "dash-placeholder": 2
+ },
+ "frontend/src/lib/epcis-upload.ts": {
+ "epcis-unlabeled": 4
+ },
+ "frontend/src/lib/mock-tenants.ts": {
+ "sample-demo": 1
+ },
+ "frontend/src/lib/roi-calculator-data.ts": {
+ "hyphen-dash": 4
+ },
+ "frontend/src/lib/trust-subprocessors.ts": {
+ "sample-demo": 1
+ },
+ "frontend/src/lib/utils.ts": {
+ "dash-placeholder": 1
+ }
+}
diff --git a/scripts/copy-lint/check-copy.mjs b/scripts/copy-lint/check-copy.mjs
index 43f84f7c1..632166a4b 100644
--- a/scripts/copy-lint/check-copy.mjs
+++ b/scripts/copy-lint/check-copy.mjs
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// RegEngine copy rule check. Node only, no dependencies.
-// Usage: node check-copy.mjs [repoRoot] [--rules copy-rules.json] [--allow allowlist.json] [--json] [--github] [--strict]
+// Usage: node check-copy.mjs [repoRoot] [--rules copy-rules.json] [--allow allowlist.json] [--baseline baseline.json] [--base-baseline main-baseline.json] [--json] [--github] [--strict] [--gate] [--update-baseline]
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -91,11 +91,19 @@ const word = s => new RegExp(`(? m[1]);
- if (!groups[name].length) throw new Error(`Empty ${name} capability registry`);
+ if (name !== 'planned' && !groups[name].length) throw new Error(`Empty ${name} capability registry`);
+ for (const feature of groups[name]) {
+ if (seen.has(feature)) throw new Error(`Duplicate capability status for ${feature}`);
+ seen.add(feature);
+ }
}
return groups;
}
@@ -122,7 +130,7 @@ export function check(seg, srcLine, cfg, capabilities, copyLine = '') {
if (t.includes('\u2013')) add('en-dash', t);
}
if (/[A-Za-z0-9.,)"'%] - [A-Za-z0-9("'$]/.test(t) && !/calc\(/.test(t) && !(seg.kind === 'str' && looksLikeCode(t))) add('hyphen-dash', t);
- if (capabilities.neverShown.some(name => new RegExp(`(^|[^A-Za-z0-9_])${escapeRegex(name)}(?![A-Za-z0-9_])`).test(t)) && !(seg.kind === 'str' && /^[A-Z_]+$/.test(t.trim()) && t.trim().length > 4)) add('edi-sftp', t);
+ if (capabilities.off.some(name => new RegExp(`(^|[^A-Za-z0-9_])${escapeRegex(name)}(?![A-Za-z0-9_])`).test(t)) && !(seg.kind === 'str' && /^[A-Z_]+$/.test(t.trim()) && t.trim().length > 4)) add('edi-sftp', t);
const previewNames = [...new Set(capabilities.preview.map(name => name.split(/\s+/)[0]))];
if (previewNames.some(name => new RegExp(`\\b${escapeRegex(name)}\\b`).test(t)) && !/Preview/i.test(t) && !/Preview/i.test(srcLine)) add('epcis-unlabeled', t);
if ((seg.kind !== 'str' && /\b(NaN|undefined)\b/.test(t)) || (seg.kind === 'str' && /\b(NaN|undefined)\b/.test(t) && /\s/.test(t.trim()) && !/typeof|===|!==/.test(t))) add('nan-undefined', t);
@@ -130,7 +138,7 @@ export function check(seg, srcLine, cfg, capabilities, copyLine = '') {
cfg.denylist.forEach(d => { if (low.includes(d.toLowerCase())) add('invented-entity', d); });
const copy = copyText(seg, srcLine);
if (copy) {
- const nonLive = [...capabilities.preview, ...capabilities.neverShown];
+ const nonLive = [...capabilities.preview, ...capabilities.planned, ...capabilities.off];
const capability = nonLive.find(name => word(name).test(copy) || (name.startsWith('EPCIS ') && word('EPCIS').test(copy)));
if (capability && (/\b(?:Live|Implemented|GA|Available)\b/.test(copyLine || copy) || (/\bstatus\s*:\s*['"]available['"]/.test(srcLine) && /\bavailable\b/.test(copyLine || copy)))) add('live-claim', capability);
if (copy.trim() === 'Pilot' && /\b(?:badge|status)\b/i.test(srcLine)) add('pilot-status', copy);
@@ -186,12 +194,59 @@ export function summarize(findings, filesScanned, cfg) {
return { total: findings.length, filesWithFindings: new Set(findings.map(x => x.file)).size, filesScanned, byRule };
}
+export function countsByFileRule(findings) {
+ const counts = {};
+ for (const { file, rule } of findings) {
+ counts[file] ??= {};
+ counts[file][rule] = (counts[file][rule] || 0) + 1;
+ }
+ return Object.fromEntries(Object.keys(counts).sort().map(file => [
+ file, Object.fromEntries(Object.keys(counts[file]).sort().map(rule => [rule, counts[file][rule]])),
+ ]));
+}
+
+export function compareBaseline(current, baseline) {
+ const regressions = [];
+ const reductions = [];
+ for (const file of [...new Set([...Object.keys(current), ...Object.keys(baseline)])].sort()) {
+ const actualRules = current[file] || {};
+ const baselineRules = baseline[file] || {};
+ for (const rule of [...new Set([...Object.keys(actualRules), ...Object.keys(baselineRules)])].sort()) {
+ const actual = actualRules[rule] || 0;
+ const previous = baselineRules[rule] || 0;
+ if (actual > previous) regressions.push({ file, rule, baseline: previous, current: actual });
+ if (actual < previous) reductions.push({ file, rule, baseline: previous, current: actual });
+ }
+ }
+ return { regressions, reductions };
+}
+
+function flatCounts(counts) {
+ return Object.fromEntries(Object.entries(counts).flatMap(([file, rules]) =>
+ Object.entries(rules).map(([rule, count]) => [`${file}|${rule}`, count])));
+}
+
+export function gateCompare(base, pr, actual) {
+ const errors = [];
+ for (const key of [...new Set([...Object.keys(base), ...Object.keys(pr), ...Object.keys(actual)])].sort()) {
+ const [file, rule] = key.split('|');
+ const mainCount = base[key] ?? 0;
+ const prCount = pr[key] ?? 0;
+ const actualCount = actual[key] ?? 0;
+ const counts = `main=${mainCount}, PR baseline=${prCount}, actual=${actualCount}`;
+ if (actualCount > mainCount) errors.push(`file=${file}, rule=${rule}: count went up vs main (${counts})`);
+ if (prCount > mainCount) errors.push(`file=${file}, rule=${rule}: PR baseline loosened vs main (${counts})`);
+ if (prCount !== actualCount) errors.push(`file=${file}, rule=${rule}: baseline.json is stale, run --update-baseline (${counts})`);
+ }
+ return { ok: errors.length === 0, errors };
+}
+
const annotationEscape = s => String(s).replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
const propertyEscape = s => annotationEscape(s).replace(/:/g, '%3A').replace(/,/g, '%2C');
function main(args) {
const opt = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d; };
- const pos = args.filter((a, i) => !a.startsWith('--') && !(i > 0 && ['--rules', '--allow'].includes(args[i - 1])));
+ const pos = args.filter((a, i) => !a.startsWith('--') && !(i > 0 && ['--rules', '--allow', '--baseline', '--base-baseline'].includes(args[i - 1])));
const repo = path.resolve(pos[0] || '.');
const cfg = JSON.parse(fs.readFileSync(opt('--rules', path.join(here, 'copy-rules.json')), 'utf8'));
const allowPath = opt('--allow', path.join(here, 'allowlist.json'));
@@ -202,6 +257,12 @@ function main(args) {
const files = cfg.roots.flatMap(r => walk(path.join(repo, r), [], cfg));
const findings = files.flatMap(f => scanSource(fs.readFileSync(f, 'utf8'), path.relative(repo, f), cfg, capabilities, allow));
const summary = summarize(findings, files.length, cfg);
+ const baselinePath = path.resolve(repo, opt('--baseline', 'scripts/copy-lint/baseline.json'));
+ const current = countsByFileRule(findings);
+ if (args.includes('--update-baseline')) {
+ fs.writeFileSync(baselinePath, `${JSON.stringify(current, null, 2)}\n`);
+ console.log(`Updated copy baseline: ${baselinePath}`);
+ }
if (args.includes('--json')) console.log(JSON.stringify({ summary, findings }, null, 1));
else {
for (const x of findings) {
@@ -213,6 +274,17 @@ function main(args) {
console.log(`\n${summary.total} copy rule violations in ${summary.filesWithFindings} files (${summary.filesScanned} scanned)`);
for (const [rule, count] of Object.entries(summary.byRule)) console.log(` ${rule}: ${count} ${cfg.rules[rule].message}`);
}
+ if (args.includes('--gate')) {
+ const pr = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
+ const basePath = opt('--base-baseline');
+ const resolvedBasePath = basePath && path.resolve(repo, basePath);
+ const baseText = resolvedBasePath && fs.existsSync(resolvedBasePath) ? fs.readFileSync(resolvedBasePath, 'utf8') : '';
+ if (!baseText.trim()) console.log('Main has no baseline yet (bootstrap); using the PR baseline as main.');
+ const base = baseText.trim() ? JSON.parse(baseText) : pr;
+ const result = gateCompare(flatCounts(base), flatCounts(pr), flatCounts(current));
+ for (const error of result.errors) console.log(`Copy gate: ${error}`);
+ if (!result.ok) process.exitCode = 1;
+ }
if (summary.total && (args.includes('--strict') || process.env.COPY_LINT_STRICT === '1')) process.exitCode = 1;
}
diff --git a/scripts/copy-lint/check-copy.test.mjs b/scripts/copy-lint/check-copy.test.mjs
index 1b3751f63..c056a4021 100644
--- a/scripts/copy-lint/check-copy.test.mjs
+++ b/scripts/copy-lint/check-copy.test.mjs
@@ -3,66 +3,149 @@ import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
-import { check, parseCapabilities, scanSource, summarize } from './check-copy.mjs';
+import { check, compareBaseline, countsByFileRule, gateCompare, parseCapabilities, scanSource, summarize } from './check-copy.mjs';
const here = path.dirname(fileURLToPath(import.meta.url));
const cfg = JSON.parse(fs.readFileSync(path.join(here, 'copy-rules.json'), 'utf8'));
const registryText = fs.readFileSync(path.resolve(here, '../../frontend/src/lib/capabilities.ts'), 'utf8');
const capabilities = parseCapabilities(registryText);
const rules = (src, rel = 'frontend/src/app/page.tsx') => scanSource(src, rel, cfg, capabilities).map(x => x.rule);
+const liveName = capabilities.live[0];
+const previewTerm = capabilities.preview[0].split(' ')[0];
+const [sampleTerm, demoTerm] = 'sample-demo'.split('-');
+const sandboxTerm = 'sandbox';
+const registrySource = groups => Object.entries(groups).map(([status, names]) =>
+ `${status}: [${names.map(name => `'${name}'`).join(', ')}]`).join(',\n');
test('rule sources and capability registry are complete', () => {
- assert.deepEqual(Object.keys(capabilities), ['live', 'preview', 'neverShown']);
- assert.equal(capabilities.preview.includes('EPCIS export'), true);
- assert.equal(capabilities.live.includes('CSV upload'), true);
- assert.equal(capabilities.neverShown.includes('SFTP'), true);
+ assert.deepEqual(Object.keys(capabilities), ['live', 'preview', 'planned', 'off']);
+ assert.equal(capabilities.live.length > 0 && capabilities.preview.length > 0 && capabilities.off.length > 0, true);
+ assert.deepEqual(parseCapabilities(registrySource(capabilities)), capabilities);
assert.equal(Object.values(cfg.rules).every(rule => ['visual-designer', 'product-designer', 'both'].includes(rule.source)), true);
});
+test('a feature cannot have two capability statuses', () => {
+ const duplicate = { ...capabilities, planned: [capabilities.live[0]] };
+ assert.throws(() => parseCapabilities(registrySource(duplicate)), /Duplicate capability status/);
+});
+
+test('a double-quoted planned capability is rejected', () => {
+ const invalid = registrySource({ ...capabilities, planned: ['X'] }).replace("planned: ['X']", 'planned: ["X"]');
+ assert.throws(() => parseCapabilities(invalid), /Unparseable planned capability registry/);
+});
+
test('VD dash and preview rules remain, with stricter empty values', () => {
- assert.deepEqual(rules('
`, 'frontend/src/app/page.tsx', cfg, capabilities, [
+ { file: 'frontend/src/app/page.tsx', rule: 'sample-demo', match: demoTerm },
]);
assert.deepEqual(findings, []);
assert.equal(summarize(findings, 1, cfg).byRule['sample-demo'], 0);
assert.deepEqual(check({ text: 'NaN value', kind: 'str' }, '', cfg, capabilities).map(x => x.rule), ['nan-undefined']);
});
+
+test('copy gate detects regressions, accepts equal counts and reductions', () => {
+ const file = path.relative(path.resolve(here, '../..'), path.resolve(here, '../../frontend/src/lib/capabilities.ts'));
+ const rule = Object.keys(cfg.rules)[0];
+ const findings = [{ file, rule }];
+ const baseline = countsByFileRule(findings);
+ assert.deepEqual(compareBaseline(baseline, baseline), { regressions: [], reductions: [] });
+ assert.deepEqual(compareBaseline(countsByFileRule([...findings, ...findings]), baseline).regressions,
+ [{ file, rule, baseline: 1, current: 2 }]);
+ assert.deepEqual(compareBaseline({}, baseline).reductions,
+ [{ file, rule, baseline: 1, current: 0 }]);
+ assert.deepEqual(compareBaseline(baseline, {}).regressions,
+ [{ file, rule, baseline: 0, current: 1 }]);
+});
+
+const gateFile = path.relative(path.resolve(here, '../..'), path.resolve(here, '../../frontend/src/lib/capabilities.ts'));
+const gateRule = Object.keys(cfg.rules)[0];
+const gateFinding = { file: gateFile, rule: gateRule };
+const gateCounts = findings => Object.fromEntries(Object.entries(countsByFileRule(findings)).flatMap(([file, rules]) =>
+ Object.entries(rules).map(([rule, count]) => [`${file}|${rule}`, count])));
+const noFindings = gateCounts([]);
+const oneFinding = gateCounts([gateFinding]);
+const twoFindings = gateCounts([gateFinding, gateFinding]);
+
+test('gateCompare fails when actual count rises', () => {
+ const result = gateCompare(oneFinding, oneFinding, twoFindings);
+ const key = `${gateFile}|${gateRule}`;
+ const counts = `main=${oneFinding[key]}, PR baseline=${oneFinding[key]}, actual=${twoFindings[key]}`;
+ assert.equal(result.ok, false);
+ assert.equal(result.errors.some(error => error.includes('count went up vs main')), true);
+ assert.equal(result.errors.every(error => error.includes(gateFile) && error.includes(gateRule)
+ && error.includes(counts)), true);
+});
+
+test('gateCompare fails for a new key absent from main', () => {
+ const result = gateCompare(noFindings, oneFinding, oneFinding);
+ assert.equal(result.ok, false);
+ assert.equal(result.errors.some(error => error.includes('count went up vs main')), true);
+});
+
+test('gateCompare rejects a PR baseline loosened above main', () => {
+ const result = gateCompare(noFindings, oneFinding, noFindings);
+ assert.equal(result.ok, false);
+ assert.equal(result.errors.some(error => error.includes('PR baseline loosened vs main')), true);
+});
+
+test('gateCompare requires a baseline update after a drop', () => {
+ const result = gateCompare(oneFinding, oneFinding, noFindings);
+ assert.equal(result.ok, false);
+ assert.equal(result.errors.some(error => error.includes('baseline.json is stale, run --update-baseline')), true);
+});
+
+test('gateCompare accepts equal counts', () => {
+ assert.deepEqual(gateCompare(oneFinding, oneFinding, oneFinding), { ok: true, errors: [] });
+});
+
+test('gateCompare accepts a drop with a matching baseline update', () => {
+ assert.deepEqual(gateCompare(oneFinding, noFindings, noFindings), { ok: true, errors: [] });
+});
diff --git a/scripts/workflow_inventory.txt b/scripts/workflow_inventory.txt
index fcc63f216..debf71681 100644
--- a/scripts/workflow_inventory.txt
+++ b/scripts/workflow_inventory.txt
@@ -33,6 +33,7 @@ agent-sweep.yml
api-surface-issue.yml
backend-ci.yml
bundle-analysis.yml
+copy-gate.yml
copy-lint.yml
dependabot-lock-sync.yml
deploy.yml
From 0aafb5595436c391571af1dcb4a48ba55f6a8bde Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Sat, 24 Oct 2026 20:27:46 -0700
Subject: [PATCH 8/9] chore(schema): snapshot
supplier_facilities.agreed_sending_hours (v136)
Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
---
docs/schema/expected_schema.json | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/docs/schema/expected_schema.json b/docs/schema/expected_schema.json
index e59024e33..95c52ff48 100644
--- a/docs/schema/expected_schema.json
+++ b/docs/schema/expected_schema.json
@@ -6807,6 +6807,10 @@
},
"public.supplier_facilities": {
"columns": {
+ "agreed_sending_hours": {
+ "nullable": true,
+ "type": "integer"
+ },
"city": {
"nullable": false,
"type": "text"
From c77c4005c573bacfe0e88ac1123c78966926bd54 Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Sat, 24 Oct 2026 20:37:42 -0700
Subject: [PATCH 9/9] fix(copy): label EPCIS as Preview in supplier quality and
ingest, drop walkthrough dash
Refresh copy-lint baseline.json so it matches current counts.
Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
---
.../src/__tests__/pages/ingest-epcis.test.tsx | 2 +-
.../app/dashboard/suppliers/data-quality.tsx | 2 +-
frontend/src/app/docs/walkthrough/page.tsx | 4 +-
frontend/src/app/ingest/page.tsx | 2 +-
scripts/copy-lint/baseline.json | 88 ++-----------------
5 files changed, 12 insertions(+), 86 deletions(-)
diff --git a/frontend/src/__tests__/pages/ingest-epcis.test.tsx b/frontend/src/__tests__/pages/ingest-epcis.test.tsx
index 9b8136c4c..ccdacf7aa 100644
--- a/frontend/src/__tests__/pages/ingest-epcis.test.tsx
+++ b/frontend/src/__tests__/pages/ingest-epcis.test.tsx
@@ -136,7 +136,7 @@ describe('EPCIS 2.0 import on /ingest', () => {
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(screen.getByText('CSV uploads skip bad rows. EPCIS (Preview) files are all or nothing: if any event fails, nothing is saved.')).toBeInTheDocument();
expect(document.getElementById('epcis-file-upload')).toHaveAttribute(
'accept',
'.json,application/json,.xml,application/xml,text/xml',
diff --git a/frontend/src/app/dashboard/suppliers/data-quality.tsx b/frontend/src/app/dashboard/suppliers/data-quality.tsx
index 78a479db9..e6be4fa42 100644
--- a/frontend/src/app/dashboard/suppliers/data-quality.tsx
+++ b/frontend/src/app/dashboard/suppliers/data-quality.tsx
@@ -17,7 +17,7 @@ const METRIC_COPY: RecordA working curl with openssl, and ~15 minutes
- The flow: one receiving event, one transformation event, one shipping event —
- three events in a single supplier-to-shipping chain. Fill each request-body
+ The flow: one receiving event, one transformation event, one shipping event.
+ That is three events in a single supplier-to-shipping chain. Fill each request-body
skeleton with your own lot codes, locations, and quantities, then list the
events and export them.
diff --git a/frontend/src/app/ingest/page.tsx b/frontend/src/app/ingest/page.tsx
index ce5b9d41b..b30ade7ea 100644
--- a/frontend/src/app/ingest/page.tsx
+++ b/frontend/src/app/ingest/page.tsx
@@ -692,7 +692,7 @@ export default function IngestPage() {
- CSV uploads skip bad rows. EPCIS files are all or nothing: if any event fails, nothing is saved.
+ CSV uploads skip bad rows. EPCIS (Preview) files are all or nothing: if any event fails, nothing is saved.