GFS Platform

Decision log

Architecture Decision Records — short entries on the why behind structural choices.

Decisions
28
Source
decisions.json

How to read this

Architecture Decision Records (ADR) — short entries on the why behind structural choices. Append-only; supersede with a new entry rather than editing.

Each entry has the same shape: Context (what we were trying to solve), Decision (what we chose), Consequences (what we accepted in return). If a decision is reversed, add a new entry that supersedes the old one — never delete history.

Records

ADR-001 — Use Cloudflare D1 (SQLite) as the read-mirror of NetSuite, not Postgres

2026-05-18 · Accepted

Context: Need a queryable mirror of NetSuite data outside the NS Suitelet sandbox. Worker → D1 keeps all platform code on one Cloudflare account; no separate database hosting.

Decision: D1 SQLite. Bound to the Worker as DB binding.

Consequences: Pros: zero ops, point-in-time recovery available, queryable from Worker with sub-ms latency. Cons: no enforced FKs in current schema, single-region writes, 10GB per-DB limit (currently 185K rows, well under).

ADR-002 — Sync runs locally via sync.sh, not as a Worker scheduled() handler

2026-05-18 · Superseded by ADR-019

Context: Initial sync needed to ship fast. Chartstone Pro exposes a localhost SuiteAPI passthrough that already authenticates to NetSuite.

Decision: sync.sh on launchd every 15 min, calling Chartstone (localhost:56411) and pushing to D1 via wrangler.

Consequences: Pros: works today, no NS integration role to provision. Cons: single-Mac dependency, no observability, sync stops if laptop is off. Tracked as Tier 1 risk + Tier 2 GAP for migration to Worker-side scheduled() handler calling SuiteAPI directly.

ADR-003 — Cloudflare Worker is the only public API

2026-05-18 · Accepted

Context: Frontends (Pages dashboards, internal Suitelets) need REST access to the D1 mirror. NetSuite Suitelet calls are slow and rate-limited.

Decision: Single Worker at api.ai-globalfoodsolutions.co/* exposing GET endpoints. CORS allowlist + Bearer API_KEY auth. CSP/HSTS security headers on every response.

Consequences: Pros: one codebase, one auth model, one deploy. Cons: every endpoint runs under the same API_KEY — finer-grained scopes would need per-route token validation.

ADR-004 — Consolidate to one repo with five fixed UI surfaces

2026-05-19 · Accepted

Context: Prior structure had 3 dashboards, 2 master guides, 5 stub redirects, and overlapping references. Made it hard to find anything.

Decision: Single repo at ~/Desktop/gfs-platform. Five fixed UI surfaces (index router + admin-dashboard, netsuite-reference, flows, data-model, infrastructure), two operating docs (GAPS, BLUEPRINT), one runbook, one decisions log. All non-load-bearing originals preserved under archive/.

Consequences: Pros: single source of truth, single builder, single rebrand path. Cons: any new content has to fit one of the fixed surfaces — resists feature creep, which is the point.

ADR-005 — Brand config lives in one JSON block inlined into every HTML

2026-05-19 · Accepted

Context: Rebrand/restyle should be a single-config change, not a project-wide refactor.

Decision: assets/brand.json holds all brand strings (name, domains, colors, fonts). build_pages.py inlines it into every generated HTML inside <script id="brand" type="application/json">. Single search/replace renames the system end to end.

Consequences: Pros: rename is one file edit + one rebuild. Cons: pages must be regenerated after brand changes — markdown edits are live, HTML surfaces need the builder.

ADR-006 — Out-of-scope: revenue, sales, profit, customer-facing material

2026-05-19 · Accepted

Context: Platform is an admin and operating guide. Prior version mixed admin tools with sales KPI dashboards.

Decision: All revenue/sales/profit/customer-facing artifacts removed from the active surfaces. Preserved under archive/2026-05/ for reference but not surfaced in any new page.

Consequences: Pros: clear audience, no double-purpose pages. Cons: a sales-facing dashboard, if needed, has to live in a separate project — not this one.

ADR-007 — No external CDN / library dependencies in deployed HTML

2026-05-19 · Accepted

Context: Single-file HTML is the user's standard. External CDNs add load-time risk and an external trust boundary.

Decision: Every HTML inlines its CSS, brand block, and any JS. Diagrams are inline SVG or pre-rendered. No <script src> tags pointing to public CDNs.

Consequences: Pros: pages work offline, no third-party tracking, no broken when CDN goes down. Cons: pages are larger; some diagram options (Mermaid live, Chart.js) are off the table — replaced with inline equivalents.

ADR-008 — Cloudflare Pages deployed to gfs-netsuite.pages.dev — NOT publicly shareable yet

2026-05-19 · Accepted with critical caveat

Context: Need an internal canonical URL but Cloudflare Access setup is a Tier 1 GAP item not yet closed.

Decision: Deploy to gfs-netsuite.pages.dev without Access for now. Do not share the URL with anyone outside Michael Levine until Access (email OTP) is configured.

Consequences: Pros: usable today by the owner. Cons: anyone with the URL can read GAPS_TO_CLOSE (security gaps), netsuite-reference (all script + saved-search names), runbook (procedures). Adding Access is documented in GAPS Tier 1.

ADR-009 — data/system-manifest.json is the canonical NetSuite inventory

2026-05-19 · Accepted

Context: Inventory data was scattered across 8 separate markdown files in docs/. Hard to query, easy to drift.

Decision: Single JSON file with 15 categories (record_types, fields, saved_searches, reports, scripts, workflows, roles, permissions, suitelets, printed_forms, dashboards, integrations, lists, forms, kpis). Source markdowns moved to reference/netsuite-sources/ and feed the manifest. The manifest feeds netsuite-reference.html and the admin-dashboard counters.

Consequences: Pros: one place to update, every UI references the same numbers. Cons: refreshing requires re-extracting from NS — not yet automated.

ADR-010 — Reference research kept verbatim, not summarized into surfaces

2026-05-19 · Accepted

Context: 280K words of NetSuite research (portlets, Suitelets, pitfalls, AI patterns, automation tiers) exists in docs/01-08. Tempting to inline summaries into surfaces.

Decision: Keep research docs intact in reference/research/. Surfaces cite them but do not absorb them. New best-practice claims must trace to a research doc.

Consequences: Pros: research stays inspectable and rewriteable independent of the surfaces. Cons: readers may not click through; need clear pointers from surfaces.

ADR-011 — Native Cloudflare AI stack for the natural-language query layer over D1

2026-05-20 · Accepted

Context: Need a natural-language query layer over the D1 NetSuite mirror so admins can ask questions like 'top vendors by 2026 spend' or 'which scripts are deployed but never run' without writing SQL. Data already sits on Cloudflare. Options: (a) native Cloudflare AI (Workers AI + Vectorize + AI Gateway), (b) external LLM via AI Gateway only, (c) external LLM with own embedding store.

Decision: Native Cloudflare stack for v1: Workers AI for inference (Llama 3.3 70B Instruct) and embeddings (BGE-base-en-v1.5, 768d), Vectorize as the embedding store, AI Gateway for caching/logging/rate-limit and as the seam to swap models. Two query modes — retrieve+answer for descriptive questions, generate-SQL-then-execute for quantitative ones. SQL validator enforces SELECT-only, whitelisted tables, no DDL/DML. Corpus: D1 entities + aggregate transactions + system manifest + reference research docs + SuiteQL library + schema descriptions.

Consequences: Pros: zero data egress, one auth model, one platform to operate, AI Gateway lets us proxy to Claude/GPT later without rewriting app code, native Workers AI pricing scales with traffic. Cons: Llama 3.3 70B is not as strong on complex SQL synthesis as Claude/GPT — we accept this for v1 and monitor query failure rates; recall on BGE-base may require a quality bump (text-embedding-3-small via AI Gateway) if relevance is poor. Native models locked to Cloudflare's catalog cadence — we mitigate via AI Gateway swap. Risk: prompt injection from indexed content; mitigated by explicit instruction-vs-data separation in the system prompt + SQL validator.

ADR-012 — Customer pricing pipeline — derive from invoice history, review in platform, push to NetSuite

2026-05-20 · Accepted

Context: Customer pricing today is maintained by clicking into each customer's Item Prices sublist in NetSuite (slow, no bulk operations, no effective-date history, no audit trail). Real per-customer pricing intent lives partly in Dropbox spreadsheets, partly in negotiated SO/quote rates. We have 28,528 invoice_lines in D1 — every (customer, item) pair that's been invoiced has a real, dated price. Cap of 500 customers means per-customer price lists (not shared Price Levels) are operationally tractable. NetSuite remains the daily editing surface the user is comfortable with; the platform's role is migration, audit, and AI-assisted insight.

Decision: Build a derive → review → push pipeline. Derive: nightly Worker reads invoice_lines, computes a proposed price list per customer using median(last 3 invoices) when ≥3 exist in an 18-month window, else last_paid. Items the customer hasn't bought stay off the list (populated later at next quote/SO). Cross-reference derived prices against the existing customer_pricing table to produce three diff buckets (Listed+Recent, Listed+Stale, Not-Listed+Recent). Review: /pricing.html surface shows the diff per customer; admin approves, edits, or rejects rows. Push: Worker writes approved rows to NS via SuiteAPI RESTlet, replacing the customer's Item Pricing sublist. Promo/one-off filtering: flag rows where last_paid < median - threshold for explicit review. Returns and credit memos excluded from derivation. Full effective-date history captured in a new customer_pricing_history table on each push. AI runs anomaly detection (pricing outside peer band, drift narrative, smoothing suggestions, quote letter generation) over the derived + approved data — never auto-approves.

Consequences: Pros: empirically grounded starting state (real prices paid, not stale intent), eliminates manual per-customer click-through for the migration, surfaces the three-way diff which is itself a valuable audit deliverable, NS stays the editing surface for the user's day-to-day, AI sits on top of grounded data so suggestions are anchored in real history. Cons: derivation can be polluted by promo/one-off pricing (mitigated by median + threshold flag), bid pricing for USDA/NYCDOE may need separate handling tagged via custbody1 Bid Reference, NS Item Pricing sublist has no native effective-date column so history must live in D1 (customer_pricing_history) — acceptable, sync.sh captures NS state for cross-check. Risk: open SOs in flight on cutover day may invoice on old prices; mitigated by SO audit query before push + pilot on 3-5 customers before bulk. Risk: write-back to NS via RESTlet is a new code path; mitigated by per-row success reporting + rollback procedure documented in runbook before Phase 4.

ADR-013 — Consolidate Customer Pricing Program folder into gfs-platform as canonical bid/price/quote layer

2026-05-20 · Accepted

Context: Existing system: ~/GlobalFoodSolutions Dropbox/.../Claude_Customer Pricing Program_Original/. Mature operation with 71 commercial customers, 1 commodity, 3 COOPs, 6 direct school districts, 127 commercial items across 8 brands, 14 commodity items, 4 regions with regional pricing matrix, 34 logged external bids + 15 external bid folders + 23 internal bid review documents, 28 timestamped price history snapshots, 11 operating skill prompts (BID_PIPELINE, ADD_UPDATE_CUSTOMER_PRICING, CREATE_NEW_CUSTOMER, ADD_UPDATE_ITEM, REBUILD_REGIONAL_SHEETS, FILL_REGIONAL_PRICES, COMPARE_CUSTOMER_QUOTES, LOG_EXTERNAL_BID, ARCHIVE_CUSTOMER, SYSTEM_HEALTH_CHECK, PREPARE_NEXT_SCHOOL_YEAR), Node.js build scripts that emit .docx quote documents per customer, existing Cloudflare Pages deploy at gfs-pricing.pages.dev. Driscoll Foods alone has 35 bids and 116 priced items. This system handles the upstream pricing intent (bids, regional, programs, version control, quote generation) — gfs-platform's ADR-012 pipeline handles the downstream (NS Item Pricing mirror + derive from invoice history). The two were separate. User asked to consolidate.

Decision: The folder remains the canonical source of truth for DATA going in. Platform mirrors all of it into D1 and exposes it through one UI. HTML surfaces are derived views over the data, never write to the folder during normal operation. After cutover, edits can happen platform-side and write back to NS via the ADR-012 push-to-NS path; the folder becomes a read-only reference for the migration window. Quote output: HTML + PDF by default (Browser Rendering API from R2-stored templates), .docx on-demand for any signed/legal use. New D1 tables (additive only): pricing_school_years, pricing_customers_program (extends customers with program/pricing-type/version-log fields), pricing_items (extends items with K12/portions/cpp/usda_equiv/classification/brand), pricing_regional (item × region case price), bids (lifecycle: draft / submitted / pending review / awarded / not awarded / withdrawn / formalized / expired), bid_lines (per-(bid, customer, item) proposed price + notes), bid_attachments (R2 keys), external_bids (portal-submitted bid metadata extending bids), quotes (immutable header snapshot per customer per version), quote_lines (line items at quote time), pricing_snapshots (replaces Price_History/*.json). New endpoints: GET /api/bids, GET /api/bids/:id, POST /api/bids/:id/formalize, GET /api/quotes, GET /api/quotes/:id, POST /api/quotes/generate. New surfaces: /bids.html, /quotes.html, /pricing.html. Existing 11 skill prompts become endpoint + UI actions (the workflow logic stays; the execution moves into the platform).

Consequences: Pros: one platform end-to-end; one data layer; bid → quote → push-to-NS becomes a single chain instead of three loosely-coupled steps; AI query layer (ADR-011) gets the bid lifecycle as part of its corpus; annual roll runs from the platform with full traceability per customer per cycle; the folder's 71 customers + 35-bid-Driscoll-style data graduates from JSON files to queryable tables (D1 plus history). Cons: substantial migration (~9–10K new rows); two systems coexist during the cutover window; the .docx generation pattern doesn't move 1:1 — HTML/PDF replaces it as default (.docx becomes on-demand). Risk: schema drift if the folder is edited during migration; mitigated by treating the folder as canonical until verified, then freezing folder writes. Risk: quote output format change visible to customers — mitigated by hybrid path (HTML/PDF default, .docx export when needed) preserving the option to send Word documents. Six-week target to July 1 2026 cutover is tight; the loader and read-only surfaces ship first so the data is queryable while UI work waits for design specs.

ADR-014 — First-class customer_programs — pickup allowance, bill back, tier rebate, MDF, etc.

2026-05-20 · Accepted

Context: Today the customer.json files capture a 'program' field as Yes/Not Applicable plus free-text notes. Real programs (pickup allowance, bill back, volume tier rebate, promotional allowance, MDF, drop/stop fees, cash discount, NYCDOE PPI, COOP membership, slotting/pallet allowance) are economic structures that change the EFFECTIVE price a customer pays — not metadata. Driscoll Foods has at least three concurrent programs (FOB pickup allowance, NYCDOE PPI, likely a bill-back). Without first-class representation, the platform cannot compute effective price, cannot correctly reconcile invoice_lines.rate against the list, and cannot push the right NS sublist entries.

Decision: New table customer_programs with structured fields: program_type (pickup_allowance | bill_back | volume_tier | promo | mdf | drop_fee | cash_discount | nycdoe_ppi | coop | slotting | pallet_allowance), value + value_type (percent | dollars_per_case | dollars_per_lb | flat_amount | tier_table), applies_to_scope (all_items | category | brand | specific_items), applies_to_ids (JSON), effective_from + effective_to, billing_method (at_invoice | rebate_quarterly | rebate_annual | credit_memo | ap_check | customer_deducts), billing_account (NS GL ref), source (bid:<bid_id> | manual | historical | contract), contract_doc_r2_key, status (active | paused | expired | superseded). Companion customer_program_history (append-only audit) and customer_program_accruals (running balance for bill-back / MDF amounts owed). Effective-price calculation: invoice price = list × at_invoice_programs; net economic price = invoice price − bill_backs − MDF. Pillar 1 reconciliation now compares invoice price (not list) against invoice_lines.rate, which dramatically reduces false drift signals. Pillar 2 bids can propose programs as part of the award. Pillar 3 program changes go through the same approval flow as price changes, with their own NS billing-mapping rules (at_invoice → discount line on sublist; bill_back → no NS pricing change, separate AP/credit cadence). Pillar 4 annual roll auto-carries active programs into the next SY unless effective_to < next_SY_start.

Consequences: Pros: the platform finally models how customers actually pay; effective-price math becomes correct; bid offers can include program terms without rotting in notes; AI can answer 'who has pickup allowances?' or 'projected Q3 bill-back total' instantly. Cons: complex billing methods (bill-back especially) require a parallel accrual model + reconciliation against NS AP/credit-memo activity — that's Phase 5+ work, not v1. Risk: existing customer.json notes mention programs informally — migration needs to extract them; some will need manual entry. Risk: program-stacking ambiguity (one customer has pickup allowance AND a promo at the same time) — resolved by explicit composition rule (programs apply in declared sort order, each reduces the running base). Risk: NS doesn't natively model bill-back accruals — the accrual table in D1 is the system of record for the liability; NS sees only the eventual payment.

ADR-015 — Email intake — bids@ai-globalfoodsolutions.co → Email Worker → pre-populated bid draft

2026-05-20 · Accepted; vision parameters superseded by ADR-021

Context: Bids arrive by email from customers, portals, and internal team. Today: manual processing — Mike reads the email, downloads the attachment, manually creates a Bid_Reviewer/X.md, manually adds bids[] entries to customer.json, manually proposes prices. Slow, error-prone, no audit trail. The platform already has the items catalog, customer list, regional pricing, and historical bids — all the inputs needed to draft a first-pass automatically. Cloudflare Email Routing + Email Workers handles the inbound side cleanly.

Decision: Provision Cloudflare Email Routing on bids@ai-globalfoodsolutions.co → Email Worker (new entry point in src/email.ts, separate from the HTTP Worker). Pipeline: (1) save raw .eml + attachments to R2 under bids/inbox/<timestamp>/; (2) parse sender, subject, body; (3) detect customer via sender domain + AI fuzzy match against pricing_customers_program; (4) PDF text extraction via Workers AI vision (@cf/llava-1.5-7b-hf) for v1, AI Gateway → Claude Vision for image-PDFs in v2; XLSX parsing via minimal in-Worker parser; (5) item matching via Vectorize KNN against the pricing_items namespace, AI re-ranks with confidence score, threshold 0.75 for auto-match, below threshold flags the line; (6) price suggestion: AI reads body notes + matches each item to pricing_regional + similar past bids → proposes per-line price; (7) writes bids row (source='email', status='Pending Review') + bid_lines rows (with AI notes + confidence) + bid_attachments rows referencing R2; (8) sends admin notification email to a configurable address with a link to /bids.html?bid_id=<id>; (9) sends silent auto-receipt back to sender (acknowledges receipt; never quotes price). Reply chains matched to existing bids via in-reply-to header. Unknown senders flagged for admin assignment before any catalog work. Auth: DKIM/SPF/ARC enforced by Email Routing — Worker drops anything failing.

Consequences: Pros: turns a manual 30-minute first-pass into seconds; every bid gets the same data treatment; nothing slips through email; attachments archived to R2 with audit trail; AI confidence scores surface which lines need human review (the bid_lines table is honest about uncertainty). Cons: PDF parsing accuracy is the dominant variable — image-based PDFs need Vision model which is more expensive ($0.005+/document); some XLSX bid sheets will defeat the minimal parser and route to manual. Risk: customer fakes a 'bids@' email pretending to be an existing customer — mitigated by sender domain check + DKIM. Risk: rate-flood from spam — mitigated by Cloudflare Email Routing spam filter. Risk: a single bid generates a noisy bid_lines table with weak matches — mitigated by threshold gating; below-threshold lines explicitly flagged 'needs manual review' rather than guessed.

ADR-016 — Scoped pricing chat — /api/pricing-chat over Vectorize gfs-pricing-corpus

2026-05-20 · Accepted; retrieval policy superseded by ADR-020

Context: Admin needs to query the pricing system conversationally — 'Driscoll PUB5001H drift?', 'bids pending review?', 'suggest prices for Anderson County'. The general AI query layer in ADR-011 would answer these but mixed-corpus retrieval is slower and less precise. The pricing corpus has high-value, tightly-related entities (items + customers + bids + programs + history) that deserve their own embedding namespace.

Decision: Build /api/pricing-chat as a scoped sibling of /api/ai/query. Dedicated Vectorize index gfs-pricing-corpus (BGE-base-en-v1.5, 768d) with one namespace per source-type: pricing_items, customers_programs, bids, bid_lines, customer_programs, snapshots, history. Workers AI Llama 3.3 70B for inference v1; AI Gateway proxies to Claude when local reasoning insufficient. Two query modes (retrieve+answer, generate-SQL-then-execute) inherit from ADR-011. SQL allowlist restricted to pricing/bid/customer-pricing tables — no access to AR/AP transactions or NS-side admin tables. Bearer auth, KV rate-limit (200 queries/hour), every query audited to ai_audit_log. UI: /pricing-chat.html dedicated page + docked panel on /pricing.html, /bids.html, /quotes.html with ⌘K shortcut. Read-only in v1 — all writes go through explicit approval surfaces (no AI auto-action).

Consequences: Pros: faster retrieval (smaller, focused corpus); the platform becomes a conversational interface for pricing, not just a dashboard; everything pricing-related lives in one queryable place; admin can ask questions in natural language and get cited answers in seconds. Cons: two Vectorize indexes (general + pricing) — slightly more provisioning but cheap; corpus refresh needs to fire whenever any pricing table changes — Worker scheduled() handles nightly, manual /api/pricing-chat/reindex covers urgent updates. Risk: SQL generation against allowed tables could still produce expensive queries — mitigated by enforced LIMIT clause + query timeout. Risk: prompt injection via bid notes or version_log text — mitigated by explicit instruction-vs-data separation in the system prompt (inherited from ADR-011).

ADR-017 — Two-channel email intake (bids@ + pricerequest@) + HITL review feedback loop

2026-05-20 · Accepted

Context: User flagged two distinct intake patterns: formal bids/RFPs (full spec docs, due dates, samples, certs) and informal price requests ('can you quote on these 5 items for customer X?'). Both arrive via email; treating them the same in ADR-015 conflates volumes (price requests should be lighter weight) and review rigor. Separately, the user named the HITL review step as critical: 'we can take the notes and use as a feedback loop to make the system better.' Today admin corrections during review are ephemeral — they don't feed back into AI accuracy improvements.

Decision: Two email addresses, one Email Worker: bids@ai-globalfoodsolutions.co routes to the existing full bid intake pipeline (ADR-015 + SOP sections A-H1); pricerequest@ai-globalfoodsolutions.co routes to a lighter-weight pipeline that creates a price_requests row, parses items if attachments present, but does NOT auto-create bid_lines. A price request can be PROMOTED to a bid (promoted_to_bid_id) if review determines it warrants the full SOP, or CONVERTED directly to a quote (promoted_to_quote_id). Both channels write to inbound_email_log uniformly. HITL feedback: every override or correction admin makes during review writes a review_feedback row capturing ai_proposed_value, human_corrected_value, feedback_type (corrected | approved | rejected | flagged | question_to_ai), reason text, ai_model + ai_confidence. Aggregated feedback drives prompt refinement and item-matching threshold tuning. Flags used_in_training and used_in_prompt_refinement track which feedback has been applied.

Consequences: Pros: clear separation by intake intent — price requests get lightweight handling, bids get full rigor; HITL feedback becomes a queryable training signal not tribal knowledge; chat can answer 'what does AI consistently get wrong?' from review_feedback patterns; price requests can promote to bids when needed without losing the original context. Cons: two routing rules in Cloudflare Email Routing instead of one; price_requests adds another lifecycle to track. Risk: an external sender sends a 'price request' that should have been a formal bid — caught by admin during review who promotes it. Risk: review_feedback grows large fast; mitigated by retention policy + aggregation views.

ADR-018 — Healthcheck framework — automated audits over pricing data

2026-05-20 · Accepted

Context: User stated 'clean validated data across pricing for a business like us is most critical.' The platform now has 56 tables, 3,400+ rows of pricing data, two intake engines, four pipelines with gates, and an annual cycle. Without automated audits, drift is invisible until it surfaces in a customer's invoice or a missed bid deadline. The healthcheck framework runs scheduled SQL audits over D1, captures findings, surfaces them in the admin dashboard, and auto-promotes high-severity findings into pricing_anomalies for action.

Decision: Three new tables: health_checks (definitions library), health_check_runs (execution log), health_check_findings (individual findings per run). Each check is a row with id, category (data_quality | integrity | workflow | business | integration | compliance | security), severity_default, schedule (hourly | daily | weekly | monthly | on_demand), sql_query (returns 0-N rows), finding_template (human-readable), remediation_hint. Worker scheduled() handler runs all enabled checks on their cadence, writes a row to health_check_runs per execution, and one health_check_findings row per result. High-severity findings auto-mirror to pricing_anomalies. Admin dashboard surfaces unresolved findings grouped by severity + category. Initial library seeded with 17 checks covering: items without cost basis (CRITICAL — blocks margin), unmatched customers/items to NS (Tier 1 GAP), bid+quote workflow stalls, locked-customer changes (compliance), commodity over-drawdown, AI error rate, sync staleness, programs expiring soon.

Consequences: Pros: every data quality issue becomes a tracked row, not tribal knowledge; the chat can answer 'what's broken right now?' instantly; trends visible over time (sync staleness frequency, AI error rate); admin dashboard becomes a live ops view. Cons: SQL queries can become expensive if not scoped; healthcheck execution itself needs to be lightweight. Risk: a check with a typo could surface false positives forever; mitigated by enabled flag (can disable a check instantly) and a per-check pause if it generates noise. Risk: adding checks for everything = alert fatigue; mitigated by severity-default and the dashboard surfacing only unresolved + high-severity by default.

ADR-019 — NetSuite push contract: Worker scheduled() handler + batched RESTlet 2948 with idempotency, retry, mutex, and mirror-back verification

2026-05-20 · Proposed

Context: Phase 4 of the pricing pipeline (ADR-012) needs to push approved customer prices to NetSuite via SuiteAPI RESTlet 2948. Today's R1 read path (sync.sh) is a launchd cron on Michael's laptop hitting Chartstone Pro's localhost passthrough (ADR-002) — a single-Mac SPOF. The 2026-05-20 audit (see docs/AUDIT_2026-05-20_NS.md and AUDIT_2026-05-20_SYNTHESIS.md) surfaced ten concrete gaps in the push design as it stood: no idempotency key (transient retry = duplicate sublist rows), per-row RESTlet calls that would burn Worker sub-request budget at annual roll cutover (600 rows × 1 call vs 1000-call ceiling), no governance accounting (NS units invisible to the platform), no concurrency guard against double-click double-push, no retry/DLQ semantics, and — highest-impact — no mirror-back verification, which means an NS-side UI edit that overwrites the platform's push is undetectable until a customer disputes an invoice. The platform also leaned on Chartstone as infrastructure when it is documented as a dev convenience. The two issues — laptop SPOF and a fragile push design — share a single fix: move all NS touchpoints (read R1 + write W1) into the Worker, calling SuiteAPI RESTlet 2948 directly via TBA, with a hardened contract.

Decision: Migrate R1 (read mirror) and W1 (price push) into a Cloudflare Worker scheduled() handler that calls SuiteAPI RESTlet 2948 directly over HTTPS with TBA authentication. Chartstone Pro is demoted to interactive admin tooling only; it is never on a production write path. The push contract enforces five guarantees, each cross-referenced to a column added in migration 011: (1) Idempotency — Worker computes push_uuid = sha256(customer_id|item_id|effective_from|price|version) per row. RESTlet 2948 stores the key on a custom field (custrecord_gfs_push_key) and on receipt of a known key returns the prior result without re-writing. Dedupe window ≥ 7 days. Stored in ns_touchpoint_log.idempotency_key. (2) Batching — one RESTlet call per customer with an array payload of {item_id, price, effective_from, push_uuid, expected_prior_price}. Cap each call at ≤ 8000 NS governance units (typically 30 items). Annual-roll batch of 71 customers × 30 items = 71 RESTlet calls (was 2130 per-row). Stored in ns_touchpoint_log.push_batch_id, request_rows, success_rows, failed_rows. (3) Concurrency — Worker acquires KV mutex ns_push:customer:<id> with 5-min TTL before invoking the RESTlet; UI disables the Push control until release. Prevents double-click duplicate batches. (4) Retry — Cloudflare Queue gfs-ns-push-retry with max 5 attempts, base 30s, jitter ±25%, cap 30 min. Errors classified: transient (timeout, 5xx, governance-exhausted) → retry; deterministic (4xx, missing mapping, prior-value mismatch) → DLQ + pricing_anomalies row + admin alert. Idempotency keys protect retries from doubling. Stored in ns_touchpoint_log.retry_attempt, retry_of_touchpoint_id, error_class, error_code. (5) Mirror-back verification — a scheduled job 30 minutes after each batch re-reads affected (customer, item) prices from NS via the same RESTlet and compares to the pushed values within ±$0.001. Mismatches create a pricing_anomalies row (kind='ns_push_mirror_drift', severity='high') and surface in the admin dashboard. This is the only mechanism that catches silent NS-side overwrites from the UI or Workflows. Stored in ns_touchpoint_log.mirror_back_verified_at, mirror_back_diff_count. Pre-write check: the RESTlet also compares each row's current NS price against the pushed expected_prior_price; mismatch returns CONFLICT per-row → pricing_anomalies (kind='ns_drift_pre_push') and the admin re-reviews. Adds one read inside the RESTlet per push, cheap and worth it. Governance accounting — every RESTlet response includes getRemainingUsage() before and after; Worker records governance_units_before/after/consumed; alert if any single call burns >5000 units. scheduled() cadence: R1 every 15 minutes (matches the launchd cadence); W1 not on a schedule — fired by admin click from the review UI through a Worker endpoint that enqueues a Queue message. Supersedes ADR-002. ADR-012 push step is now governed by this contract.

Consequences: Pros: NetSuite touchpoints are first-class platform code with full observability via ns_touchpoint_log (governance metering, idempotency, mirror-back, conflicts, retries). Laptop SPOF eliminated — sync runs in the Worker so Michael's laptop being off doesn't stop the mirror. Reduced NS-side risk: idempotency kills retry duplication; per-customer batching stays under the 1000-sub-request Worker ceiling and the 8K-unit per-call governance ceiling; mirror-back closes the worst undetectable failure mode (silent UI overwrite). Chartstone returns to its documented role (admin SuiteQL exploration) so we no longer rely on a dev convenience tool as infrastructure. Cons: more Cloudflare infrastructure (Queue + scheduled() handler + KV mutex). RESTlet 2948 needs a custom field and a small dedupe layer added — coordinated with the Suitelet team. The mirror-back verification adds a 30-minute lag between push and full validation; this is acceptable given the human approval gate already enforces care, but admins should not assume push success === reality until mirror-back clears. Risks: (R1) Worker scheduled() handler exceeds 30s CPU budget on full nightly sync — mitigated by the same CPU_BUDGET_MS guard already used in derive (returns partial-success with explicit continuation_token, queues remaining via Cloudflare Queue). (R2) RESTlet 2948 deployment can fail mid-rollout — mitigated by a feature flag (custrecord_gfs_push_v2_enabled) that lets the Worker fall back to v1 (manual) until verified in a 3-customer pilot. (R3) Queue retry storm during NS outage burns Worker budget — mitigated by an inflight cap (KV-tracked count, refuses new enqueues above 50 inflight). (R4) Mirror-back can flag legitimate admin NS edits as drift — mitigated by recording the NS user id (sourced from the customer_pricing UE history if available) and surfacing the diff as review-only when source != 'platform_push'. (R5) NetSuite Workflows or UE scripts on the customer record can reject or modify the RESTlet write — mitigated by the pre-write CONFLICT check, the mirror-back verification, and a one-time docs/NS_AUTOMATION_INVENTORY.md inventory of UE + Workflow scripts on the customer record before Phase 4 ships. Open items before Phase 4 ship: RESTlet 2948 contract (custom field for idempotency, batch payload shape); Cloudflare Queue gfs-ns-push-retry setup; KV mutex helper in Worker; admin alert channel (Slack webhook or email) for mirror-back drift; pilot plan (3 customers — Driscoll, NYCDOE, one small-volume — for two weeks before opening to the full 71).

ADR-020 — Pricing chat retrieval & cost policy: top-2 per namespace + rerank to 5, 5-minute cache bucket, dynamic namespace caps, AI Gateway routing with prompt caching

2026-05-20 · Proposed

Context: ADR-016 set the retrieval budget at 8 namespaces × 8 chunks × 500 tokens = 32,000 retrieval tokens. The 2026-05-20 audit (docs/AUDIT_2026-05-20_AI.md) caught the math: Llama 3.3 70B on Workers AI has an 8K context window. With system prompt (~600), conversation history (~500), JSON schema scaffolding (~300), and SQL example shots (~400), the request was projected at ~34K — 4× the window. The Worker would silently truncate retrieval and return garbage. The audit also flagged the cache key (hash(question + role + minute)) as keyed on per-minute buckets, defeating the 5-minute TTL (estimated hit rate ~5% vs target ~70%). Workers AI does not support prompt caching natively, but AI Gateway → Claude does, and three fixed prefixes (chat system prompt, vision system prompt, bid review rubric) repeat on every call and would amortize cleanly. Per-namespace caps in ADR-016 were uniform; the audit observed that operating_model chunks are background-only while bids and customers_programs deserve more chunks. None of this requires re-litigating ADR-011's Cloudflare-native preference — Workers AI remains the default; routing through AI Gateway to Claude is reserved for complex presets and prefix-caching wins.

Decision: Five changes to the retrieval and cost path, all to land before the pricing chat ships. (1) Retrieval budget — switch from top-8 × 8 namespaces to top-2 per namespace then cross-encoder rerank to top-5 total. New math: 5 chunks × ~500 tokens = 2,500 retrieval tokens; full request envelope ~4K, comfortable inside an 8K window with headroom for the response. (2) Cache bucket — change cache key from hash(question + role + minute) to hash(normalized_question + role + bucket) where bucket = floor(now / 300s). Normalize question: lowercase, strip whitespace, collapse punctuation, expand {placeholders} after substitution. KV TTL stays 300s. (3) Dynamic namespace caps — bids and customer_programs get top-3; pricing_items, pricing_customer_prices, customer_pricing_history, customer_pricing_proposed get top-2; operating_model gets top-1 (background only). Rerank still selects 5 from the union. (4) Incremental embed-on-write for four namespaces — bids, pricing_customer_prices, customer_programs, pricing_items — so freshly arrived data is chat-queryable within seconds, not next morning. Cost is negligible (~$0.0002/day at current scale). Nightly cron stays for customer_pricing_history aggregate, bid_lines aggregate, and operating_model. (5) AI Gateway routing — Llama 3.3 70B (Workers AI) remains the default for short retrieval-grounded answers. Route to Claude Haiku 3.5 via AI Gateway for complex presets (cmp_*, roll_cycle_summary, comm_compare_*) where the larger context, prompt caching, and stronger SQL synthesis pay off. Mark the three fixed prefixes (chat system prompt, vision system prompt, bid rubric) with cache_control: ephemeral on Claude calls. AI Gateway also gives unified logging across providers, falling naturally into the ai_audit_log table (migration 013 adds the token + cache_hit columns). Output validation cap: max 2 retries on Zod failure, then drop to manual review. Supersedes the retrieval-budget and cache-key and per-namespace decisions from ADR-016. ADR-016's core architecture (per-source-type Vectorize namespaces, BGE-base embeddings, the LLM-as-Worker model) stands.

Consequences: Pros: chat actually fits in the model's window (math no longer projects overflow). Cache hit rate should improve from ~5% to ~70%, dropping average latency on hot queries. Bids and customer_programs get the depth they deserve while operating_model stops crowding the retrieval slot it doesn't earn. Bids arrived 18:00 are queryable by 18:01 instead of next morning — the operational-interface promise of the chat surface is preserved. AI Gateway gives one observability plane across Workers AI and Claude. Prompt caching on the three repeated prefixes saves ~50% of input token cost at 10x (bid rubric caching alone saves ~360K input tokens/day at 50 bids/day). Cons: cross-encoder rerank adds ~100–200ms per query (acceptable for chat; total budget is ~2s p95). AI Gateway introduces a second hop on routed queries; falls back to direct Workers AI on Gateway outage. Per-namespace dynamic caps require the chat endpoint to know namespace identities at compile time — they live in a config constant updated alongside ADR-020. Incremental embed-on-write means the bid review path now has a side effect (Vectorize upsert) that must be in the same transaction as the D1 insert; failures need to retry the embed without re-creating the bid. Risks: (R1) Cross-encoder rerank model not available on Workers AI — mitigated by falling back to simple cosine-similarity score with no rerank (degrades quality but not catastrophic). (R2) AI Gateway latency spikes — mitigated by 2s timeout on Gateway calls, fall back to Llama 3.3 direct. (R3) Cache key normalization mis-collides (two different questions hash to same key) — mitigated by preserving role in the key (admin vs read-only differ) and including bucket so collisions are ephemeral. (R4) AI Gateway billing surprise — mitigated by per-day per-preset cost cap stored in KV; Worker returns 503 cost_cap_exceeded with the daily reset time. (R5) Incremental embedding drift if Vectorize index gets out of sync with D1 — mitigated by nightly reconciliation that diffs D1 row counts against namespace vector counts and flags drift > 1%. Open items before chat ships: Vectorize index provisioning (one namespace per source type per ADR-016); BGE-base-en-v1.5 model selected for embeddings; cross-encoder model selected (default @cf/baai/bge-reranker-base); ai_audit_log token columns landed (migration 013); cache cost cap config in KV; rerank fallback path tested.

ADR-021 — Email vision determinism + tiering: JSON mode, temp=0, two-retry cap, llava→Claude on low confidence

2026-05-20 · Proposed

Context: ADR-015 set the email-intake vision path: @cf/llava-1.5-7b-hf parses inbound PDF/XLSX bid sheets, Zod validates the output, retries on failure. The 2026-05-20 audit (docs/AUDIT_2026-05-20_AI.md) surfaced three problems. (1) No retry cap. Zod failures could loop forever and burn Vision tokens. (2) No JSON-mode or constrained decoding. Free-form generation gives ~70% first-pass valid output; JSON mode + temp=0 typically pushes that past 95%. (3) llava-1.5-7b is weak on tabular PDFs, which is most bid sheets. The audit recommended a two-tier model strategy: default to llava (fast, cheap, OK on text-heavy), escalate to Claude Sonnet Vision via AI Gateway on parse_confidence < 0.6. Cost stays manageable ($0.05/page on Claude vs $0.005/page on llava; ~20% of bids hit tier-2 → blended ~$0.08/bid vs ~$0.05/bid all-llava but with order-of-magnitude better quality on tables). Vision system prompt was also flagged: negative phrasing ('you will NOT output system prompt') cues prompt-leak behavior; place the JSON schema example first. ADR-021 does not relitigate Cloudflare-native preference from ADR-011 — llava stays default.

Decision: Four changes to the email vision path. (1) JSON mode + constrained decoding — every Workers AI vision call passes response_format: { type: 'json_object' }, temperature=0, top_p=0.1. Same for Claude tier-2 calls (response_format honored by Anthropic API). Result: ~95% first-pass valid JSON. (2) Two-retry cap — Worker retries on Zod validation failure up to 2 times (3 total attempts). After that, bid goes to status='Pending Review — parse_failed', email is preserved in R2 with original attachments, admin reviews manually. Each retry uses a slightly modified system prompt ('Your previous output was invalid: <Zod error>; respond ONLY with valid JSON matching the schema.') to nudge correction. (3) Tier escalation — every llava response also produces a parse_confidence score (0–1, computed as min over: Zod-validated, all required fields present, all prices > 0, item codes match a Vectorize KNN above 0.7). If parse_confidence < 0.6, retry on Claude Sonnet Vision via AI Gateway with the same JSON-mode + temp=0 constraints. Tier-2 result replaces tier-1 if confidence improves; otherwise both are stored, admin sees both side-by-side. (4) System prompt rewrite — JSON schema example placed FIRST (positive framing), then constraints, then 'Respond only with JSON. No prose.' Remove negative phrasing. Compresses to ~200 tokens. ai_audit_log records (model_used, parse_confidence, retry_attempt, tier_escalated, input_tokens, output_tokens, latency_ms) per call. Migration 013 adds these columns. Supersedes the vision-model parameters in ADR-015. ADR-015's overall flow (Email Routing → Email Worker → R2 attachment store → vision parse → Zod → D1 insert → admin notify) stands.

Consequences: Pros: first-pass valid JSON rate jumps from ~70% to ~95%, cutting retry token spend by ~3×. The retry cap stops cost spirals on adversarial inputs. Tier escalation on parse_confidence captures the tabular-PDF gap llava can't close, at modest blended cost (~$0.08/bid vs $0.05). Vision system prompt is shorter and prompt-leak-safer. AI Gateway gives unified observability across the two model tiers via ai_audit_log. Cons: tier-2 (Claude) introduces external dependency on Anthropic API + AI Gateway availability; Workers AI direct fallback used if Gateway unreachable. Two-retry cap means some bids that would succeed on a 4th try get sent to manual review; acceptable given retry token cost outweighs the marginal recovery. parse_confidence is heuristic and may misclassify ~5% (false-low triggers unnecessary tier-2 escalation; false-high lets through bad parses) — mitigated by review feedback table that lets admin grade individual parses and retrain the confidence scorer over time. Risks: (R1) Anthropic API costs spike on tier-2 escalation storm (e.g., scanned-PDF batch where every parse hits tier-2) — mitigated by per-day cost cap in KV (default $5/day, configurable); above cap, tier-2 disabled and bids fall to manual review until reset. (R2) Claude Sonnet Vision rate limited under burst — mitigated by exponential backoff with jitter, max 30s wait. (R3) Vision system prompt updates change parse_confidence distribution — track in ai_audit_log and compare pre/post deploy. (R4) AI Gateway routing config drift between dev and prod — config lives in a versioned JSON in data/ai_gateway_routes.json, deployed alongside Worker code. Open items before email intake ships: AI Gateway provisioning (already in CF account); parse_confidence scoring function written + unit-tested with sample bids; cost cap KV key convention (cost_cap:vision_tier2:YYYY-MM-DD); system prompt rewrite reviewed for prompt-injection patterns per docs/SECURITY_THREAT_MODEL_2026-05-20.md; migration 013 token columns landed.

ADR-022 — Genesis vs live operational store — Dropbox holds source documents, D1 holds the platform's operational state

2026-05-20 · Proposed

Context: Over rounds 1-7 the platform grew to 62 D1 tables + 12 views + 42 endpoints, all populated from the Dropbox-resident folder `Claude_Original/Claude_Customer Pricing Program_Original`. That folder holds 23 Bid_Reviewer .md files, 2 Programs/*.md, External_Bids/, Commercial_Price_Quote/, Commodity_Price_Quote/, Price_History/, Prompts/, plus loose .xlsx and .docx artifacts. ADR-013 moved 'folder consolidation into gfs-platform' to canonical but didn't articulate what the long-run relationship between Dropbox and D1 should be — whether the folder updates back from D1, whether the folder is read-only once consolidated, or whether the folder simply stops being canonical altogether. After loading 782 bid_lines from Bid_Reviewer in round 8 + consolidating Desktop sprawl into ~/Desktop/GFS/, the question is unavoidable: where does each piece of data live, who owns writes, and when can we delete the source files. The user explicitly asked about moving the source to Google Cloud — the underlying issue is fragmentation, not the storage provider.

Decision: Three-tier responsibility model. (1) **Source documents** — the genesis archive — live in Dropbox at `~/GlobalFoodSolutions™ Dropbox/GFS New Team Folder/GFS x Claude AI/Claude_Original/Claude_Customer Pricing Program_Original/`. This holds raw human-readable artifacts: Bid_Reviewer .md files, External_Bids JSON + folders, Programs/, Price_History/, Commercial_Price_Quote/, Commodity_Price_Quote/, Prompts/, plus the original .xlsx workbooks. Dropbox version history is the backup. The platform NEVER writes back to these files — they are write-once-by-human, read-many-by-loader. A symlink at ~/Desktop/GFS/source-documents/ provides visibility from the master folder without duplicating storage. (2) **Operational state** — the live store — lives in Cloudflare D1. Per-customer prices (pricing_customer_prices), customer programs (customer_programs), bid metadata (bids) + lines (bid_lines) + requirements (bid_requirements), pricing history (customer_pricing_history), proposed prices (customer_pricing_proposed), mappings (customer_name_mapping, pricing_item_mapping), audit logs (ai_audit_log, ns_touchpoint_log, cron_locks), healthchecks (health_check_runs + findings) — all live in D1. D1 is the SoR for the platform; queries by Worker, chat, dashboards, and the future Email Worker all read D1. Writes are admin-initiated through HTTP endpoints. (3) **Generated artifacts** — derived state — live in R2. Quote PDFs, signed contracts, email attachments, monthly archival of audit tables (older than 90 days), R2 path conventions documented. The platform code itself (Worker, migrations, docs, frontend HTML, build scripts) lives in the git repo at ~/Desktop/gfs-platform/. Git is the SoR for the code. Dropbox + git is forbidden (causes merge conflicts on every save). The git repo is NEVER cloud-synced; deploys happen via wrangler. A symlink at ~/Desktop/GFS/platform-repo/ provides visibility from the master folder. Cross-references: ADR-013 (folder consolidation) is the prior decision that started this; this ADR extends it. ADR-019 (NS push contract) is the only place where D1 and NetSuite cross-write — and even there, NS is the SoR for live orders and D1 is the mirror with proposed prices.

Consequences: Pros: clean ownership model. Future-you opening a folder asks one question — is this source, operational, or generated? The answer determines what writes go where. Dropbox's version history serves as the source-document backup, R2's archival serves as the audit-table backup, D1's own Cloudflare-side replication handles the live state. The platform never overwrites the genesis files, so they remain authoritative for what was originally received from the customer/broker — useful for legal/dispute resolution. Migration from Dropbox to Google Drive (or anywhere else) becomes a one-time symlink-pointer change with no platform impact. Cons: source-documents are read-only by convention only; nothing enforces it. If admin manually edits a Bid_Reviewer .md to fix a typo, the platform won't notice — but a re-run of migration 015 will pick up the change next time. We accept this as the cost of keeping the genesis files human-editable. Risks: (R1) Source-documents and D1 can drift if admin re-edits .md files without re-running the loader — mitigated by tracking source_doc + indexed_at on each bid row, and a healthcheck (future) that flags drift. (R2) Dropbox sync conflicts on the genesis files (Dropbox occasionally writes conflict copies when two devices edit the same .md) — mitigated by keeping these files low-traffic and admin-only. (R3) The master folder ~/Desktop/GFS/ vs the actual repo at ~/Desktop/gfs-platform/ is a logical-vs-physical mismatch that requires the README to explain. Round 8 attempted to consolidate physically but ran into macOS TCC (Files & Folders permission blocks bash from executing scripts at the new path). The symlink restores logical consolidation without disturbing TCC. (R4) If we ever lose the Dropbox folder (account closure, sync corruption), the genesis archive is gone — mitigated by Dropbox's own retention + an opportunity for the next-round work: nightly R2 push of source-documents/ for off-site backup independent of Dropbox. Open items: write the source-document → D1 drift healthcheck (load Bid_Reviewer .md, compare row count + file content hash against indexed_at value on each bid row). Add nightly R2 backup of source-documents/ as a Worker cron (separate from the existing audit-table archival).

ADR-023 — Assembly Costing domain — second major data ingestion parallel to pricing, with FOB dock cost roll-up from vendor inputs through BOM to finished good

2026-05-20 · Proposed

Context: GFS sells 141 finished goods across 9 brands. Some are pure resale, some are co-packed (vendor produces with GFS branding), some are GFS-manufactured (single-serve pouches, sandwiches, meal kits, parfaits). The Dropbox folder `Claude_Original/Claude_Assembly Costing Program_Original/` (65 MB) holds a sophisticated cost-tracking system organized by 7 programs (VFF / MealKit / Sandwich / Parfait / Resale / CoPack / Sauces) with ~141 item .md files (each with frontmatter + BOM lines + vendor refs + packaging defaults + labor placeholders), a vendor_pricing/ folder with the GFS_Master_Pricing.xlsx master vendor catalog, and cost_breakdowns/ with 14 per-item computed FOB dock cost rollups. The cost flow is: vendor input prices → assembly costing rolls up FOB dock cost per finished good → customer pricing uses dock cost as cost-floor for quotes. Today the platform has customer pricing (ADR-012) but no assembly costing — the cost_basis_per_case column on pricing_customer_prices is mostly null. The user explicitly asked for parallel ingestion of assembly costing so the chat can answer questions like 'what does it cost to make RS9310W?' and 'which assemblies are exposed to Sun-Rich price changes?' and so future customer quotes can incorporate live cost data instead of stale spreadsheet snapshots.

Decision: Mirror the pricing pipeline architecture (ADR-012/013/022) for assembly costing. Six new D1 tables (migration 017 ships the DDL today): (1) **assembly_programs** — 7 program types with packaging_defaults_json, labor_defaults_json, overhead_defaults_json, freight_defaults_json. Each row links to the source .md file path. (2) **assemblies** — 1 row per finished good, links to pricing_items.code + items.itemid (NS), stores units_per_case, serving size, case weight, source_doc + source_doc_hash for drift detection. UNIQUE on item_code. (3) **assembly_bom** — bill-of-materials lines (one row per component). component_type enum: raw / packaging / labor / overhead / freight / other. Stores qty_per_case + qty_unit + yield_pct + cost_per_unit + cost_per_unit_override + cost_per_case_calc + vendor + vendor_item_code + source_doc + source_note + expires + status. (4) **vendor_items** — vendor side catalog (vendor_name + vendor_item_code UNIQUE). Maps to NS vendor_id + item_id when known. Source enum: master_workbook / netsuite_mirror / po_history / broker_quote. (5) **vendor_costs** — dated vendor prices (effective_from + effective_to + unit_cost + currency + freight_terms + fob_origin + source). Source enum: quote_email / master_workbook / po / broker / manual. (6) **assembly_cost_rollup** — computed FOB dock cost per assembly (effective-dated). Stores total_cost_per_case + raw + packaging + labor + overhead + freight breakouts + components_json snapshot. source_event enum: vendor_cost_update / bom_change / admin_recompute / scheduled_refresh. Plus two views: v_assembly_latest_cost (one row per assembly with latest active cost), v_assembly_vendor_dependencies (which assemblies depend on which vendor SKUs — for impact analysis when a vendor changes prices). Loader migration 018 (round 10) parses the 7 program .md files + 141 item .md files into assembly_programs + assemblies + assembly_bom rows. Loader migration 019 (round 10) parses GFS_Master_Pricing.xlsx into vendor_items + vendor_costs. Loader migration 020 computes initial assembly_cost_rollup rows. Worker endpoints (round 10): GET /api/assemblies (filter by program, brand, status), GET /api/assemblies/<id> (full BOM + latest cost + vendor dependencies), GET /api/vendor-costs (filter by vendor, effective range), GET /api/assembly-cost-rollup/refresh?confirm=true (recompute), GET /api/vendor-impact/<vendor_name> (which assemblies hit by this vendor's price changes). Vectorize namespaces extend to include `assemblies` and `vendor_costs` for chat. Per ADR-022: source .md files stay in Dropbox; D1 holds operational state; R2 holds R2 archival of vendor_cost_history + assembly_cost_rollup_history snapshots.

Consequences: Pros: parallel structure to the pricing domain makes the architecture predictable; same admin patterns (review proposed, approve, log audit) apply to both. Chat becomes order-of-magnitude more powerful — 'cost of RS9310W' returns the live FOB dock cost with full vendor dependency tree; 'impact of Sun-Rich price change' surfaces every affected assembly. pricing_customer_prices.cost_basis_per_case gets populated from v_assembly_latest_cost going forward. Vendor-impact analysis becomes a single query (was previously impossible without manual spreadsheet work). Cons: 6 new tables increase D1 schema surface from 62 → 66 (12 → still 12 views; round 10 adds v_assembly_latest_cost + v_assembly_vendor_dependencies, total 14). The .md → D1 loaders are non-trivial — assembly item .md files use YAML frontmatter (frontmatter parsing required, not just markdown tables). GFS_Master_Pricing.xlsx loader needs openpyxl + sheet schema discovery. Vendor name normalization is hard (the .md files use free-text vendor names like 'Sun-Rich', NS uses vendors.companyname like 'Sun-Rich Foods Inc' — fuzzy match required as we did for customer_name_mapping). Risks: (R1) BOM source_doc drift — admin edits the .md file but loader hasn't re-run; source_doc_hash + nightly drift healthcheck mitigates. (R2) Vendor cost staleness — quotes expire (expires field) but rollup computation may use stale values; the assembly_cost_rollup re-fires when any vendor_costs row updates. (R3) yield_pct estimation errors compound across BOM lines — admin review surface required before any rollup is marked active. (R4) Mismatch between v_assembly_latest_cost and pricing_customer_prices.cost_basis_per_case (one is a cost, one is a denormalized snapshot) — nightly sync job copies latest cost into pcp.cost_basis_per_case for the active SY. (R5) Loader for .md files with YAML frontmatter is more brittle than the pricing .md loader; requires a YAML library (pyyaml) + careful schema validation. Open items before round 10 ships: confirm GFS_Master_Pricing.xlsx structure (sheets, columns); decide whether vendor_costs.unit_cost is per-uom or per-case (mixed in source today); decide whether labor_defaults are flat per-case or per-minute × time-per-case; design admin review UI for proposed cost rollups before they mark active; specify the recompute trigger (manual via endpoint vs scheduled vs on every vendor_cost write).

ADR-024 — Multi-model chat routing (Workers AI / Kimi / Qwen / Claude) and chat-data provenance display requirement

2026-05-20 · Proposed

Context: Round 9 wired the pricing chat to Llama 3.3 70B via Workers AI. The user surfaced two related requirements: (1) consider Kimi (Moonshot K2) and Qwen (Alibaba Tongyi) as cheaper or faster alternatives, alongside Claude; (2) the chat response must always show users HOW the data was pulled — which source row, which date, which scoring path — so they can validate every figure before acting. ADR-011 set Cloudflare-native as the default with AI Gateway routing to Claude for complex presets (ADR-020). ADR-020 added the cost cap framework (ai_gateway_cost_caps). Today's implementation returns a `retrieved` array + a `provenance` array in the JSON response — both carry namespace + source_id + similarity score + metadata — but this isn't yet rendered as a user-visible 'show your work' panel in the dashboard. The user's framing — 'I always want on data in that chat to be validated and show the user how the datapoint was pulled from' — applies to every chat answer, every quote draft, every healthcheck finding.

Decision: Two locked decisions: (1) **Multi-model routing**. Continue Workers AI Llama 3.3 70B as the default for short retrieval-grounded answers (fast, cheap, no external dependency). Route via AI Gateway when warranted to: - **Kimi K2 (Moonshot)** for very-long-context summarization (200K context, often cheaper than Claude per token at high volumes). Useful when the chat needs to digest 50+ retrieved chunks or whole bid review docs at once. - **Qwen 2.5 72B (Alibaba)** for cheap bulk classification (e.g., the 700+ unmatched bid_lines that need a tentative GFS code suggestion). Cheaper per million tokens than Claude or Kimi. - **Claude Haiku 3.5 (Anthropic)** for complex multi-step reasoning (annual roll planning, margin scenario analysis). Best reasoning quality on SQL synthesis per the ADR-020 audit. - **Claude Sonnet Vision** for vision tier-2 on parse_confidence < 0.6 (already locked in ADR-021). Routing decision goes through AI Gateway. Cost cap rows extend to include `kimi_long_context`, `qwen_bulk_class`, `claude_complex_reason` tiers (in addition to existing chat_complex_routed, vision_tier2, embedding_refresh). Default daily caps: $3 / $1 / $5 respectively. The chat endpoint selects model based on preset_id (or `model_hint` query param when caller knows what they want); default is Workers AI Llama. Failure modes fall back to Llama (always available). (2) **Chat provenance display contract**. Every chat response must include three blocks: - **answer** — the LLM's text response. - **provenance** — for every numeric or factual claim, a structured citation: { citation_marker: '[1]', namespace: 'pricing_customer_prices', source_id: 'pcp:Driscoll:RS9310W:SY2627', similarity_score: 0.87, as_of_date: '2026-05-20', sql_lineage: 'SELECT case_price FROM pricing_customer_prices WHERE customer_name=X AND item_code=Y AND school_year=Z', metadata: { ... } }. The chat HTML surface renders this as a 'How we got this' panel below each answer, showing exact row source + date stamp + raw SQL trace. - **confidence_notes** — when provenance is incomplete (e.g., retrieved score < 0.6, or LLM hedged with 'I don't have that data'), the response includes a hint about what data is missing and a suggested action ('refresh /api/embed/refresh for namespace X' or 'admin needs to load missing bid review file Y'). Round 10 work: extend chat response shape; extend admin-dashboard.html chat surface to render provenance panel; add `sql_lineage` field to the retrieval pipeline (each Vectorize chunk records the SQL that produced it). Round 11+ work: provision AI Gateway endpoints to Kimi + Qwen + Claude; extend ai_gateway_cost_caps with new tier rows; add model_hint dispatch in /api/pricing-chat.

Consequences: Pros: chat-as-a-tool becomes trustworthy because every number is traceable to a row + date + SQL. Admin can spot-check any answer before acting. Multi-model routing gives cost-per-task optimization without compromising answer quality (Llama for hot path, Kimi/Qwen for bulk, Claude for nuance). The cost cap framework keeps each tier bounded. Failure mode falls back to Workers AI (no external dependency, always available). The provenance contract becomes a forcing function: if a chunk can't be traced (e.g., scraped from an unsourced doc), it doesn't get embedded. Cons: per-call latency increases when external models are routed (Workers AI ~200-500ms cold; Anthropic ~1-2s; Kimi/Qwen ~1-2s). Cost increases — bulk Qwen still costs more than Llama-on-Workers-AI (Workers AI Llama is essentially free up to neuron quota). The provenance JSON adds ~200-400 bytes per chunk to every response (acceptable for the UX gain). Maintaining 4 model integrations adds operational complexity (4 sets of API keys, 4 cost-cap rows, 4 error classes). Risks: (R1) Kimi/Qwen API stability varies by region — mitigated by always-fallback-to-Llama. (R2) Provenance can be wrong (LLM cites [n] but didn't actually use that retrieved chunk) — mitigated by faithfulness check (regex the answer for facts, verify each fact appears in cited chunk text; flag mismatches). Faithfulness check runs nightly across last 24h of ai_audit_log. (R3) Cost-cap-exceeded errors degrade UX — mitigated by surfacing 'today's budget' in the dashboard so admin sees runway. (R4) Provider key rotation — Workers Secrets handles it but requires manual rotation; document in CLOUDFLARE_SERVICES.md. (R5) Multi-provider routing config drift between dev/prod — config lives in data/ai_gateway_routes.json, deployed alongside Worker code. Open items before round 10/11 ships: API keys for Kimi (Moonshot), Qwen (Alibaba DashScope), and Claude (Anthropic) need to be created and stored as Worker Secrets; AI Gateway endpoints set up in Cloudflare dashboard; faithfulness check job specced + scheduled; admin-dashboard chat panel designed (provenance + confidence_notes + cost ticker).

ADR-025 — Specification Sheet Program — the shared product catalog that ties customer quotes to assembly builds

2026-05-20 · Proposed

Context: The platform now has clean parallel ingestion for customer pricing (ADR-012 / ADR-013) and assembly costing (ADR-023). The third Dropbox folder `Claude_Specification Sheet Program_Original/` (~80 MB) holds the canonical product catalog with `data/master_products.csv` as the single source of truth for item attributes (allergens, nutritionals, claims, pack specs, CN credits), plus `brands/<brand>/` per-brand customization (alfresco-italian, bransons-roadhouse, gfs, harvest-promise, meltmates, power-up-foods, rsf, tijuana-tortilla) and `outputs/<brand>/` with generated spec sheets per item per brand. This catalog is used by BOTH the customer side (quote letters cite spec attributes when bidding to schools) AND the assembly side (each assembly's spec_item provides allergen/nutritional context for the finished good). Today the platform has items via NS mirror (`items` table, 1,265 rows) but those lack the spec-sheet enrichment (allergens, CN credits, etc.). The NS items.custitem_as_* allergen flags are a partial overlap but the spec sheet program has the authoritative version — and the user pointed out that NS items are 'lagging' behind the spec sheet program. This ADR plans the ingestion.

Decision: Add a shared `spec_items` catalog to D1 as the cross-domain item enrichment layer. Three new tables (migration 022 ships the DDL): (1) **spec_items** — one row per (item_code, brand). Stores allergens (12 columns mirroring NS items.custitem_as_*), nutritional facts (calories, fat, sodium, sugar, protein, fiber, calcium, iron), CN credits (mma_credit, grain_credit, etc.), pack specs (units_per_case, serving_size, case_weight), claims (kosher, halal, vegan, organic, non_gmo), shipping group, classification, source_doc (path to master_products.csv row + brand override file), source_doc_hash, last_synced_at. UNIQUE on (item_code, brand). (2) **spec_attributes** — generic key-value extension for attributes not in the column set (certifications, custom flags). Keyed on (spec_item_id, attribute_key). (3) **spec_versions** — effective-dated history of attribute changes (when the manufacturer reformulates, when the lab updates nutritionals). Stores version_number, previous_value, new_value, change_reason, changed_by, changed_at. Plus three views: v_spec_with_pricing (spec × pricing_customer_prices for active prices), v_spec_with_assembly (spec × assemblies for build context), v_spec_drift_vs_ns (spec_items vs items.custitem_as_* — surfaces where NS is lagging). Loader migration 023 (round 12) parses `data/master_products.csv` + `brands/<brand>/brand_config.json` + per-brand `outputs/<brand>/<item>/*.md` into spec_items + spec_attributes rows. Idempotent via INSERT OR REPLACE on (item_code, brand). Worker endpoints (round 12): GET /api/spec-items (filter by brand, claim, allergen-free); GET /api/spec-items/<item_code> (full spec with cross-domain joins to pricing + assembly + bid history); GET /api/spec-drift-vs-ns (which items have NS data older than the spec sheet); GET /api/spec/by-claim?claim=kosher (find items with a specific certification). Vectorize namespace `spec_items` (8th namespace), embedded with text like 'Item CODE (brand): description. Allergens: free of X,Y. CN credit: 2G. Calories per serving: 130. Claims: kosher, halal. Classification: dry grocery.'

Consequences: Pros: chat answers cross-domain questions that span pricing + assembly + spec data — 'what's the calorie count and our margin on RS9310W for NYCDOE?' joins three domains in one answer. Customer quote letters auto-populate with the right CN credits and allergen disclosure. The 'NS is lagging' problem becomes visible (and fixable) via v_spec_drift_vs_ns. Brand-specific spec variants (e.g., NYCDOE non-RS6000B variant) are first-class instead of buried in file paths. Cons: spec_items × brand cross-product can blow up — 146 assembly items × 8 brands = up to 1,168 spec_items rows (manageable). Reconciling spec_items.allergens with items.custitem_as_* becomes a real synchronization concern — we accept spec_items as SoR for allergens going forward and add a healthcheck for divergence. Per-brand brand_config.json may have nested structures we have to handle (JSON column + extraction views). Risks: (R1) master_products.csv format may have inconsistent headers per row — mitigated by loader strict schema check + a fail-soft path that logs unmappable rows to a staging table. (R2) Brand overrides may conflict with master (different allergen flags for the same item across brands) — admin reviews via UI; for now `spec_items.notes` captures the divergence. (R3) Customer-facing claims (kosher, halal) have legal implications — admin signs off before any spec row is marked status='active' for use in quotes. spec_versions audit trail captures who approved. (R4) Spec sheet PDFs in outputs/<brand>/ are generated artifacts — they live in R2, not D1; the spec_items.output_pdf_r2_key column points to the latest generated artifact per (item, brand). (R5) NS items.last_modified vs spec_items.last_synced_at drift can grow if the spec sheet program isn't re-ingested — nightly drift healthcheck + monthly admin review surfaces this. Open items before round 12 ships: confirm master_products.csv column schema; map brand_config.json fields to brand-specific overrides on spec_items; design admin approval UI for status transitions; specify which NS items.custitem_as_* fields are 'NS owns this' vs 'spec_items owns this'.

ADR-026 — Suggestion engine and continuous-learning corpus — the platform gets faster the more the team uses it

2026-05-20 · Proposed

Context: User directive on 2026-05-20: 'as we do more and more bids and do more and more price quotes it should learn and be smarter and help make my users faster we just do a lot of the same types of things over and over again.' The platform has the write surface today (round 15: bid create/close, spec versioning, vendor cost create, customer contact, program create/update) and the read surface across 3 domains (customer/vendor/shared catalog) plus 8 Vectorize namespaces. What it does NOT have is a memory of past admin decisions — when a similar bid arrived 3 months ago, what price did we set? When this vendor raised, how did we re-quote the affected customers? When this customer asked for terms, what did we offer? Today those decisions live in ai_audit_log (chat queries), platform_writes_log (admin writes), bid status changes, and spec_versions — but none of that is queryable as a suggestion engine. The team does ~5-20 bids/month and the patterns are highly repetitive (school district + item set + program type + margin band). A suggestion engine would: (a) auto-fill new bid drafts based on similar past bids, (b) suggest customer-program templates when adding a new customer, (c) pre-populate vendor-cost ripple impact when a new price arrives, (d) recommend spec changes when allergen/regulation patterns emerge, (e) flag anomalies (this customer's price moved 18% MoM — was that intentional?).

Decision: Build a learning corpus on top of the existing audit infrastructure. Three pieces: (1) **decision_corpus** D1 table (round 16 migration 026) — one row per admin decision worth remembering. Fields: id, decision_type ('bid_price_set', 'quote_terms', 'vendor_cost_accepted', 'spec_change', 'program_offered', 'bid_status_set', 'contact_role_assigned'), situation_json (snapshot of inputs — customer, item, vendor, market context), action_json (what admin chose), outcome_json (what happened — bid won/lost, customer accepted/rejected), confidence_score (admin self-rated, 0-1), tags (free-text labels), created_at, created_by, source ('admin' / 'inferred' / 'imported'). Populated by writeAudit() decorator at every relevant write endpoint. (2) **decision_corpus** Vectorize namespace — embed each row as 'When [situation], we chose [action] and the outcome was [outcome].' Similarity search at suggestion time. (3) **Suggestion endpoints** — /api/suggest/bid-price (input: customer + items + context; output: ranked price suggestions with rationale + closest past matches as provenance), /api/suggest/customer-template (input: new customer + segment; output: typical programs + terms), /api/suggest/vendor-impact (input: new vendor cost; output: ripple-projected customer-quote adjustments). Each suggestion includes provenance (which past decisions informed it). Admin accepts/rejects via ?confirm or ?reject, which writes back to decision_corpus with the outcome label — closing the learning loop. Use ADR-024's council mode to validate high-stakes suggestions (bid prices on accounts >$50K/yr). Use ADR-021's vision tier-2 (Claude) for ambiguous pattern detection. Vectorize index `gfs-decision-memory` (new, separate from `gfs-pricing-corpus`) so chat retrieval and suggestion retrieval don't interfere. Faithfulness: every suggestion shows its source decisions as citations (sql_lineage + as_of_date per ADR-024 contract). User can reject + add note → trains the next suggestion. Cloudflare services involved: D1 (decision_corpus), Vectorize (separate index), Workers AI (embedding + reranker + suggestion synthesis), AI Gateway → Claude for nuance, Workflows for multi-step suggestion orchestration (e.g. bid draft = fetch bid + match past similar + suggest prices + suggest programs + assemble draft response).

Consequences: Pros: the platform genuinely gets faster the more the team uses it. New bid in an hour instead of a half-day. Spec changes ripple automatically with admin-confirmation. Customer onboarding templates emerge from history rather than requiring rebuild. Vendor-cost ripples flag impact before admin notices. The 'AI proposes, admin approves' contract from the Architectural Identity is honored — suggestions are never auto-applied, always require ?confirm=true. Cons: garbage in, garbage out. If admin makes inconsistent or wrong decisions, the engine learns those patterns and confidently suggests them. Mitigated by (a) faithfulness contract (every suggestion shows its sources), (b) admin can reject + correct (closes learning loop), (c) council mode validates high-stakes calls before applying. Storage grows linearly with decisions — ~5K decisions/year is trivial for D1+Vectorize. Risks: (R1) suggestion bias from a small initial corpus — for the first ~3 months, suggestions are low-confidence; supplement with rule-based templates from operating model docs. (R2) Stale decisions cited as relevant — add as_of_date filtering, decay relevance for decisions older than 1 year. (R3) Vendor or customer changes that invalidate past decisions silently — drift healthcheck flags it. (R4) Privacy of customer-specific suggestions if Cloudflare ever has a multi-tenant Worker incident — Vectorize index is per-account scoped, low risk today. (R5) Suggestions can be wrong in non-obvious ways (e.g. extrapolating last year's margin to a brand new account type). Mitigated by displaying the closest source decisions inline + admin spot-checking. Open items before suggestion engine ships: migration 026 (decision_corpus DDL), populate from ai_audit_log + platform_writes_log + bid status changes + spec_versions, Vectorize index provisioning, three suggestion endpoints, dashboard surface for accept/reject.

ADR-027 — Enterprise services posture — adopt the right Cloudflare services and external models, skip cheap shortcuts

2026-05-20 · Proposed

Context: User directive on 2026-05-20: 'I have an unlimited cloudflare budget to make this amazing. Skip and don't cheap or be cheap on anything this is enterprise production grade system we are building.' Prior cost-conscious decisions documented in CLOUDFLARE_SERVICES.md (Workers Paid $5/mo, recommended Access $3/mo, skipped Zone Pro $20/mo and Business $200/mo). This ADR supersedes the conservative cost-control posture from earlier rounds and adopts an enterprise-grade service mix where the marginal value justifies the spend. The platform is the company's single operating system; downtime, data loss, or weak validation are existential risks that rounding-error spend savings can't justify.

Decision: Adopt the following Cloudflare services and operational posture: (1) **Cloudflare Access (Zero Trust Standard) — enable.** $3/user/mo per ADR-014 + audit recommendation. SSO with Google Workspace; identity-aware policies on Pages + Worker routes; audit log of every human access. (2) **AI Gateway with multi-provider routing — enable.** Anthropic + Moonshot (Kimi) + Alibaba (Qwen) + OpenAI as fallback. AI Gateway handles prompt caching (Anthropic ephemeral), retries, cost tracking, observability. Per ADR-020/024 plan. Add Worker Secrets ANTHROPIC_API_KEY, MOONSHOT_API_KEY, DASHSCOPE_API_KEY. (3) **Cloudflare Workflows — enable.** Durable multi-step orchestration. Use cases: annual price roll cycle (Pillar 4 — 11 steps), suggestion engine pipelines (fetch+match+suggest), ADR-019 NS push consumer (idempotency + retry + mirror-back). Workflows replaces what would otherwise be ad-hoc cron+queue combinations. (4) **Durable Objects — enable.** Per-customer mutex for ADR-019 push (replaces racy KV mutex), per-session chat state (Agents SDK), atomic cost cap (audit Chat-Top-2). (5) **Browser Rendering — enable.** Quote PDFs (POST /api/quotes/generate, round 16), spec sheet PDFs, invoice/AR aging exports. Renders HTML templates → PDF → R2. (6) **Email Workers — enable.** bids@ + pricerequest@ intake per ADR-015/017. JSON-mode vision per ADR-021. Tier-2 escalation to Claude Sonnet Vision when parse_confidence<0.6. (7) **Logpush — enable.** Push Worker request logs to R2 monthly for compliance + forensics. (8) **Analytics Engine — enable.** Streaming metrics for chat usage, suggestion accept-rate, endpoint latency, cost per question. Beat-frequency observability beyond per-call ai_audit_log. (9) **Cloudflare Pages zone plan: stay Free.** Zone Pro/Business doesn't add value here; Access + Workers Paid + the above stack cover the requirements. (10) **NetSuite SuiteAPI + Chartstone — keep current.** ADR-019 push contract uses RESTlet 2948 via Worker scheduled() (not Chartstone) for write side; Chartstone remains for admin SuiteQL exploration. **External models** (provisioned this round, used per ADR-024 routing): - Anthropic Claude (Haiku 3.5 for complex reasoning + Sonnet for vision) - Moonshot Kimi K2 for long-context summarization - Alibaba Qwen 2.5 72B for bulk classification - Workers AI Llama 3.3 70B as the default + always-fallback Projected monthly spend (post-adoption): Workers Paid $5 + Access $3/user + AI Gateway routing ~$50-150/mo at expected volume + Workflows ~$5/mo + Browser Rendering ~$3/mo + Logpush included = ~$70-200/mo. At enterprise-grade reliability, this is rounding error.

Consequences: Pros: every architectural commitment from ADRs 011-026 finally has its supporting service. The platform shifts from 'mostly free tier with stubs' to 'enterprise production grade with real wiring everywhere.' Single-admin team has the same access controls, observability, and reliability as a 50-person engineering org. Chat quality leaps when multi-provider routing kicks in. Suggestion engine becomes feasible with Workflows orchestration. Cons: managing 4 external model API keys + their rotation. Cost ceiling is no longer the natural rate-limiter — cost caps (ai_gateway_cost_caps) become more important. Operational complexity rises (more services = more rotations, more dashboards). Risks: (R1) API key leakage of any provider key = unbounded spend on that provider — mitigated by Worker Secrets (not env vars), monthly rotation policy, AI Gateway's own throttling. (R2) Vendor lock-in to Cloudflare across multiple services — accepted (ADR-011 Cloudflare-native posture); risk amortized over decreasing per-service migration cost. (R3) Cost surprise from any single provider — AI Gateway daily cost cap per tier (existing ai_gateway_cost_caps framework extends to each new tier). (R4) Email Worker + Browser Rendering + Workflows are all newer Cloudflare products — small risk of API changes; mitigated by versioning binding configs in wrangler.jsonc with compatibility_date. (R5) Multi-provider routing config drift between dev/prod — config lives in data/ai_gateway_routes.json + Workflows definitions in wrangler.jsonc, deployed atomically with Worker code. Open items before this ADR is fully landed: provision API keys for Anthropic/Moonshot/Alibaba; enable AI Gateway in Cloudflare dashboard; provision first Workflow (NS push consumer); enable Access on Pages + Worker; enable Browser Rendering; enable Logpush; enable Analytics Engine. Each comes with its own configuration commit; all land round 16.

ADR-028 — Dual-intake + HITL review queue + chat-as-omni-interface — with Karpathy-inspired learning loop underneath

2026-05-20 · Proposed

Context: User question 2026-05-20: 'should the two intake engines for updates be email to the portals and we have some workflow and admin dashboard to check step all the updates that human in the loop step. And then also the chat interface like a chatgpt thats linked to all of this? we have all the cloudflare tools available to us to ensure these pipelines, how this all works, everything is best.' Plus earlier directive: 'as we do more and more bids it should learn and be smarter and help my users faster.' This ADR consolidates the user's intended end-state into one architecture and maps every layer to a specific Cloudflare service. The 10 Karpathy patterns (eval set as spec, few-shot retrieval, closed-loop diff capture, two-tier model distillation, process supervision, tool-use over generation, anomaly detection on repeated patterns, confidence-gated autonomy, curriculum from difficulty tags, chat-as-memory) are adopted as the learning substrate. ADR-026 introduced the suggestion engine; ADR-027 the enterprise services posture. This ADR specifies how they fit together end-to-end and the wiring order.

Decision: Four-layer architecture, each layer mapped to specific Cloudflare services. **LAYER 1 — INTAKE (data flowing IN)**. Three engines: (a) Email Worker on bids@ai-globalfoodsolutions.co + pricerequest@ai-globalfoodsolutions.co (Cloudflare Email Routing + Email Workers, ADR-015/017/021). PDF/XLSX attachments to R2, vision parse via Workers AI llava (tier 1) or AI Gateway → Claude Sonnet Vision (tier 2 when parse_confidence<0.6). Writes to inbound_email_log + bids_pending_review. (b) Direct admin API writes (round 15 endpoints): /api/bids/create, /api/spec-items/:code/version, /api/vendor-costs/create, /api/customers/:id/contact, /api/programs/create, etc. Every write logs to platform_writes_log. (c) Future: vendor portal email parser (vendor sends new price sheet → auto-extract → propose vendor_costs row → admin reviews). Same Email Worker pipeline with different routing rule. **LAYER 2 — HITL REVIEW QUEUE (human-in-the-loop)**. Cloudflare Workflows orchestrate every inbound + every system-detected change as a durable, retryable, step-supervised review item. New D1 table review_queue (round 16 migration 026): item_id, source (email_bid / api_write / drift_detection / suggestion / anomaly), context_json, suggested_action, status (pending / approved / rejected / escalated), confidence_score, ambiguities_json, assigned_to, created_at, decided_at, decided_by, decision_notes. Admin dashboard adds /review tab showing the queue with color-coded confidence per Karpathy Pattern 8: green ≥0.85 (one-click accept), yellow 0.60-0.85 (requires "I read this" click which itself is a signal), red <0.60 (chat refuses to suggest, escalates). Each review records step_grades (Pattern 5) so we know per-step failure modes instead of just outcome wins. Workflows manages retry + state. **LAYER 3 — CHAT-AS-OMNI-INTERFACE (the front door)**. ChatGPT-style HTML at /chat.html (new Pages surface, round 16). Backed by /api/pricing-chat (already built — intent routing + faithfulness + council mode) PLUS new /api/chat/session for durable conversation state via Durable Objects (Karpathy Pattern 10 — chat as memory). Tool-use enforced (Pattern 6): chat NEVER generates a numeric figure — it calls /api/customers/:id, /api/vendor-costs, /api/pricing/compare, etc. The Anthropic tool_use protocol routes the LLM to the right endpoint with typed args. Provenance contract from ADR-024 stays. New: chat surfaces accept/reject buttons for every suggestion (closes Pattern 3 correction-capture loop). Session memory across days — when user asks 'what did we decide about Driscoll last week' it actually knows. **LAYER 4 — LEARNING SUBSTRATE (the engine that gets smarter)**. Sits under all three above. Components: (i) pricing_evals + pricing_eval_runs (Pattern 1) — eval-driven development; every Worker deploy runs the suite and refuses to merge if easy/medium pass-rate regresses. (ii) decision_corpus (Vectorize namespace gfs-decision-memory) — every approved review item + every confirmed write embeds into this corpus, retrievable as few-shot examples for similar future decisions (Pattern 2). (iii) suggestion_corrections (Pattern 3) — diff between suggested vs accepted. Nightly cron promotes high-signal diffs to pricing_evals. (iv) two-tier routing (Pattern 4) — Workers AI Llama serves; AI Gateway → Claude Haiku/Sonnet called only when confidence<threshold OR when needed for distillation labels. model_disagreements table captures every Sonnet ≠ Llama case for weekly human grading. (v) step_grades (Pattern 5) per Workflow step — diagnose where reasoning fails. (vi) repeated_failures (Pattern 7) — cluster suggestion_corrections weekly; clusters ≥3 in 30 days become testable patterns (Promote to eval / Add hard rule). (vii) confidence calibration (Pattern 8) — weekly bucket prediction confidence vs actual pass-rate; adjust thresholds. (viii) difficulty curriculum (Pattern 9) — pricing_evals.difficulty tag drives test ordering. **Cloudflare service-to-layer mapping** (per ADR-027): - Email Routing + Email Workers → Layer 1 (intake) - Workflows → Layer 2 (HITL durable orchestration) + Layer 4 (eval pipeline orchestration) - Pages (already used) + new /chat.html → Layer 3 (chat UI) + admin dashboard /review tab - Durable Objects → Layer 3 (chat session state) + atomic cost cap (ADR-027) + per-customer push mutex (ADR-019) - D1 → all four layers (review_queue, pricing_evals, decision_corpus rows, etc.) - Vectorize → Layer 3 retrieval (existing gfs-pricing-corpus) + Layer 4 decision memory (new gfs-decision-memory namespace) - Workers AI → embedding + Llama default reasoning - AI Gateway → multi-provider routing (Anthropic + Moonshot + Alibaba) for council mode + distillation labels - R2 → attachment storage + quote PDFs (Browser Rendering output) + monthly archival - Browser Rendering → quote PDF generation + spec sheet PDFs - Logpush → compliance retention of Worker logs - Analytics Engine → streaming metrics for chat usage + suggestion accept-rate - Cloudflare Access → SSO/Zero Trust on the dashboard + chat surfaces Wiring order (Karpathy-discipline: minimum viable, then compound): Week 1: Pattern 1 (eval table + 50 seed cases) + Pattern 6 (tool-use enforced in chat). Week 2: Pattern 3 (correction capture) + Pattern 2 (few-shot retrieval at chat time). Week 3: Pattern 8 (confidence gating in review queue) + Pattern 10 (Durable Objects session). Week 4: Pattern 4 (two-tier routing via AI Gateway) + Pattern 5 (Workflow step grades). Week 5+: Pattern 7 (anomaly clustering) + Pattern 9 (curriculum). Patterns 7 and 9 are leverage — they only pay off after 4+ weeks of corrections + eval runs accumulate. Building them earlier is over-engineering.

Consequences: Pros: the platform is now ONE thing — intake → HITL review → admin acts → learning captures → future suggestions get better. ChatGPT-style chat is the front door for both querying ('what's Driscoll's margin on RS9310W') and acting ('approve the pending bid for Wappingers'). The user operates the entire company from one interface. Every action improves the next. Every Cloudflare service has a load-bearing job — no orphan products. Cons: this is the most complex ADR by far. Operationally it spans 11+ Cloudflare services + 4 external model providers + 6+ new D1 tables + 2 new Vectorize namespaces + new Pages surfaces. Failure modes multiply. Mitigated by the wiring order — minimum-viable foundation first, leverage patterns only after data accumulates. Risks: (R1) over-engineering at any phase blocks the next — discipline against building Pattern 7/9 in week 1. (R2) Workflows is a newer Cloudflare product; API changes possible — mitigated by versioning binding configs. (R3) Multi-provider AI cost surprise — daily cost caps per tier (existing ai_gateway_cost_caps framework). (R4) The HITL queue becomes the bottleneck if review volume spikes — mitigated by confidence-gated autonomy: high-confidence items can auto-accept with after-the-fact audit instead of pre-approval. (R5) chat-as-omni creates a single point of UX failure — mitigated by keeping the 65+ direct API endpoints as operational alternatives. (R6) Eval set quality drift — mitigated by Pattern 9 curriculum rotation + nightly diff-capture review. (R7) Worker bundle size grows with all the routing logic — mitigated by Workflows offloading orchestration, Durable Objects offloading state. (R8) Each new external provider key is a leak surface — mitigated by Worker Secrets + monthly rotation + AI Gateway's own throttling. Open items before Layer 1+2 ships: provision Workflows, Durable Objects, Email Workers, Browser Rendering, Logpush, Analytics Engine in Cloudflare dashboard; API keys for Anthropic + Moonshot + DashScope as Worker Secrets; migration 026 for review_queue + pricing_evals + suggestion_corrections + step_grades + repeated_failures + chat_sessions tables; chat.html on Pages; /review tab on admin-dashboard.html; first 50 graded historical bids seed pricing_evals.

Adding a new ADR

  1. Open data/decisions.json.
  2. Append a new object to entries[] with id ADR-NNN (next number), date, title, status (Proposed / Accepted / Superseded), context, decision, consequences.
  3. Run python3 assets/build_pages.py.
  4. Commit and deploy.

Related surfaces