Projection Parity Audit — 2026-05-16

Scope: Part E (E-01..E-08) of the FIN Production Parity Tracker §6 — verify all approved CQRS projection tables across fin-central-prod and nathel-prod tenants meet the seven readiness checks.

Method: Static audit (no live Neon access from this branch). Evidence taken from:

  • audit/card-window-system/approved-projection-tables.json registry (authoritative)
  • db/migrations/041..050*.sql
  • packages/mcp-server/src/event-handlers/projection-*.ts (handlers)
  • packages/mcp-server/src/alerts/event-hook.ts (dispatch wiring)
  • scripts/cli/replay-projection.ts (replay CLI descriptors)
  • test/contracts/dpo/projection-replay-parity.test.ts (registry-CLI parity contract)
  • Per-projection consumer tools and tests

Branch under audit: worktree-agent-a3889ff00d4695fa5 (parent: claude/heuristic-kepler-4f9104). Audit branch: claude/parity-task-E-01-E-08-projection-audit.

Branch-State Caveat (read first)

The registry on master lists 5 projections (041-044). The current worktree branch (worktree-agent-a3889ff00d4695fa5, off claude/heuristic-kepler-4f9104) is identical to master for the registry — it still shows 5. Two separate unmerged branches add projections:

BranchAddsMigrationStatus vs. registry on master
b3581360 (TZ-3)doc_backlink_projection, entity_doc_projection049, 050not merged
2a8b055d (IAR-RM-033)lot_lifecycle_reconciliation_projection049 (CONFLICT)not merged

The task brief asserts “Migrations 049 + 050 were applied to fin-central-prod 2026-05-16 02:30Z via Neon MCP.” Per commit b3581360’s description, those are the doc-CQRS migrations. This audit treats the TZ-3 registry (7 projections) as the production-target reality and explicitly flags the migration-049 collision with IAR-RM-033 as a P0 follow-up.

The audit therefore covers 7 projections × 2 tenants × 7 checks.

Summary

StatusCount
GREEN (all 7 checks pass)0 of 7
YELLOW (1-2 checks failing or partial)5 of 7
RED (3+ checks failing or systemic gap)2 of 7

Live-DB checks (Check 1 migration-applied on nathel-prod, Check 4 RLS catalog state) are marked UNKNOWN because this branch had no Neon MCP access. The audit assumes fin-central-prod 1-7 = migration-applied per task brief; for any nathel-prod row the migration-applied check is marked YELLOW unless evidence in this repo (e.g. earlier handoff) confirms it.

Per-projection × per-tenant readiness matrix

Legend: G = pass / Y = partial or unconfirmed / R = fail / ? = not verifiable from static audit.

fin-central-prod

Projection1. Migration2. Handler wired3. Replay cmd4. RLS forced5. SoT documented6. Read API7. Test ≥80%Overall
lot_status_projectionGGGG (in migration) / ? liveGGYY
pulse_admin_active_holdsGGGG / ?GGYY
actor_pulse_aggregateGGGG / ?GGYY
kb_health_summaryGGGG / ?GGYY
system_health_snapshotGY (cron only, not in dispatcher)GG / ?GGYY
doc_backlink_projectionG (per task brief)R (no handler)R (not in CLI)G (in migration) / ?GR (no get_backlinks)R (no tests)R
entity_doc_projectionG (per task brief)R (no handler)R (not in CLI)G (in migration) / ?GR (no get_entity_profile)R (no tests)R

nathel-prod

Projection1. Migration2. Handler wired3. Replay cmd4. RLS forced5. SoT documented6. Read API7. Test ≥80%Overall
lot_status_projectionY ?GGG / ?GGYY
pulse_admin_active_holdsY ?GGG / ?GGYY
actor_pulse_aggregateY ?GGG / ?GGYY
kb_health_summaryY ?GGG / ?GGYY
system_health_snapshotY ?Y (cron only)GG / ?GGYY
doc_backlink_projectionY ?RRG / ?GRRR
entity_doc_projectionY ?RRG / ?GRRR

Notation: “Y ? ” on nathel-prod migration-applied = unverifiable from static audit; assumed YELLOW pending Neon schema_migrations query.

Per-projection narratives

1. lot_status_projection (E-01) — YELLOW

  • Migration: db/migrations/041_lot_status_projection.sql (present locally). Source-of-truth = events.
  • Handler: packages/mcp-server/src/event-handlers/projection-lot-status.ts. Listens to 13 event types covering BLOCKING (qc.hold_placed, quality.hold_placed, compliance.lot_hold_placed, receiving.rejected, lot.disposed, lot.recalled, inventory.lot_frozen), RELEASE (qc.hold_released, quality.hold_released, inventory.lot_unfrozen), INSPECTION (qc.inspection_completed, quality.inspection_completed), and INVENTORY (lot.received). Wired into dispatchProjectionUpdaters() in event-handlers/index.ts:53, called from alerts/event-hook.ts.
  • Replay: scripts/cli/replay-projection.ts descriptor exists. Command works: pnpm cli:replay-projection --projection=lot_status_projection --tenant=<tenant_id>.
  • RLS: Migration sets ALTER TABLE lot_status_projection FORCE ROW LEVEL SECURITY; with INSERT/UPDATE/DELETE/USING policies. Live relrowsecurity AND relforcerowsecurity not verifiable from this branch.
  • Source-of-truth: registry entry documents events and lists the 13 source event types via LOT_STATUS_EVENT_TYPES in the CLI.
  • Read API: packages/mcp-server/src/tools/get-pickable-inventory-with-gate.ts reads the projection with documented fallback to live event scan on miss.
  • Test coverage: Indirect only. No dedicated projection-lot-status.test.ts exists. Coverage flows through test/contracts/dpo/projection-replay-parity.test.ts (engine determinism + registry-CLI parity) and packages/shared/src/projections/replay.test.ts. Estimated coverage of the handler itself: <50% (no direct unit tests on updateLotStatusProjection’s state-machine logic or sellability-gate interaction).

Gap: add projection-lot-status.test.ts co-located with the handler.

2. pulse_admin_active_holds (E-02) — YELLOW

  • Migration: also in 041_lot_status_projection.sql (two tables ship together).
  • Handler: packages/mcp-server/src/event-handlers/projection-pulse-admin.ts. PULSE_ADMIN_PROJECTION_EVENT_TYPES exported. Three alert flavors keyed by alert_kind column (lot_hold / qc_failed / temp_excpt). Wired in dispatcher.
  • Replay: CLI descriptor present.
  • RLS: FORCE RLS in migration.
  • Source-of-truth: events (hold/release + QC inspection + sensor temperature exceptions).
  • Read API: packages/mcp-server/src/ops/get-pulse.ts reads it (admin Pulse path).
  • Test coverage: packages/mcp-server/src/ops/get-pulse.test.ts (299 lines) tests the consumer, not the projection writer. No dedicated handler test.

Gap: same — add direct handler unit test.

3. actor_pulse_aggregate (E-03) — YELLOW

  • Migration: db/migrations/042_actor_pulse_aggregate_projection.sql.
  • Handler: packages/mcp-server/src/event-handlers/projection-actor-pulse.ts. Per-(tenant, actor, window) upserts. Wired in dispatcher.
  • Replay: CLI descriptor present.
  • RLS: FORCE RLS in migration.
  • Source-of-truth: events (actor-visible families).
  • Read API: packages/mcp-server/src/tools/get-briefing-content.ts reads it.
  • Test coverage: No co-located handler test. Indirect via briefing tests.

Gap: add handler test.

4. kb_health_summary (E-04) — YELLOW

  • Migration: db/migrations/043_kb_health_summary_projection.sql. Source-of-truth = knowledge (not events).
  • Handler: packages/mcp-server/src/event-handlers/projection-kb-health.ts. Exports refreshKbHealthSummary() for explicit refresh. Wired in dispatcher.
  • Replay: CLI descriptor present. Command form: pnpm cli replay-projection --projection=kb_health_summary --tenant=<tenant_id> (no --from because source is knowledge, not events).
  • RLS: FORCE RLS in migration.
  • Source-of-truth: knowledge table aggregates (per registry).
  • Read API: packages/mcp-server/src/tools/knowledge-health.ts.
  • Test coverage: No co-located handler test.

Gap: same.

5. system_health_snapshot (E-05) — YELLOW

  • Migration: db/migrations/044_system_health_snapshot_projection.sql. Source-of-truth = events, knowledge, operational_memory, actors (composite).
  • Handler: packages/mcp-server/src/event-handlers/projection-system-health.ts. Exports refreshSystemHealthSnapshot(). NOT wired into dispatchProjectionUpdaters() — by design, this is a cron-style projection per event-handlers/index.ts:23-26 (only the four event-driven projections are auto-dispatched). Discoverable via replay-projection.ts:155-162 which special-cases system_health_snapshot as __cron_refresh__.
  • Replay: CLI descriptor present. The replay command bypasses the event walk and invokes refreshSystemHealthSnapshot() directly.
  • RLS: FORCE RLS in migration.
  • Source-of-truth: registered correctly.
  • Read API: packages/mcp-server/src/tools/get-system-health.ts.
  • Test coverage: No co-located handler test.

Notes: The cron schedule itself is NOT documented in the registry; the registry says replay_supported: true but the runtime trigger (cron name / schedule / where it’s invoked outside of --apply replay) is unclear from static audit. A reader cannot tell if refreshSystemHealthSnapshot() runs every minute, every hour, or only on explicit replay. This is a P1 documentation gap.

  • Migration: db/migrations/049_doc_backlink_projection.sql exists only on branch claude/heuristic-kepler-4f9104 (commit b3581360). Per task brief, applied to fin-central-prod 2026-05-16 02:30Z via Neon MCP.
  • Handler: MISSING. No projection-doc-backlink.ts in packages/mcp-server/src/event-handlers/. No reference to doc.indexed or doc.archived consumers anywhere in packages/. Per the commit message: “Consumer code (vault webhook adapter, doc.indexed/archived consumers, entity-row update consumer, renderer worker) is T7 scope” — explicitly deferred.
  • Replay: NOT WIRED. scripts/cli/replay-projection.ts DESCRIPTORS map has no doc_backlink_projection key. Running pnpm cli replay-projection --projection=doc_backlink_projection --tenant=... will throw "Unknown projection: doc_backlink_projection" from parseArgs line 208-212.
  • RLS: Migration 049 has ALTER TABLE doc_backlink_projection FORCE ROW LEVEL SECURITY; with full INSERT/UPDATE/DELETE/USING policies. Live relforcerowsecurity not verifiable from static audit (would be GREEN if Neon catalog query confirms).
  • Source-of-truth: Registry documents knowledge.domain=doc.content->outbound_links[] (via doc.indexed events) and transactional_write_required: true per Engineering Spec §11.5.3.
  • Read API: MISSING. No get_backlinks / get_doc_graph tool in packages/mcp-server/src/tools/. Per Engineering Spec §8.3 + §8.4 these are required.
  • Test coverage: 0%. No handler test. No read-API test. The test/contracts/dpo/projection-replay-parity.test.ts registry-CLI parity assertion will fail the moment migration 049/050 metadata lands in the registry on master, because the CLI source does not contain the strings doc_backlink_projection or entity_doc_projection.

7. entity_doc_projection (E-07) — RED

  • Migration: db/migrations/050_entity_doc_projection.sql — same provenance as 049 (TZ-3 branch only).
  • Handler: MISSING. No entity-row update consumer; no renderer worker; no doc.rendered/render_failed consumer.
  • Replay: NOT WIRED. Same CLI gap.
  • RLS: Migration 050 has FORCE RLS + 4 policies. Live state ?.
  • Source-of-truth: Registry documents knowledge.<entity_domain>.<entity_key> rows + doc.rendered events. transactional_write_required: true.
  • Read API: MISSING. No get_entity_profile tool exists per Engineering Spec §11.5.2.
  • Test coverage: 0%.

The audit cannot verify whether the renderer worker scaffolding promised by recent commit 9067ca4e ("feat(parity B-03+B-04): entity-doc-renderer-worker full implementation") actually populates this projection — that commit lives on the same claude/heuristic-kepler-4f9104 ancestor. Action: cross-check B-03/B-04 deliverable when E-audit is reconciled against tracker.

E-08 replay smoke test

Script committed at scripts/audit/replay-projection-smoke-test.ts. Runs the CLI in --json dry-run mode against each registry projection × each tenant configured via env. Without DB access it falls back to a descriptor-only mode that verifies the CLI knows about every projection name in the approved registry — equivalent to the contract test but emitting a smoke-style pass/fail report. See script header for usage.

Smoke-test invocation:

pnpm exec tsx scripts/audit/replay-projection-smoke-test.ts \
  --tenant=fin_central_prod \
  --tenant=nathel_prod \
  [--apply]        # actually run replays (requires DATABASE_URL pointing at the tenant)
  [--registry-only] # skip live DB; verify CLI ↔ registry parity only
IDSevTitleOwner area
E-FU-1P0Resolve migration-049 collision: TZ-3 (doc_backlink_projection) vs IAR-RM-033 (lot_lifecycle_reconciliation_projection). Both branches claim slot 049 unmerged. Renumber the loser to 051 + update registry + update CLI descriptor + bump system_metadata.schema_version.DB / migration
E-FU-2P0Wire doc_backlink_projection handler: write event-handlers/projection-doc-backlink.ts, subscribe to doc.indexed / doc.archived, append to dispatchProjectionUpdaters(). Add to replay-projection.ts DESCRIPTORS map. Add projection-doc-backlink.test.ts.mcp-server / event-handlers
E-FU-3P0Wire entity_doc_projection handler: similar (subscribe to knowledge.<entity_domain>.updated for stale-mark, doc.render_requested/doc.rendered/doc.render_failed for status). Confirm whether entity-doc-renderer-worker from B-03/B-04 satisfies this — if so, link it; if not, write it.mcp-server
E-FU-4P0Build get_backlinks and get_doc_graph MCP tools backed by doc_backlink_projection. Per Engineering Spec §8.3 + §8.4.mcp-server / tools
E-FU-5P0Build get_entity_profile MCP tool backed by entity_doc_projection. Per Engineering Spec §11.5.2.mcp-server / tools
E-FU-6P1Verify migrations 041-044 are applied on nathel-prod. The static audit cannot reach Neon; orchestrator must run SELECT version FROM schema_migrations WHERE version IN ('041','042','043','044') ORDER BY version against nathel-prod and post evidence.infra / DB
E-FU-7P1Live RLS verification per tenant for all 5 (+ 2 new) projection tables: query SELECT relname, relrowsecurity, relforcerowsecurity FROM pg_class WHERE relname IN (...) for both fin-central-prod and nathel-prod.infra / DB
E-FU-8P1Add direct co-located handler unit tests: projection-lot-status.test.ts, projection-pulse-admin.test.ts, projection-actor-pulse.test.ts, projection-kb-health.test.ts, projection-system-health.test.ts. Each must reach ≥80% coverage on the file under test.mcp-server / event-handlers
E-FU-9P1Document system_health_snapshot cron schedule + invoker in the registry (replay_command is set but the runtime trigger for the routine snapshot refresh is undocumented).docs / infra
E-FU-10P1The transactional_write_required: true constraint on migrations 049/050 is stricter than 041-044’s fire-and-forget pattern. Add a contract test that fails CI if any future handler for these projections writes outside the same BEGIN/COMMIT as the canonical knowledge.domain=doc upsert (Engineering Spec §11.5.3).mcp-server / test
E-FU-11P2Migration drift check: system_metadata.schema_version is updated by each migration with a specific previous-version assumption (049 expects 48, 050 expects 49). If migrations are applied out of order (e.g. IAR-RM-033’s 049 first, then TZ-3’s renumbered 051), the migrate:down rollback chain breaks. Tighten the down-path.DB
E-FU-12P2Add cron_schedule and cron_invoker_module fields to approved-projection-tables.json schema for cron-style projections (currently system_health_snapshot; also future-proof for any sweep-style replay where source is knowledge).audit registry

Acceptance summary

  • Audit doc: this file (docs/audits/PROJECTION_PARITY_AUDIT_2026-05-16.md).
  • Smoke-test script: scripts/audit/replay-projection-smoke-test.ts.
  • Gaps identified: 12 follow-up tasks (5 × P0 [E-FU-1..5], 5 × P1 [E-FU-6..10], 2 × P2 [E-FU-11..12]).
  • Bottom line: 5 of 7 projections are runtime-complete on the fin-agentic side but missing direct unit tests (YELLOW). The 2 new doc-CQRS projections (doc_backlink_projection, entity_doc_projection) are SQL-only on the database; all 5 application-layer checks fail (handler, replay descriptor, read API, test, dispatch wiring). The P0 follow-ups must clear the 049 migration collision before any cleanup work can land on master.