QC System — Projection Architecture

Audience: orchestrators + subagents building the QC projector family Status: canonical (per Rob, 2026-05-18) Prerequisite knowledge: Engineering Spec §11.7 — 3-rung Taproot

0. TL;DR

Every qc.inspection_completed event ripples into 5 vault MD docs + 3 JSONB projections + 1 photo bundle + ~8 backlink edges using infrastructure shipped in Waves 1-5 + Program A overlap. The QC system is the first live-test surface — what we build for QC becomes the template for every other event-rich domain (receiving, dispatch, sales).

Single source of truth: qc.inspection_completed event (and its sibling qc.photo_captured, qc.voice_transcribed).

Derived surfaces:

LayerFormatLatencyConsumer
events tableevent-sourced rowsliveappend-only audit; everything else derives
qc_queue_projectionJSONB<100msinspector mobile UI + foreman dashboard
qc_inspection_history_projectionJSONB<100msper-load / per-vendor / per-commodity history reads
vendor_qc_score_projectionJSONB<100msvendor scorecard reads
commodity_defect_pattern_projectionJSONB<100mscommodity pattern reads
entity_doc_projection (commodity, vendor, lot, inspection rows)MD-rendering metadata<100ms (status flip on event)renderer worker queue
knowledge.domain='doc' (vault-ingested MD)full MD + frontmatter~30s after vault pushagent reading via search_entity_profiles / get_entity_profile
doc_backlink_projectionJSONB edgeslive (dispatcher)drill-through graph traversal
vault docs/qc/inspections/<id>.mdgit-versioned MD~30s after qc.inspection_completedhuman reviewers, archival, vault search
vault docs/qc/queue/<date>.mdgit-versioned MDdaily 06:00 ETmorning briefing
vault docs/qc/vendor-scorecards/<vid>.mdgit-versioned MDweekly + on-eventaccount managers
vault docs/qc/commodity-patterns/<slug>.mdgit-versioned MDweekly + on-eventbuyers + agent reasoning
vault docs/qc/inspector-voices/<actor>.mdgit-versioned MDweeklyProgram A narrative library

0.6 Central-overlay reads contract (3-rung taproot — see AOL §13.6)

Every projector in this family is a rung-3 tenant overlay renderer. Each one reads tenant operational data (rung-3 inputs) AND reads Central knowledge MDs mirrored under _taproot_mirror/ (rung-1 outputs synced via rung-2 transport). The fusion is at the projector layer — NOT only at the agent surface via get_entity_profile.

Per QC projector:

ProjectorCentral MDs read from _taproot_mirror/Why
qc_inspection_reportdocs/commodities/<slug>.md, docs/vendors/<slug>.mdCanonical commodity defect baseline + vendor profile context for the narrative
qc_load_summarydocs/commodities/<slug>.md per commodity on load, docs/vendors/<slug>.md”Expected quality given this commodity + this vendor” baseline used in summary
qc_queuedocs/commodities/<slug>.mdShelf-life clock + handling-SOP context per commodity in queue
vendor_qc_scorecarddocs/vendors/<slug>.mdCentral vendor MD is the canonical cross-tenant view; tenant overlay adds Nathel-specific QC outcomes on top
commodity_defect_patterndocs/commodities/<slug>.mdCentral commodity MD lists the standard defect-class taxonomy; tenant overlay adds frequency observations
inspector_voice(none — pure tenant)Inspector voices are tenant-private actor.memory artifacts

Read graceful-degradation contract: if _taproot_mirror/docs/commodities/<slug>.md is missing (mirror not yet synced or commodity not in Central), the projector MUST render the tenant overlay alone with a central_pending: true flag in frontmatter. Never block.

Write contract: projectors write to tenant paths (docs/qc/...) only. Never write to _taproot_mirror/ (that’s rung-2’s space).


1. The 5 vault MD projectors

Each follows the P0-W5-06 daily_arrivals pattern (commit a3a6d101 Wave 4 + EIF v2 commit 8b067afa):

  • Pure renderer (deterministic; testable with stubs)
  • GitHub Contents API writer (fetch + PUT, no octokit dep)
  • Orchestrator (run<Name>Projector) — query → render → PUT → emit event
  • Cron + event triggers
  • Injectable DB + GitHub fetcher

1.1 qc-inspection-report (per-inspection, event-triggered)

PropertyValue
Vault pathdocs/qc/inspections/<inspection_id>.md
Triggerqc.inspection_completed event
Latency target<30s after event
Source eventsqc.inspection_completed, qc.photo_captured (siblings), qc.voice_transcribed (optional)
Renderer source filepackages/mcp-server/src/workers/tenant-projector/qc-inspection-report.ts
Emitsprojector.qc_inspection_rendered { inspection_id, vault_path, photo_count, ... }

Body sections:

  • Header (commodity • vendor • lot • inspector • timestamp)
  • Disposition (accept / reject / conditional / partial) + scores (color, firmness, decay, pest, pack)
  • Photo gallery — embedded ![defect:<area>](r2://photos/<sha256_hash>) refs
  • Inspector narrative (full text, preserves voice signature)
  • Linked context (drill-through to commodity profile, vendor scorecard, lot status, PO line items)
  • Action trail (any cascades: vendor scorecard update, customer notification, credit_required flag)

1.2 qc-load-summary (per-load, event-triggered)

PropertyValue
Vault pathdocs/qc/load-summary/<load_id>.md
Triggerqc.inspection_completed (any inspection on the load) + inbound.load_closed
Latency target<30s after event
IdempotencySame vault path on multiple triggers — re-renders accumulate inspections

Body sections:

  • Load metadata (vendor, ETA, total cases, doors)
  • Per-PO breakdown (PO_id → inspections → dispositions)
  • Per-line-item table (commodity, lot, disposition, scores)
  • Photos collage (all photos across all inspections on this load)
  • Customer-allocation impact (which open SOs were affected; auto-link to notification trail if dispositions trigger comms)

1.3 qc-queue (daily, cron-triggered + on high-priority arrival)

PropertyValue
Vault pathdocs/qc/queue/<YYYY-MM-DD>.md
TriggerCron 0 11 * * * UTC (06:00 ET — beginning of day shift) + inbound.load_expected high-priority
Latency targetCron-scheduled
EIF fusion (per P0-W5-06-B pattern)Yes — pulls signal.quality_observation history per incoming commodity/vendor for risk-ranking

Body sections:

  • Today’s expected loads (from inbound.load_expected)
  • Risk-ranked (high quality risk first per commodity_defect_pattern_projection + vendor history)
  • Per-inspector pre-assignment (Glenn AM, Anthony PM, Pedro request-originator)
  • Outstanding inspection requests (from inbound.inspection_requested events)
  • Backlog (any prior-day inspections still status=‘pending’)

1.4 vendor-qc-scorecard (per-vendor, weekly + on-event)

PropertyValue
Vault pathdocs/qc/vendor-scorecards/<vendor_id>.md
TriggerCron 0 12 * * 1 UTC weekly (07:00 ET Monday) + qc.inspection_completed for that vendor (debounced)
Latency targetWeekly + ~5min after qc events

Body sections:

  • 30/90/365-day pass rate trends
  • Defect pattern distribution (per commodity from this vendor)
  • Inspector narrative quotes (“WISHNATZKI / 00267158 picks up matched PP signal block…“)
  • Cost-of-quality (rejection cost + credit claims filed)
  • Comparison to peer vendors on same commodities

1.5 commodity-defect-pattern (per-commodity, weekly + on-event)

PropertyValue
Vault pathdocs/qc/commodity-patterns/<commodity_slug>.md
TriggerCron weekly + qc.inspection_completed for that commodity (debounced)
Latency targetWeekly + ~5min after qc events

Body sections:

  • Defect type frequency (color drift, firmness loss, decay, pest, pack damage)
  • Seasonal patterns (which months show what issues)
  • Vendor × defect heatmap
  • Anthony-style cross-load comparisons ("a little better size than lot# 548591")
  • Successful pack patterns (what works well, by vendor + season)

1.6 inspector-voice (per-actor, weekly — overlaps Program A)

PropertyValue
Vault pathdocs/qc/inspector-voices/<actor_id>.md
TriggerCron weekly
CoordinationReplaces standalone Program A qc_inspector_narrative_projection — use vault MD as the canonical voice corpus; Program A becomes the historical backfill that populates the first version of this doc

Body sections:

  • Voice signature metadata (avg sentence length, signature phrases, prose vs terse classifier)
  • Recent narratives (last 20)
  • Vendor/commodity coverage breakdown
  • Style examples (what makes Glenn vs Anthony vs Pedro recognizable)

2. The 3 JSONB projections (sub-100ms reads)

These power the operational UI surfaces. Mirror the W2 P0-05/P0-06 dispatcher pattern.

2.1 qc_queue_projection

ColumnPurpose
tenant_idRLS
load_id (PK part)The load being inspected
inspection_id (PK part, nullable until inspection starts)Becomes set when assigned
commodity_idFor grouping/filtering
vendor_idFor per-vendor queue
priorityRisk-ranked (incoming + vendor history + commodity pattern)
assigned_inspector_idnullable
requested_atIf from inbound.inspection_requested
expected_etaFrom inbound.load_expected
statuspending / in_progress / completed / cancelled
last_state_event_idFor replay

Consumed by: get_qc_work_queue MCP tool (already exists)

2.2 qc_inspection_history_projection

Per (load_id, vendor_id, commodity_id, inspection_id) row. Append-only after qc.inspection_completed.

ColumnPurpose
tenant_idRLS
inspection_id (PK)
load_id, vendor_id, commodity_id, lot_idMulti-dimensional lookup
inspector_id
disposition, scores (JSONB)
vault_pathReference to docs/qc/inspections/<id>.md
photo_count, photo_storage_keys (array)
narrative_excerptFirst 280 chars for previews
inspected_at

Consumed by: new MCP tools get_qc_inspection_history (per any dimension), get_vendor_qc_history, get_commodity_qc_history

2.3 vendor_qc_score_projection + commodity_defect_pattern_projection

These are RUNNING AGGREGATES updated on each qc.inspection_completed.

vendor_qc_score_projection (tenant_id, vendor_id, commodity_id):

  • pass_rate_30d, pass_rate_90d, pass_rate_365d
  • avg_score_30d (composite of color+firmness+decay+pest+pack)
  • inspections_count_30d
  • last_inspection_at
  • vault_scorecard_path (reference to weekly MD)

commodity_defect_pattern_projection (tenant_id, commodity_id):

  • defect_distribution_30d (JSONB — { color: 0.05, firmness: 0.12, decay: 0.02, pest: 0.01, pack: 0.08 })
  • seasonal_index_current_month
  • vault_pattern_path

Consumed by: get_vendor_360, get_commodity_360 (already shipped in W3 P1-05 — extend to query these new projections)


3. Photo path

3.1 Today (live)

Inspector takes photo in Teams chat
  ↓
Teams attachment URL → SharePoint:/sites/QualityControl/Shared Documents/...
  ↓
Periodic backfill cron picks up the URL
  ↓
Program B1 parsers (just shipped commit 6288939b) extract content via:
  - AcroForm extractor for SC-237 PDFs
  - OCR fallback for scanned PDFs
  - ffmpeg frame extractor for .mov
  ↓
Program B2 (blocked on Azure AD app — doc at docs/handoffs/2026-05-18-azure-ad-app-setup-for-graph-sdk.md)
  Once Pedro grants admin consent, Graph SDK fetcher gets the binary by webUrl
  ↓
Bytes routed through parser facade → stored in R2 (or knowledge as binary)
  ↓
storage_key referenced in qc inspection MD via ![evidence](r2://photos/<sha256>)

3.2 Future (mobile-native — flagged as the unblock)

Inspector phone → camera capture
  ↓
POST /api/qc/photo (multipart) → uploaded to R2 with sha256 content-address
  ↓
Emit qc.photo_captured { storage_key, inspector_id, lot_id, defect_area, ts }
  ↓
Inspector continues / completes inspection on mobile workspace
  ↓
Emit qc.inspection_completed { ..., photo_storage_keys: [<hash>, ...] }
  ↓
qc-inspection-report projector renders MD with embedded R2 refs

Why mobile-first matters: removes the Teams roundtrip + SharePoint backfill + Graph SDK dependency from the critical path. Inspector photos go to authoritative storage immediately. Teams remains the chat surface but photos no longer travel through it.

Implementation gate: mobile workspace state. Needs:

  • R2 binding on the staging Worker (likely already there for other assets)
  • Multipart upload endpoint at /api/qc/photo
  • Mobile workspace CardSpec for camera input + thumbnail review
  • Permission flow (camera access)
  • Offline buffering (queue captures when network drops)

Estimated effort once mobile workspace exists: ~3-5 days. Out of scope for this architecture doc; flagged for separate program.


4. Native comms integration

Comm eventPath
Pedro requests inspection in Teamssignal.teams_message → S0.5 intake classifier (Wave 3 P0-08 propagates inbox-lineage) → emits inbound.inspection_requested { lot_id, priority, requested_by: pedro }qc_queue_projection adds high-priority row
Glenn/Anthony writes narrative in Teams (real-time during inspection)signal.teams_message → resolver candidate (currently 0.2% link rate — Program A tunes) → on link, composes into qc.inspection_completed.narrative field OR qc.inspector_note_added companion event
Inspector dictates voice memo on mobileqc.voice_captured → STT pipeline → qc.voice_transcribed { text, confidence } → composes into inspection narrative
Inspector completes inspection on mobileDirect qc.inspection_completed emission with structured payload (preferred over Teams scrape)
Customer needs credit notificationOn qc.inspection_completed.disposition='reject' or 'conditional' AND line_item has open sales.order_allocation: auto-emit comms.credit_notification_requested → routes to comms channel per customer preference (SMS/email/Slack)
Foreman wants daily summaryqc-queue projector (1.3) renders morning briefing → also pushed to foreman’s preferred Slack channel via comms.qc_queue_pushed event

5. Frontmatter contract (every QC vault doc)

---
schema_version: 1
doc_id: <unique-snake-case>
doc_kind: qc_inspection_report | qc_load_summary | qc_queue_daily | vendor_qc_scorecard | commodity_defect_pattern | inspector_voice
title: <human-readable>
slug: <kebab-case>
tenant_id: nathel
visibility: tenant_private          # NEVER customer-visible (vendor identity + cost)
data_plane: tenant_operational
renderer_version: <semver>
generated_from_event_ids: [...]
source_attribution: ["events:qc.inspection_completed", "events:qc.photo_captured", ...]
linked_entities:
  - { entity_type: load, entity_key: <id>, link_kind: subject_of }
  - { entity_type: po, entity_key: <id>, link_kind: covers }
  - { entity_type: line_item, entity_key: <id>, link_kind: inspected }
  - { entity_type: commodity, entity_key: <slug>, link_kind: category }
  - { entity_type: vendor, entity_key: <id>, link_kind: source }
  - { entity_type: lot, entity_key: <id>, link_kind: physical }
  - { entity_type: actor, entity_key: <id>, link_kind: authored_by }
  - { entity_type: photo, entity_key: <r2_key>, link_kind: evidence }
disposition: <if applicable>
scores: <if applicable>
photos_count: <int>
tags: [qc, inspection|load|queue|scorecard|pattern|voice, <commodity>, <vendor_short>]
created_at: "<ISO>"
updated_at: "<ISO>"
---

Critical: datetimes MUST be quoted strings (per fin-agentic#819 fix). The YAML date coercion handles unquoted but quoted is the documented style.

Why linked_entities matters: makes doc_backlink_projection (W2 P0-05, currently 2,620 edges from commodity profiles) immediately useful for QC — every inspection MD becomes a hub that drill-throughs reach from commodity/vendor/lot entity pages.


6. Build order

Sequence Q1 — Backend projection infrastructure (~2-3 days)

  1. Add migrations for 4 new JSONB projections (qc_queue, qc_inspection_history, vendor_qc_score, commodity_defect_pattern) — mirror W2 mig 065/066 pattern
  2. Write event handlers in packages/mcp-server/src/event-handlers/projection-qc-*.ts — register in dispatchProjectionUpdaters (W2 pattern)
  3. Backfill from existing events (~146 Teams messages + any historical inspections in operational_memory)
  4. Add MCP readers: get_qc_inspection_history, get_vendor_qc_history, get_commodity_qc_history
  5. Add to role catalogs (qc_inspector, foreman, buyer, sales, admin) — mirror W3 P1-02 pattern

Sequence Q2 — Vault MD projectors (~3-4 days)

  1. Build qc-inspection-report projector (per-inspection, event-triggered) — mirror P0-W5-06 daily_arrivals
  2. Build qc-load-summary projector (per-load, event-triggered + on-close)
  3. Build qc-queue projector (cron + high-priority arrival) — EIF-fused like P0-W5-06-B
  4. Build vendor-qc-scorecard projector (weekly + event)
  5. Build commodity-defect-pattern projector (weekly + event)
  6. Build inspector-voice projector (weekly — coordinates with Program A)

Each follows the 3-piece composition: pure renderer + GitHub Contents API writer + orchestrator with injectable deps.

Sequence Q3 — Agent surface + UI (~2 days)

  1. Update agent system prompt (P1-01 pattern) — teach the new QC tools + drill chain
  2. Update EntityRoute drill paths so commodity → vendor → lot all reach QC inspection MDs
  3. Pixel + click-through proof: “show me last week’s lime inspections” → agent uses search_entity_profiles → drills to inspection MD → photos render

Sequence Q4 — Mobile photo capture (out of scope; needs mobile workspace)

Flag for when mobile workspace milestone hits.


7. Coordination notes

  • Program A (spawned chip — 4 Teams-derived projections): merge qc_inspector_narrative_projection into this architecture’s §1.6 inspector-voice vault projector. Don’t build two parallel structures.
  • Program B1 (SharePoint parsers, shipped commit 6288939b): the photo extraction pipeline is ready. Once Azure AD lands, B2 plugs in.
  • EIF fusion pattern (commit 8b067afa): qc-queue projector should EIF-fuse per the daily_arrivals v2 template.
  • Inbox-lineage (W3 P0-08): inspection requests from Pedro’s emails now carry source_email_inbox so the QC queue projector can attribute correctly.
  • Doc-backlink graph (W2 P0-05): immediately starts populating for every QC inspection MD that lands via webhook.
  • YAML date coercion (fin-agentic#819): quote your datetimes in QC frontmatter.

8. Open questions

  1. qc.inspection_completed schema — does the current event payload schema in packages/shared/src/schemas/events/qc.ts (or wherever) include all fields needed (scores object, photo_storage_keys array, narrative, disposition)? If not, schema migration first.
  2. Customer notification trigger — what’s the policy? Auto-notify on any reject/conditional, or only when line_item.allocation_status=‘confirmed_to_customer’? Worth a decision before building.
  3. Inspector mobile workspace — milestone status? This blocks the §3.2 future state.
  4. Photo retention policy — R2 storage cost grows. Define retention (90d hot / 1y warm / 7y archival for FSMA?).