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:
| Layer | Format | Latency | Consumer |
|---|---|---|---|
| events table | event-sourced rows | live | append-only audit; everything else derives |
qc_queue_projection | JSONB | <100ms | inspector mobile UI + foreman dashboard |
qc_inspection_history_projection | JSONB | <100ms | per-load / per-vendor / per-commodity history reads |
vendor_qc_score_projection | JSONB | <100ms | vendor scorecard reads |
commodity_defect_pattern_projection | JSONB | <100ms | commodity 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 push | agent reading via search_entity_profiles / get_entity_profile |
doc_backlink_projection | JSONB edges | live (dispatcher) | drill-through graph traversal |
vault docs/qc/inspections/<id>.md | git-versioned MD | ~30s after qc.inspection_completed | human reviewers, archival, vault search |
vault docs/qc/queue/<date>.md | git-versioned MD | daily 06:00 ET | morning briefing |
vault docs/qc/vendor-scorecards/<vid>.md | git-versioned MD | weekly + on-event | account managers |
vault docs/qc/commodity-patterns/<slug>.md | git-versioned MD | weekly + on-event | buyers + agent reasoning |
vault docs/qc/inspector-voices/<actor>.md | git-versioned MD | weekly | Program 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:
| Projector | Central MDs read from _taproot_mirror/ | Why |
|---|---|---|
qc_inspection_report | docs/commodities/<slug>.md, docs/vendors/<slug>.md | Canonical commodity defect baseline + vendor profile context for the narrative |
qc_load_summary | docs/commodities/<slug>.md per commodity on load, docs/vendors/<slug>.md | ”Expected quality given this commodity + this vendor” baseline used in summary |
qc_queue | docs/commodities/<slug>.md | Shelf-life clock + handling-SOP context per commodity in queue |
vendor_qc_scorecard | docs/vendors/<slug>.md | Central vendor MD is the canonical cross-tenant view; tenant overlay adds Nathel-specific QC outcomes on top |
commodity_defect_pattern | docs/commodities/<slug>.md | Central 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)
| Property | Value |
|---|---|
| Vault path | docs/qc/inspections/<inspection_id>.md |
| Trigger | qc.inspection_completed event |
| Latency target | <30s after event |
| Source events | qc.inspection_completed, qc.photo_captured (siblings), qc.voice_transcribed (optional) |
| Renderer source file | packages/mcp-server/src/workers/tenant-projector/qc-inspection-report.ts |
| Emits | projector.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
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)
| Property | Value |
|---|---|
| Vault path | docs/qc/load-summary/<load_id>.md |
| Trigger | qc.inspection_completed (any inspection on the load) + inbound.load_closed |
| Latency target | <30s after event |
| Idempotency | Same 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)
| Property | Value |
|---|---|
| Vault path | docs/qc/queue/<YYYY-MM-DD>.md |
| Trigger | Cron 0 11 * * * UTC (06:00 ET — beginning of day shift) + inbound.load_expected high-priority |
| Latency target | Cron-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_requestedevents) - Backlog (any prior-day inspections still status=‘pending’)
1.4 vendor-qc-scorecard (per-vendor, weekly + on-event)
| Property | Value |
|---|---|
| Vault path | docs/qc/vendor-scorecards/<vendor_id>.md |
| Trigger | Cron 0 12 * * 1 UTC weekly (07:00 ET Monday) + qc.inspection_completed for that vendor (debounced) |
| Latency target | Weekly + ~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)
| Property | Value |
|---|---|
| Vault path | docs/qc/commodity-patterns/<commodity_slug>.md |
| Trigger | Cron weekly + qc.inspection_completed for that commodity (debounced) |
| Latency target | Weekly + ~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)
| Property | Value |
|---|---|
| Vault path | docs/qc/inspector-voices/<actor_id>.md |
| Trigger | Cron weekly |
| Coordination | Replaces 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
| Column | Purpose |
|---|---|
tenant_id | RLS |
load_id (PK part) | The load being inspected |
inspection_id (PK part, nullable until inspection starts) | Becomes set when assigned |
commodity_id | For grouping/filtering |
vendor_id | For per-vendor queue |
priority | Risk-ranked (incoming + vendor history + commodity pattern) |
assigned_inspector_id | nullable |
requested_at | If from inbound.inspection_requested |
expected_eta | From inbound.load_expected |
status | pending / in_progress / completed / cancelled |
last_state_event_id | For 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.
| Column | Purpose |
|---|---|
tenant_id | RLS |
inspection_id (PK) | |
load_id, vendor_id, commodity_id, lot_id | Multi-dimensional lookup |
inspector_id | |
disposition, scores (JSONB) | |
vault_path | Reference to docs/qc/inspections/<id>.md |
photo_count, photo_storage_keys (array) | |
narrative_excerpt | First 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 
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 event | Path |
|---|---|
| Pedro requests inspection in Teams | signal.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 mobile | qc.voice_captured → STT pipeline → qc.voice_transcribed { text, confidence } → composes into inspection narrative |
| Inspector completes inspection on mobile | Direct qc.inspection_completed emission with structured payload (preferred over Teams scrape) |
| Customer needs credit notification | On 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 summary | qc-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)
- Add migrations for 4 new JSONB projections (qc_queue, qc_inspection_history, vendor_qc_score, commodity_defect_pattern) — mirror W2 mig 065/066 pattern
- Write event handlers in
packages/mcp-server/src/event-handlers/projection-qc-*.ts— register indispatchProjectionUpdaters(W2 pattern) - Backfill from existing events (~146 Teams messages + any historical inspections in operational_memory)
- Add MCP readers: get_qc_inspection_history, get_vendor_qc_history, get_commodity_qc_history
- Add to role catalogs (qc_inspector, foreman, buyer, sales, admin) — mirror W3 P1-02 pattern
Sequence Q2 — Vault MD projectors (~3-4 days)
- Build
qc-inspection-reportprojector (per-inspection, event-triggered) — mirror P0-W5-06 daily_arrivals - Build
qc-load-summaryprojector (per-load, event-triggered + on-close) - Build
qc-queueprojector (cron + high-priority arrival) — EIF-fused like P0-W5-06-B - Build
vendor-qc-scorecardprojector (weekly + event) - Build
commodity-defect-patternprojector (weekly + event) - Build
inspector-voiceprojector (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)
- Update agent system prompt (P1-01 pattern) — teach the new QC tools + drill chain
- Update EntityRoute drill paths so commodity → vendor → lot all reach QC inspection MDs
- 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_projectioninto this architecture’s §1.6inspector-voicevault 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-queueprojector should EIF-fuse per the daily_arrivals v2 template. - Inbox-lineage (W3 P0-08): inspection requests from Pedro’s emails now carry
source_email_inboxso 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
qc.inspection_completedschema — does the current event payload schema inpackages/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.- 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.
- Inspector mobile workspace — milestone status? This blocks the §3.2 future state.
- Photo retention policy — R2 storage cost grows. Define retention (90d hot / 1y warm / 7y archival for FSMA?).