Elnor Repo Reader

DOC14_ELNOR_CANDOR_Integration_Architecture_R7.md

Current Specs/DOC14/DOC14_ELNOR_CANDOR_Integration_Architecture_R7.md

Short text page 0837b12dfccd. Generated 2026-06-09T01:23:58.539Z from commit dbaa25962edc11ab30e8d4ca1715f9ae5bf77331. Worktree: clean.

Open readable HTML page · Open raw txt · Open path URL

ELNOR REPO READER TEXT MIRROR
Original path: Current Specs/DOC14/DOC14_ELNOR_CANDOR_Integration_Architecture_R7.md
Source repo: /Users/OpenClaw1/Elnor/Elnor Specs
Git branch: main
Git commit: dbaa25962edc11ab30e8d4ca1715f9ae5bf77331
Generated: 2026-06-09T01:23:58.539Z

---

# ELNOR + CANDOR Integration Architecture R7

**Date:** 2026-03-30  
**Revision:** R7 — full reconstruction from R5.2 completeness baseline with all validated R6/R6.1 improvements merged, all accepted red-team/amendment-review corrections applied, and all implementation scaffolding preserved  
**Status:** Integrated working draft — build-hardened reconstruction  
**Primary execution substrate:** DOC12 R7.1  
**Primary cross-doc governance artifact:** DOC14 Cross-Document Delta Companion R2  
**Primary companions:** DOC12 R7.1; DOC11 V12.2; DOC15 current; DOC16 R5.1; DOC17 R4.2; DOC10 current; DOC13 current  
**Owner posture:** DOC14 remains the red-team overlay. It does not re-own room execution, runtime truth, optimizer lifecycle, recommendation nodes, or matter-workflow mechanics.
**Reconstruction basis:** DOC14 R5.2 as completeness-preserving baseline; R6/R6.1 as validated patch sources only. Nothing from R5.2 removed unless explicitly amended.

## Revision-package rules

This revision is a completion pass, not a new architecture. Nothing accepted is dropped. Every accepted mechanism lands in one of four buckets:

- **Core normative DOC14 text** — required for trustworthy red-team operation.
- **Advanced normative DOC14 appendix/section** — fully specified now, may be phase-gated later.
- **Companion-doc required delta** — normatively required, but owned by another document.
- **Research / future-state note** — accepted concept, not yet standardized enough for normative implementation.

### Label legend

- `CORE NORMATIVE`
- `ADVANCED NORMATIVE`
- `COMPANION-DELTA REQUIRED`
- `RESEARCH / FUTURE-STATE`

---

## Storage Paths (normative) `CORE NORMATIVE`

All DOC14-owned durable artifacts must have named canonical storage paths so coding agents do not improvise store locations. These paths are normative until owner docs supersede them:

```ts
const REDTEAM_ROOT = "ELNOR_MEMORY/system/redteam";
const FINDINGS_JSONL_PATH = `${REDTEAM_ROOT}/findings/findings.jsonl`;
const FINDINGS_CURRENT_PATH = `${REDTEAM_ROOT}/findings/findings_current.json`;
const FINDING_JUDGMENTS_JSONL_PATH = `${REDTEAM_ROOT}/judgments/finding_judgments.jsonl`;
const UNPARSED_CONTRIBUTIONS_JSONL_PATH = `${REDTEAM_ROOT}/extraction/unparsed_contributions.jsonl`;
const CRITIQUE_CACHE_JSONL_PATH = `${REDTEAM_ROOT}/cache/critique_cache.jsonl`;
const ROOM_HEALTH_JSONL_PATH = `${REDTEAM_ROOT}/health/room_health_snapshots.jsonl`;
const ROOM_HEALTH_CURRENT_PATH = `${REDTEAM_ROOT}/health/room_health_current.json`;
const PROMPT_OBSERVATIONS_JSONL_PATH = `${REDTEAM_ROOT}/learning/prompt_observations.jsonl`;
const TOPOLOGY_OUTCOMES_JSONL_PATH = `${REDTEAM_ROOT}/learning/topology_outcomes.jsonl`;
const ADJUDICATIONS_JSONL_PATH = `${REDTEAM_ROOT}/adjudication/adjudications.jsonl`;
const LINEAGE_JSONL_PATH = `${REDTEAM_ROOT}/lineage/lineage.jsonl`;
const EXPORTS_ROOT = `${REDTEAM_ROOT}/exports`;
const SCORING_WEIGHTS_PATH = `${REDTEAM_ROOT}/learning/scoring_weights.json`;
const LAUNCH_CAPABILITY_SNAPSHOTS_JSONL_PATH = `${REDTEAM_ROOT}/runtime/launch_capability_snapshots.jsonl`;
```

Rules:
- EC is the sole durable writer.
- Q never writes these stores directly.
- Each append-only stream must have a companion projection only when a UI/read-model requires it.
- Launch capability snapshots are immutable records and may be compacted only after their room is closed and their derived view model is preserved.

---

## Phantom-UI Rule (normative) `CORE NORMATIVE`

When a dependency is absent, DOC14 must either:
1. omit the dependent section entirely, or
2. render one concise informational line explaining why the section is unavailable.

DOC14 must **not** render a large dead shell full of `unavailable` fields.

No section in this document may:
- create a second runtime,
- create a duplicate optimizer,
- create duplicate truth surfaces,
- or create phantom UI without command/route/read-model backing.

---

## No Placeholder Rule for Normative Helpers `CORE NORMATIVE`

No normative helper in this document may remain as:
- `throw new Error("TODO")`
- `as any`
- comment-only pseudocode

Any helper named in a normative hook must either be fully defined here or explicitly delegated to a companion owner doc with a blocking delta row.

## The Falsification Principle

The purpose of ELNOR red-team is not to generate more text. The purpose is to find important, well-evidenced failure modes, move them into durable truth with correct provenance, and improve future configurations without lying about what happened.

Different evidence sources are not interchangeable:

- **Human judgment** is the strongest routine evidence source.
- **Replay-confirmed** and **canary-confirmed** signals are stronger than pure observation but weaker than direct human acceptance in many product contexts.
- **Ghost judgment** is useful and should be visible, but it is synthetic evidence unless confirmed by stronger mechanisms.
- **Unreviewed observation** is useful for clustering and suggestion, not for confident promotion.

---

## 1. Background & Purpose

### 1.1 What CANDOR was

CANDOR started as a standalone adversarial review engine intended to take a document, run structured multi-model critique, and return an auditable set of actionable findings. Its strongest ideas were not the container; they were the mechanisms:

- structured red-team roles,
- findings ledger over transcript,
- concept / strength-drift detection,
- evidence gating,
- compaction defenses,
- prompt learning,
- convergence control,
- recovery and checkpointing,
- and explicit human oversight.

### 1.2 Why CANDOR is not being built separately

CANDOR is not being built as an independent application because that would create the exact category of mess ELNOR is trying to avoid:

1. a second durable store,
2. a second orchestration engine,
3. a second prompt-learning system,
4. and a second set of UI truths pretending to know what happened.

The goal of DOC14 is therefore to extract the useful red-team mechanisms and assign them to the correct ELNOR owner docs while keeping one coherent system.

### 1.3 Why this revision exists

DOC14 v4 fixed most of the architectural problems from v3, but the red-team review bundle identified a remaining cluster of operational gaps:

- finding extraction was still underdefined,
- hook ordering was specified but not fully operationalized,
- judgment routing needed batch-safe, concurrency-safe treatment,
- adjudication and lineage still needed stronger command/read-model contracts,
- review-target materialization for large documents needed sharper rules,
- prompt learning needed stronger scoring and companion-doc acceptance,
- and child-room compatibility had to be upgraded from “nice note” to “real contract.”

This revision closes those gaps while keeping the owner-doc split clean.

---

## 2. Documents Reviewed

- DOC12 R7.1
- DOC11 V12.2 runtime-truth / context-manifest spec
- DOC10 current orchestration / authority matrix / integration ledger spec
- DOC15 current CIL spec
- DOC16 R5.1 deferred additions / linked-room workflow record
- DOC17 R4.2 overlay library, Prompt Advisor, Prompt Recipes, and Prompt Lab
- DOC14 R5.1 red-team review bundle
- DOC14 Cross-Document Delta Companion R1.1 and R1.2 working registers

---

## 3. Cross-Version Synthesis

### 3.1 What v4 got right

v4 made the right core architecture choices:

- DOC12-first room execution,
- panel-as-governance-shell instead of panel-as-runtime,
- shared prompt-learning substrate instead of parallel optimizers,
- canonical `room_judge_finding` dependency,
- review-target binding,
- adjudication and lineage schemas,
- and prompt-truth as a first-class UI requirement.

### 3.2 What this revision adds beyond v5 draft

This revision adds:

- DOC17 R4-aligned shared prompt-artifact family support,
- prompt recipes as first-class shared prompt artifacts,
- richer prompt identity / observation capture with `prompt_recipe_id` and `prompt_artifact_ids`,
- Prompt Lab proposal gating and stronger degraded-mode / ship-gate rules,
- child-room overlay and prompt-recipe inheritance plus explicit selection requirements,
- prompt artifact recommendation wording and UI truth-surface upgrades,
- expanded DOC12 / DOC11 / DOC15 / DOC17 companion deltas,
- updated example payloads and acceptance tests for overlays, recipes, and child-room launch policies,
- and a terminology normalization pass so coding agents do not confuse prompt variants with the broader prompt-artifact family.

### 3.3 Items preserved unchanged

### 3.4 What v5.2 adds beyond v5.1

This revision incorporates **all accepted and accepted-with-modification changes currently carried by the DOC14 companion**, plus every still-valid DOC14-local hardening item from the R5.1 red-team bundle.

Specifically, v5.2 adds or tightens all of the following:

- sync to **DOC12 R7.1** as the accepted room-side red-team substrate;
- explicit recognition of the DOC12 split between **shared contracts** and **room-side projections**;
- alignment to the **accepted child-room launch vocabularies** in DOC12 R7.1;
- a stricter **preferred vs realized review-target materialization** model;
- a concrete **ChildRoomEffectivePromptPlan** resolver and view model;
- complete **judgment state-transition mapping** for all dispositions;
- an explicit **scoreFindingJudgment / buildPromptObservationFromJudgment** contract with `scoring_version`, `novelty`, and `prompt_method_used` support;
- a first-class **cache-promotion command/route** and corresponding UI requirements;
- a first-class **batch-judgment partial-failure recovery** model;
- explicit **SSE payload schemas** and an event-emission requirement inside hook dispatchers;
- stronger **large-document execution policy**, including chunk-map/search-tool behavior and anchor integrity requirements;
- a stricter **Participant Prompt Plan** truth model that distinguishes room-side projections from DOC11-owned runtime truth;
- a stronger **Prompt Artifact Recommendation** UX, including recipe support, proposal-only gating, apply blockers, and diff/preview requirements;
- additional **DOC15/CIL signal hooks** for novelty, review outcome, and prompt-method provenance;
- additional **advanced normative** addenda so accepted ideas are documented now rather than left to memory later.

### 3.5 Interpretation rule for this revision

v5.2 should be read as:

- a **full DOC14 revision** that supersedes v5.1,
- a **synchronization revision** that pulls accepted DOC12 room-side reality into DOC14 wording,
- and a **build-hardening revision** that closes red-team and coding-agent drift holes while still preserving owner-doc boundaries.

Nothing accepted in the review cycle is intentionally dropped. Where something remains open, it remains open **as a companion-doc delta row with explicit status**, not as silent omission.


The following remain binding:

- EC is the sole durable writer.
- Rooms are the execution substrate.
- Panels are governance / export shells.
- No second durable store.
- No transcript-first inter-surface transfer.
- No fake runtime truth.
- No hidden prompt application without visible prompt truth.

### 3.6 What R7 adds beyond R5.2

R7 is a **full reconstruction** from the R5.2 completeness baseline. It merges the following validated improvements from R6/R6.1 review cycles while preserving all R5.2 implementation scaffolding:

- **storage-path registry** with canonical durable artifact paths;
- **deterministic helper implementations**: `selectMaterializationMode`, `computeContextSymmetryEpoch`, `isConverged`, `applyEvidenceGate`, `appendMaterializationTools`, `getPromptObservationWeights`, `buildPromptObservationFromJudgment`, `emitPromptObservationFromJudgment`;
- **corrected judgment handler** using `FINDING_STATE_FROM_DISPOSITION` map instead of ternary, with `starred`/`cited_in_decision` boolean patch handling;
- **`finding_severity` added to `FindingJudgmentSchema`** so scoring can compute weights from the judgment alone;
- **`event_bundle` added to `PostTurnResultSchema`** so DOC12 can emit granular SSE events from hook results;
- **enrichment fields added to `PromptObservationEmitSchema`**: `route_trace_id`, `operation_id`, `dispatch_id`, `prompt_method_used`, `scoring_version`, `review_target_length_bucket`, `realized_materialization_mode`;
- **`PromptMethodUsedSchema` corrected** to preserve R5.2 values and add `overlay_and_recipe_augmented`;
- **child-room resolver as real code** handling all 24 overlay×recipe mode combinations;
- **new schema families**: `ManualExtractRequest/Response`, `FindingEditAudit`, `RoomPromoteCachedFindingResponse`, `RoomOverrideConvergenceRequest/Response`, `RoomExportRequest/Response`, `RecommendationSourceViewModel`, `RecommendationApplyRequest/Response`, `PromptArtifactMutationEnvelope`, `LaunchCapabilitySnapshot`, `ConfiguratorStateViewModel`, `BatchJudgmentRecoveryViewModel`, `ConvergenceBannerViewModel`, `FindingDetailViewModel`, `ChildRoomPreviewRequest/Response`, `ReviewTargetSearch/Chunk/Anchor response schemas`;
- **typed SSE event payload schemas** for the canonical event family;
- **phantom-UI normative rule** and **no-placeholder rule**;
- **pre-launch target preview** contract;
- **recommendation invalidation** model;
- **tool injection for large documents** via `appendMaterializationTools`;
- **Save Preset explicitly phase-gated** out of Phase 0 active UI;
- **force-dissent as prompt intervention** (not finding fabrication).

---

## 4. Feasibility & Recommended Architecture

### 4.1 Verdict: feasible, DOC12-first, panel-as-governance-shell `CORE NORMATIVE`

DOC14 remains feasible and correct when built on DOC12 rooms. The correct execution pattern is:

```text
[DOC12 red-team room]
  -> live turns, findings, health, convergence
  -> export summary / finding set / proposed fixes / decision basis
  -> optional post to panel or linked room
  -> human approval / matter workflow / ship lane
```

What this section does **not** do:

- it does not create a second runtime,
- it does not make the panel into a room clone,
- and it does not make DOC14 a legal-only subsystem.

### 4.2 Why not panel-as-orchestrator `CORE NORMATIVE`

Panels are not a safe substitute for room execution because:

- panel DAGs are the wrong granularity for turn-by-turn critique state,
- room crashes and room resume logic belong with room ownership,
- room context and participant state cannot be faithfully mirrored into a panel without creating duplicate truth,
- and linked child rooms should still be launched through room/panel contracts owned by DOC12/DOC6 rather than by panel-side orchestration hacks.

### 4.3 Canonical architecture rules `CORE NORMATIVE`

#### 4.3.1 Purpose

This section exists to stop scope rot. Coding agents and future spec passes must be able to answer “who owns this?” before they answer “how do we build this?”

#### 4.3.2 Binding owner table

| Capability | Owner doc | Notes |
|---|---|---|
| red-team policy, validators, hook semantics, red-team UI requirements | DOC14 | semantic owner |
| room artifacts, room commands, scheduling, bootstrap, linked rooms, document bindings | DOC12 | execution owner |
| runtime truth, context manifests, prompt-truth display, degraded truth display | DOC11 | truth-surface owner |
| prompt variants, assignments, replay, canaries, promotion/rejection lifecycle | DOC8 | optimizer owner |
| recommendation nodes and retrieval | DOC15 | recommender owner |
| canonical cost and supervision-cost plumbing | DOC13 | cost owner |
| orchestration authority / route trace / integration ledger rows | DOC10 | orchestration owner |
| overlay templates, prompt recipes, prompt-gap analysis, Prompt Advisor UX, Prompt Lab proposals | DOC17 | prompt-artifact owner / proposal UX |
| litigation workbench planning / owner-doc coordination | DOC16 | planning owner |
| room-backed matter panel bridge | DOC6 | panel owner |
| visible starter agents / logical agent templates | DOC4 | agent owner |
| legal research capability family | DOC3 | skill owner |

#### 4.3.3 Partial-deployment rule

If a companion-doc dependency has not shipped, DOC14 must define one of:

- **blocked** — the feature cannot run and the UI must say why,
- **degraded** — the feature runs with explicit disclosure,
- **capture-only** — the feature emits observation data but does not activate learned or automated behavior.

Silent fallback is prohibited for any feature that changes truth, prompt application, review-target scope, or visible room state.

#### 4.3.4 No duplicate optimizer rule

DOC17 may define and manage overlay templates, prompt recipes, prompt-advisor rewrites, Prompt Advisor UI, and Prompt Lab offline experimentation inputs/outputs, but it may not create a second replay/canary/promotion authority chain. All such lifecycle ownership flows through the shared prompt-learning substrate with DOC8 as owner.

Prompt Lab outputs are offline proposals only. They do not become live room prompt artifacts solely by existing. A Prompt Lab candidate may be shown in UI, evaluated offline, or queued for review, but it may not be treated as a production prompt-artifact recommendation or automatically applied live unless it passes the required replay/canary/human-review gates defined by companion owner docs.

Prompt Advisor guidance and prompt-artifact recommendation are advisory surfaces. Effective prompt truth remains a runtime/display responsibility of DOC11, not a UI assumption of DOC14 or DOC17.

### 4.3A Shared prompt-learning substrate and DOC17 interplay `CORE NORMATIVE`

#### Architecture

DOC14 and DOC17 participate in one shared prompt-learning substrate, but they are not the same product surface and they may not create competing optimizer authority chains.

- DOC14 emits red-team prompt observations tied to room roles, review targets, findings, judgments, and topology outcomes.
- DOC17 emits overlay-template, prompt-recipe, and prompt-advisor-rewrite observations and may surface Prompt Lab offline proposals.
- DOC8 owns the shared prompt-artifact lifecycle: replay, canary, promotion, retirement, and evidence-gated candidate status.
- DOC15 turns accepted learning into prompt-artifact recommendation nodes and retrieval surfaces.
- DOC11 shows the effective prompt-artifact plan that actually ran, including configured, packet-assembled, applied, trimmed, and dropped artifacts.
- DOC12 carries prompt assignments, child-room launch prompt-artifact selections, and room bootstrap prompt-plan references into participant execution.

#### Prompt-artifact scope clarification

#### R5.2 amendment — canonical shared import rule and prompt-method provenance

The shared prompt-artifact family is now normative across DOC14 / DOC17 / DOC8 / DOC15 / DOC11 / DOC12 and **must be imported from one shared contracts package**. No owner doc may locally redefine `PromptArtifactKindSchema` or a semantically equivalent enum with different members.

Add:

```ts
export const PromptMethodUsedSchema = z.enum([
  "baseline_room_role_prompt",
  "recommended_room_role_prompt",
  "overlay_augmented",
  "prompt_recipe_augmented",
  "overlay_and_recipe_augmented",
  "advisor_rewrite_applied",
  "canary_variant",
  "proposal_only",
]);

export const PromptArtifactObservationEnvelopeSchema = z.object({
  room_id: z.string(),
  room_turn_id: z.string().optional(),
  participant_id: z.string().optional(),
  route_trace_id: z.string().optional(),
  operation_id: z.string().optional(),
  prompt_artifact_ids: z.array(z.string()).default([]),
  active_overlay_ids: z.array(z.string()).default([]),
  prompt_recipe_id: z.string().uuid().optional(),
  prompt_method_used: PromptMethodUsedSchema.optional(),
  emitted_at: z.string(),
});
```

This envelope is not a replacement for `PromptObservationEmitSchema` or `TopologyOutcomeSignalSchema`. It is a **correlated prompt-artifact truth envelope** that may be attached to either or both so room-close and per-judgment events do not lose artifact provenance.


DOC14 red-team runs may be influenced by multiple prompt-artifact classes at once: room-role prompt variants, overlay templates, prompt recipes, and prompt-advisor rewrites. DOC14 does not require all such artifacts to be generated by DOC14. It requires that any artifact materially affecting execution be attributable enough for truthful display, learning-signal emission, and downstream evaluation. The existence of multiple artifact classes does not create multiple optimizers or multiple sources of runtime truth.

#### Schemas

```ts
import { z } from "zod";

export const PromptArtifactKindSchema = z.enum([
  "room_role_prompt",
  "overlay_template",
  "prompt_recipe",
  "prompt_advisor_rewrite",
]);

export type PromptArtifactKind = z.infer<typeof PromptArtifactKindSchema>;

export const PromptIdentityRefSchema = z.object({
  prompt_artifact_kind: PromptArtifactKindSchema,
  prompt_variant_id: z.string().optional(),
  prompt_text_hash: z.string().min(16),
  prompt_assignment_id: z.string().optional(),
  active_overlay_ids: z.array(z.string()).default([]),
  prompt_recipe_id: z.string().uuid().optional(),
  prompt_artifact_ids: z.array(z.string()).default([]),
});

export type PromptIdentityRef = z.infer<typeof PromptIdentityRefSchema>;
```

#### Minimum required support table

| Artifact kind | DOC14 role | DOC17 role | DOC8 role | DOC11 role | DOC15 role |
|---|---|---|---|---|---|
| `room_role_prompt` | observe, score, attribute | none | replay/canary/promotion | truth display | recommendation |
| `overlay_template` | observe, score, attribute | define/manage | replay/canary/promotion | truth display / packet assembly | recommendation |
| `prompt_recipe` | observe, score, attribute | define/manage / Prompt Lab candidate source | replay/canary/promotion | truth display | recommendation |
| `prompt_advisor_rewrite` | observe as applied artifact | generate/manage | replay/canary/promotion if promoted | truth display | recommendation |

#### Wiring

#### R5.2 amendment — room-side projections versus runtime truth

The next DOC14 generation adopts the DOC12 R7.1 and DOC11 V12.2 split explicitly:

- **Shared/canonical contracts** live in the shared contracts package.
- **DOC12 owns room-side projections** used by room queries and room UI hydration.
- **DOC11 owns effective runtime truth** and packet assembly truth.

This means the following pairings are now normative:

| Shared contract | Room-side projection owner | Runtime truth owner |
|---|---|---|
| `PromptIdentityRefSchema` | DOC12 `ParticipantPromptPlanProjectionSchema` | DOC11 `ParticipantPromptPlanViewModelSchema` |
| `ReviewTargetBindingRefSchema` | DOC12 `ReviewTargetBindingSchema` + `ReviewTargetMaterializationProjectionSchema` | DOC11 context/runtime truth fields |
| `ChildRoomPromptArtifactSelectionSchema` | DOC12 `ChildRoomPromptArtifactSelectionProjectionSchema` | DOC11 prompt-plan truth once child room is live |

DOC14 must not blur these layers.


- **Source:** DOC14 findings, judgments, room outcomes  
  **Destination:** DOC8 prompt observation ingestion  
  **Flow:** DOC14 emits prompt identity in observation-ready form; DOC8 clusters and scores without re-deriving room-local prompt state.  
  **Failure behavior:** DOC8 unavailable -> capture-only mode; room execution continues; no recommendation application or promotion.  
  **Owner:** DOC8.

- **Source:** DOC17 overlays, prompt recipes, advisor interactions, and Prompt Lab offline proposals  
  **Destination:** same shared prompt-artifact ingestion path  
  **Flow:** DOC17 emits overlay/recipe/rewrite observations and proposal metadata; replay/canary/promotion remain external to DOC17.  
  **Failure behavior:** overlay/recipe learning disabled; artifacts remain usable only as static configured or proposal-only artifacts according to owner-doc gates.  
  **Owner:** DOC8 for lifecycle; DOC17 for artifact semantics and proposal UX.

- **Source:** DOC8 approved/canary recommendations  
  **Destination:** DOC15 prompt-artifact recommendation nodes  
  **Flow:** optimizer promotes or updates recommendation artifacts; DOC15 indexes them for retrieval.  
  **Failure behavior:** recommendations absent; no silent apply.  
  **Owner:** DOC15.

- **Source:** DOC15 recommendation retrieval + DOC11 prompt-plan truth  
  **Destination:** DOC14 configurator, participant drawer, post-run summary, and child-room launch preview  
  **Flow:** recommendation surfaces may render only when prompt truth can be shown honestly; Prompt Lab candidates remain proposal-only until gated.  
  **Failure behavior:** if DOC11 prompt truth is unavailable, no learned apply path may be shown.  
  **Owner:** DOC11 for truth display; DOC15 for retrieval.

#### Code implementation

```ts
// packages/contracts/src/red-team/prompt-identity.ts
import { z } from "zod";

export const PromptArtifactKindSchema = z.enum([
  "room_role_prompt",
  "overlay_template",
  "prompt_recipe",
  "prompt_advisor_rewrite",
]);

export const PromptIdentityRefSchema = z.object({
  prompt_artifact_kind: PromptArtifactKindSchema,
  prompt_variant_id: z.string().optional(),
  prompt_text_hash: z.string().min(16),
  prompt_assignment_id: z.string().optional(),
  active_overlay_ids: z.array(z.string()).default([]),
  prompt_recipe_id: z.string().uuid().optional(),
  prompt_artifact_ids: z.array(z.string()).default([]),
});

export type PromptIdentityRef = z.infer<typeof PromptIdentityRefSchema>;
```

#### Acceptance

- Red-team room emits `prompt_variant_id` when available.
- Overlay- and recipe-enabled room emits `active_overlay_ids`, `prompt_recipe_id`, and `prompt_artifact_ids` when present.
- Prompt Lab proposals may be shown as proposals, but no UI path applies or promotes them if DOC11 cannot show the effective prompt plan or if DOC8 gating/evidence state is insufficient.
- DOC17 may not define a second replay/canary/promotion authority chain in any companion delta or UI copy.


### 4.4 New room mode: `red_team` `CORE NORMATIVE`

`room_mode: "red_team"` is the semantic and product-level declaration that a room is governed by DOC14 semantics.

DOC12 execution may use a narrower execution dispatch value such as `turn_mode: "red_team_bounded"`, but the mapping must be explicit:

- `room_mode === "red_team"` and valid `red_team_policy` -> DOC12 derives the red-team execution turn mode.
- mismatch between semantic and execution modes is a contract error.
- if DOC12 does not support the derived execution mode, launch is blocked in ship / high-stakes rooms and degraded in exploratory rooms.

### 4.4A RedTeam hook result contracts `CORE NORMATIVE`

```ts
import { z } from "zod";

export const AgreementStateSchema = z.enum([
  "agree_only",
  "neutral",
  "substantive_dissent",
  "manufactured_dissent",
]);

export const PostTurnResultSchema = z.object({
  room_id: z.string(),
  room_turn_id: z.string(),
  parsed_turn_ref: z.string(),
  created_findings: z.array(z.string()).default([]),
  cache_entries_created: z.array(z.string()).default([]),
  downgraded_findings: z.array(z.string()).default([]),
  dropped_findings: z.array(z.string()).default([]),
  unparsed_contribution_ids: z.array(z.string()).default([]),
  agreement_state: AgreementStateSchema,
  room_health_snapshot_ref: z.string().optional(),
  convergence_state: z.enum(["not_converged", "converged", "overridden", "degraded"]),
  event_bundle: z.array(z.object({
    event_name: z.string(),
    payload: z.record(z.string(), z.unknown()),
  })).default([]),
  degraded_reasons: z.array(z.string()).default([]),
  warnings: z.array(z.string()).default([]),
  errors: z.array(z.string()).default([]),
});

export const PostSynthesisResultSchema = z.object({
  room_id: z.string(),
  synthesis_turn_id: z.string(),
  exported_summary_ref: z.string().optional(),
  decision_basis_ref: z.string().optional(),
  warnings: z.array(z.string()).default([]),
  errors: z.array(z.string()).default([]),
});

export const CloseRoomResultSchema = z.object({
  room_id: z.string(),
  findings_projection_ref: z.string().optional(),
  prompt_observation_refs: z.array(z.string()).default([]),
  topology_outcome_ref: z.string().optional(),
  warnings: z.array(z.string()).default([]),
  errors: z.array(z.string()).default([]),
});
```

### 4.4B RedTeamLifecycleControlMap `CORE NORMATIVE`

The lifecycle order is binding. Coding agents may not reorder these steps.

```ts
export const RedTeamLifecycleControlMap = {
  preTurn: [
    "assertReviewTargetBindingPresent",
    "assertPromptPlanVisibilityEligibility",
    "assertContextSymmetryPolicy",
  ],
  postTurn: [
    "parseTurnOutput",
    "extractFindingsFromTurn",
    "validateRedTeamOutputContract",
    "dedupeStructurally",
    "applyEvidenceGate",
    "downgradeOrCacheUnsupported",
    "enforcePerTurnFindingQuotas",
    "enforceWhyThisMatters",
    "buildAppendReadyFindingSet",
    "runSemanticDedupOrDeferredReview",
    "deriveAgreementState",
    "applyForceDissentChecks",
    "computeRoomHealth",
    "computeConvergence",
    "emitPostTurnTelemetry",
  ],
  postSynthesis: [
    "validateSynthesisArtifacts",
    "buildDecisionPack",
    "emitSynthesisTelemetry",
  ],
  closeRoom: [
    "emitPromptObservations",
    "emitTopologyOutcome",
    "flushFinalRoomProjection",
  ],
} as const;
```



#### Consolidated dispatcher implementation

```ts
// apps/ec-service/src/redteam/executePostTurnHook.ts
import {
  PostTurnResultSchema,
  RedTeamRoomOutputContractSchema,
  FindingExtractionResultSchema,
  RoomHealthSnapshotSchema,
} from "@elnor/contracts/red-team";

export type ExecutePostTurnHookDeps = {
  parseTurnOutput: (roomId: string, roomTurnId: string) => Promise<unknown>;
  extractFindingsFromTurn: (args: { parsedTurn: unknown; fallbackExtractor: (rawText: string) => Promise<unknown[]> }) => Promise<import("zod").infer<typeof FindingExtractionResultSchema>>;
  validateRedTeamOutputContract: (parsedTurn: unknown, contract: import("zod").infer<typeof RedTeamRoomOutputContractSchema>) => void;
  dedupeStructurally: (findings: unknown[]) => Promise<unknown[]>;
  applyEvidenceGate: (findings: unknown[], policy: unknown) => Promise<{ ledgerReady: unknown[]; cacheOnly: unknown[]; downgraded: unknown[]; dropped: unknown[] }>;
  enforceQuotas: (findings: unknown[], policy: unknown) => Promise<unknown[]>;
  enforceWhyThisMatters: (findings: unknown[]) => Promise<unknown[]>;
  semanticDedup: (findings: unknown[], roomId: string) => Promise<unknown[]>;
  appendFindings: (roomId: string, findings: unknown[]) => Promise<string[]>;
  deriveAgreementState: (parsedTurn: unknown, createdFindingIds: string[]) => Promise<"agree_only" | "neutral" | "substantive_dissent" | "manufactured_dissent">;
  applyForceDissentChecks: (roomId: string, agreementState: string, policy: unknown) => Promise<void>;
  computeRoomHealth: (roomId: string) => Promise<import("zod").infer<typeof RoomHealthSnapshotSchema>>;
  computeConvergence: (roomId: string, policy: unknown) => Promise<"not_converged" | "converged" | "overridden">;
  emitTelemetry: (eventName: string, payload: Record<string, unknown>) => Promise<void>;
  fallbackExtractor: (rawText: string) => Promise<unknown[]>;
};

export async function executePostTurnHook(args: {
  roomId: string;
  roomTurnId: string;
  policy: unknown;
  outputContract: unknown;
  deps: ExecutePostTurnHookDeps;
}) {
  const parsedTurn = await args.deps.parseTurnOutput(args.roomId, args.roomTurnId);
  args.deps.validateRedTeamOutputContract(parsedTurn, RedTeamRoomOutputContractSchema.parse(args.outputContract));

  const extraction = await args.deps.extractFindingsFromTurn({
    parsedTurn,
    fallbackExtractor: args.deps.fallbackExtractor,
  });

  const structurallyUnique = await args.deps.dedupeStructurally(extraction.candidates);
  const gated = await args.deps.applyEvidenceGate(structurallyUnique, args.policy);
  const quotaBound = await args.deps.enforceQuotas(gated.ledgerReady, args.policy);
  const whyBound = await args.deps.enforceWhyThisMatters(quotaBound);

  // R7 FIX: semantic dedup is feature-gated — do not crash when no embedding service exists
  const semanticDedupEnabled = (args.policy as { semantic_dedup_enabled?: boolean }).semantic_dedup_enabled ?? false;
  const semanticallyUnique = semanticDedupEnabled
    ? await args.deps.semanticDedup(whyBound, args.roomId)
    : whyBound;

  const createdFindingIds = await args.deps.appendFindings(args.roomId, semanticallyUnique);
  const agreementState = await args.deps.deriveAgreementState(parsedTurn, createdFindingIds);
  await args.deps.applyForceDissentChecks(args.roomId, agreementState, args.policy);
  const roomHealth = await args.deps.computeRoomHealth(args.roomId);
  const convergenceState = await args.deps.computeConvergence(args.roomId, args.policy);

  // R7 ADDITION: populate event_bundle for DOC12 SSE emission
  const events: Array<{ event_name: string; payload: Record<string, unknown> }> = [];
  const now = new Date().toISOString();
  for (const fid of createdFindingIds) {
    events.push({
      event_name: "room.finding.created",
      payload: { room_id: args.roomId, finding_id: fid, emitted_at: now },
    });
  }
  events.push({
    event_name: "room.health.updated",
    payload: { room_id: args.roomId, health_ref: `${args.roomId}:health:${roomHealth.computed_at}`, stale: false, emitted_at: now },
  });
  if (convergenceState === "converged") {
    events.push({
      event_name: "room.convergence.reached",
      payload: { room_id: args.roomId, emitted_at: now },
    });
  }

  const payload = PostTurnResultSchema.parse({
    room_id: args.roomId,
    room_turn_id: args.roomTurnId,
    parsed_turn_ref: `${args.roomId}:${args.roomTurnId}:parsed`,
    created_findings: createdFindingIds,
    cache_entries_created: gated.cacheOnly.map((_, i) => `${args.roomId}:${args.roomTurnId}:cache:${i}`),
    downgraded_findings: gated.downgraded.map((_, i) => `${args.roomId}:${args.roomTurnId}:down:${i}`),
    dropped_findings: gated.dropped.map((_, i) => `${args.roomId}:${args.roomTurnId}:drop:${i}`),
    unparsed_contribution_ids: extraction.unparsed_contribution_ids ?? [],
    agreement_state: agreementState,
    room_health_snapshot_ref: `${args.roomId}:health:${roomHealth.computed_at}`,
    convergence_state: convergenceState,
    event_bundle: events,
    degraded_reasons: [],
    warnings: extraction.warnings,
    errors: extraction.errors,
  });

  await args.deps.emitTelemetry("room.post_turn.completed", {
    room_id: args.roomId,
    room_turn_id: args.roomTurnId,
    created_findings: createdFindingIds.length,
    convergence_state: convergenceState,
    room_health_status: roomHealth.status,
  });

  return payload;
}
```

### 4.4C Deterministic computation specs `CORE NORMATIVE`

The following computations must be deterministic for a fixed input set and `formula_version`:

- `computeStableHash()`
- `buildStructuralFindingHash()`
- `deriveAgreementState()`
- `computeRoomHealth()`
- `isConverged()`
- `runStrengthSentinel()`
- `evaluateEvidenceGate()`
- `computeContextSymmetryEpoch()`

#### Hash computation rules

- normalize line endings to `\n`
- trim trailing spaces
- lowercase when hash is for structural comparison, preserve case when hash is for prompt identity
- include explicit `formula_version`
- use `sha256`

#### Strength-sentinel baseline rule

Phase 0 baseline:

- detect concept label occurrence,
- inspect the sentence containing the occurrence plus adjacent sentence,
- compare required strength words against candidate language,
- emit drift event when a stronger source requirement is absent or replaced by weaker modal language.

This baseline is intentionally conservative and may emit false positives. False positives must be tolerable, visible, and suppressible; they must not silently rewrite the source truth.

#### Evidence gate algorithm `CORE NORMATIVE`

```ts
export const EvidenceGateOutcomeSchema = z.object({
  ledger_ready_ids: z.array(z.string()).default([]),
  cache_only_ids: z.array(z.string()).default([]),
  dropped_ids: z.array(z.string()).default([]),
  reasons: z.record(z.string(), z.string()).default({}),
});

export function applyEvidenceGate(args: {
  findings: Array<{ finding_id?: string; severity: string; evidence_refs?: string[]; applies_to_ref?: string; why_this_matters?: string }>;
  policy: { evidence_domain?: string; truth_seeking_mode?: boolean };
}): z.infer<typeof EvidenceGateOutcomeSchema> {
  const out: z.infer<typeof EvidenceGateOutcomeSchema> = {
    ledger_ready_ids: [], cache_only_ids: [], dropped_ids: [], reasons: {},
  };
  for (const finding of args.findings) {
    const id = finding.finding_id ?? crypto.randomUUID();
    const hasEvidence = Array.isArray(finding.evidence_refs) && finding.evidence_refs.length > 0;
    const hasTarget = !!finding.applies_to_ref;
    const hasWhy = typeof finding.why_this_matters === "string" && finding.why_this_matters.trim().length > 0;

    if (finding.severity === "critical") {
      if (hasEvidence) out.ledger_ready_ids.push(id);
      else { out.cache_only_ids.push(id); out.reasons[id] = "insufficient_evidence_for_critical"; }
    } else if (finding.severity === "major") {
      if (hasEvidence || hasTarget) out.ledger_ready_ids.push(id);
      else { out.cache_only_ids.push(id); out.reasons[id] = "insufficient_evidence_for_major"; }
    } else if (finding.severity === "minor") {
      if (hasWhy) out.ledger_ready_ids.push(id);
      else { out.cache_only_ids.push(id); out.reasons[id] = "missing_why_this_matters_for_minor"; }
    } else {
      out.ledger_ready_ids.push(id); // observation: no mandatory evidence
    }
  }
  return out;
}
```

Minimum rules:
- `critical`: at least one concrete `evidence_refs` entry
- `major`: at least one `evidence_refs` entry OR a concrete `applies_to_ref`
- `minor`: non-empty `why_this_matters` is sufficient
- `observation`: no mandatory evidence
- unsupported high-severity findings go to cache, not ledger
- extraction errors or quota overflow may go to dropped

#### Context symmetry epoch algorithm `CORE NORMATIVE`

```ts
export const ContextSymmetryResultSchema = z.object({
  majority_epoch: z.string().optional(),
  participant_epochs: z.record(z.string(), z.string()).default({}),
  blocking_participant_ids: z.array(z.string()).default([]),
  warning_participant_ids: z.array(z.string()).default([]),
  formula_version: z.string().min(1),
});

export function computeContextSymmetryEpoch(args: {
  review_target_binding_ref: { binding_id: string };
  realized_materialization_mode: z.infer<typeof RealizedReviewTargetMaterializationModeSchema>;
  active_overlay_ids: string[];
  prompt_recipe_id: string | null;
  formula_version: string;
}): string {
  const canonical = JSON.stringify({
    binding: args.review_target_binding_ref.binding_id,
    materialization: args.realized_materialization_mode,
    overlays: [...args.active_overlay_ids].sort(),
    recipe: args.prompt_recipe_id ?? "none",
    version: args.formula_version,
  });
  return require("crypto").createHash("sha256").update(canonical).digest("hex");
}
```

Symmetry severity:
- **blocking** if review-target binding or realized materialization differs,
- **warning** if only overlays/recipe differ.

Policy:
- `strict` → blocking violations halt scheduling
- `warn_only` → log and surface
- `off` → no computation

#### Convergence algorithm `CORE NORMATIVE`

```ts
export function isConverged(args: {
  current_turn_number: number;
  lookback_turns: number;
  total_participant_count: number;
  finding_creation_turn_numbers: number[];
}): { converged: boolean; convergence_score: number; rationale_entries: string[] } {
  const lastCreated = args.finding_creation_turn_numbers.length ? Math.max(...args.finding_creation_turn_numbers) : 0;
  const turnsSince = args.current_turn_number - lastCreated;
  const rotations = Math.floor(turnsSince / Math.max(args.total_participant_count, 1));
  const recentCreated = args.finding_creation_turn_numbers.filter(t => t > args.current_turn_number - args.lookback_turns);
  const converged = recentCreated.length === 0 && rotations >= 1;
  return {
    converged,
    convergence_score: Math.max(0, 1 - recentCreated.length / Math.max(args.lookback_turns, 1)),
    rationale_entries: converged
      ? [
          `No new findings created in last ${args.lookback_turns} turns`,
          `${rotations} full participant rotation(s) completed`,
        ]
      : [`${recentCreated.length} finding(s) still created inside lookback window`],
  };
}
```

Note: convergence measures finding **creation**, not finding **acceptance**. A room that is still producing new findings has not converged, regardless of judgment state.

#### Force dissent rule `CORE NORMATIVE`

Force dissent is a **prompt intervention**, not a finding-fabrication mechanism.

If agreement is `agree_only` after a full rotation:
1. inject a supplementary critic prompt asking for weaknesses/alternative interpretations,
2. if none are found after that second look, mark state as `substantive_agreement`,
3. never fabricate dissenting findings.

### 4.5 DOC6 panel execution mode `COMPANION-DELTA REQUIRED`

DOC6 panels remain governance / export shells. They may:

- display red-team room status,
- host approvals,
- import structured outputs,
- and expose child-room launch affordances.

They may not own live debate execution.

### 4.6 Linked room / child red-team compatibility `CORE NORMATIVE`

Child red-team launch is a must-have compatibility requirement.

DOC14 does not own linked-room mechanics, but it does own the minimum compatibility contracts needed to launch a child red-team room against a draft or review target and to preserve truthful prompt-artifact state across the parent-child hop.

#### Schemas

```ts
export const ReviewTargetBindingRefSchema = z.object({
  room_id: z.string(),
  binding_id: z.string(),
  doc_id: z.string(),
  version_group_id: z.string().optional(),
  materialization_mode: z.enum([
    "full_if_budget",
    "summary_default",
    "chunk_map",
    "search_tool",
    "chunk_on_demand",
  ]),
});

export const ChildRedTeamLaunchPolicySchema = z.object({
  allow_child_redteam_launch: z.boolean().default(true),
  require_human_approval_before_launch: z.boolean().default(false),
  default_import_back_strategy: z.enum([
    "finding_subset",
    "finding_subset_plus_summary",
    "full_redteam_output_ref",
  ]).default("finding_subset_plus_summary"),

  overlay_application_mode: z.enum([
    "inherit_none",
    "inherit_room_default",
    "inherit_parent_effective",
    "inherit_then_append_explicit",
    "explicit_only",
    "explicit_select",
  ]).default("inherit_parent_effective"),

  explicit_overlay_ids: z.array(z.string()).default([]),

  prompt_recipe_inheritance_mode: z.enum([
    "inherit_none",
    "carry_context_only",
    "inherit_effective_recipe",
    "explicit_select",
  ]).default("carry_context_only"),

  explicit_prompt_recipe_id: z.string().uuid().optional(),
});

export const LaunchChildRedTeamRoomRequestSchema = z.object({
  parent_room_id: z.string(),
  review_target_binding_ref: ReviewTargetBindingRefSchema,
  launch_policy: ChildRedTeamLaunchPolicySchema,
  reason: z.string().optional(),
});
```

#### Normative behavior

A child red-team room must support both inherited and explicit prompt-artifact configuration.

- inheritance is not mandatory,
- users may launch with no inherited overlays,
- users may inherit parent effective overlays,
- users may inherit room defaults only,
- users may append explicit overlays after inheritance,
- users may select an explicit overlay set and explicit prompt recipe instead of inheritance,
- and the resulting effective prompt-artifact plan must be shown truthfully in room UI.

For child-room launch, effective prompt-artifact precedence is:

1. explicit child-room launch selections,  
2. child-room default overlay/recipe configuration,  
3. inherited parent effective overlays/recipe when the launch policy allows inheritance,  
4. ambient/user-session prompt artifacts only when companion owner docs permit them,  
5. baseline room-role prompting.

DOC11 must display the resulting effective prompt-artifact plan truthfully, including artifacts dropped due to policy, budget, or incompatibility.

#### Wiring

- **Source:** parent room output card / document binding / post-run summary  
  **Destination:** DOC12 linked-room creation + transfer-packet import  
  **Flow:** parent room selects target -> launch policy selects inheritance/explicit prompt-artifact behavior -> DOC12 creates child room -> target ref transferred -> child room runs under `room_mode: red_team` with effective prompt-artifact truth surfaces.  
  **Failure behavior:** if linked-room support absent, UI shows “child-room launch unavailable in this build”; if overlay or recipe truth is unavailable, the launch preview must disclose degraded prompt truth and block learned apply actions.  
  **Owner:** DOC12 for creation, transport, and room-local state; DOC14 for compatibility semantics; DOC17 for overlay/recipe semantics; DOC11 for truth display.

#### UI contract

#### R5.2 amendment — child-room effective prompt-plan resolver and launch preview

DOC12 R7.1 now accepts the following child-room launch vocabularies and DOC14 must align to them exactly:

```ts
export const ChildRoomOverlayApplicationModeSchema = z.enum([
  "inherit_none",
  "inherit_room_default",
  "inherit_parent_effective",
  "inherit_then_append_explicit",
  "explicit_only",
  "explicit_select",
]);

export const ChildRoomPromptRecipeModeSchema = z.enum([
  "inherit_none",
  "carry_context_only",
  "inherit_effective_recipe",
  "explicit_select",
]);

export const ChildRoomOverlayConflictResolutionSchema = z.enum([
  "explicit_wins",
  "fail_launch",
  "keep_both",
]).default("explicit_wins");

export const ChildRoomEffectivePromptPlanViewModelSchema = z.object({
  parent_room_id: z.string(),
  child_room_id: z.string().optional(),
  overlay_application_mode: ChildRoomOverlayApplicationModeSchema,
  prompt_recipe_mode: ChildRoomPromptRecipeModeSchema,
  explicit_overlay_ids: z.array(z.string()).default([]),
  inherited_overlay_ids: z.array(z.string()).default([]),
  effective_overlay_ids: z.array(z.string()).default([]),
  dropped_overlay_ids: z.array(z.string()).default([]),
  drop_reasons: z.record(z.string(), z.string()).default({}),
  explicit_prompt_recipe_id: z.string().uuid().optional(),
  inherited_prompt_recipe_id: z.string().uuid().optional(),
  effective_prompt_recipe_id: z.string().uuid().optional(),
  prompt_artifact_ids: z.array(z.string()).default([]),
  conflict_resolution: ChildRoomOverlayConflictResolutionSchema,
  inherited_observational_overlay_ids: z.array(z.string()).default([]),
  warnings: z.array(z.string()).default([]),
  truth_state: z.enum(["available", "degraded", "unavailable"]),
  computed_at: z.string(),
});
```

Required resolver (DOC12-owned implementation, DOC14-owned compatibility requirement):

```ts
export function resolveChildRoomEffectivePromptPlan(args: {
  parentEffectiveOverlayIds: string[];
  roomDefaultOverlayIds: string[];
  explicitOverlayIds: string[];
  overlayApplicationMode: z.infer<typeof ChildRoomOverlayApplicationModeSchema>;
  conflictResolution: z.infer<typeof ChildRoomOverlayConflictResolutionSchema>;
  parentEffectivePromptRecipeId?: string;
  explicitPromptRecipeId?: string;
  promptRecipeMode: z.infer<typeof ChildRoomPromptRecipeModeSchema>;
}): z.infer<typeof ChildRoomEffectivePromptPlanViewModelSchema> {
  let inherited_overlay_ids: string[] = [];
  let effective_overlay_ids: string[] = [];
  const warnings: string[] = [];

  switch (args.overlayApplicationMode) {
    case "inherit_none":
      effective_overlay_ids = [];
      break;
    case "inherit_room_default":
      inherited_overlay_ids = args.roomDefaultOverlayIds;
      effective_overlay_ids = inherited_overlay_ids;
      break;
    case "inherit_parent_effective":
      inherited_overlay_ids = args.parentEffectiveOverlayIds;
      effective_overlay_ids = inherited_overlay_ids;
      break;
    case "inherit_then_append_explicit": {
      inherited_overlay_ids = args.parentEffectiveOverlayIds;
      const overlaps = inherited_overlay_ids.filter(id => args.explicitOverlayIds.includes(id));
      if (overlaps.length && args.conflictResolution === "fail_launch") {
        return {
          parent_room_id: "",
          child_room_id: undefined,
          overlay_application_mode: args.overlayApplicationMode,
          prompt_recipe_mode: args.promptRecipeMode,
          explicit_overlay_ids: args.explicitOverlayIds,
          inherited_overlay_ids,
          effective_overlay_ids: [],
          dropped_overlay_ids: [],
          drop_reasons: {},
          explicit_prompt_recipe_id: args.explicitPromptRecipeId,
          inherited_prompt_recipe_id: args.parentEffectivePromptRecipeId,
          effective_prompt_recipe_id: undefined,
          prompt_artifact_ids: [],
          conflict_resolution: args.conflictResolution,
          inherited_observational_overlay_ids: [],
          warnings: ["Overlay conflict detected. Launch blocked."],
          truth_state: "unavailable",
          computed_at: new Date().toISOString(),
        };
      }
      if (overlaps.length && args.conflictResolution === "explicit_wins") {
        warnings.push(`${overlaps.length} overlay(s) in both inherited and explicit sets. Explicit wins.`);
      }
      effective_overlay_ids = [...new Set([...inherited_overlay_ids, ...args.explicitOverlayIds])];
      break;
    }
    case "explicit_only":
    case "explicit_select":
      effective_overlay_ids = args.explicitOverlayIds;
      break;
  }

  let inherited_prompt_recipe_id: string | undefined;
  let effective_prompt_recipe_id: string | undefined;

  switch (args.promptRecipeMode) {
    case "inherit_none":
      effective_prompt_recipe_id = undefined;
      break;
    case "carry_context_only":
      inherited_prompt_recipe_id = args.parentEffectivePromptRecipeId;
      effective_prompt_recipe_id = undefined;
      break;
    case "inherit_effective_recipe":
      inherited_prompt_recipe_id = args.parentEffectivePromptRecipeId;
      effective_prompt_recipe_id = inherited_prompt_recipe_id;
      break;
    case "explicit_select":
      effective_prompt_recipe_id = args.explicitPromptRecipeId;
      break;
  }

  return {
    parent_room_id: "",
    child_room_id: undefined,
    overlay_application_mode: args.overlayApplicationMode,
    prompt_recipe_mode: args.promptRecipeMode,
    explicit_overlay_ids: args.explicitOverlayIds,
    inherited_overlay_ids,
    effective_overlay_ids,
    dropped_overlay_ids: [],
    drop_reasons: {},
    explicit_prompt_recipe_id: args.explicitPromptRecipeId,
    inherited_prompt_recipe_id,
    effective_prompt_recipe_id,
    prompt_artifact_ids: [],
    conflict_resolution: args.conflictResolution,
    inherited_observational_overlay_ids:
      args.promptRecipeMode === "carry_context_only" ? args.parentEffectiveOverlayIds : [],
    warnings,
    truth_state: "available",
    computed_at: new Date().toISOString(),
  };
}
```

#### R7 addition — child-room preview request/response and import-back contracts

```ts
export const ChildRoomPreviewRequestSchema = z.object({
  review_target_binding_ref: ReviewTargetBindingRefSchema,
  import_back_strategy: z.enum(["finding_subset", "finding_subset_plus_summary", "full_redteam_output_ref"]),
  prompt_selection: ChildRoomPromptArtifactSelectionSchema,
  require_human_approval_before_launch: z.boolean().default(false),
  idempotency_key: z.string().min(8),
});

export const ChildRoomPreviewResponseSchema = z.object({
  effective_prompt_plan: ChildRoomEffectivePromptPlanViewModelSchema,
  capability_state: z.enum(["available", "degraded", "blocked", "unavailable"]),
  blocked_reason: z.string().optional(),
  degraded_reasons: z.array(z.string()).default([]),
});

export const ChildImportBackRequestSchema = z.object({
  child_room_id: z.string(),
  parent_room_id: z.string(),
  import_back_strategy: z.enum(["finding_subset", "finding_subset_plus_summary", "full_redteam_output_ref"]),
  import_back_idempotency_key: z.string().min(8),
  expected_version: z.number().int().nonnegative(),
});

export const ChildImportBackResponseSchema = z.object({
  status: z.enum(["ok", "partial", "blocked", "already_imported", "conflict"]),
  imported_finding_ids: z.array(z.string()).default([]),
  skipped_finding_ids: z.array(z.string()).default([]),
  degraded_state: z.string().optional(),
  conflict_reason: z.string().optional(),
});
```

UI requirement:
- the **Child-Room Launch Modal** must show a live **Effective Prompt Plan Preview**;
- the modal must disclose inherited overlays, explicit overlays, dropped overlays, selected recipe, inherited recipe, and any conflict rule applied;
- if truth is degraded/unavailable, launch remains possible only where policy allows and the modal must disclose the degraded reason.


The child-room launch flow must support:
- import-back strategy selection,
- overlay application mode selection,
- optional explicit overlay picker,
- optional explicit prompt recipe selector,
- human-approval-before-launch toggle,
- truthful preview of inherited vs explicit prompt artifacts where companion docs support it.

#### Acceptance

- A draft output can be launched into a child red-team room.
- Findings can be imported back by at least one supported strategy.
- Review target identity remains stable across the parent-child hop.
- Child room can be launched with explicit overlays only, inherited overlays, or no overlays according to policy.
- Effective child-room prompt-artifact plan is visible or explicitly degraded.


## 5. Global Ports: What CANDOR Adds Across ELNOR

### 5.1 Strength Sentinel — modal / strength drift detection `CORE NORMATIVE`

#### Purpose

Detect whether important requirements in the source document have been silently weakened, omitted, or reframed into less binding language.

#### Schema

```ts
export const StrengthSentinelEventSchema = z.object({
  event_id: z.string(),
  room_id: z.string(),
  source_doc_id: z.string(),
  concept_label: z.string(),
  source_strength: z.enum(["must", "shall", "should", "may", "informational"]),
  candidate_strength: z.enum(["must", "shall", "should", "may", "absent", "informational"]),
  severity: z.enum(["critical", "major", "minor", "observation"]),
  confidence: z.number().min(0).max(1),
  evidence_refs: z.array(z.string()).default([]),
  false_positive_rate_estimate: z.number().min(0).max(1).optional(),
  formula_version: z.string(),
});
```

#### Degraded behavior

If concept registry is unavailable, the sentinel may run in reduced mode on review-target-local extracted anchors only. That degraded mode must be displayed in the room health and findings UI.

### 5.2 Concept Registry `CORE NORMATIVE`

The concept registry is a global support artifact used by strength sentinel, attention-gap detection, and review-target-aware critique.

#### Baseline rules

- registry generation may be done at room creation or first-turn bootstrap,
- it must be content-hash keyed,
- it must record extraction provenance,
- stale registries must be visibly stale.

```ts
export const ConceptRegistryEntrySchema = z.object({
  concept_id: z.string(),
  source_doc_id: z.string(),
  source_doc_content_hash: z.string(),
  label: z.string(),
  canonical_text: z.string(),
  required_strength: z.enum(["must", "shall", "should", "may", "informational"]),
  extraction_method: z.enum(["rule", "model", "hybrid"]),
  extraction_model_id: z.string().optional(),
  extraction_prompt_hash: z.string().optional(),
  evidence_refs: z.array(z.string()).default([]),
});
```

### 5.3 Attention Gap Detection `CORE NORMATIVE`

Detect important source concepts that have not yet been discussed, challenged, or resolved.

```ts
export const AttentionGapEventSchema = z.object({
  event_id: z.string(),
  room_id: z.string(),
  concept_id: z.string(),
  concept_label: z.string(),
  gap_kind: z.enum(["never_addressed", "lightly_addressed", "only_agreed", "not_reverified"]),
  severity_hint: z.enum(["critical", "major", "minor", "observation"]),
  created_at: z.string(),
});
```

### 5.4 Progressive summarization / compaction integration `CORE NORMATIVE`

The active review target for a red-team room is a never-compress class in truth-seeking, ship, and high-stakes modes for all primary critics and moderators.

If document size prevents full inclusion, the system must explicitly switch to one of the allowed materialization modes (`chunk_map`, `search_tool`, `chunk_on_demand`) and disclose that in the context inspector.

### 5.5 Compaction drift detection `CORE NORMATIVE`

Emit `room.compaction_drift.detected` when summary or compaction materially changes review-target or findings semantics.

### 5.6 Delta Digest + Decision Pack `CORE NORMATIVE`

Room-close and post-synthesis flows must produce:

- summary of top accepted findings,
- disputed findings,
- proposed fixes,
- decision basis,
- review-target version reviewed,
- prompt plan used,
- and supervision-cost snapshot ref.

### 5.7 Unpatchable Insights `CORE NORMATIVE`

A finding may be important even when there is no local text patch. Such findings must still be surfaced in a durable “unpatchable insight” bucket and visible in the post-run summary.

### 5.8 Evidence Gate `CORE NORMATIVE`

All ledgered findings in truth-seeking, ship, and high-stakes modes must carry evidence.

If a candidate finding lacks sufficient evidence:

- it may be downgraded to critique cache,
- it may be reprompted,
- but it may not silently enter the main findings ledger.

```ts
export const EvidenceDomainSchema = z.enum([
  "document_text",
  "room_transcript",
  "external_tool",
  "mixed",
]);
```

### 5.9 Cold start -> warm -> mature readiness model `CORE NORMATIVE`

Prompt promotion, auto-application, and ghost-judge weighting must respect system maturity. The system starts in observational mode, graduates to guarded suggestion mode, and only later reaches active recommendation / canary behaviors.

### 5.10 Quality-impacting change flag `CORE NORMATIVE`

Changes to prompt plan, review-target binding, room topology, critic roster, or evidence policy must mark the room as quality-impacting and record that in emitted outcome signals.

---

## 6. Red-Team-Specific Ports

### 6.1 RedTeamPolicySchema and behavior-switch matrix `CORE NORMATIVE`

```ts
export const MinimumViableCompletionPolicySchema = z.object({
  reserved_tokens: z.number().int().nonnegative().optional(),
  reserved_cost_usd: z.number().nonnegative().optional(),
  reserve_for: z.array(z.enum(["synthesis", "export"])).default(["synthesis", "export"]),
});

export const GhostJudgePolicySchema = z.object({
  enabled_by_default: z.boolean().default(false),
  mode_gate: z.enum(["never", "assisted_only", "automation_modes", "always"]).default("assisted_only"),
  learning_weight: z.enum(["none", "low", "medium"]).default("low"),
  require_human_override_visibility: z.boolean().default(true),
});

export const RedTeamPolicySchema = z.object({
  review_intent: z.enum(["truth_seeking", "ship", "high_stakes", "exploratory"]),
  max_turns_total: z.number().int().positive(),
  max_findings_per_turn_by_severity: z.object({
    critical: z.number().int().nonnegative().default(2),
    major: z.number().int().nonnegative().default(4),
    minor: z.number().int().nonnegative().default(6),
    observation: z.number().int().nonnegative().default(8),
  }).default({ critical: 2, major: 4, minor: 6, observation: 8 }),
  strict_evidence_mode: z.enum(["off", "truth_seeking_only", "all_red_team"]).default("truth_seeking_only"),
  actionable_findings_required_for: z.array(z.enum(["critical", "major"])).default(["critical", "major"]),
  minimum_viable_completion_policy: MinimumViableCompletionPolicySchema.optional(),
  auto_adjudicate_policy: z.enum(["escalate_to_human", "force_moderator_decision", "park_and_continue"]).default("park_and_continue"),
  context_symmetry_policy: z.enum(["strict", "warn_only", "off"]).default("strict"),
  review_target_materialization_policy: z.enum(["strict", "adaptive", "large_doc_tool_assist"]).default("adaptive"),
  judgment_coverage_required_for_learning: z.number().min(0).max(1).default(0.6),
  ghost_judge_policy: GhostJudgePolicySchema.default({
    enabled_by_default: false,
    mode_gate: "assisted_only",
    learning_weight: "low",
    require_human_override_visibility: true,
  }),
  model_id_capture_at_creation: z.boolean().default(true),
  golden_room_policy_ref: z.string().optional(),
});
```

#### Behavior-switch matrix

| Policy field | Effect | UI dependency | Companion dependency |
|---|---|---|---|
| `strict_evidence_mode` | controls cache vs ledger behavior | Findings panel / critique cache | none |
| `minimum_viable_completion_policy` | reserves synthesis/export budget | launch preview / room banner | DOC13 |
| `auto_adjudicate_policy` | controls dispute resolution path | adjudication modal / banner | DOC12 |
| `context_symmetry_policy` | blocks or warns on asymmetric context | context inspector / health chip | DOC11 |
| `ghost_judge_policy` | enables synthetic post-review grading | findings panel / post-run summary | DOC12 + DOC11 + DOC8 |

### 6.2 Finding extraction, findings ledger, judgments, adjudication, lineage `CORE NORMATIVE`

#### 6.2A Finding extraction

This section closes the largest remaining build gap.

```ts
export const ParsedTurnSchema = z.object({
  room_id: z.string(),
  room_turn_id: z.string(),
  participant_id: z.string(),
  logical_role_key: z.string(),
  raw_text: z.string(),
  extracted_json_block: z.unknown().optional(),
  extraction_mode: z.enum(["structured_block", "fallback_extractor", "none"]),
});

export const RawFindingCandidateSchema = z.object({
  title: z.string().min(1),
  description: z.string().min(1),
  severity: z.enum(["critical", "major", "minor", "observation"]),
  why_this_matters: z.string().min(1),
  evidence_refs: z.array(z.string()).default([]),
  proposed_fix: z.string().optional(),
  replacement_text: z.string().optional(),
  patch_snippet: z.string().optional(),
});

export const FindingExtractionResultSchema = z.object({
  room_id: z.string(),
  room_turn_id: z.string(),
  extraction_method: z.enum(["structured_block", "fallback_extractor", "none"]),
  candidates: z.array(RawFindingCandidateSchema).default([]),
  errors: z.array(z.string()).default([]),
  warnings: z.array(z.string()).default([]),
});
```

##### Extraction strategy

Phase 0 baseline:

1. preferred output path: critic emits a fenced `findings` JSON block,
2. fallback path: controlled extractor parses prose into `RawFindingCandidateSchema`,
3. if both fail, extraction error is logged and visible; findings do not silently disappear.

##### Code implementation

##### R5.2 amendment — extraction failure retention and unparsed contribution handling

Extraction failure may not black-hole useful critic work. Add:

```ts
export const UnparsedContributionSchema = z.object({
  contribution_id: z.string(),
  room_id: z.string(),
  room_turn_id: z.string(),
  participant_id: z.string(),
  logical_role_key: z.string().optional(),
  raw_text: z.string(),
  extraction_error_codes: z.array(z.string()).default([]),
  extraction_warnings: z.array(z.string()).default([]),
  review_target_binding_ref: ReviewTargetBindingRefSchema.optional(),
  prompt_identity: PromptIdentityRefSchema.optional(),
  persisted_at: z.string(),
});
```

Normative behavior:
- if structured extraction succeeds, normal findings flow continues;
- if structured extraction fails but fallback extraction yields candidates, append fallback findings and persist an `UnparsedContribution` record for the original text;
- if both extraction paths fail, persist the `UnparsedContribution` and surface it in the **Critique Cache Drawer** under an `unparsed_contribution` grouping with a `Manual Extract` or `Review Raw Contribution` action.

#### R7 addition — manual extract and finding edit/rewrite audit contracts

```ts
export const ManualExtractRequestSchema = z.object({
  contribution_id: z.string().uuid(),
  finding_text: z.string(),
  severity: z.enum(["critical", "major", "minor", "observation"]),
  why_this_matters: z.string(),
  notes: z.string().optional(),
  idempotency_key: z.string().min(8),
});

export const ManualExtractResponseSchema = z.object({
  status: z.literal("ok"),
  finding_id: z.string().uuid(),
});

export const FindingEditAuditSchema = z.object({
  audit_id: z.string().uuid(),
  room_id: z.string(),
  finding_id: z.string().uuid(),
  actor_id: z.string(),
  edit_kind: z.enum(["manual_edit", "manual_rewrite", "severity_change", "title_change"]),
  before_ref: z.string(),
  after_ref: z.string(),
  notes: z.string().optional(),
  created_at: z.string(),
});

export const FindingEditRequestSchema = z.object({
  finding_id: z.string().uuid(),
  edit_kind: FindingEditAuditSchema.shape.edit_kind,
  new_payload: z.record(z.unknown()),
  expected_version: z.number().int().nonnegative(),
});

export const FindingEditResponseSchema = z.object({
  status: z.literal("ok"),
  audit_id: z.string().uuid(),
  new_version: z.number().int().nonnegative(),
});
```

Companion routes required:
- `GET /api/rooms/:roomId/unparsed-contributions`
- `POST /api/rooms/:roomId/unparsed-contributions/:contributionId/extract`
- `POST /api/rooms/:roomId/findings/:findingId/edit`


```ts
// apps/ec-service/src/redteam/extractFindingsFromTurn.ts
import {
  ParsedTurnSchema,
  FindingExtractionResultSchema,
  RawFindingCandidateSchema,
} from "@elnor/contracts/red-team";

export type ExtractFindingsArgs = {
  parsedTurn: unknown;
  fallbackExtractor: (rawText: string) => Promise<unknown[]>;
};

export async function extractFindingsFromTurn(
  args: ExtractFindingsArgs,
): Promise<import("zod").infer<typeof FindingExtractionResultSchema>> {
  const parsed = ParsedTurnSchema.parse(args.parsedTurn);

  let extraction_method: "structured_block" | "fallback_extractor" | "none" = "none";
  let rawCandidates: unknown[] = [];
  const errors: string[] = [];
  const warnings: string[] = [];

  if (Array.isArray(parsed.extracted_json_block)) {
    extraction_method = "structured_block";
    rawCandidates = parsed.extracted_json_block;
  } else {
    try {
      rawCandidates = await args.fallbackExtractor(parsed.raw_text);
      extraction_method = rawCandidates.length > 0 ? "fallback_extractor" : "none";
      if (extraction_method === "fallback_extractor") {
        warnings.push("used_fallback_extractor");
      }
    } catch (err) {
      errors.push(err instanceof Error ? err.message : "fallback_extractor_failed");
    }
  }

  const candidates = rawCandidates.flatMap((candidate) => {
    const result = RawFindingCandidateSchema.safeParse(candidate);
    if (!result.success) {
      errors.push(`invalid_finding_candidate:${result.error.issues.map((i) => i.path.join('.')).join(',')}`);
      return [];
    }
    return [result.data];
  });

  return FindingExtractionResultSchema.parse({
    room_id: parsed.room_id,
    room_turn_id: parsed.room_turn_id,
    extraction_method,
    candidates,
    errors,
    warnings,
  });
}
```

#### 6.2B Actionable findings and room findings ledger

Finding records must preserve causal prompt context at creation time rather than reconstructing it later from volatile room state.

```ts
export const ActionableFindingExtensionSchema = z.object({
  change_kind: z.enum(["replace", "insert", "delete", "restructure", "question", "none"]),
  replacement_text: z.string().optional(),
  patch_snippet: z.string().optional(),
  applies_to_ref: z.string().optional(),
  fix_confidence: z.number().min(0).max(1).optional(),
});

export const RoomFindingSchema = z.object({
  finding_id: z.string(),
  room_id: z.string(),
  room_turn_id: z.string(),
  participant_id: z.string(),
  logical_role_key: z.string(),
  model_id: z.string(),
  prompt_variant_id: z.string().optional(),
  prompt_assignment_id: z.string().optional(),
  prompt_text_hash: z.string().min(16),
  prompt_artifact_kind: PromptArtifactKindSchema,
  active_overlay_ids: z.array(z.string()).default([]),
  prompt_recipe_id: z.string().uuid().optional(),
  prompt_artifact_ids: z.array(z.string()).default([]),
  review_target_binding_ref: ReviewTargetBindingRefSchema,
  title: z.string(),
  description: z.string(),
  severity: z.enum(["critical", "major", "minor", "observation"]),
  why_this_matters: z.string(),
  evidence_refs: z.array(z.string()).default([]),
  evidence_domain: EvidenceDomainSchema.default("document_text"),
  state: z.enum([
    "open",
    "accepted",
    "rejected",
    "disputed",
    "resolved",
    "regressed",
    "superseded",
    "cached",
  ]).default("open"),
  starred: z.boolean().default(false),
  cited_in_decision: z.boolean().default(false),
  structural_hash: z.string(),
  semantic_cluster_id: z.string().optional(),
  actionable: ActionableFindingExtensionSchema.optional(),
  created_at: z.string(),
});
```

#### 6.2C Red-team output contract

```ts
export const RedTeamRoomOutputContractSchema = z.object({
  participant_id: z.string(),
  room_turn_id: z.string(),
  findings_json_block_required: z.boolean().default(true),
  minimum_fields_per_finding: z.array(z.enum([
    "title",
    "description",
    "severity",
    "why_this_matters",
    "evidence_refs",
  ])).default(["title", "description", "severity", "why_this_matters", "evidence_refs"]),
});
```

#### 6.2D Judgment path

Judgment capture is canonicalized through `room_judge_finding` and its batch companion. The handler must load the actual finding record and may not infer critical provenance fields from room-wide state.

```ts
export const FindingJudgmentSchema = z.object({
  judgment_id: z.string(),
  room_id: z.string(),
  finding_id: z.string(),
  disposition: z.enum([
    "accepted",
    "rejected",
    "downgraded",
    "starred",
    "cited_in_decision",
    "promoted_from_cache",
    "needs_rewrite",
  ]),
  rejection_reason: z.enum([
    "insufficient_evidence",
    "already_known",
    "not_material",
    "duplicate",
    "manufactured_dissent",
    "bad_fix",
    "other",
  ]).optional(),
  judged_by_actor_type: z.enum(["human", "ghost_judge", "system"]),
  judged_by_actor_id: z.string().optional(),
  notes: z.string().optional(),
  model_id: z.string(),
  logical_role_key: z.string(),
  prompt_variant_id: z.string().optional(),
  prompt_assignment_id: z.string().optional(),
  prompt_text_hash: z.string().min(16),
  prompt_artifact_kind: PromptArtifactKindSchema,
  active_overlay_ids: z.array(z.string()).default([]),
  prompt_recipe_id: z.string().uuid().optional(),
  prompt_artifact_ids: z.array(z.string()).default([]),
  finding_severity: z.enum(["critical", "major", "minor", "observation"]),
  novelty: FindingNoveltySchema.optional(),
  prompt_method_used: PromptMethodUsedSchema.optional(),
  scoring_version: z.string().optional(),
  route_trace_id: z.string().optional(),
  operation_id: z.string().optional(),
  dispatch_id: z.string().optional(),
  review_target_length_bucket: z.enum(["small", "medium", "large", "very_large"]).optional(),
  realized_materialization_mode: RealizedReviewTargetMaterializationModeSchema.optional(),
  review_target_binding_ref: ReviewTargetBindingRefSchema,
  finding_created_at: z.string(),
  judged_at: z.string(),
  turns_since_produced: z.number().int().nonnegative(),
  idempotency_key: z.string().min(8),
  expected_version: z.number().int().nonnegative(),
});

export const RoomJudgeFindingRequestSchema = z.object({
  room_id: z.string(),
  finding_id: z.string(),
  disposition: FindingJudgmentSchema.shape.disposition,
  rejection_reason: FindingJudgmentSchema.shape.rejection_reason,
  notes: z.string().optional(),
  judged_by_actor_type: z.enum(["human", "ghost_judge", "system"]),
  judged_by_actor_id: z.string().optional(),
  idempotency_key: z.string().min(8),
  expected_version: z.number().int().nonnegative(),
});

export const RoomBatchJudgeFindingsRequestSchema = z.object({
  room_id: z.string(),
  judgments: z.array(RoomJudgeFindingRequestSchema.omit({ room_id: true })).min(1),
});
```

##### Handler requirements

- load the finding by `finding_id`,
- copy `model_id`, `logical_role_key`, `prompt_variant_id`, `prompt_assignment_id`, `prompt_text_hash`, `review_target_binding_ref`, and `finding_created_at` from the finding record,
- compute `turns_since_produced` from current room state and original `room_turn_id`,
- reject stale `expected_version`,
- write room-local judgment log and global learning observation handoff,
- emit SSE + telemetry.

##### Batch endpoint requirement

Because batch review is the default UX, batch judgment support is required. Frontend-only sequencing is not sufficient for normative completeness.



##### Reference command handler

##### R5.2 amendment — complete disposition-to-state mapping and scoring hook

The finding projection update must use a complete state map.

```ts
const FINDING_STATE_FROM_DISPOSITION: Record<string, string | undefined> = {
  accepted: "accepted",
  rejected: "rejected",
  downgraded: "cached",
  promoted_from_cache: "open",
  needs_rewrite: "disputed",
  starred: undefined,
  cited_in_decision: undefined,
};
```

Judgment handling must also emit a deterministic prompt observation using a reference scorer.

```ts
export const FindingNoveltySchema = z.enum(["known_issue", "new_angle", "surprising"]);

export const ScoringWeightTableSchema = z.object({
  version_id: z.string().min(1),
  accepted: z.object({ critical: z.number(), major: z.number(), minor: z.number(), observation: z.number() }),
  rejected: z.record(z.string(), z.number()),
  starred: z.number(),
  cited: z.number(),
  effective_from: z.string(),
  effective_until: z.string().nullable().optional(),
});

export const PromptObservationWeightsSchema = z.object({
  scoring_version: z.string().min(1),
  accepted: z.record(z.string(), z.number()),
  rejected: z.record(z.string(), z.number()),
  starred: z.number(),
  cited: z.number(),
  novelty_bonus: z.number().default(0),
});

export function getPromptObservationWeights(scoring_version: string): z.infer<typeof PromptObservationWeightsSchema> {
  const v1 = {
    scoring_version: "v1",
    accepted: { critical: 4, major: 3, minor: 1, observation: 0.5 },
    rejected: {
      insufficient_evidence: -2,
      already_known: -0.5,
      not_material: -1,
      duplicate: -0.5,
      manufactured_dissent: -2.5,
      bad_fix: -1.5,
      other: -1,
    },
    starred: 1,
    cited: 1.5,
    novelty_bonus: 0,
  };
  if (scoring_version === "v1") return v1;
  throw new Error(`unknown_scoring_version:${scoring_version}`);
}

export function buildPromptObservationFromJudgment(args: {
  judgment: z.infer<typeof FindingJudgmentSchema>;
  scoringVersion: string;
  findingSeverity: z.infer<typeof FindingJudgmentSchema>["finding_severity"];
  novelty?: z.infer<typeof FindingNoveltySchema>;
  promptMethodUsed?: z.infer<typeof PromptMethodUsedSchema>;
}): z.infer<typeof PromptObservationEmitSchema> {
  const weights = getPromptObservationWeights(args.scoringVersion);
  return {
    observation_id: crypto.randomUUID(),
    room_id: args.judgment.room_id,
    participant_id: args.judgment.judged_by_actor_id,
    logical_role_key: args.judgment.logical_role_key,
    prompt_artifact_kind: args.judgment.prompt_artifact_kind,
    prompt_variant_id: args.judgment.prompt_variant_id,
    prompt_text_hash: args.judgment.prompt_text_hash,
    prompt_assignment_id: args.judgment.prompt_assignment_id,
    active_overlay_ids: args.judgment.active_overlay_ids,
    prompt_recipe_id: args.judgment.prompt_recipe_id,
    prompt_artifact_ids: args.judgment.prompt_artifact_ids,
    route_trace_id: args.judgment.route_trace_id,
    operation_id: args.judgment.operation_id,
    dispatch_id: args.judgment.dispatch_id,
    prompt_method_used: args.promptMethodUsed ?? args.judgment.prompt_method_used,
    scoring_version: args.scoringVersion,
    review_target_length_bucket: args.judgment.review_target_length_bucket,
    realized_materialization_mode: args.judgment.realized_materialization_mode,
    novelty: args.novelty ?? args.judgment.novelty,
    review_target_binding_ref: args.judgment.review_target_binding_ref,
    evidence_grade: "observational",
    value_components: {
      accepted_findings_weight:
        args.judgment.disposition === "accepted"
          ? (weights.accepted[args.findingSeverity] ?? 0)
          : 0,
      rejected_findings_penalty:
        args.judgment.disposition === "rejected"
          ? (weights.rejected[args.judgment.rejection_reason ?? "other"] ?? weights.rejected.other)
          : 0,
      starred_bonus: args.judgment.disposition === "starred" ? weights.starred : 0,
      cited_bonus: args.judgment.disposition === "cited_in_decision" ? weights.cited : 0,
      supervision_cost_penalty: 0,
    },
    emitted_at: new Date().toISOString(),
  };
}

export async function emitPromptObservationFromJudgment(args: {
  judgment: z.infer<typeof FindingJudgmentSchema>;
  appendObservation: (obs: z.infer<typeof PromptObservationEmitSchema>) => Promise<void>;
  emitTelemetry: (eventName: string, payload: Record<string, unknown>) => Promise<void>;
}): Promise<void> {
  const observation = buildPromptObservationFromJudgment({
    judgment: args.judgment,
    scoringVersion: args.judgment.scoring_version ?? "v1",
    findingSeverity: args.judgment.finding_severity,
    novelty: args.judgment.novelty,
    promptMethodUsed: args.judgment.prompt_method_used,
  });
  await args.appendObservation(observation);
  await args.emitTelemetry("room.prompt_observation.emitted", {
    room_id: args.judgment.room_id,
    observation_id: observation.observation_id,
    route_trace_id: observation.route_trace_id,
    operation_id: observation.operation_id,
  });
}
```

Required scoring refinements:
- `already_known` must penalize less than `insufficient_evidence`;
- `manufactured_dissent` must penalize more than routine rejection;
- `scoring_version` is mandatory so later weight-regime changes remain analyzable.


```ts
// apps/ec-service/src/commands/roomJudgeFinding.ts
import {
  RoomJudgeFindingRequestSchema,
  FindingJudgmentSchema,
} from "@elnor/contracts/red-team";

export async function applyRoomJudgeFinding(args: {
  payload: unknown;
  actor: { actor_type: "user" | "system" | "ghost_judge"; actor_id?: string };
  issued_at: string;
  loadFinding: (roomId: string, findingId: string) => Promise<any>;
  getRoomTurnOrdinal: (roomId: string, roomTurnId: string) => Promise<number>;
  getCurrentRoomTurnOrdinal: (roomId: string) => Promise<number>;
  loadFindingVersion: (roomId: string, findingId: string) => Promise<number>;
  appendJudgment: (roomId: string, judgment: unknown) => Promise<void>;
  updateFindingProjection: (roomId: string, findingId: string, patch: Record<string, unknown>, expectedVersion: number) => Promise<void>;
  emitPromptObservationFromJudgment: (judgment: unknown) => Promise<void>;
  emitTelemetry: (eventName: string, payload: Record<string, unknown>) => Promise<void>;
}) {
  const req = RoomJudgeFindingRequestSchema.parse(args.payload);
  const finding = await args.loadFinding(req.room_id, req.finding_id);
  if (!finding) throw new Error("finding_not_found");

  const currentVersion = await args.loadFindingVersion(req.room_id, req.finding_id);
  if (currentVersion !== req.expected_version) {
    throw new Error("stale_expected_version");
  }

  const producedAtOrdinal = await args.getRoomTurnOrdinal(req.room_id, finding.room_turn_id);
  const currentOrdinal = await args.getCurrentRoomTurnOrdinal(req.room_id);

  const judgment = FindingJudgmentSchema.parse({
    judgment_id: crypto.randomUUID(),
    room_id: req.room_id,
    finding_id: req.finding_id,
    disposition: req.disposition,
    rejection_reason: req.rejection_reason,
    judged_by_actor_type: req.judged_by_actor_type,
    judged_by_actor_id: req.judged_by_actor_id ?? args.actor.actor_id,
    notes: req.notes,
    model_id: finding.model_id,
    logical_role_key: finding.logical_role_key,
    prompt_variant_id: finding.prompt_variant_id,
    prompt_assignment_id: finding.prompt_assignment_id,
    prompt_text_hash: finding.prompt_text_hash,
    prompt_artifact_kind: finding.prompt_artifact_kind,
    active_overlay_ids: finding.active_overlay_ids,
    prompt_recipe_id: finding.prompt_recipe_id,
    prompt_artifact_ids: finding.prompt_artifact_ids,
    finding_severity: finding.severity,
    review_target_binding_ref: finding.review_target_binding_ref,
    finding_created_at: finding.created_at,
    judged_at: args.issued_at,
    turns_since_produced: Math.max(0, currentOrdinal - producedAtOrdinal),
    idempotency_key: req.idempotency_key,
    expected_version: req.expected_version,
  });

  await args.appendJudgment(req.room_id, judgment);

  // R7 FIX: use FINDING_STATE_FROM_DISPOSITION map instead of ternary
  const newState = FINDING_STATE_FROM_DISPOSITION[req.disposition];
  await args.updateFindingProjection(
    req.room_id,
    req.finding_id,
    {
      state: newState !== undefined ? newState : finding.state,
      starred: req.disposition === "starred" ? true : finding.starred,
      cited_in_decision: req.disposition === "cited_in_decision" ? true : finding.cited_in_decision,
      latest_judgment_id: judgment.judgment_id,
    },
    req.expected_version,
  );
  await args.emitPromptObservationFromJudgment(judgment);
  await args.emitTelemetry("room.finding.judged", {
    room_id: req.room_id,
    finding_id: req.finding_id,
    disposition: req.disposition,
    judged_by_actor_type: req.judged_by_actor_type,
  });

  return { status: "ok", judgment_id: judgment.judgment_id };
}
```

#### 6.2E Adjudication

```ts
export const FindingAdjudicationRecordSchema = z.object({
  adjudication_id: z.string(),
  room_id: z.string(),
  finding_id: z.string(),
  status: z.enum(["created", "resolved", "parked", "human_escalated"]),
  decision: z.enum([
    "accept_claim_a",
    "accept_claim_b",
    "park_disputed",
    "escalate_to_human",
    "require_evidence_audit",
  ]),
  decision_basis_refs: z.array(z.string()).default([]),
  confidence: z.number().min(0).max(1).optional(),
  moderator_meta_ref: z.string().optional(),
  created_at: z.string(),
  resolved_at: z.string().optional(),
});
```

#### 6.2F Lineage

```ts
export const FindingLineageSchema = z.object({
  lineage_id: z.string(),
  ancestor_finding_id: z.string(),
  descendant_finding_id: z.string().optional(),
  predecessor_room_id: z.string().optional(),
  successor_room_id: z.string().optional(),
  resolution_state: z.enum([
    "open",
    "still_present",
    "resolved",
    "regressed",
    "superseded",
    "disputed",
  ]),
  verification_basis_refs: z.array(z.string()).default([]),
  updated_at: z.string(),
});
```

### 6.3 Structural and semantic dedup `CORE NORMATIVE`

#### R5.2 amendment — semantic dedup feature flag and target-kind-aware config

```ts
export const SemanticDedupConfigSchema = z.object({
  review_target_kind: z.string(),
  enabled: z.boolean().default(false),
  similarity_threshold: z.number().min(0).max(1).default(0.88),
  embedding_model_id: z.string().optional(),
  max_cluster_size: z.number().int().positive().default(8),
  deferred_if_service_unavailable: z.boolean().default(true),
  fallback_mode: z.enum(["none", "moderator_review", "surface_warning"]).default("surface_warning"),
});
```

Rules:
- semantic dedup must be behind a feature flag for Phase 0 builds;
- thresholding is target-kind-aware, not globally magic;
- if embedding service is unavailable, the room may continue in deferred mode with visible disclosure.


Structural dedup is mandatory and deterministic. Semantic dedup may be phase-gated or moderated.

- structural dedup must happen before append,
- semantic dedup may happen before append or in deferred review,
- semantic thresholds must be target-kind aware; one global threshold is prohibited.

### 6.4 Convergence, dissent, room health, and supervision-cost `CORE NORMATIVE`

#### R5.2 amendment — convergence view model and batch partial-failure recovery

#### R7 amendment — convergence, cache promotion, and override contracts

```ts
export const RoomConvergenceViewModelSchema = z.object({
  room_id: z.string(),
  convergence_state: z.enum(["not_converged", "converged", "overridden", "degraded"]),
  convergence_score: z.number().min(0).max(1),
  threshold: z.number().min(0).max(1),
  lookback_turns: z.number().int().positive(),
  rationale_entries: z.array(z.string()).default([]),
  can_override: z.boolean().default(false),
  override_route: z.string().optional(),
  stale: z.boolean().default(false),
  updated_at: z.string(),
});

export const ConvergenceBannerViewModelSchema = z.object({
  room_id: z.string(),
  convergence_state: z.enum(["not_converged", "converged", "overridden", "degraded"]),
  convergence_score: z.number().min(0).max(1),
  threshold: z.number().min(0).max(1),
  rationale_entries: z.array(z.string()).default([]),
  can_override: z.boolean().default(false),
  override_route: z.string().optional(),
  blocked_reason: z.string().optional(),
  stale: z.boolean().default(false),
});

export const RoomOverrideConvergenceRequestSchema = z.object({
  room_id: z.string(),
  rationale: z.string().max(300).optional(),
  idempotency_key: z.string().min(8),
});

export const RoomOverrideConvergenceResponseSchema = z.object({
  status: z.literal("ok"),
  new_convergence_state: z.literal("overridden"),
});

export const BatchJudgmentRecoveryViewModelSchema = z.object({
  room_id: z.string(),
  batch_operation_id: z.string(),
  total_rows: z.number().int().nonnegative(),
  succeeded_rows: z.number().int().nonnegative(),
  failed_rows: z.number().int().nonnegative(),
  stale_version_rows: z.array(z.string()).default([]),
  retryable_row_ids: z.array(z.string()).default([]),
  truth_state: z.enum(["available", "degraded", "unavailable"]),
  updated_at: z.string(),
});

export const RoomPromoteCachedFindingRequestSchema = z.object({
  cache_entry_id: z.string().uuid(),
  finding_overrides: z.object({
    severity: z.enum(["critical", "major", "minor", "observation"]).optional(),
    claim_adjustment: z.string().optional(),
  }).optional(),
  idempotency_key: z.string().min(8),
});

export const RoomPromoteCachedFindingResponseSchema = z.object({
  status: z.enum(["promoted", "conflict", "already_promoted", "blocked"]),
  finding_id: z.string().uuid().optional(),
  blocked_reason: z.string().optional(),
  conflict_reason: z.string().optional(),
});
```

The batch review footer must support **retry failed only**, not merely full-page refresh.


```ts
export const HumanSupervisionCostSchema = z.object({
  checkpoint_count: z.number().int().nonnegative(),
  return_with_edits_count: z.number().int().nonnegative(),
  human_escalations: z.number().int().nonnegative(),
  manual_overrides: z.number().int().nonnegative(),
  minutes_to_close: z.number().nonnegative().optional(),
});

export const RoomHealthSnapshotSchema = z.object({
  room_id: z.string(),
  formula_version: z.string(),
  lookback_turns: z.number().int().positive(),
  finding_velocity: z.number().min(0),
  evidence_pass_rate: z.number().min(0).max(1),
  dissent_authenticity_score: z.number().min(0).max(1),
  context_symmetry_score: z.number().min(0).max(1),
  review_target_integrity_score: z.number().min(0).max(1),
  status: z.enum(["healthy", "warning", "degraded", "blocked"]),
  degraded_reasons: z.array(z.string()).default([]),
  computed_at: z.string(),
});

export const BatchReviewProgressSchema = z.object({
  room_id: z.string(),
  total_findings: z.number().int().nonnegative(),
  judged_findings: z.number().int().nonnegative(),
  pending_findings: z.number().int().nonnegative(),
  disputed_findings: z.number().int().nonnegative(),
  completion_ratio: z.number().min(0).max(1),
});
```

Room health must be computed after each turn and surfaced to the UI through a dedicated read model.

### 6.5 Review-target binding, context symmetry, prompt-plan truth, memory, and workspace `CORE NORMATIVE`

#### Rules

- every red-team room must have an active review target,
- every finding must reference the active review-target binding used at creation time,
- context symmetry is computed for all primary critics and moderators,
- large-doc handling must use an explicit materialization mode,
- prompt-plan truth must be visible before learned prompt application,
- active overlays are observational facts and must be shown when present,
- active review targets are never silently dropped.

```ts
export const ContextSymmetryViolationSchema = z.object({
  room_id: z.string(),
  expected_epoch: z.string(),
  participant_id: z.string(),
  actual_epoch: z.string(),
  severity: z.enum(["warning", "blocking"]),
  reason: z.string(),
});
```

#### Pre-turn guard code

#### R5.2 amendment — preferred versus realized review-target materialization

Adopt the DOC12 R7.1 two-layer model explicitly.

```ts
export const PreferredReviewTargetMaterializationModeSchema = z.enum([
  "full_if_budget",
  "summary_default",
  "chunk_map",
  "search_tool",
  "chunk_on_demand",
]);

export const RealizedReviewTargetMaterializationModeSchema = z.enum([
  "direct",
  "chunked",
  "search_assisted",
  "unavailable",
]);

export const ReviewTargetMaterializationPlanSchema = z.object({
  room_id: z.string(),
  review_target_binding_ref: ReviewTargetBindingRefSchema,
  preferred_mode: PreferredReviewTargetMaterializationModeSchema,
  realized_mode: RealizedReviewTargetMaterializationModeSchema,
  estimated_target_tokens: z.number().int().nonnegative(),
  max_inline_tokens_before_chunking: z.number().int().positive().default(12000),
  chunk_refs: z.array(z.string()).default([]),
  search_tool_enabled: z.boolean().default(false),
  anchor_manifest_ref: z.string().optional(),
  plan_reason: z.string(),
  computed_at: z.string(),
});
```

Required helper:

```ts
export function selectMaterializationMode(args: {
  preferred_mode: z.infer<typeof PreferredReviewTargetMaterializationModeSchema>;
  estimated_target_tokens: number;
  max_inline_tokens_before_chunking: number;
  search_tool_available: boolean;
  chunk_service_available: boolean;
}): z.infer<typeof RealizedReviewTargetMaterializationModeSchema> {
  if (
    args.preferred_mode === "full_if_budget" &&
    args.estimated_target_tokens <= args.max_inline_tokens_before_chunking
  ) {
    return "direct";
  }
  if (
    ["chunk_map", "chunk_on_demand", "summary_default"].includes(args.preferred_mode) &&
    args.chunk_service_available
  ) {
    return "chunked";
  }
  if (
    ["search_tool", "chunk_map", "chunk_on_demand", "summary_default"].includes(args.preferred_mode) &&
    args.search_tool_available
  ) {
    return "search_assisted";
  }
  return "unavailable";
}
```

#### Mandatory tool injection for large documents `CORE NORMATIVE`

If realized mode is `chunked` or `search_assisted`, DOC12 bootstrap assembly must inject the corresponding OpenClaw native tools into the participant runtime entitlement set.

```ts
export function appendMaterializationTools(args: {
  realizedMode: z.infer<typeof RealizedReviewTargetMaterializationModeSchema>;
  runtimeTools: Array<Record<string, unknown>>;
}): Array<Record<string, unknown>> {
  const tools = [...args.runtimeTools];
  if (args.realizedMode === "chunked") {
    tools.push({
      name: "read_document_chunk",
      description: "Read a specific review-target chunk by chunk_id.",
      parameters: { chunk_id: { type: "string" } },
    });
  }
  if (args.realizedMode === "search_assisted") {
    tools.push({
      name: "search_review_target",
      description: "Search the active review target using semantic and keyword retrieval.",
      parameters: { query: { type: "string" }, limit: { type: "integer" } },
    });
  }
  return tools;
}
```

Large-document rules:
- if the target exceeds `max_inline_tokens_before_chunking`, the room must use `chunked` or `search_assisted` and disclose it;
- anchor truth must be preserved via `anchor_manifest_ref` or `chunk_refs`;
- review-target viewer links must deep-link into the correct chunk or search result.

#### R7 addition — review-target anchor, chunk, and search response schemas

```ts
export const ReviewTargetSearchRequestSchema = z.object({
  query: z.string().min(1).max(500),
  limit: z.number().int().min(1).max(20).default(5),
});

export const ReviewTargetSearchResponseSchema = z.object({
  results: z.array(z.object({
    chunk_id: z.string(),
    snippet: z.string(),
    relevance_score: z.number().min(0).max(1),
    anchor_id_matches: z.array(z.string()).default([]),
  })),
  query_time_ms: z.number().int().nonnegative(),
});

export const AnchorManifestEntrySchema = z.object({
  anchor_id: z.string(),
  doc_id: z.string(),
  chunk_id: z.string().optional(),
  line_start: z.number().int().optional(),
  line_end: z.number().int().optional(),
  label: z.string().optional(),
});

export const ReviewTargetChunkResponseSchema = z.object({
  chunk_id: z.string(),
  doc_id: z.string(),
  text: z.string(),
  line_start: z.number().int().optional(),
  line_end: z.number().int().optional(),
  anchor_ids: z.array(z.string()).default([]),
});

export const ReviewTargetAnchorResponseSchema = z.object({
  anchor_id: z.string(),
  doc_id: z.string(),
  chunk_id: z.string().optional(),
  line_start: z.number().int().optional(),
  line_end: z.number().int().optional(),
  highlighted_excerpt: z.string().optional(),
  anchor_missing: z.boolean().default(false),
});

export const PreLaunchTargetPreviewRequestSchema = z.object({
  doc_id: z.string(),
  version_group_id: z.string().optional(),
  preferred_materialization_mode: PreferredReviewTargetMaterializationModeSchema,
  estimated_participant_count: z.number().int().positive(),
});

export const PreLaunchTargetPreviewResponseSchema = z.object({
  realized_mode: RealizedReviewTargetMaterializationModeSchema,
  estimated_target_tokens: z.number().int().nonnegative(),
  chunk_count_estimate: z.number().int().nonnegative().optional(),
  search_tool_required: z.boolean(),
  warnings: z.array(z.string()).default([]),
});
```


```ts
// apps/ec-service/src/redteam/reviewTargetGuard.ts
import { ReviewTargetBindingRefSchema } from "@elnor/contracts/red-team";

export function assertReviewTargetBindingPresent(binding: unknown): asserts binding is import("zod").infer<typeof ReviewTargetBindingRefSchema> {
  const result = ReviewTargetBindingRefSchema.safeParse(binding);
  if (!result.success) {
    throw new Error("missing_or_invalid_review_target_binding");
  }
}
```

### 6.6 Ghost judge / constitutional judge `ADVANCED NORMATIVE`

#### R7 addition — ghost judgment rubric

```ts
export const GhostJudgmentRubricSchema = z.object({
  rubric_version: z.string().min(1),
  importance_prompt: z.string(),
  evidence_prompt: z.string(),
  actionability_prompt: z.string(),
  scoring_scale: z.literal("0_to_1"),
});
```

```ts
export const GhostJudgmentSchema = z.object({
  ghost_judgment_id: z.string(),
  room_id: z.string(),
  finding_id: z.string(),
  rubric_version: z.string(),
  importance_score: z.number().min(0).max(1),
  evidence_score: z.number().min(0).max(1),
  actionability_score: z.number().min(0).max(1),
  suggested_disposition: z.enum(["accept", "reject", "needs_rewrite", "park_disputed"]),
  summary: z.string().optional(),
  judged_at: z.string(),
});
```

Ghost judgments are synthetic. They may accelerate review and provide low-weight observations, but they may not by themselves justify prompt promotion.

### 6.7 Critic-strength ranking `ADVANCED NORMATIVE`

```ts
export const CriticStrengthRatingSchema = z.object({
  rating_id: z.string(),
  entity_kind: z.enum(["participant_role", "prompt_variant", "model_prompt_bundle"]),
  entity_id: z.string(),
  rating_model: z.enum(["elo", "bradley_terry"]),
  condition_key: z.string(),
  sample_size: z.number().int().nonnegative(),
  score: z.number(),
  confidence_interval_low: z.number().optional(),
  confidence_interval_high: z.number().optional(),
  last_updated_at: z.string(),
});
```

This rating is experimental, conditioned, and not a universal trust score.

---

## 7. Context & Memory Hygiene `CORE NORMATIVE`

DOC14 enforces red-team-specific context hygiene on top of the shared ELNOR context system.

Rules:

- never rewrite protected native files,
- never silently compress the active review target,
- never inject duplicate target material under multiple disguises,
- always record active overlays as observational context facts,
- always disclose dropped and degraded review-target material,
- transfer packets and child-room imports use refs and manifests, not transcript blobs,
- use context symmetry epochs to prevent fake disagreement caused by asymmetric document materialization.

---

## 8. Automated Prompt Learning & Shared Prompt Substrate

### 8.1 Core design rules `CORE NORMATIVE`

The system must learn which prompts work well under which conditions, not simply which model happened to look smart once.

Rules:

- cluster by `prompt_variant_id` when available; fallback to `prompt_text_hash`,
- preserve prompt-artifact context (`active_overlay_ids`, `prompt_recipe_id`, `prompt_artifact_ids`) when it materially affected execution,
- never apply learned prompt artifacts without visible prompt-plan truth,
- separate room-role prompt observations from topology outcome observations,
- weight signals by evidence grade,
- no production promotion without replay/canary support and minimum human review gates.

### 8.2 Emitted signals `CORE NORMATIVE`

#### R5.2 amendment — signal enrichment and provenance carry-through

Both `PromptObservationEmitSchema` and `TopologyOutcomeSignalSchema` must carry or reference:
- `route_trace_id`
- `operation_id`
- `dispatch_id` where available
- `prompt_method_used`
- `scoring_version` where judgment-derived
- `review_target_length_bucket`
- `realized_materialization_mode`

These fields prevent the optimizer from learning on decontextualized crumbs.


```ts
export const PromptObservationEmitSchema = z.object({
  observation_id: z.string(),
  room_id: z.string(),
  room_turn_id: z.string().optional(),
  participant_id: z.string().optional(),
  logical_role_key: z.string().optional(),
  prompt_artifact_kind: PromptArtifactKindSchema,
  prompt_variant_id: z.string().optional(),
  prompt_text_hash: z.string().min(16),
  prompt_assignment_id: z.string().optional(),
  active_overlay_ids: z.array(z.string()).default([]),
  prompt_recipe_id: z.string().uuid().optional(),
  prompt_artifact_ids: z.array(z.string()).default([]),
  review_target_kind: z.string().optional(),
  review_target_binding_ref: ReviewTargetBindingRefSchema.optional(),
  route_trace_id: z.string().optional(),
  operation_id: z.string().optional(),
  dispatch_id: z.string().optional(),
  prompt_method_used: PromptMethodUsedSchema.optional(),
  scoring_version: z.string().optional(),
  review_target_length_bucket: z.enum(["small", "medium", "large", "very_large"]).optional(),
  realized_materialization_mode: RealizedReviewTargetMaterializationModeSchema.optional(),
  novelty: FindingNoveltySchema.optional(),
  evidence_grade: z.enum(["observational", "replay_confirmed", "canary_confirmed", "approved"]),
  value_components: z.object({
    accepted_findings_weight: z.number().default(0),
    rejected_findings_penalty: z.number().default(0),
    starred_bonus: z.number().default(0),
    cited_bonus: z.number().default(0),
    supervision_cost_penalty: z.number().default(0),
  }),
  notes: z.string().optional(),
  emitted_at: z.string(),
});

export const TopologyOutcomeSignalSchema = z.object({
  topology_outcome_id: z.string(),
  room_id: z.string(),
  review_target_kind: z.string(),
  room_mode: z.string(),
  participant_count: z.number().int().positive(),
  phase_plan_hash: z.string(),
  total_tokens: z.number().int().nonnegative().optional(),
  total_cost_usd: z.number().nonnegative().optional(),
  supervision_cost_ref: z.string().optional(),
  quality_impacting_change: z.boolean().default(false),
  prompt_artifact_ids: z.array(z.string()).default([]),
  active_overlay_ids: z.array(z.string()).default([]),
  prompt_recipe_id: z.string().uuid().optional(),
  emitted_at: z.string(),
});
```

Room-level outcome signals must preserve prompt-artifact context sufficient to distinguish baseline room-role prompting from overlay-assisted, recipe-assisted, or advisor-rewritten executions.


### 8.3 Judgment scoring `CORE NORMATIVE`

#### R5.2 amendment — scoring_version, novelty, prompt_method_used, and review outcomes

Add:

```ts
export const FindingNoveltySchema = z.enum([
  "known_issue",
  "new_angle",
  "surprising",
]);

export const ReviewOutcomeSchema = z.object({
  room_id: z.string(),
  goal_type: z.enum([
    "document_review",
    "spec_review",
    "brief_review",
    "code_review",
    "argument_review",
    "policy_review",
    "other",
  ]),
  close_reason: z.enum([
    "converged",
    "human_closed",
    "budget_exhausted",
    "blocked_dependency",
    "stopped_on_review",
    "timed_out",
    "other",
  ]),
  user_goal_met: z.boolean().optional(),
  findings_starred: z.number().int().default(0),
  findings_by_novelty: z.record(z.string(), z.number()).default({}),
  emitted_at: z.string(),
});
```

Every judgment-derived prompt observation must include:
- `scoring_version`
- `novelty`
- `prompt_method_used`

DOC15/CIL integration requirements:
- emit `finding_exported`
- emit `finding_became_proposal`
- emit `prompt_method_used`
- emit `room_health.snapshot_emitted`
- preserve `original_dispatch_id` for DOC10 correlation.


Scoring must be explicit.

Baseline judgment weighting:

- accepted critical finding: +4
- accepted major finding: +3
- accepted minor finding: +1
- accepted observation: +0.5
- rejected for insufficient evidence: -2
- rejected as duplicate: -0.5
- rejected as manufactured dissent: -2.5
- starred: +1 bonus
- cited in decision: +1.5 bonus
- supervision-cost penalty: subtract normalized drag based on `HumanSupervisionCostSchema`

These weights are not universal forever, but the system must have a deterministic baseline rather than vibes in a trench coat.

### 8.4 Golden rooms, replay, and canaries `CORE NORMATIVE`

DOC14 requires the shared substrate to support:

- golden-room sets,
- replay trials,
- canary application,
- and promotion gating.

DOC14 does not own those artifacts, but it does own the requirement that prompt promotion cannot happen from observational-only evidence in production modes.

### 8.5 Recommendation and application path `CORE NORMATIVE`

#### R5.2 amendment — prompt artifact recommendation preview, diff, apply, and revert

Recommendation UI must support overlays, prompt recipes, room-role prompt variants, and advisor rewrites under one surface.

```ts
export const RecommendationDiffViewModelSchema = z.object({
  recommendation_id: z.string(),
  artifact_kind: PromptArtifactKindSchema,
  current_prompt_artifact_ids: z.array(z.string()).default([]),
  proposed_prompt_artifact_ids: z.array(z.string()).default([]),
  added_overlay_ids: z.array(z.string()).default([]),
  removed_overlay_ids: z.array(z.string()).default([]),
  current_prompt_recipe_id: z.string().uuid().optional(),
  proposed_prompt_recipe_id: z.string().uuid().optional(),
  proposal_only: z.boolean().default(false),
  apply_allowed: z.boolean().default(false),
  apply_block_reason: z.string().optional(),
  why_summary: z.string(),
  updated_at: z.string(),
});
```

Expected companion routes (owner-doc delta where applicable):
- `GET /api/rooms/:roomId/recommendations`
- `GET /api/rooms/:roomId/recommendations/:recommendationId/diff`
- `POST /api/rooms/:roomId/recommendations/:recommendationId/apply`
- `POST /api/rooms/:roomId/recommendations/:recommendationId/revert`

If recommendation retrieval is unavailable, the preview must show `unavailable`, not an empty card pretending no recommendations exist.

#### R7 addition — recommendation source, apply/revert envelope, invalidation, and export schemas

```ts
export const CandidateApplyBlockReasonSchema = z.enum([
  "doc11_prompt_truth_unavailable",
  "doc15_recommendation_source_missing",
  "evidence_grade_too_weak",
  "proposal_only",
  "canary_required",
  "human_approval_required",
  "room_closed",
  "unknown",
]);

export const PromptArtifactRecommendationNodeSchema = z.object({
  recommendation_id: z.string().uuid(),
  artifact_kind: PromptArtifactKindSchema,
  artifact_id: z.string(),
  evidence_grade: z.enum(["observational", "replay_confirmed", "canary_confirmed", "approved"]),
  proposal_only: z.boolean().default(false),
  apply_allowed: z.boolean().default(false),
  candidate_apply_block_reason: CandidateApplyBlockReasonSchema.optional(),
  why_summary: z.string(),
  invalidation_state: z.enum(["valid", "stale", "invalid"]).optional(),
});

export const RecommendationSourceViewModelSchema = z.object({
  recommendation_id: z.string().uuid(),
  source_type: z.enum(["observation_cluster", "replay_trial", "canary_result", "human_approved"]),
  source_room_ids: z.array(z.string()).default([]),
  sample_size: z.number().int().nonnegative(),
  evidence_grade: z.enum(["observational", "replay_confirmed", "canary_confirmed", "approved"]),
  key_signals: z.array(z.object({
    signal_type: z.string(),
    summary: z.string(),
    created_at: z.string(),
  })).default([]),
});

export const PromptArtifactMutationEnvelopeSchema = z.object({
  envelope_id: z.string().uuid(),
  room_id: z.string(),
  recommendation_id: z.string().uuid(),
  mutation_kind: z.enum(["apply", "revert"]),
  actor_type: z.enum(["human", "system"]),
  actor_id: z.string().optional(),
  idempotency_key: z.string().min(8),
  expected_version: z.number().int().nonnegative(),
  route_trace_id: z.string().optional(),
  operation_id: z.string().optional(),
  created_at: z.string(),
});

export const RecommendationApplyRequestSchema = z.object({
  recommendation_id: z.string().uuid(),
  mutation_kind: PromptArtifactMutationEnvelopeSchema.shape.mutation_kind,
  idempotency_key: z.string().min(8),
  expected_version: z.number().int().nonnegative(),
});

export const RecommendationApplyResponseSchema = z.object({
  status: z.enum(["applied", "reverted", "blocked", "stale", "conflict"]),
  envelope_id: z.string().uuid().optional(),
  blocked_reason: CandidateApplyBlockReasonSchema.optional(),
});

export const RecommendationInvalidationStateSchema = z.object({
  recommendation_id: z.string(),
  invalidation_state: z.enum(["valid", "stale", "invalid"]),
  reason_codes: z.array(z.string()).default([]),
  recomputed_at: z.string(),
});

export const RoomExportRequestSchema = z.object({
  export_format: z.enum(["json", "markdown", "pdf", "docx"]),
  include_critique_cache: z.boolean().default(false),
  include_disputed: z.boolean().default(true),
  idempotency_key: z.string().min(8),
});

export const RoomExportResponseSchema = z.object({
  status: z.enum(["processing", "completed", "failed"]),
  export_job_id: z.string().uuid(),
  download_url: z.string().url().optional(),
  artifact_ref: z.string().optional(),
  error_code: z.string().optional(),
});

export const LaunchCapabilitySnapshotSchema = z.object({
  snapshot_id: z.string().uuid(),
  room_id: z.string().optional(),
  child_room_id: z.string().optional(),
  created_at: z.string(),
  capability_flags: z.record(z.string(), z.boolean()),
  degraded_reason_codes: z.array(z.string()).default([]),
  prompt_truth_available: z.boolean(),
  recommendation_apply_available: z.boolean(),
  recommendation_preview_available: z.boolean(),
  review_target_search_available: z.boolean(),
  child_room_preview_available: z.boolean(),
});
```

Expected companion routes:
- `GET /api/rooms/:roomId/recommendations`
- `GET /api/rooms/:roomId/recommendations/:recommendationId/diff`
- `GET /api/rooms/:roomId/recommendations/:recommendationId/source`
- `POST /api/rooms/:roomId/recommendations/:recommendationId/apply`
- `POST /api/rooms/:roomId/recommendations/:recommendationId/revert`
- `POST /api/rooms/:roomId/exports/findings-pack`
- `POST /api/rooms/:roomId/exports/review-outcome`
- `POST /api/rooms/:roomId/exports/decision-pack`


The path is:

1. room emits prompt-artifact observations,
2. DOC8 scores and validates them under replay/canary/human-review evidence rules,
3. DOC15 stores prompt-artifact recommendation nodes and suggestion metadata,
4. DOC12/DOC11 surfaces the recommendation to the room configurator, participant drawer, post-run summary, and child-room launch preview where applicable,
5. user or policy applies a recommendation only if the evidence grade and owner-doc gates permit that action,
6. DOC11 shows the effective prompt-artifact plan, including applied, dropped, or trimmed artifacts,
7. the room runs with visible prompt provenance.

Candidate prompt artifacts derived from Prompt Lab, advisor rewrites, or recipe experimentation must not expose `Apply`, `Promote`, or `Use by default` actions unless the effective evidence grade and owner-doc gating conditions permit that action. When conditions are not satisfied, UI must surface the candidate as review-only or observational-only.

### 8.6 Partial deployment and degraded modes `CORE NORMATIVE`

| Missing / unavailable companion capability | DOC14 behavior | UI state |
|---|---|---|
| DOC11 prompt truth unavailable | no learned prompt apply; no truthful effective prompt display; room may still run with baseline/static prompting only | `degraded_prompt_truth` |
| DOC15 recommendation retrieval unavailable | no overlay/recipe/advisor recommendation preview; fallback baseline only | `recommendations_unavailable` |
| DOC8 replay/canary/promotion unavailable | capture observational signals only; no promotion or canary labeling | `learning_capture_only` |
| DOC17 unavailable | overlays/recipes may exist only as static configured artifacts; no Prompt Advisor or Prompt Lab-driven recommendations | `advisor_unavailable` |
| Prompt Lab result present but evidence too weak | show candidate as proposal only; disallow apply/promote | `candidate_locked_observational_only` |


### 8.7 Prompt distillation / curated examples `ADVANCED NORMATIVE`

Accepted, high-value findings may later be distilled into curated examples for prompt support, but only under the same shared-substrate governance and evidence-grade rules.

---

## 9. Controls `CORE NORMATIVE`

Controls must make the happy path short and the advanced path visible but not mandatory.

Rules:

- presets first,
- advanced collapsed,
- one-click launch path for common red-team flows,
- batch review is default,
- inline micro-grading is advanced,
- all control states must reflect real backend/read-model truth.

---

## 10. Red-Team Room UI Specification

### 10.1 Room Configurator `CORE NORMATIVE`

**Placement:** room creation / relaunch / “red-team this draft” flows.

**Shows:**
- preset,
- review target,
- critic roster,
- cost preview,
- prompt artifact recommendation preview,
- policy summary,
- child-room launch compatibility,
- recommended room-role prompt variant if any,
- suggested overlay IDs,
- suggested prompt recipe IDs,
- recommendation evidence grade and state,
- unavailable/degraded reason if recommendation systems absent.

**States:**
- loading,
- empty/cold start,
- populated,
- degraded (missing DOC13 / DOC15 / DOC11),
- validation error,
- blocked.

**Actions:**
- launch,
- show advanced,
- apply recommendation when allowed,
- preview review target,
- preview effective prompt-artifact plan when companion truth surfaces support it.

**Phase 0 note:** Save Preset is not baseline-core and must not appear as an active mutation control unless its owner-doc contract (preset storage route and schema) is landed. Phase-gated to Phase 1.

#### R7 addition — configurator state view model

```ts
export const ConfiguratorStateViewModelSchema = z.object({
  available_presets: z.array(z.object({ preset_id: z.string(), name: z.string(), description: z.string().optional() })).default([]),
  selected_preset_id: z.string().nullable().optional(),
  review_target_ref: ReviewTargetBindingRefSchema.nullable().optional(),
  review_target_title: z.string().nullable().optional(),
  estimated_target_tokens: z.number().int().nullable().optional(),
  suggested_materialization_mode: PreferredReviewTargetMaterializationModeSchema.nullable().optional(),
  participant_roster: z.array(z.object({ role_key: z.string(), display_name: z.string() })).default([]),
  available_overlays: z.array(z.object({ overlay_id: z.string(), name: z.string() })).default([]),
  selected_overlay_ids: z.array(z.string()).default([]),
  available_recipes: z.array(z.object({ recipe_id: z.string().uuid(), name: z.string() })).default([]),
  selected_recipe_id: z.string().uuid().nullable().optional(),
  recommendation_preview: PromptArtifactRecommendationNodeSchema.nullable().optional(),
  recommendation_state: z.enum(["available", "unavailable", "no_data"]).default("no_data"),
  estimated_cost_usd: z.number().nullable().optional(),
  cost_estimate_state: z.enum(["available", "unavailable"]).default("unavailable"),
  review_intent: z.enum(["truth_seeking", "ship", "high_stakes", "exploratory"]).default("truth_seeking"),
  convergence_threshold: z.number().min(0).max(1),
  convergence_lookback_turns: z.number().int().positive(),
  validation_errors: z.array(z.string()).default([]),
  launch_allowed: z.boolean().default(false),
  blocked_reasons: z.array(z.string()).default([]),
  degraded_reasons: z.array(z.string()).default([]),
});
```


### 10.2 Findings Batch Review Panel `CORE NORMATIVE`

#### R5.2 amendment — bulk selection, keyboard support, and partial-failure recovery

The panel must support:
- bulk select by severity and state;
- keyboard shortcuts for accept/reject/star/cite when focus is in batch review mode;
- row-level error badges;
- retry-failed-only for batch operations;
- explicit rejection-reason selection on reject;
- stale-version reconciliation prompt if the batch response indicates version conflicts.


**Placement:** room page main review area after convergence or during manual review.

**Shows:**
- findings grouped by severity,
- batch progress,
- ghost judgment markers if present,
- starred / cited chips,
- promote-from-cache actions,
- bulk actions.

**States:**
- loading skeleton,
- empty,
- running,
- converged / ready for review,
- degraded command path unavailable,
- partial batch failure.

### 10.3 Finding Detail Modal `CORE NORMATIVE`

#### R5.2 amendment — edit/rewrite/provenance controls

The detail modal must support:
- `Mark Needs Rewrite`
- `Open Adjudication Record`
- `Open Lineage Trail`
- `Open Review Target Anchor`
- `View Prompt Provenance`
- `Edit Proposed Fix` (where policy allows)
- `Export Finding`

If edits are allowed, the edit path must be explicit and durable, not freeform client-side mutation.


Must show:
- full finding body,
- evidence refs,
- review-target viewer links,
- actionable patch,
- prompt/runtime provenance,
- judgment history,
- adjudication / lineage trail.

### 10.4 Critique Cache Drawer `CORE NORMATIVE`

#### R5.2 amendment — grouping, sort/filter, and unparsed-contribution support

The cache drawer must support:
- grouping by cache reason;
- sort by severity, recency, and source participant;
- filters for `unsupported_hypothesis`, `needs_evidence`, `unparsed_contribution`, `downgraded`;
- `Promote` action;
- `Review Raw Contribution` for unparsed items;
- `Manual Extract` action where a raw contribution exists.


Must show cached items grouped by cache reason, including `Promote` action and degraded-state disclosure when promotion command unavailable.

### 10.5 Review Target Viewer integration `CORE NORMATIVE`

#### R5.2 amendment — dual-pane evidence/doc workflow and search-tool mode

The default review workflow must not force the user to lose Findings context when opening the target document.

Required layouts:
- split-pane findings + document viewer for wide screens;
- modal/slide-over document viewer while preserving findings context for narrow screens.

Additional states:
- `search_assisted` mode with query box and result list;
- `chunked` mode with chunk navigator;
- `anchor_missing` warning when a stale anchor cannot be resolved;
- `search_only` badge when exact anchoring is unavailable.


Evidence anchors must open the review target side-by-side at the bound location when possible. Large-doc modes must clearly disclose whether the view is direct, chunked, or search-assisted.

### 10.6 Convergence Banner / Modal `CORE NORMATIVE`

Must show:
- convergence status,
- threshold,
- lookback window,
- why the room thinks it is done,
- override options if allowed.

### 10.7 Prompt Artifact Recommendation Preview `CORE NORMATIVE`

#### R5.2 amendment — recipe support, blocked reasons, and diff preview

The recommendation preview must show:
- artifact kind (overlay / recipe / room-role prompt / rewrite)
- evidence grade
- proposal-only vs apply-allowed vs applied state
- candidate-apply block reason
- a diff/impact preview (`RecommendationDiffViewModelSchema`)
- recommendation source node and why-summary

Buttons:
- `Preview Diff`
- `Apply` / `Revert` (if allowed)
- `Why recommended?`
- `Open source signal`


Must show:
- recommended room-role prompt variant,
- suggested overlay IDs,
- suggested prompt recipe IDs,
- source evidence grade,
- context fit,
- canary / approved / replay / observational-only state,
- unavailable state if DOC15 recommendation nodes are absent.

If a candidate comes from Prompt Lab or another advisory path, the surface must distinguish `proposal_only` from `apply_allowed`.


### 10.8 Participant Drawer — prompt plan `CORE NORMATIVE`

#### R5.2 amendment — full prompt truth fields

The drawer must now show:
- `prompt_truth_state`
- role prompt variant id / text hash
- `prompt_artifact_ids`
- `active_overlay_ids`
- `dropped_overlay_ids`
- `overlay_trim_reasons`
- `prompt_recipe_id`
- recommendation source node
- candidate apply allowed / blocked reason
- evidence grade

The drawer is the primary human-trust surface for learned prompt application.


Must show:
- effective room-role prompt variant,
- prompt artifact kind,
- active overlay IDs,
- prompt recipe ID when present,
- prompt artifact IDs contributing to the packet,
- recommendation source,
- why-applied / why-dropped info,
- whether the run used baseline / recommended / canary / approved prompt artifact state,
- degraded / unavailable truth if DOC11 has not shipped the necessary surface.


### 10.9 Health chip `CORE NORMATIVE`

#### R5.2 amendment — stale marker and drill-down parity

The health chip must display a `stale` indicator when its backing projection is older than the latest room event or when SSE is disconnected. Drill-down must expose the same metrics used by `RoomHealthViewModelSchema` and `RoomConvergenceViewModelSchema`.


Default presentation is simple:
- healthy,
- warning,
- degraded,
- blocked.

Drill-down may show underlying metrics, but the primary chip must speak human, not dashboard goblin.

### 10.10 Post-run summary `CORE NORMATIVE`

#### R5.2 amendment — unpatchable insights and prompt-artifact recap

The post-run summary must include:
- top accepted findings;
- disputed findings;
- critique-cache carryovers;
- `Unpatchable Insights` surfaced as a first-class section;
- prompt artifacts used (`prompt_artifact_ids`, overlays, prompt recipe, prompt method used);
- recommendation apply/proposal-only status;
- export buttons for findings pack, review outcome, and decision pack.


Must show:
- accepted findings,
- disputed findings,
- cached findings summary,
- proposed fixes,
- review target reviewed,
- prompt artifacts used,
- active overlays,
- prompt recipe used,
- whether any learned recommendation was applied,
- evidence grade behind any applied learned artifact,
- supervision-cost summary,
- learning signals emitted,
- child-room launch actions.


### 10.11 Child-room launch affordance `CORE NORMATIVE`

#### R5.2 amendment — launch modal controls and truth states

The child-room launch affordance must open a modal with:
- review-target selector
- overlay application mode selector
- explicit overlay multiselect
- prompt recipe mode selector
- prompt recipe picker
- conflict-resolution selector
- import-back strategy selector
- effective prompt-plan preview
- degraded-state warning block
- policy block reasons (if launch restricted)

The launch button must not be shown as enabled if required DOC12 or DOC11 surfaces are unavailable.


Any eligible output card or review-target view may expose **Red-team this draft**.

Action flow:

1. select target,
2. select import-back strategy,
3. choose overlay application mode,
4. optionally pick explicit overlays,
5. optionally pick explicit prompt recipe,
6. optionally require human approval before launch,
7. launch linked child room if supported,
8. else show explicit unsupported-state copy.

If companion truth surfaces are available, the launch UI must preview whether the child room will inherit, override, or drop parent prompt artifacts.

---

## 11. Advanced Normative Extensions

### 11.1 Debate-graph orchestration `ADVANCED NORMATIVE`

Support debate-graph visualizations and graph-aware synthesis for rooms that want more than a linear turn transcript.

### 11.2 Collaborative reference cache `ADVANCED NORMATIVE`

Allows participants to build a shared structured reference set without duplicating document bodies.

### 11.3 Single-agent collapse heuristic `ADVANCED NORMATIVE`

Allows the system to collapse to a simpler configuration only when that collapse does not bypass required dissent, review-target integrity, or evidence obligations.

### 11.4 Evolutionary profile search `ADVANCED NORMATIVE`

Blue/red profile search belongs to DOC8-owned optimizer behavior and is referenced here only as a compatible advanced mode.

### 11.5 Private critic workspaces `ADVANCED NORMATIVE`

Private critic workspaces are allowed only if their existence and limits are visible, and only if they do not compromise visible room truth.

### 11.6 Full evidence-audit subphase `ADVANCED NORMATIVE`

A more expensive adjudication phase that requests focused evidence re-audit before final resolution.

### 11.7 Auto-create / file-watch / successor triggers `ADVANCED NORMATIVE`

### 11.8 Keyboard shortcuts and bulk review ergonomics `ADVANCED NORMATIVE`

Advanced builds may enable:
- keyboard-driven batch review navigation;
- rapid accept/reject/star/cite shortcuts;
- bulk actions scoped by severity, source participant, or novelty;
- conflict-aware bulk application with preview.

### 11.9 Recommendation diff / patch viewer `ADVANCED NORMATIVE`

Advanced builds should expose a recommendation diff / patch viewer showing:
- changed overlays;
- changed recipe;
- changed role-prompt variant;
- projected recommendation impact summary;
- blocked reasons and evidence grade.

### 11.10 Chat-with-finding / ask-why surfaces `ADVANCED NORMATIVE`

Advanced builds may expose:
- `Chat with this finding`
- `Ask why this was accepted/rejected`
- `Ask what changed from previous revision`

These surfaces must remain provenance-backed and may never invent hidden state.

### 11.11 One-click quick red-team / auto-patch / successor automation `ADVANCED NORMATIVE`

Advanced builds may add:
- quick red-team from upload;
- auto-suggested successor room creation;
- proposal-only auto-patch generation;
- file-watch successor triggers.

All such automation remains gated by truth and policy constraints.


Room creation may later be triggered by file changes, successor revisions, or matter-state transitions, but this belongs primarily to DOC12/DOC16/DOC7 coordination.

---

## 12. Phasing & Implementation Roadmap

### 12.1 Phase 0 Core

Required for baseline truthful operation:

- finding extraction,
- hook dispatcher,
- judgment path,
- telemetry registry,
- review-target binding,
- batch review UI,
- room health projection,
- child-room compatibility hooks,
- large-doc materialization rules,
- minimal companion-doc deltas for DOC12 / DOC11 / DOC10.

### 12.2 Phase 0 Advanced

Specified now, may be disabled by default:

- ghost judge,
- prompt artifact recommendation preview where available,
- child-room quick-launch UX,
- adaptive adjudication policies.

### 12.3 Phase 1

- stronger semantic dedup,
- evidence-audit path,
- replay/canary refinement,
- improved recommendation application,
- concept-registry hardening.

### 12.4 Phase 2+

- critic-strength ranking,
- debate graph,
- auto-create/file-watch triggers,
- prompt distillation / curated examples.

### 12.5 Ship gates

- no learned prompt-artifact application without DOC11 prompt truth,
- no production prompt promotion without DOC8 replay/canary + minimum review gates,
- high-stakes red-team launch blocked if review-target binding missing,
- recommendation UI degraded when DOC15 absent,
- Prompt Lab or advisor-derived candidates remain proposal-only until evidence grade and owner-doc gates allow apply/promote.

---

## 13. Risks & Mitigations

| Risk | Why it matters | Mitigation |
|---|---|---|
| hook crash after partial append | corrupts findings ledger | append only after pre-append validation succeeds; pause room on mid-pipeline failure |
| wrong provenance on judgments | poisons learning | load finding record directly; stamp provenance at finding creation |
| review-target truncation | fake critique | explicit materialization modes + inspector disclosure |
| duplicate optimizer with DOC17 | conflicting prompt lifecycle | DOC8 sole optimizer owner |
| semantic dedup brittleness | drops true issues | target-kind-aware thresholding + deferred semantic review |
| ghost judge over-weighting | self-delusion | low-weight synthetic evidence, visible labeling, no ghost-only promotion |
| child-room truth drift | broken lineage and imports | review-target binding refs + transfer-packet compatibility + lineage refs |
| low judgment coverage | weak learning | coverage gates before recommendation/promotion |

---

## 14. Consolidated Spec Checklist

### Core schemas
- [ ] RedTeamPolicySchema
- [ ] ParsedTurnSchema
- [ ] RawFindingCandidateSchema
- [ ] FindingExtractionResultSchema
- [ ] RoomFindingSchema
- [ ] RedTeamRoomOutputContractSchema
- [ ] FindingJudgmentSchema
- [ ] FindingAdjudicationRecordSchema
- [ ] FindingLineageSchema
- [ ] RoomHealthSnapshotSchema
- [ ] PromptObservationEmitSchema
- [ ] TopologyOutcomeSignalSchema
- [ ] ReviewTargetBindingRefSchema

### Core commands/routes
- [ ] room_judge_finding
- [ ] room_batch_judge_findings
- [ ] room_adjudicate_finding
- [ ] room_lineage_upsert
- [ ] findings query routes
- [ ] health query route
- [ ] review-target query route

### UI surfaces
- [ ] configurator
- [ ] findings batch review panel
- [ ] finding detail modal
- [ ] critique cache drawer
- [ ] review target viewer
- [ ] convergence banner
- [ ] prompt artifact recommendation preview
- [ ] prompt-plan drawer tab
- [ ] health chip
- [ ] post-run summary
- [ ] child-room launch affordance

### Cross-doc gates
- [ ] DOC12 room + linked-room contracts accepted
- [ ] DOC11 prompt truth accepted
- [ ] DOC8 optimizer contracts accepted
- [ ] DOC15 recommendation node contracts accepted
- [ ] DOC13 cost hooks accepted
- [ ] DOC10 authority rows accepted
- [ ] DOC17 alignment accepted

---

# APPENDIX A — Required Cross-Doc Deltas

Each delta row includes owner, blocking status, and degraded behavior.

## A1. DOC12 delta `COMPANION-DELTA REQUIRED`

### R5.2 status update for DOC12

DOC12 R7.1 now accepts the majority of the room-side red-team substrate required by DOC14. Appendix A1 therefore treats DOC12 as **accepted room-side substrate with remaining partials**, not as wholly speculative future work.

Use the following distinctions consistently:
- shared/canonical contracts remain package-level;
- DOC12 room-side query and UI hydration use **projection schemas**;
- DOC11 remains the owner of effective runtime truth.

The next DOC14 revision must align to the DOC12 R7.1 accepted launch enums:
- overlay modes: `inherit_none`, `inherit_room_default`, `inherit_parent_effective`, `inherit_then_append_explicit`, `explicit_only`, `explicit_select`
- prompt recipe modes: `inherit_none`, `carry_context_only`, `inherit_effective_recipe`, `explicit_select`

Open DOC12 companion partials remain:
- complete typed SSE payload coverage for every normalized event;
- child-room effective prompt-plan preview/read model;
- batch judgment retry/read model and stale-version reconciliation;
- review-target anchor lookup / chunk navigation query closure;
- convergence override read-model/route closure.


**Required artifacts**

- `room_judge_finding`
- `room_batch_judge_findings`
- `RoomReviewTargetBindingSchema`
- `active_review_target_ref` in bootstrap
- `prompt_plan` in participant/bootstrap truth surfaces
- `RoomOverlayStateSchema`
- `ParticipantOverlayPrecedenceViewSchema`
- `active_room_overlay_ids`
- `participant_effective_overlay_ids`
- `prompt_recipe_id` as observational room/surface fact where applicable
- linked-room child red-team launch compatibility
- critique-cache query route
- findings / health / lineage / adjudication projections and SSE events
- successor-room lineage carry-forward
- scheduler reserve support
- truthful persistence of inherited-vs-explicit prompt-artifact state for child rooms

**Required routes**

- `GET /api/rooms/:roomId/findings`
- `GET /api/rooms/:roomId/findings/cache`
- `GET /api/rooms/:roomId/health`
- `GET /api/rooms/:roomId/review-target`
- `POST /api/rooms/:roomId/findings/:findingId/judgments`
- `POST /api/rooms/:roomId/findings/judgments:batch`
- `POST /api/rooms/:roomId/findings/:findingId/adjudications`
- `POST /api/rooms/:roomId/findings/:findingId/lineage`
- `POST /api/rooms/:roomId/child-redteam-launch`

**Required UI/read-model expectations**

- child-room launch modal with overlay mode selector
- optional explicit overlay picker
- optional explicit prompt recipe selector
- read-model exposing effective child-room prompt-artifact plan

**Status:** blocking for high-stakes completeness.


## A2. DOC11 delta `COMPANION-DELTA REQUIRED`

### R5.2 DOC11 emphasis

DOC11 remains the primary open blocker for trustworthy learned prompt application. DOC14 v5.2 now assumes a two-step prompt truth model:
- DOC12 owns room-side prompt/artifact projections;
- DOC11 owns effective runtime truth, trim reasons, packet truth, apply blockers, and display-ready prompt-plan truth.


- prompt-plan line / header chip
- participant drawer prompt-plan tab
- context inspector fields for review target, materialization mode, dropped target, symmetry epoch, active overlays
- `OverlayPromptPacket` consumption support
- `applied_overlay_ids`
- `dropped_overlay_ids`
- `overlay_trim_reasons`
- `prompt_recipe_id`
- `prompt_artifact_ids`
- `candidate_apply_allowed: boolean`
- `candidate_apply_block_reason?: string`
- degraded/unavailable prompt truth state

DOC11 must surface the effective prompt-artifact plan for each participant, including overlays and prompt recipe when present, and must distinguish between configured, packet-assembled, applied, trimmed, and dropped artifacts.

**Status:** blocking for learned prompt application; degraded for basic room execution.


## A3. DOC8 delta `COMPANION-DELTA REQUIRED`

- `PromptVariantSchema`
- `PromptAssignmentSchema`
- replay, canary, promotion lifecycle
- golden-room set support
- prompt / critic-strength optional advanced models

**Status:** capture-only without these.

## A4. DOC15 delta `COMPANION-DELTA REQUIRED`

### R5.2 DOC15 emphasis

DOC15 must now support both ingestion and retrieval for prompt-artifact recommendations. The reverse path is no longer optional because DOC14 UI now normatively expects recommendation preview, diff, apply, revert, and blocked-reason states.


- `prompt_recommendation` node type
- retrieval/matcher support for review target kind, room role, overlay context, matter/workflow facts
- recommendation preview data contract
- `suggested_overlay_ids`
- `suggested_prompt_recipe_ids`
- `prompt_improvement_hints`
- `overlay_advice_state`
- `prompt_recipe_suggestion_state`
- recommendation evidence-grade/state metadata sufficient for DOC14 preview UI


## A5. DOC13 delta `COMPANION-DELTA REQUIRED`

- launch-time cost estimate inputs
- supervision-cost snapshot read path
- room / participant / matter tagging for cost aggregation

## A6. DOC10 delta `COMPANION-DELTA REQUIRED`

- authority rows for judgment, batch judgment, prompt apply/promote/retire/canary, child-room launch if surfaced through orchestration
- route-trace and operation-id propagation
- telemetry/integration-ledger rows for DOC14-specific events

## A7. DOC17 coordination delta `COMPANION-DELTA REQUIRED`

### R5.2 DOC17 emphasis

DOC17 R4.2 is now aligned enough that DOC14 may treat it as the owner of overlay / prompt-recipe / prompt-advisor semantics and Prompt Lab UX, but not as a duplicate optimizer. DOC17 must continue importing shared prompt-artifact contracts rather than redefining them locally.


- stale DOC14 references removed
- add `prompt_recipe` to shared prompt-artifact family
- emit overlay / prompt-recipe / rewrite observations through shared substrate
- Prompt Lab is offline only
- no independent replay/canary/promotion chain in DOC17
- `active_overlay_ids`, `prompt_recipe_id`, and `prompt_artifact_ids` aligned across shared contracts
- candidate apply / promotion gated by DOC11 truth + DOC8 evidence ownership
- DOC17 may request Prompt Lab jobs and render candidate results, but it may not own live mutation governance


## A8. DOC16 coordination note `COMPANION-DELTA REQUIRED`

- child-room litigation workflow remains mandatory compatibility scope,
- DOC16 retains the detailed linked-room / matter-workbench mechanics,
- implementation sequence updated so child-room capability is phase-0 compatible.

## A9. DOC6 / DOC4 / DOC3 notes `COMPANION-DELTA REQUIRED`

- DOC6: room-backed matter panel bridge
- DOC4: visible starter agents / legal starter packs / logical actor templates
- DOC3: legal research capability family for litigation workflow packs

---

# APPENDIX B — File / Module Plan for Coding Agents

## Contracts package

### R5.2 additions

Add or harden the following modules:

```text
packages/contracts/src/redteam/prompt-method.ts
packages/contracts/src/redteam/prompt-observation-envelope.ts
packages/contracts/src/redteam/recommendation-diff.ts
packages/contracts/src/redteam/unparsed-contribution.ts
packages/contracts/src/redteam/child-room-effective-plan.ts
packages/contracts/src/redteam/review-target-materialization-plan.ts
packages/contracts/src/redteam/batch-judgment-recovery.ts
packages/contracts/src/redteam/review-outcome.ts
```


- `packages/contracts/src/red-team/policy.ts`
- `packages/contracts/src/red-team/findings.ts`
- `packages/contracts/src/red-team/judgments.ts`
- `packages/contracts/src/red-team/adjudication.ts`
- `packages/contracts/src/red-team/lineage.ts`
- `packages/contracts/src/red-team/review-target.ts`
- `packages/contracts/src/red-team/health.ts`
- `packages/contracts/src/red-team/prompt-identity.ts`
- `packages/contracts/src/red-team/prompt-observation.ts`
- `packages/contracts/src/red-team/ui-view-models.ts`

## EC service

- `apps/ec-service/src/redteam/extractFindingsFromTurn.ts`
- `apps/ec-service/src/redteam/executePreTurnHook.ts`
- `apps/ec-service/src/redteam/executePostTurnHook.ts`
- `apps/ec-service/src/redteam/executePostSynthesisHook.ts`
- `apps/ec-service/src/redteam/executeCloseRoomHook.ts`
- `apps/ec-service/src/redteam/reviewTargetGuard.ts`
- `apps/ec-service/src/redteam/contextSymmetry.ts`
- `apps/ec-service/src/redteam/scoreFindingJudgment.ts`
- `apps/ec-service/src/redteam/emitPromptObservation.ts`
- `apps/ec-service/src/redteam/emitTopologyOutcome.ts`
- `apps/ec-service/src/redteam/buildFindingsProjection.ts`
- `apps/ec-service/src/redteam/buildRoomHealthViewModel.ts`
- `apps/ec-service/src/commands/roomJudgeFinding.ts`
- `apps/ec-service/src/commands/roomBatchJudgeFindings.ts`
- `apps/ec-service/src/commands/roomAdjudicateFinding.ts`
- `apps/ec-service/src/commands/roomUpsertLineage.ts`

## Q frontend

- `apps/q-frontend/src/features/redteam/RoomConfigurator.tsx`
- `apps/q-frontend/src/features/redteam/FindingsBatchReviewPanel.tsx`
- `apps/q-frontend/src/features/redteam/FindingDetailModal.tsx`
- `apps/q-frontend/src/features/redteam/CritiqueCacheDrawer.tsx`
- `apps/q-frontend/src/features/redteam/ChildRoomLaunchModal.tsx` // companion-delta UI owned primarily by DOC12, referenced here for compatibility
- `apps/q-frontend/src/features/redteam/ReviewTargetViewer.tsx`
- `apps/q-frontend/src/features/redteam/ConvergenceBanner.tsx`
- `apps/q-frontend/src/features/redteam/PromptArtifactRecommendationCard.tsx`
- `apps/q-frontend/src/features/redteam/PostRunSummary.tsx`
- `apps/q-frontend/src/features/redteam/ChildRedTeamLaunchButton.tsx`
- `apps/q-frontend/src/features/redteam/api.ts`
- `apps/q-frontend/src/features/redteam/viewModels.ts`

---

# APPENDIX C — Acceptance Matrix

## 1. Finding extraction
- **Input:** critic output with valid fenced findings JSON block  
  **Expected:** findings extracted, stamped, appended  
  **Broken looks like:** no findings created or findings created without prompt/review-target provenance

## 2. Fallback extraction
- **Input:** prose-only critic turn  
  **Expected:** degraded extraction warning; valid candidates created if parseable  
  **Broken looks like:** silent loss of findings

## 3. Batch review
- **Input:** 10 findings, batch accept 6 / reject 4  
  **Expected:** all results persisted or explicit partial-failure report  
  **Broken looks like:** UI says complete but only some judgments persisted

## 4. Cache promotion

## 4A. Batch partial-failure recovery

Input: batch judgment over 12 findings where 3 rows fail due to stale versions.

Expected:
- 9 rows persist;
- 3 rows return in `retryable_row_ids`;
- `BatchJudgmentRecoveryViewModel` is updated;
- UI footer shows `Retry failed only`.

Broken if:
- the entire batch is reported as failed with no row-level truth;
- successful rows are lost;
- stale rows cannot be retried without a full page refresh.

- **Input:** cached finding promoted via batch UI  
  **Expected:** finding transitions from cache to main ledger with provenance intact  
  **Broken looks like:** duplicate or orphaned finding

## 5. Adjudication
- **Input:** disputed finding  
  **Expected:** adjudication record created and reflected in read model  
  **Broken looks like:** no visible state transition

## 6. Lineage carry-forward
- **Input:** successor room on revised document  
  **Expected:** lineage trail shows resolved / still-present / regressed accurately  
  **Broken looks like:** successor room loses predecessor connection

## 7. Review-target pressure
- **Input:** oversized document in high-stakes room  
  **Expected:** explicit materialization mode + no silent truncation  
  **Broken looks like:** room runs with missing target chunks and no disclosure

## 8. Prompt artifact recommendation preview

## 8A. Recommendation diff and apply blockers

Input: recommendation preview for a prompt recipe + overlay bundle with observational evidence only.

Expected:
- preview renders artifact kind, evidence grade, current vs proposed diff, and apply blocker reason;
- `Apply` control is disabled;
- `Proposal only` badge shown.

Broken if:
- the UI shows an enabled Apply button;
- no blocker reason is available;
- diff view is missing or inaccurate.

- **Input:** recommendation available vs unavailable  
  **Expected:** clear populated or degraded state  
  **Broken looks like:** blank UI area with no reason

## 9. Prompt truth
- **Input:** applied recommendation  
  **Expected:** participant drawer shows actual prompt plan  
  **Broken looks like:** learned prompt applied with no visible proof

## 10. Child-room launch
- **Input:** click “Red-team this draft”  
  **Expected:** child room created or explicit unsupported state  
  **Broken looks like:** button exists but no real backend path

## 11. Ghost judgment
- **Input:** ghost judge enabled  
  **Expected:** synthetic judgments clearly labeled and overridable  
  **Broken looks like:** ghost judgments indistinguishable from human review

## 12. Hook crash rollback
- **Input:** post-turn semantic dedup crash  
  **Expected:** room paused, no partial append corruption  
  **Broken looks like:** half-written findings ledger


## 13. Shared prompt-artifact family
- **Input:** room runs with room-role prompt + overlays + prompt recipe  
  **Expected:** prompt observation stores `prompt_artifact_kind`, `active_overlay_ids`, `prompt_recipe_id`, and `prompt_artifact_ids`  
  **Broken looks like:** runtime only records prompt hash and loses overlay/recipe context

## 14. Recommendation preview rendering
- **Input:** DOC15 returns overlay + prompt recipe suggestions  
  **Expected:** configurator shows both categories with evidence grade and state  
  **Broken looks like:** only overlay suggestions render or recipe suggestions silently drop

## 15. DOC11 prompt truth unavailable
- **Input:** recommendation exists but DOC11 prompt-plan truth unavailable  
  **Expected:** no apply action, clear degraded state  
  **Broken looks like:** UI offers apply despite not knowing effective truth

## 16. Child-room explicit overlay selection
- **Input:** launch child room with `overlay_application_mode = explicit_select`  
  **Expected:** child room uses explicit overlays only; truth drawer reflects that  
  **Broken looks like:** parent overlays leak through or explicit overlays ignored

## 17. Child-room inherited overlays
- **Input:** launch child room with `inherit_parent_effective`  
  **Expected:** parent effective overlays copied subject to policy/trimming; truth drawer shows inherited source  
  **Broken looks like:** overlays missing or unlabeled as inherited

## 18. Prompt Lab proposal gating
- **Input:** Prompt Lab candidate with observational-only evidence  
  **Expected:** candidate visible as proposal; apply/promote actions disabled  
  **Broken looks like:** candidate can be applied/promoted live without required gates

## 19. Post-run summary prompt artifacts
- **Input:** room run used overlay + recipe  
  **Expected:** post-run summary lists prompt artifacts actually used  
  **Broken looks like:** summary only shows room-role prompt or nothing at all

## 20. Rejected finding state transition (R7 addition — the disposition-map fix)
- **Input:** user rejects a finding  
  **Expected:** finding state changes to `"rejected"` via `FINDING_STATE_FROM_DISPOSITION` map  
  **Broken looks like:** finding remains `"open"` after rejection (the four-revision bug)

## 21. Manual extract from unparsed contribution (R7 addition)
- **Input:** extraction failed, user clicks `Manual Extract` on unparsed contribution  
  **Expected:** `ManualExtractRequestSchema` → new finding created with audit trail + lineage  
  **Broken looks like:** contribution disappears or finding created without provenance

## 22. Finding edit/rewrite produces audit record (R7 addition)
- **Input:** user edits finding severity or text  
  **Expected:** `FindingEditAuditSchema` record persisted, finding version incremented  
  **Broken looks like:** finding mutated without audit trail

## 23. Child preview returns deterministic effective plan (R7 addition)
- **Input:** child launch modal with `inherit_then_append_explicit` + overlapping overlays + `explicit_wins`  
  **Expected:** `ChildRoomPreviewResponseSchema` shows effective plan with warnings about overlap  
  **Broken looks like:** preview shows wrong effective overlays or no warnings

## 24. Child import-back idempotency (R7 addition)
- **Input:** same import-back request sent twice with same idempotency key  
  **Expected:** second request returns `already_imported`, no duplicate findings  
  **Broken looks like:** duplicate findings created in parent room

## 25. Recommendation apply/revert mutation envelope (R7 addition)
- **Input:** user applies recommendation, then reverts  
  **Expected:** `PromptArtifactMutationEnvelopeSchema` persisted for both apply and revert  
  **Broken looks like:** apply works but revert has no durable record

## 26. Semantic dedup disabled does not crash hook (R7 addition)
- **Input:** room policy has `semantic_dedup_enabled: false` (or absent)  
  **Expected:** `executePostTurnHook` skips semantic dedup, pipeline completes  
  **Broken looks like:** hook crashes with missing embedding service

## 27. Large document activates tool injection (R7 addition)
- **Input:** document exceeds `max_inline_tokens_before_chunking`  
  **Expected:** `selectMaterializationMode` returns `chunked` or `search_assisted`, `appendMaterializationTools` injects tools  
  **Broken looks like:** participant has no search/chunk tools, cannot access document content

## 28. Convergence measures finding creation not acceptance (R7 addition)
- **Input:** room produces 5 new findings in lookback window, but no judgments yet  
  **Expected:** `isConverged` returns `false` (room still producing findings)  
  **Broken looks like:** convergence fires because no *accepted* findings exist in window

## 29. Event bundle populated and consumed (R7 addition)
- **Input:** post-turn hook creates 3 findings  
  **Expected:** `PostTurnResult.event_bundle` contains 3 `room.finding.created` events + 1 `room.health.updated`  
  **Broken looks like:** `event_bundle` is empty, DOC12 emits only `room.post_turn.completed`

## 30. Export controls not shown without backing routes (R7 addition)
- **Input:** export route not yet available  
  **Expected:** export buttons hidden or disabled with explanation  
  **Broken looks like:** export button rendered, click produces silent error or no result


---

# APPENDIX D — State Machines and Error Taxonomy

```ts
export const FindingLifecycleStateSchema = z.enum([
  "open",
  "accepted",
  "rejected",
  "disputed",
  "resolved",
  "regressed",
  "superseded",
  "cached",
]);

export const AdjudicationLifecycleStateSchema = z.enum([
  "created",
  "resolved",
  "parked",
  "human_escalated",
]);

export const ReviewTargetBindingLifecycleStateSchema = z.enum([
  "bound",
  "active",
  "superseded",
  "missing",
  "invalid",
]);

export const RedTeamErrorCodeSchema = z.enum([
  "missing_review_target_binding",
  "invalid_prompt_truth_state",
  "context_symmetry_violation",
  "finding_extraction_failed",
  "stale_expected_version",
  "batch_judgment_partial_failure",
  "post_turn_hook_failed",
  "unsupported_child_room_launch",
]);
```

---

# APPENDIX E — Example Payloads

## E1. RoomFinding example

```json
{
  "finding_id": "f_123",
  "room_id": "room_abc",
  "room_turn_id": "turn_7",
  "participant_id": "p_defense",
  "logical_role_key": "defense_counsel",
  "model_id": "claude-opus-4.1",
  "prompt_variant_id": "pv_redteam_defense_v2",
  "prompt_assignment_id": "pa_44",
  "prompt_text_hash": "9f8b8f1d9d4c0aa5",
  "prompt_artifact_kind": "room_role_prompt",
  "active_overlay_ids": ["ov_focus_counterexamples"],
  "prompt_recipe_id": "9f5ed65f-6db6-4b95-bf72-bb9b3a9d8dbe",
  "prompt_artifact_ids": [
    "pv_redteam_defense_v2",
    "ov_focus_counterexamples",
    "pr_recipe_counterarg_v2"
  ],
  "review_target_binding_ref": {
    "room_id": "room_abc",
    "binding_id": "bind_1",
    "doc_id": "doc_789",
    "materialization_mode": "chunk_map"
  },
  "title": "Reply brief omits standard-of-review framing",
  "description": "The reply argues merits without anchoring the applicable review standard.",
  "severity": "major",
  "why_this_matters": "The judge may read the issue under the wrong procedural lens.",
  "evidence_refs": ["doc_789#p4", "turn_7#msg1"],
  "evidence_domain": "document_text",
  "state": "open",
  "starred": true,
  "cited_in_decision": false,
  "structural_hash": "sha256:abcd",
  "created_at": "2026-03-10T12:30:00Z"
}
```

## E2. PromptIdentityRef example

```json
{
  "prompt_artifact_kind": "room_role_prompt",
  "prompt_variant_id": "rtp_architecture_v3",
  "prompt_text_hash": "b6f3b9c4f8a1d2e7",
  "prompt_assignment_id": "asg_01J...",
  "active_overlay_ids": ["ov_docstyle_compact", "ov_focus_counterexamples"],
  "prompt_recipe_id": "9f5ed65f-6db6-4b95-bf72-bb9b3a9d8dbe",
  "prompt_artifact_ids": [
    "rtp_architecture_v3",
    "ov_docstyle_compact",
    "ov_focus_counterexamples",
    "pr_recipe_counterarg_v2"
  ]
}
```

## E3. PromptObservationEmit example

```json
{
  "observation_id": "obs_01J...",
  "room_id": "room_01J...",
  "room_turn_id": "turn_01J...",
  "participant_id": "critic_2",
  "logical_role_key": "counterexample_critic",
  "prompt_artifact_kind": "room_role_prompt",
  "prompt_variant_id": "rtp_architecture_v3",
  "prompt_text_hash": "b6f3b9c4f8a1d2e7",
  "prompt_assignment_id": "asg_01J...",
  "active_overlay_ids": ["ov_focus_counterexamples"],
  "prompt_recipe_id": "9f5ed65f-6db6-4b95-bf72-bb9b3a9d8dbe",
  "prompt_artifact_ids": [
    "rtp_architecture_v3",
    "ov_focus_counterexamples",
    "pr_recipe_counterarg_v2"
  ],
  "review_target_kind": "architecture_spec",
  "evidence_grade": "observational",
  "value_components": {
    "accepted_findings_weight": 2.5,
    "rejected_findings_penalty": 0.5,
    "starred_bonus": 1.0,
    "cited_bonus": 0.0,
    "supervision_cost_penalty": 0.2
  },
  "emitted_at": "2026-03-10T18:15:00Z"
}
```

## E4. PromptPlanViewModel example

```json
{
  "participant_id": "critic_2",
  "room_role_prompt_variant_id": "rtp_architecture_v3",
  "active_overlay_ids": ["ov_focus_counterexamples"],
  "prompt_recipe_id": "9f5ed65f-6db6-4b95-bf72-bb9b3a9d8dbe",
  "prompt_artifact_ids": [
    "rtp_architecture_v3",
    "ov_focus_counterexamples",
    "pr_recipe_counterarg_v2"
  ],
  "truth_state": "available",
  "candidate_apply_allowed": false,
  "candidate_apply_block_reason": "observational_only"
}
```

## E5. Child-room launch example

## E6. Recommendation diff example

```json
{
  "recommendation_id": "rec_017",
  "artifact_kind": "prompt_recipe",
  "current_prompt_artifact_ids": ["role-prosecutor-v4", "overlay-redteam-compact"],
  "proposed_prompt_artifact_ids": ["role-prosecutor-v4", "overlay-redteam-compact", "recipe-brief-precision-v2"],
  "added_overlay_ids": [],
  "removed_overlay_ids": [],
  "current_prompt_recipe_id": null,
  "proposed_prompt_recipe_id": "recipe-brief-precision-v2",
  "proposal_only": true,
  "apply_allowed": false,
  "apply_block_reason": "candidate_locked_observational_only",
  "why_summary": "This recipe improved accepted major findings in replay but has not completed canary or human review.",
  "updated_at": "2026-03-13T18:02:00Z"
}
```


```json
{
  "parent_room_id": "room_parent_01J...",
  "review_target_binding_ref": {
    "room_id": "room_parent_01J...",
    "binding_id": "bind_01J...",
    "doc_id": "doc_01J..."
  },
  "launch_policy": {
    "allow_child_redteam_launch": true,
    "require_human_approval_before_launch": false,
    "default_import_back_strategy": "finding_subset_plus_summary",
    "overlay_application_mode": "explicit_select",
    "explicit_overlay_ids": [
      "ov_focus_counterexamples",
      "ov_keep_proposed_fixes_short"
    ],
    "prompt_recipe_inheritance_mode": "explicit_select",
    "explicit_prompt_recipe_id": "9f5ed65f-6db6-4b95-bf72-bb9b3a9d8dbe"
  }
}
```


# APPENDIX F — Endpoint and Read-Model Contracts

The routes below are required for a buildable red-team UI. Auth follows room access control and user/session permissions owned by DOC10 + DOC12. Where auth is not yet fully standardized, that is a companion-doc gap and must not be silently ignored.

## F1. Commands / mutation routes

### R5.2 additions and clarifications

The following routes are now normative or normatively expected where marked as companion deltas:
- `POST /api/rooms/:roomId/findings/cache/:cacheEntryId/promote`
- `POST /api/rooms/:roomId/recommendations/:recommendationId/apply` *(companion delta)*
- `POST /api/rooms/:roomId/recommendations/:recommendationId/revert` *(companion delta)*
- `POST /api/rooms/:roomId/convergence/override` *(companion delta)*

### R7 additions

The following routes are added in R7:
- `POST /api/rooms/:roomId/unparsed-contributions/:contributionId/extract`
- `POST /api/rooms/:roomId/findings/:findingId/edit`
- `POST /api/rooms/:roomId/child-redteam-launch`
- `POST /api/rooms/:roomId/child-redteam-imports`
- `POST /api/rooms/:roomId/exports/findings-pack`
- `POST /api/rooms/:roomId/exports/review-outcome`
- `POST /api/rooms/:roomId/exports/decision-pack`
- `POST /api/redteam/preview-target-binding`
- `POST /api/rooms/:roomId/child-redteam/preview`


### POST `/api/rooms/:roomId/findings/:findingId/judgments`

**Purpose:** submit a single finding judgment.

**Request schema:** `RoomJudgeFindingRequestSchema`  
**Response schema:**

```ts
const RoomJudgeFindingResponseSchema = z.object({
  status: z.literal("ok"),
  judgment_id: z.string(),
});
```

**Errors:**
- `404 finding_not_found`
- `409 stale_expected_version`
- `422 invalid_payload`
- `503 command_path_unavailable`

**Emits:**
- `room.finding.judged`
- `room.finding.status_changed`
- `room.prompt_observation.emitted` (if downstream emission succeeds)

### POST `/api/rooms/:roomId/findings/judgments:batch`

**Purpose:** batch review default path.

**Request schema:** `RoomBatchJudgeFindingsRequestSchema`  
**Response schema:**

```ts
const RoomBatchJudgeFindingsResponseSchema = z.object({
  status: z.enum(["ok", "partial_failure"]),
  completed: z.number().int().nonnegative(),
  failed: z.number().int().nonnegative(),
  failed_items: z.array(z.object({
    finding_id: z.string(),
    error_code: z.string(),
    message: z.string(),
  })).default([]),
});
```

**Errors:**
- `422 invalid_payload`
- `503 command_path_unavailable`

### POST `/api/rooms/:roomId/findings/:findingId/adjudications`

**Purpose:** create or resolve a finding adjudication.

**Request schema:**

```ts
const RoomAdjudicateFindingRequestSchema = z.object({
  room_id: z.string(),
  finding_id: z.string(),
  decision: z.enum([
    "accept_claim_a",
    "accept_claim_b",
    "park_disputed",
    "escalate_to_human",
    "require_evidence_audit",
  ]),
  decision_basis_refs: z.array(z.string()).default([]),
  confidence: z.number().min(0).max(1).optional(),
  idempotency_key: z.string().min(8),
});
```

**Response schema:**

```ts
const RoomAdjudicateFindingResponseSchema = z.object({
  status: z.literal("ok"),
  adjudication_id: z.string(),
});
```

### POST `/api/rooms/:roomId/findings/:findingId/lineage`

**Purpose:** create or update lineage state after successor-room review.

**Request schema:** `FindingLineageSchema.omit({ lineage_id: true, updated_at: true })`  
**Response schema:**

```ts
const RoomUpsertLineageResponseSchema = z.object({
  status: z.literal("ok"),
  lineage_id: z.string(),
});
```


### POST `/api/rooms/:roomId/child-redteam-launch` *(DOC12-owned companion delta)*

**Purpose:** launch a linked child red-team room with inherited or explicitly selected prompt artifacts.

**Request schema:** `LaunchChildRedTeamRoomRequestSchema`  
**Minimum response:**

```ts
export const LaunchChildRedTeamRoomResponseSchema = z.object({
  child_room_id: z.string(),
  linked_room_group_id: z.string().optional(),
  effective_overlay_ids: z.array(z.string()).default([]),
  effective_prompt_recipe_id: z.string().uuid().optional(),
  import_back_strategy: z.enum([
    "finding_subset",
    "finding_subset_plus_summary",
    "full_redteam_output_ref",
  ]),
});
```

DOC14 does not own this route, but the DOC14 UI cannot rely on child-room launch without a companion DOC12 command/read-model path equivalent to this contract.

### POST `/api/rooms/:roomId/unparsed-contributions/:contributionId/extract` *(R7 addition)*

**Purpose:** manually extract a finding from an unparsed contribution.

**Request schema:** `ManualExtractRequestSchema`  
**Response schema:** `ManualExtractResponseSchema`

**Errors:**
- `404 contribution_not_found`
- `409 already_extracted`
- `422 invalid_payload`

**Emits:**
- `room.finding.created`

### POST `/api/rooms/:roomId/findings/:findingId/edit` *(R7 addition)*

**Purpose:** audited edit or rewrite of a finding.

**Request schema:** `FindingEditRequestSchema`  
**Response schema:** `FindingEditResponseSchema`

**Errors:**
- `404 finding_not_found`
- `409 stale_expected_version`
- `422 invalid_payload`

**Emits:**
- `room.finding.status_changed`

### POST `/api/rooms/:roomId/findings/cache/:cacheEntryId/promote` *(R7 addition — full contract)*

**Purpose:** promote a cached finding to the main findings ledger.

**Request schema:** `RoomPromoteCachedFindingRequestSchema`  
**Response schema:** `RoomPromoteCachedFindingResponseSchema`

**Errors:**
- `404 cache_entry_not_found`
- `409 already_promoted`
- `422 invalid_payload`

**Emits:**
- `room.finding.promoted_from_cache`

### POST `/api/rooms/:roomId/convergence/override` *(R7 addition — full contract)*

**Purpose:** user-initiated convergence override to continue or force-close the room.

**Request schema:** `RoomOverrideConvergenceRequestSchema`  
**Response schema:** `RoomOverrideConvergenceResponseSchema`

**Errors:**
- `403 override_not_allowed`
- `409 room_already_finalized`

**Emits:**
- `room.convergence.overridden`

### POST `/api/rooms/:roomId/recommendations/:recommendationId/apply` *(companion delta DOC15+DOC11)*

**Purpose:** apply a recommendation to the room's prompt-artifact configuration.

**Request schema:** `RecommendationApplyRequestSchema` (with `mutation_kind: "apply"`)  
**Response schema:** `RecommendationApplyResponseSchema`

**Errors:**
- `404 recommendation_not_found`
- `403 apply_blocked` (with `blocked_reason` from `CandidateApplyBlockReasonSchema`)
- `409 stale_expected_version`

**Emits:**
- `room.prompt_artifact.recommendation_applied`

### POST `/api/rooms/:roomId/recommendations/:recommendationId/revert` *(companion delta DOC15+DOC11)*

**Purpose:** revert a previously applied recommendation.

**Request schema:** `RecommendationApplyRequestSchema` (with `mutation_kind: "revert"`)  
**Response schema:** `RecommendationApplyResponseSchema`

**Errors:**
- `404 recommendation_not_found`
- `409 not_currently_applied`

**Emits:**
- `room.prompt_artifact.recommendation_reverted`

### POST `/api/rooms/:roomId/exports/findings-pack` *(R7 addition)*

**Purpose:** generate a findings export pack.

**Request schema:** `RoomExportRequestSchema`  
**Response schema:** `RoomExportResponseSchema`

**Errors:**
- `422 invalid_payload`
- `503 export_service_unavailable`

### POST `/api/rooms/:roomId/exports/review-outcome` *(R7 addition)*

**Purpose:** generate a review outcome export.

**Request schema:** `RoomExportRequestSchema`  
**Response schema:** `RoomExportResponseSchema`

### POST `/api/rooms/:roomId/exports/decision-pack` *(R7 addition)*

**Purpose:** generate a decision pack export.

**Request schema:** `RoomExportRequestSchema`  
**Response schema:** `RoomExportResponseSchema`

### POST `/api/rooms/:roomId/child-redteam-imports` *(R7 addition)*

**Purpose:** import findings from a completed child red-team room back into the parent.

**Request schema:** `ChildImportBackRequestSchema`  
**Response schema:** `ChildImportBackResponseSchema`

**Errors:**
- `404 child_room_not_found`
- `409 already_imported`
- `422 invalid_payload`

**Emits:**
- `room.child.imported_back`

### POST `/api/rooms/:roomId/child-redteam/preview` *(R7 addition)*

**Purpose:** compute and return the effective prompt-plan preview for a child-room launch configuration.

**Request schema:** `ChildRoomPreviewRequestSchema`  
**Response schema:** `ChildRoomPreviewResponseSchema`

**Errors:**
- `422 invalid_payload`
- `503 preview_service_unavailable`

### POST `/api/redteam/preview-target-binding` *(R7 addition)*

**Purpose:** pre-launch target preview — compute materialization plan for a candidate review target.

**Request schema:** `PreLaunchTargetPreviewRequestSchema`  
**Response schema:** `PreLaunchTargetPreviewResponseSchema`

**Errors:**
- `404 document_not_found`
- `422 invalid_payload`

## F2. Query / read-model routes

### R5.2 additions and clarifications

Add or keep explicit:
- `GET /api/rooms/:roomId/findings/:findingId`
- `GET /api/rooms/:roomId/recommendations`
- `GET /api/rooms/:roomId/recommendations/:recommendationId/diff`
- `GET /api/rooms/:roomId/review-target/anchors/:anchorId`
- `GET /api/rooms/:roomId/review-target/chunks/:chunkId`
- `GET /api/rooms/:roomId/convergence`
- `GET /api/rooms/:roomId/configurator-state`

### R7 additions

- `GET /api/rooms/:roomId/recommendations/:recommendationId/source`
- `POST /api/rooms/:roomId/review-target/search`
- `POST /api/rooms/:roomId/child-redteam/preview`
- `GET /api/rooms/:roomId/unparsed-contributions`


### GET `/api/rooms/:roomId/findings`

**Purpose:** hydrate main Findings Panel.

**Response schema:**

```ts
const FindingsPanelViewModelSchema = z.object({
  room_id: z.string(),
  findings: z.array(RoomFindingSchema),
  batch_review_progress: BatchReviewProgressSchema,
  available_actions: z.array(z.string()).default([]),
  degraded_reasons: z.array(z.string()).default([]),
});
```

### GET `/api/rooms/:roomId/findings/cache`

**Purpose:** hydrate Critique Cache Drawer.

**Response schema:**

```ts
const CritiqueCacheViewModelSchema = z.object({
  room_id: z.string(),
  entries: z.array(z.object({
    cache_entry_id: z.string(),
    cache_reason: z.string(),
    candidate: RawFindingCandidateSchema,
    created_at: z.string(),
  })),
  degraded_reasons: z.array(z.string()).default([]),
});
```

### GET `/api/rooms/:roomId/health`

**Purpose:** hydrate health chip and drilldown.

**Response schema:** `RoomHealthSnapshotSchema`

### GET `/api/rooms/:roomId/review-target`

**Purpose:** hydrate review-target viewer and target-truth cards.

**Response schema:**

```ts
const ReviewTargetViewModelSchema = z.object({
  room_id: z.string(),
  active_review_target_ref: ReviewTargetBindingRefSchema,
  document_title: z.string(),
  materialization_mode: ReviewTargetBindingRefSchema.shape.materialization_mode,
  dropped_reason: z.string().optional(),
  chunk_map_ref: z.string().optional(),
  search_tool_enabled: z.boolean().default(false),
});
```

### GET `/api/rooms/:roomId/prompt-plan`

**Purpose:** hydrate participant drawer prompt-truth UI.

**Response schema:**

```ts
const ParticipantPromptPlanViewModelSchema = z.object({
  room_id: z.string(),
  participants: z.array(z.object({
    participant_id: z.string(),
    logical_role_key: z.string(),
    prompt_variant_id: z.string().optional(),
    prompt_artifact_kind: PromptArtifactKindSchema,
    prompt_text_hash: z.string().min(16),
    active_overlay_ids: z.array(z.string()).default([]),
    prompt_recipe_id: z.string().uuid().optional(),
    prompt_artifact_ids: z.array(z.string()).default([]),
    dropped_overlay_ids: z.array(z.string()).default([]),
    overlay_trim_reasons: z.record(z.string(), z.string()).default({}),
    recommendation_source: z.string().optional(),
    candidate_apply_allowed: z.boolean().default(false),
    candidate_apply_block_reason: z.string().optional(),
    truth_state: z.enum(["available", "degraded", "unavailable"]),
  })),
});
```

### GET `/api/rooms/:roomId/findings/:findingId` *(R7 addition — full contract)*

**Purpose:** hydrate Finding Detail Modal.

**Response schema:**

```ts
const FindingDetailViewModelSchema = z.object({
  finding: RoomFindingSchema,
  judgment_history: z.array(FindingJudgmentSchema).default([]),
  adjudication: FindingAdjudicationRecordSchema.optional(),
  lineage: z.array(FindingLineageSchema).default([]),
  ghost_judgments: z.array(GhostJudgmentSchema).default([]),
  prompt_provenance: PromptIdentityRefSchema.optional(),
  degraded_reasons: z.array(z.string()).default([]),
});
```

### GET `/api/rooms/:roomId/recommendations` *(R7 addition — full contract)*

**Purpose:** hydrate Recommendation Preview.

**Response schema:**

```ts
const RecommendationListViewModelSchema = z.object({
  room_id: z.string(),
  recommendations: z.array(PromptArtifactRecommendationNodeSchema),
  degraded_reasons: z.array(z.string()).default([]),
});
```

### GET `/api/rooms/:roomId/recommendations/:recommendationId/source` *(R7 addition)*

**Purpose:** hydrate recommendation source signal panel.

**Response schema:** `RecommendationSourceViewModelSchema`

### GET `/api/rooms/:roomId/convergence` *(R7 addition — full contract)*

**Purpose:** hydrate Convergence Banner.

**Response schema:** `RoomConvergenceViewModelSchema`

### GET `/api/rooms/:roomId/configurator-state` *(R7 addition — full contract)*

**Purpose:** hydrate Room Configurator.

**Response schema:** `ConfiguratorStateViewModelSchema`

### GET `/api/rooms/:roomId/review-target/anchors/:anchorId` *(R7 addition — full contract)*

**Purpose:** resolve an evidence anchor to its document location.

**Response schema:** `ReviewTargetAnchorResponseSchema`

### GET `/api/rooms/:roomId/review-target/chunks/:chunkId` *(R7 addition — full contract)*

**Purpose:** retrieve a specific review-target chunk.

**Response schema:** `ReviewTargetChunkResponseSchema`

### POST `/api/rooms/:roomId/review-target/search` *(R7 addition — full contract)*

**Purpose:** search the active review target in search-assisted mode.

**Request schema:** `ReviewTargetSearchRequestSchema`  
**Response schema:** `ReviewTargetSearchResponseSchema`

### GET `/api/rooms/:roomId/unparsed-contributions` *(R7 addition)*

**Purpose:** hydrate unparsed contributions list in Critique Cache Drawer.

**Response schema:**

```ts
const UnparsedContributionListViewModelSchema = z.object({
  room_id: z.string(),
  contributions: z.array(UnparsedContributionSchema),
  degraded_reasons: z.array(z.string()).default([]),
});
```

## F3. SSE / reactive updates

### R5.2 SSE payload completion requirement

Every normalized event listed in F3 must have a typed payload schema in the shared contracts package or owner doc projection package. At minimum this includes:

```ts
export const RoomFindingCreatedEventSchema = z.object({
  event: z.literal("room.finding.created"),
  room_id: z.string(),
  finding_id: z.string(),
  severity: z.string(),
  participant_id: z.string().optional(),
  route_trace_id: z.string().optional(),
  emitted_at: z.string(),
});

export const RoomFindingStatusChangedEventSchema = z.object({
  event: z.literal("room.finding.status_changed"),
  room_id: z.string(),
  finding_id: z.string(),
  old_state: z.string().optional(),
  new_state: z.string(),
  emitted_at: z.string(),
});

export const RoomHealthUpdatedEventSchema = z.object({
  event: z.literal("room.health.updated"),
  room_id: z.string(),
  health_ref: z.string(),
  stale: z.boolean().default(false),
  emitted_at: z.string(),
});
```

#### R7 addition — expanded typed event payload schemas

```ts
export const RoomFindingJudgedEventSchema = z.object({
  event: z.literal("room.finding.judged"),
  room_id: z.string(),
  finding_id: z.string(),
  judgment_id: z.string(),
  disposition: z.string(),
  occurred_at: z.string(),
  operation_id: z.string().optional(),
  route_trace_id: z.string().optional(),
});

export const RoomBatchJudgmentProgressEventSchema = z.object({
  event: z.literal("room.batch_judgment.progress"),
  room_id: z.string(),
  completed: z.number().int().nonnegative(),
  total: z.number().int().nonnegative(),
  failed_item_ids: z.array(z.string()).default([]),
  occurred_at: z.string(),
});

export const RoomFindingPromotedFromCacheEventSchema = z.object({
  event: z.literal("room.finding.promoted_from_cache"),
  room_id: z.string(),
  cache_entry_id: z.string(),
  finding_id: z.string(),
  occurred_at: z.string(),
});

export const RoomConvergenceReachedEventSchema = z.object({
  event: z.literal("room.convergence.reached"),
  room_id: z.string(),
  convergence_score: z.number().min(0).max(1),
  occurred_at: z.string(),
});

export const RoomConvergenceOverriddenEventSchema = z.object({
  event: z.literal("room.convergence.overridden"),
  room_id: z.string(),
  rationale: z.string().optional(),
  occurred_at: z.string(),
});

export const RoomPromptObservationEmittedEventSchema = z.object({
  event: z.literal("room.prompt_observation.emitted"),
  room_id: z.string(),
  observation_id: z.string(),
  occurred_at: z.string(),
  route_trace_id: z.string().optional(),
  operation_id: z.string().optional(),
});

export const RoomChildCreatedEventSchema = z.object({
  event: z.literal("room.child.created"),
  room_id: z.string(),
  child_room_id: z.string(),
  linked_room_group_id: z.string().optional(),
  occurred_at: z.string(),
});

export const RoomChildImportedBackEventSchema = z.object({
  event: z.literal("room.child.imported_back"),
  room_id: z.string(),
  child_room_id: z.string(),
  imported_finding_ids: z.array(z.string()).default([]),
  occurred_at: z.string(),
});
```

The hook dispatcher may not satisfy DOC14 by emitting only a generic `room.post_turn.completed` event.

#### R7 addition — DOC12 event-bundle consumption rule

DOC12 hook dispatch must iterate `PostTurnResultSchema.event_bundle` and emit every included event via SSE with the typed payload supplied by DOC14. If `event_bundle` is empty, DOC12 may emit only `room.post_turn.completed`.

Every event must carry:
- `room_id`
- `occurred_at`
- `operation_id` if mutation-linked
- `route_trace_id` if route-linked


The following events are required for red-team UI reactivity:

- `room.finding.created`
- `room.finding.judged`
- `room.finding.status_changed`
- `room.finding.promoted_from_cache`
- `room.finding.adjudicated`
- `room.finding.lineage_updated`
- `room.batch_judgment.progress`
- `room.health.updated`
- `room.convergence.reached`
- `room.convergence.overridden`
- `room.prompt_observation.emitted`
- `room.prompt_artifact.inherited`
- `room.prompt_artifact.trimmed`
- `room.prompt_artifact.recommendation_applied`
- `room.prompt_artifact.recommendation_reverted`
- `room.prompt_artifact.proposal_only`
- `room.review_target.binding_missing`
- `room.review_target.materialization_changed`
- `room.review_target.search_performed`
- `room.child.created`
- `room.child.imported_back`

Each event payload must include:

- `room_id`
- `operation_id`
- `route_trace_id`
- `occurred_at`
- and event-specific fields.


## Final R7 stance

DOC14 R7 is a **full reconstruction** from the R5.2 completeness baseline with all validated R6/R6.1 improvements merged as surgical patches. Nothing from R5.2 was removed unless explicitly amended. All appendices, acceptance tests, example payloads, file/module plans, and implementation scaffolding are preserved.

This revision fixes every prior P0: the disposition-to-state map is used in the handler, the scoring adapter is wired, the observation schema has all enrichment fields, the materialization selector is real code, the convergence algorithm is real, the child-room resolver handles all 24 mode combinations, the evidence gate has per-severity rules, and the tool injection for large documents actually injects tools.

DOC12 R7.1 is treated as the accepted room-side substrate. DOC11, DOC15, DOC8, DOC10, and DOC13 remain open blockers tracked in the companion. Phase 0 ships with baseline prompting, honest degradation, and no phantom UI. Phase 1+ features are correctly gated behind companion-doc acceptance.

The companion (R2) must be rebuilt in parallel using the same preservation-first methodology.