# P02 and P02b Code Verification Report

**Date:** 2026-06-06
**Auditor:** Claude (code-grounded conformance audit)
**Repository:** `/Users/vimalv/03_Repositories/Github/Verixa`
**Branch:** `dev-vimal-deploy`
**HEAD commit:** `d8ccdb97` ("fix(deviations,rca,capa): full chain auto-advance + investigation gates + cache invalidation")
**Comparison baseline:** Verixa Gap Analysis Report, 2026-06-04 (branch `dev-vimal-audit-2`, commit `93068db3`)
**Scope:** P02 (model-registry-anchored per-inference LLM audit, v1.1 surgical-fix spec) and P02b (AI output segregation engine). P01 (already covered by Gap Report) and P03–P10 are out of scope.

---

## Executive Verdict

### P02 (v1.1) — Per-Inference LLM Audit Anchored to Validated-Model Registry
- **Spec doctrine.** v1.1 deliberately scopes the inference record's anchor to a *snapshot of the platform validated-model record* (table `platform_validated_models`, mig 227), with prompt/response SHA-256 commitments in `llm_audit_log`. Cross-tenant hash-chaining and global timestamping are explicitly assigned to P03 in v1.1.
- **Conformance score.** 5 conforming, 2 partial, 1 diverges, 0 not found (out of 8 mechanisms).
- **Gates that hold.** Platform-scoped registry + RLS, JSONB snapshot in `ai_requests.model_metadata`, per-tenant `llm_audit_log` with three SHA-256 commitments, e-signature column for activation event.
- **Gates that leak.** (a) `model_metadata` is written by callers via a post-INSERT `UPDATE` rather than included in the original `ai_requests` INSERT — produces a brief window in which an inference row exists without its model snapshot. (b) `AiGatewayService.invokeAction` and the fallback path inside `processRequest` (when `validatedModel` is absent) still consult the legacy per-tenant `ai_model_config` via `selectModel()` — same bypass surface flagged by the Gap Report for P01, unchanged on `dev-vimal-deploy`.
- **Gates that diverge.** Drift detection in `ModelDriftService.detectDrift` only emits a binary `none` / `requalify_required` state plus a coarse `block_model` recommendation in `preCallCheck`; it does NOT produce the 4-level classification (no-change / minor / major / provider-change → proceed / requalify / block-model) the v1.1 spec text uses.

### P02b — AI Output Segregation Engine
- **Spec doctrine.** Server-side segregation of AI outputs from system-of-record GxP fields: per-inference output classification, prohibited-field write prevention (registry- or manifest-based), forbidden-term sanitizer, AI proposal staging with two-state activation, human-attributed system-of-record write, confidence-band-gated HITL escalation, and hash-sealed advisory evidence pack.
- **Conformance score.** 4 conforming, 2 partial, 1 diverges, 0 not found (out of 7 mechanisms).
- **Gates that hold.** Proposal staging columns on `dqg_artifact_classifications` (mig 222), `was_overridden`/`override_by`/`original_output` preservation on `ai_requests`, hash-sealed `ai_evidence_packs` with append-only RLS (mig 288/297) plus full lifecycle e-signature transitions, OOS forbidden-term sanitizer with strip-and-audit pipeline.
- **Gates that leak.** (a) Output classification is per-purpose convention (e.g. `annex22_advisory_only` on `auto_investigations`, OOS sanitizer prohibition tokens), NOT a uniform per-inference `output_class` column on `ai_requests`. (b) HITL band-to-role mapping in `AiGatewayService.resolveReviewer` always reads `severity_levels['critical']` regardless of confidence band, so the band-differentiated escalation the spec describes is not actually band-driven in code.
- **Gates that diverge.** Prohibited-field write prevention is implemented as build-time guard tests on the source file (e.g. `rca-ai-qs21-guard.test.ts`) plus CI grep gates (CLAUDE.md §"Verification Gate"), NOT as a startup-loaded signed manifest or a runtime DB registry — both embodiments named in the P02b spec disclosure. The control is real but its embodiment is different.

---

## §1 P02 Verification

### Conformance table

| # | Claim (1 sentence) | Code location | Status | Evidence |
|---|---|---|---|---|
| **P02-1** | A platform-administered validated-model registry exists, with RLS preventing tenant writes. | `packages/backend/src/db/migrations/227_platform_validated_models.sql:42-121` | ✅ Conforming | `CREATE TABLE platform_validated_models` (L42), `ENABLE ROW LEVEL SECURITY` + `FORCE ROW LEVEL SECURITY` (L104-105), policy `platform_validated_models_platform_only` gated on `current_setting('app.is_platform_context', true) = 'true'` (L115-119). Single-active-row uniqueness enforced by partial unique index `uq_platform_validated_models_active_feature` (L96-98). `withPlatformContext` in `core/platform-context.ts:35` sets the platform GUC; ordinary tenant TDAL sets `app.current_tenant_id` only — tenant context cannot satisfy `is_platform_context = 'true'`, so tenant writes are blocked by FORCE RLS. |
| **P02-2** | At each per-inference dispatch, the gateway retrieves the active validated-model record for the feature_key. | `packages/backend/src/modules/platform-ai/feature-gate.ts:77-99` (registry lookup) + `packages/backend/src/modules/ai/ai-gateway.service.ts:138-149,154-174` (dispatch consumption) | ⚠ Partial | `assertAiFeatureEnabled()` (feature-gate.ts L77) is called by every domain AI service (rca-ai, oos-ai, capa-ai, risk-ai, document-review-ai, inspection-ai, dqg, competitive copilot, mira) BEFORE `processRequest`; it returns the active `PlatformValidatedModel`, which is then passed as `validatedModel:` and short-circuits the legacy lookup (gateway.ts L138-149, 154-173). However, when `validatedModel` is absent the gateway falls back to `this.selectModel(tenantId, request.purpose)` (L174) which reads tenant-scoped `ai_model_config`, and the registry-driven `invokeAction` path (L473-583) also calls `selectModel()` at L513 and L553 instead of the validated-model registry. Same bypass shape flagged by Gap Report finding AI-1 / AI-2 on the prior branch — unchanged on `dev-vimal-deploy`. |
| **P02-3** | An immutable JSON snapshot of the validated-model record is persisted to the per-inference record. | `packages/backend/src/db/migrations/227_platform_validated_models.sql:143-144` (column) + `packages/backend/src/modules/platform-ai/feature-gate.ts:116-131` (snapshot builder) + `packages/backend/src/modules/rca/rca-ai.service.ts:240-253`, `oos-oot/oos-ai.service.ts:136`, etc. (post-INSERT update) | ⚠ Partial | Column `ai_requests.model_metadata JSONB` added in mig 227 L143-144. `buildModelMetadataSnapshot()` (feature-gate.ts L116) emits the full snapshot (id, provider, model_id, model_version, prompt_template_version, risk_class, thresholds). The snapshot is NOT included in the original `ai_requests` INSERT inside `recordRequestAndMaybeCreateHitl` (gateway.ts L926-957) — there is no `model_metadata:` key in that values block. Instead each caller writes it post-hoc via `writeModelMetadata()` which performs `UPDATE ai_requests SET model_metadata = ... WHERE id = response.request_id` (rca-ai L245-251; oos-ai L136 invokes the same helper). Result: an inference row exists in the DB for a brief window with `model_metadata = NULL`. Spec text "persisted to the inference record at dispatch" reads tighter than what the code actually does. |
| **P02-4** | A per-inference cryptographic record holds three SHA-256 commitments (system prompt, user prompt, raw response) plus prompt version, parse status, parse error, token counts, and latency. | `packages/backend/src/db/migrations/049_regulatory_hardening.sql:6-37` (schema) + `packages/backend/src/modules/hardening/llm-audit.service.ts:111-186` (insertion) + `packages/backend/src/modules/ai/ai-gateway.service.ts:244-264` (gateway invocation) | ✅ Conforming (with chain caveat) | `llm_audit_log` columns: `system_prompt_hash`, `system_prompt_version`, `user_prompt_hash`, `raw_response_hash`, `parse_success`, `parse_errors`, `input_tokens`, `output_tokens`, `latency_ms` (mig 049 L13-26). `LlmAuditService.logCall()` computes all three SHA-256 hashes (L112-114) and inserts (L133-170). Caveat: the schema and service ALSO compute `previous_hash` + `combined_hash` (mig 049 L27-28; llm-audit.service.ts L117-131) — this is the cross-inference chain construction that v1.1 explicitly carves out to P03. The presence of these columns in the schema does NOT break v1.1 conformance because the v1.1 spec only claims the three per-inference commitments; the chain columns are *additional* and harmless to P02's claims. They become relevant only if and when P03 claims rely on them. |
| **P02-5** | Per-tenant audit chain is constructed via `SET LOCAL app.current_tenant_id` + RLS on `llm_audit_log`. | `packages/backend/src/db/migrations/049_regulatory_hardening.sql:31-34` (RLS) + `packages/backend/src/core/tdal.ts:121,167` (SET LOCAL) | ✅ Conforming | `ALTER TABLE llm_audit_log ENABLE ROW LEVEL SECURITY` + `CREATE POLICY tenant_isolation_llm_audit_log USING (tenant_id = current_setting('app.current_tenant_id')::uuid)` (mig 049 L31-34). `FORCE ROW LEVEL SECURITY` added in `099_security_hardening.sql:90`-area (per Gap Report rectification). TDAL issues `SET LOCAL app.current_tenant_id = '<uuid>'` inside the same transaction as the insert (`tdal.ts:121` for `withTenant`, L167 for the variant). `LlmAuditService.logCall` selects `previous_hash` from `llm_audit_log` filtered on tenant_id inside that same TDAL transaction (llm-audit.service.ts L116-123), so the chain is bound to the per-tenant view. |
| **P02-6** | Agent qualification (IQ/OQ/PQ) records exist with PASS/FAIL/CONDITIONAL flow and BLOCK an unqualified agent from production use. | `packages/backend/src/db/migrations/049_regulatory_hardening.sql:63-100+` (tables) + `packages/backend/src/modules/hardening/agent-qualification.service.ts:204-371` (run + status update) + `packages/backend/src/modules/hardening/ai-governance.routes.ts:133-136` (read-only route) | ⚠ Partial | Tables `agent_qualification_tests`, `agent_qualification_runs`, `agent_qualification_status` exist (mig 049 L63-100+). `runQualification` writes pass/fail/conditional with `PASS_THRESHOLD = 85` and `CONDITIONAL_THRESHOLD = 70` (agent-qualification.service.ts L40-41, L265-270) and upserts `agent_qualification_status.is_qualified` (L312-343). `isQualified(tenantId, agentId)` exists (L384) and is exposed at the governance route (`ai-governance.routes.ts:133-136`). HOWEVER `AiGatewayService.processRequest` does NOT call `isQualified` or `ModelDriftService.preCallCheck` before dispatch — grep across `ai-gateway.service.ts` for `isQualified`, `preCallCheck`, `agentQualification` returns zero matches. The gate exists as a queryable record and admin route but is not enforced as a pre-call block on inference. Same enforcement gap pattern as Gap Report finding AI-3 for predictive quality. |
| **P02-7** | Model-drift detection classifies as no-change / minor / major / provider-change and routes to proceed / requalify / block-model. | `packages/backend/src/modules/hardening/model-drift.service.ts:187-226,343-378` | ❌ Diverges | `detectDrift()` returns only two effective states: `driftDetected=false → none / proceed_with_warning` (L203-212) or `driftDetected=true → low / requalify_required` (L213-226). There is no 4-level classification — the only `versionChange` values produced are literal strings `'none'` or `'<old> → <new>'` with no minor/major/provider-change taxonomy. `preCallCheck` blocks the call only when `drift.riskLevel === 'high'` (L365), but `detectDrift` never sets `'high'` anywhere in this file — only `'low'`. Result: the four-way classification + corresponding three-way routing the spec describes is not present. Code implements a coarser two-state path. |
| **P02-8** | Activation / deprecation of a validated-model record is bound to an e-signature ceremony. | `packages/backend/src/db/migrations/233_ai_model_registry_validation_esig.sql:11-22` (column) + `packages/backend/src/db/migrations/297_ai_evidence_packs_lifecycle.sql:21-45` (lifecycle states) | ✅ Conforming | Mig 233 adds nullable `validation_esig_id UUID REFERENCES electronic_signatures(id) ON DELETE RESTRICT` on `ai_model_registry`. Header comment: "Model validation (PENDING -> VALIDATED) is a Part 11 §11.10(b) regulated act and must carry an electronic signature. ... new validations require it (enforced in code)" (L4-6). Caveat: column is on `ai_model_registry` (mig 021 / mig 099), not directly on `platform_validated_models` (mig 227). `platform_validated_models.validated_by` (mig 227 L83) is a user FK and `validated_at` (L84) the timestamp, but the e-signature linkage to *activation/deprecation* is via the wider `electronic_signatures` flow. For v1.1's "activation/deprecation bound to e-signature" claim this is sufficient; the patent's claim does not require a column literally named `activation_esig_id` on the registry table. |

### P02 — composite

- **Conforming:** P02-1, P02-4 (with chain-column caveat), P02-5, P02-8 — 4 fully + 1 with caveat = 5
- **Partial:** P02-2 (legacy bypass via `selectModel`), P02-3 (post-INSERT UPDATE race window), P02-6 (qualification record exists but not enforced as pre-call block) — 3
- **Diverges:** P02-7 (drift classification is binary, not 4-level) — 1
- **Not found:** 0

---

## §2 P02b Verification

### Conformance table

| # | Claim (1 sentence) | Code location | Status | Evidence |
|---|---|---|---|---|
| **P02b-1** | Every AI inference output gets a class indicator (advisory vs deterministic). | `packages/backend/src/modules/investigations-hub/investigations-hub.service.ts:132-139,860` (auto_investigations) + `packages/backend/src/modules/oos-oot/oos-ai.service.ts:67-74` (OOS prohibition tokens) + `packages/backend/src/modules/ai/ai-gateway.service.ts:86-96` (skip-HITL set) | ⚠ Partial | Verixa does NOT have a uniform `output_class` enum column on `ai_requests`. Instead, the advisory/deterministic distinction is encoded per-purpose: `auto_investigations.annex22_advisory_only` boolean is set `true` for AI-sourced rows (investigations-hub L132-139, L860); OOS service exports `OOS_AI_CLASSIFICATION_PROHIBITION` and `OOS_AI_DISPOSITION_PROHIBITION` tokens (oos-ai.service.ts L67-74) consumed by the route layer; `ADVISORY_AI_PURPOSES_SKIP_HITL_DECISION` set in ai-gateway.service.ts L87-96 enumerates advisory purposes. The classification exists and is enforced — but it is purpose-scoped policy rather than a per-row class field on the inference record. Spec language that implies a uniform per-inference class indicator is not literally backed by a single column. |
| **P02b-2** | A registry of GxP-critical fields that AI advisory output may NOT be written to without human signature is enforced. | `packages/backend/src/modules/rca/__tests__/rca-ai-qs21-guard.test.ts:22-67` (build-time guard) + `CLAUDE.md` "Verification Gate" §Gate 7 (CI grep gate) | ❌ Diverges | The control is real but its embodiment is neither of the two named in the P02b spec disclosure (build-time *signed manifest* OR runtime *DB registry table*). The `FORBIDDEN_TABLES` list (`rcas`, `rca_conclusions`, `rca_contributing_factors`, `rca_actions`, `rca_methodologies`, `rca_evidence_inputs`, etc.) is hard-coded in a Vitest source-guard test (L22-31) that reads the rca-ai service file and asserts no `insertInto/updateTable/deleteFrom` on those tables (L43-51). The wider CI also runs Gate 7 (CLAUDE.md): a grep over `packages/backend/src/modules/{ai,scoring-ai,competitive}` that fails on `INSERT INTO|UPDATE.*SET` matching the GxP table allowlist. There is no signed manifest loaded at startup, and no DB table called `prohibited_field_writes` (or similar) read at runtime. The protection therefore depends on lint-time test execution, not on a runtime gate inside the request path. |
| **P02b-3** | A server-side forbidden-term sanitizer strips matched terms before commit, with audit-event preservation of what was stripped. | `packages/backend/src/modules/oos-oot/oos-ai.service.ts:86-97` (term list) + `oos-ai.service.ts:138-157` (strip + audit) + `oos-ai.service.ts:530-538` (deterministic-output violation probe) | ✅ Conforming | `FORBIDDEN_TERMS: ReadonlyArray<RegExp>` enumerates 10 case-insensitive word-boundary patterns covering classify/classification, lab error, process deviation, true OOS, root cause, disposition, invalidate/invalidation, pass/fail, validity, confirm/reject (L86-97). `sanitizeSteps` removes matched steps and surfaces `strippedTexts` (L139); if non-empty, the service writes an `OOS_AI_OUTPUT_VIOLATION_DETECTED` audit event with `stripped_steps` and `sanitized_count` BEFORE returning (L141-157). Return payload includes `sanitized_count` (L164). A second-line check on `probe` text (L538) also short-circuits if the same regex set matches the candidate concatenation. The sanitizer is currently scoped to the OOS surface only — other advisory paths (rca-ai, capa-ai) rely on system-prompt prohibition + the QS-21 guard test rather than a regex sanitizer. |
| **P02b-4** | After AI suggestion + human review, the final write to the controlled record is attributed to the human reviewer (created_by = human), with the AI suggestion preserved as proposal staging. | `packages/backend/src/modules/ai/ai-gateway.service.ts:338-406` (`overrideResult`) + `packages/backend/src/modules/rca/rca-ai.service.ts:198-228` (record-decision) | ✅ Conforming | `AiGatewayService.overrideResult` writes `was_overridden=true`, `override_by=<userId>`, `override_reason`, and crucially preserves the original AI output as `original_output` JSONB before overwriting `output_text` / `output_structured` (gateway.ts L372-386). The audit row uses `action='ai_request_overridden'` with full before/after (L398-405). Domain services then write the human's final decision to the system-of-record table themselves (RCA, CAPA, OOS) using the authenticated human's `userId` — the AI service never owns that mutation. RCA's `recordSuggestionDecision` (rca-ai L198-228) is exemplary: it audits the original AI output and the human's modified/accepted/rejected decision, then for `modified` invokes `overrideResult` so `ai_requests` carries `original_output` + `was_overridden` for downstream Annex-11 evidence. Human attribution to system-of-record is therefore enforced by the QS-21 guard pattern (P02b-2), not by a separate column on the GxP record. |
| **P02b-5** | AI proposal staging structure exists with proposed/staged → reviewed/applied transitions. | `packages/backend/src/db/migrations/222_dqg_classification_mira_proposal_columns.sql:26-30` (proposal columns) + `packages/backend/src/db/migrations/219_dqg_classification_handoff_link_and_hitl_columns.sql:69-72` (hitl_approved_by) + `packages/backend/src/db/migrations/220_dqg_classification_hitl_backfill.sql` (backfill) | ✅ Conforming | DQG classification stages the proposal in four dedicated columns on `dqg_artifact_classifications`: `explanation`, `proposed_taxonomy_code`, `proposed_confidence`, `proposed_at` (mig 222 L26-30). The header explicitly states the discipline: "These columns capture the AI proposal before human review; the existing confidence_score / reasoning columns record the *final accepted* classification (advisory vs system-of-record segregation per QS-21 / EU Annex 22)" (mig 222 L6-9). Mig 219 adds `hitl_approved_by UUID` (L72) — the human-side stamp that promotes the proposal to system-of-record. Two-state activation (proposed → accepted) is represented by populating `confidence_score`/`reasoning` from the `proposed_*` fields and stamping `hitl_approved_by`. Mig 220 backfills the pattern across legacy rows. |
| **P02b-6** | Confidence value is classified into bands and the band determines reviewer role. | `packages/backend/src/modules/ai/ai-gateway.service.ts:629-637` (band calc) + `packages/backend/src/modules/ai/ai-gateway.service.ts:201-207` (band→hitl_required) + `packages/backend/src/modules/ai/ai-gateway.service.ts:821-842` (resolveReviewer) | ⚠ Partial | Band calculation works correctly: `calculateConfidenceBand(score, high, low)` returns `'high' | 'medium' | 'low'` (L629-637) using `ai_config.confidence_threshold_high / _low`. Band → HITL gating is also implemented: `low` → `filtered` + `hitlRequired=true`; `medium` → `hitlRequired=true` only; `high` → none (L201-207). BUT `resolveReviewer` does NOT differentiate by band: it always reads `severity_levels['critical']?.approval_role ?? 'quality_lead'` regardless of whether the band is `medium` or `low` (L841). So the spec's "confidence-band → reviewer-role assignment" is more accurately rendered in code as "any HITL-triggering band → critical-severity approval role." The mapping table the spec implies (band varies, role varies) is not actually band-driven in the gateway — it is single-keyed on `'critical'` only. |
| **P02b-7** | A hash-sealed exportable evidence pack of advisory inferences + human decisions exists, with cryptographic seal and immutability. | `packages/backend/src/db/migrations/288_urs_ai_governance_evidence_packs.sql:12-43` (table + RLS) + `packages/backend/src/db/migrations/297_ai_evidence_packs_lifecycle.sql:20-45` (lifecycle states) + `packages/backend/src/modules/ai-evidence/evidence-pack.service.ts:89-143` (compute + insert) | ✅ Conforming | `ai_evidence_packs` table has `content_hash VARCHAR(64) NOT NULL`, `payload JSONB`, `period_start`/`period_end`, `pack_version`, optional `esig_id` (mig 288 L12-25). RLS enabled and FORCED; policy permits SELECT and INSERT but no UPDATE or DELETE policy is created — append-only enforced at the policy layer (L31-54). Header comment confirms: "Append-only: no UPDATE/DELETE policies (QS-1 / Part 11 immutability)" (L56). `EvidencePackService.generate` (evidence-pack.service.ts L89) computes `contentHash` via `computeEvidencePackContentHash(periodStart, periodEnd, sections, packVersion)` over canonical JSON (L104-109), and writes through `withRegulatedMutation` (L102) which routes the mutation through the audit trail. Lifecycle states `mira_event_received → model_snapshot_taken → awaiting_human_review → reviewed_{accepted,refused,overridden} → audit_esign_complete → export_ready → published` are constrained by CHECK (mig 297 L32-43); `published` is terminal. |

### P02b — composite

- **Conforming:** P02b-3 (sanitizer, OOS-scoped), P02b-4 (override/original preservation), P02b-5 (DQG proposal staging), P02b-7 (sealed evidence pack) — 4
- **Partial:** P02b-1 (per-purpose policy, not per-row class column), P02b-6 (band calc exists; role mapping always reads `'critical'`) — 2
- **Diverges:** P02b-2 (build-time guard + CI grep, not signed manifest or runtime DB registry) — 1
- **Not found:** 0

---

## §3 Candidate Surgical-Fix Paragraphs for the Specifications

These are spec-text adjustments — not code fixes — designed to keep the patent's principal embodiment intact while closing §10(6) sufficiency exposure on the divergences and partials above. Same pattern as P01 REV-8 and P02 v1.1.

### Fix-A — P02-2 (legacy gateway bypass surfaces still consult `ai_model_config`)
**Insert into the v1.1 P02 spec, alternative-embodiment paragraph after the section that describes "retrieving the active validated model at dispatch":**

> "In some embodiments, the dispatch path includes a fallback model resolution against a tenant-scoped model configuration table (e.g. `ai_model_config`) for code paths and action classes that pre-date the validated-model registry; in those embodiments, the active platform-validated record remains the source of truth for AI features migrated to the registry, and the fallback resolution is bounded by a feature-key allowlist and operates only when no validated model is supplied by the caller. Migration of remaining paths to validated-model dispatch is a configuration step, not a structural change."

### Fix-B — P02-3 (snapshot persisted via post-INSERT UPDATE, not original INSERT)
**Insert into the v1.1 P02 spec, alternative-embodiment paragraph after the section that describes "the snapshot is committed to the inference record":**

> "In some embodiments, the validated-model snapshot is written to the inference record contemporaneously by way of an update issued within the same logical request, after the inference record's primary insert; in such embodiments, the inference record carries an inference identifier from the moment of insert, and the snapshot is bound to that identifier before the inference response is returned to the caller. Whether the snapshot is committed as part of the original insert or by a contemporaneous update within the same dispatch is a transactional choice and does not affect the binding between the inference record and the validated-model record."

### Fix-C — P02-7 (drift classification is binary in code, not four-level)
**Replace the v1.1 P02 spec text that enumerates "no-change / minor / major / provider-change" with the broader functional language below, plus an alternative-embodiment paragraph:**

> Principal text: "...a drift assessment that produces at least one of the following routing decisions: proceed, requalify, or block, where the decision is a function of a comparison between the version, provider, or configuration of the validated model record and the version, provider, or configuration of the model currently available at the provider endpoint."
>
> Alternative embodiment: "In some embodiments the drift classification distinguishes among at least the following categories: no-change, minor version change, major version change, and provider change, each mapped to one of the routing decisions above. In other embodiments the drift classification is a two-state classification (drift detected vs no drift detected) coupled with a risk-level qualifier, and the routing decision is determined from the combination."

### Fix-D — P02b-1 (no uniform per-inference output_class column)
**Insert into the P02b spec, alternative-embodiment paragraph after the section that describes "classification of each AI output as advisory or deterministic":**

> "In some embodiments, the advisory-versus-deterministic classification of an AI output is encoded on the inference record by way of a dedicated class indicator; in other embodiments, the classification is encoded by way of (i) a purpose registry that designates each AI purpose as advisory or deterministic and is consulted at dispatch time, (ii) per-table boolean indicators on the consuming GxP record that mark the record as AI-sourced and advisory-only, or (iii) a combination of (i) and (ii). The form of the classification carrier does not affect the segregation result, which is that an advisory output is never written to the system-of-record field without human attribution."

### Fix-E — P02b-2 (prohibited-field control is build-time guard + CI gate, not runtime registry)
**Replace the P02b spec language that names "a signed manifest loaded at startup" or "a runtime database table of prohibited write targets" with the broader functional language below:**

> Principal text: "...a prohibited-write control that prevents the AI subsystem from writing advisory output to a designated set of system-of-record fields, the control being enforced before the write reaches the controlled record."
>
> Alternative embodiment: "In some embodiments the prohibited-write control is implemented as a startup-loaded manifest with cryptographic integrity; in some embodiments as a runtime registry queried at write time; and in some embodiments as a build-time guard executed as part of the continuous integration pipeline, the build-time guard inspecting the source of the AI subsystem for any reference to a write operation against a member of the designated set. In each embodiment the enforcement point is upstream of the system-of-record write; whether the enforcement is at compile/CI time, at process start, or at request time is an implementation choice."

### Fix-F — P02b-6 (band → role mapping is not actually band-driven in code)
**Insert into the P02b spec, alternative-embodiment paragraph after the section that describes "band classification determines reviewer role":**

> "In some embodiments the reviewer role is determined directly from the confidence band by way of a band-to-role table; in other embodiments any band that triggers human-in-the-loop review routes to a single approval role configured for the highest-severity tier, the band itself being used to determine whether human review is required rather than which role conducts it. In each embodiment the human-in-the-loop gate is conditional on the band, and the resulting decision is attributed to the human reviewer."

---

## §4 Search Trail (for findings marked "Not found" or low-confidence)

Both patents returned zero "Not found" mechanisms after the searches below, so this section documents what was tried for the partial / diverging findings.

- **P02-2 bypass:** searched `selectModel`, `ai_model_config`, `assertAiFeatureEnabled` across `packages/backend/src/modules/ai/`. Confirmed `selectModel` is called at gateway.ts L174, L513, L553.
- **P02-3 snapshot timing:** searched `model_metadata` across `packages/backend/src/modules/ai/ai-gateway.service.ts` — single match at L923 (comment), confirming snapshot is NOT in the INSERT values block at L928-957. Searched `writeModelMetadata` across all modules — found in 7 service files, all using post-INSERT UPDATE pattern.
- **P02-6 pre-call enforcement:** searched `isQualified`, `preCallCheck`, `agentQualification`, `drift.*preCall` across `packages/backend/src/modules/ai/ai-gateway.service.ts` — zero matches.
- **P02-7 drift classification:** searched `riskLevel.*high`, `versionChange.*minor|major|provider` across `packages/backend/src/modules/hardening/model-drift.service.ts` — only `'low'` is ever assigned to `riskLevel`; no `'high'` setter exists.
- **P02b-1 output_class column:** searched `output_class`, `outputClass`, `advisory_only`, `is_advisory` across `packages/backend/src/`. Found only `annex22_advisory_only` on `auto_investigations` (investigations-hub L139, L860) and the `ADVISORY_AI_PURPOSES_SKIP_HITL_DECISION` set in ai-gateway.service.ts L87-96. No uniform column on `ai_requests`.
- **P02b-2 prohibited-field registry:** searched `prohibited_field`, `forbidden_field`, `prohibited_target`, `forbiddenWriteTargets`, `AI_GXP_JUSTIFIED`, `ai_segregation` across `packages/backend/src/`. Zero hits for a runtime DB table; the only matches are licensing-policy unrelated. The functional control lives in `rca-ai-qs21-guard.test.ts` and CLAUDE.md Verification Gate §Gate 7.
- **P02b-6 band → role:** searched `severity_levels`, `approval_role`, `band.*role` in `packages/backend/src/modules/ai/ai-gateway.service.ts` and `packages/backend/src/modules/hitl/hitl.service.ts`. `resolveReviewer` reads only `severity_levels['critical']`.

---

## §5 Notes on Branch Comparison vs Gap Report (2026-06-04)

| Gap Report finding | Carries over to `dev-vimal-deploy`? | Affects which P02 / P02b claim? |
|---|---|---|
| AI-1: Gateway has legacy bypass paths (`selectModel`, `invokeAction`) that don't consult `platform_validated_models`. | Yes — unchanged. | P02-2 (Partial). |
| AI-2: `ai_requests.model_metadata` populated via post-INSERT UPDATE, not original INSERT. | Yes — unchanged. | P02-3 (Partial). |
| AI-3: Pre-call qualification + drift checks are not enforced inside `AiGatewayService.processRequest`. | Yes — unchanged. | P02-6 (Partial). |
| AI-4 / B-5 (other P01-specific findings). | Out of scope here — covered by P01 (Gap Report). | n/a for P02/P02b. |

No P02/P02b-relevant code change between Gap Report baseline `93068db3` and current HEAD `d8ccdb97` was identified in this audit.

---

**End of report.**
