Elnor Repo Reader

DOC24_R3_1_1.md

Current Specs/DOC24/DOC24_R3_1_1.md

Short text page f0d3159f40f5. 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/DOC24/DOC24_R3_1_1.md
Source repo: /Users/OpenClaw1/Elnor/Elnor Specs
Git branch: main
Git commit: dbaa25962edc11ab30e8d4ca1715f9ae5bf77331
Generated: 2026-06-09T01:23:58.539Z

---

# DOC24 — ELNOR Unified Knowledge, Capability, Onboarding, Routing, Invocation, and Delivery Architecture

**Version:** R3.1.1
**Status:** Consolidated (§0 through §38 + Appendices A–D; 7 audit-identified gaps closed per audit report 2026-05-17; ready for fresh-window reviewer prompts)
**Lineage:** R3 + V1 (136 adjudication cards) + V1.1 (cross-card normalization) + V1.2 (citation hygiene) + V1.3 (HH preflight closure) + V1.4 (II missed R3 field restorations + lint citations) + R3.1 audit closure

| Version | Date | Summary |
|---|---|---|
| R0 | 2025-12-12 | Initial layered architecture (knowledge, capability, onboarding, routing, invocation, delivery). |
| R1 | 2026-01-08 | DeliveryDirective unification; KDA rendering tier integration; PropA policy-gated retrieval. |
| R2 | 2026-02-10 | Three-lane retrieval split (entity / bucket / authority); compaction pipeline; PBE-lite scope. |
| R2.5 | 2026-03-04 | Tool capability graph; Thompson sampling foundations; OutcomeClass; readiness assessment. |
| R3 | 2026-04-26 | 125-card red team adjudication across four reviewers. New §38 Runtime Lifecycle: packet lifecycle states, ContextAssemblyTrace, UnifiedInjectionManifest, race-safety with policy_generation_id, MatterContextSlot stack, suspension/resume with SavedContextProfile/Snapshot split, demonstration segments, disambiguation state machine, attribution-safe feedback, pre-dispatch linter, pipeline ordering. DeliveryDirective unified across DOC24/BDSM/KDA. Five-step entity resolution chain. Source-exclusion privacy fix. Tool capability edge wiring + atomic registration + Thompson selection. Authority anchor cascade through DOC72/DOC73. Onboarding studio session abandonment policy. Digest disposition split. Transient instruction promotion. Unified outcome taxonomy. Adversarial acceptance test harness (79 tests). 32 cross-doc obligations on companion docs. |
| **R3.1** | **2026-05-16** | **V1.x adjudication closure incorporated.** All 136 V1 cards' spec changes integrated. V1.1 canonical type ownership (Y), patch families (Z), OP-A dispositions (AA), R3.1 §-anchor allocations (BB), test mutations (CC), buildability lint (DD). V1.2 citation hygiene (GG). V1.3 R3.1 preflight closure (HH): state machine restoration (HH.1), manifest contracts (HH.2), reconciliation event model (HH.3), algorithm corrections (HH.4), authority/policy (HH.5), canonical types (HH.6), PBE-lite provenance (HH.7), saga concurrency (HH.9), test taxonomy (HH.10), schema scoping (HH.11), long-chunk extraction (HH.12). V1.4 missed R3 field restorations (II): ManifestCardEntry KDA variant-tracking fields, PacketInjectionManifest `assembly_completed_at` + `superseded_by_packet_id`, ToolOutcomeEvidenceEvent `selection_decision_id` thread, three DD.1 lint closure citations. |
| **R3.1.1** | **2026-05-17** | **Audit closure patch.** Seven gaps identified in R3.1 consolidation audit closed: (1) HH.4.10 `applyMatrixConfidenceAdjustmentSafe` body added to §13.4B.2 with `BETA_EPSILON`/`EFFECTIVE_BETA_TOTAL_CAP` constants; (2) HH.12 `ExtractionPromptSectionId` A–I enum + `DEFAULT`/`LONG_CHUNK` orderings + `LongChunkMitigation` added as new §38.19 (dangling §38.X reference fixed); (3) HH.15.8 token-count write-time rule + `TokenizerDriftCheckPolicy`/`TokenizerDriftObservation` added to §38.11.5; (4) Y.14 `WatchpointRef` discriminated union added as §5.4.1.Z; (5) Y.15 `ObservedActionRef` added as §5.4.1.AA; (6) Y.23 `SessionInjectionManifest` added as §5.4.1.BB with `reconciliation_overlay` variant renamed to `reconciliation_event` per HH.3 event model; (7) II.4.2 `walkPacketDecisionGraph` paste-ready body added to §38.10.1. No directional changes — all patches are pure-add from V1.x canonical sources. |

| Addendum | Lines (est.) | Domain | R3.1 relationship |
|---|---|---|---|
| **Addendum A — BDSM (Behavioral Dynamics & Salience Matrix) V6.5+** | ~3,200 | DeliveryDirective canonical schema; satisfaction scoring; attribution matrix; Shapley computation (BDSM-owned) | §26.5/§26.6/§29 reference BDSM V6.5+ §2 DeliveryDirective as canonical. §38.2 manifest schema integrates with BDSM attribution per 3-way join contract (HH.2.7). OP-A §6.23 tracks BDSM V6.5+ obligations. |
| **Companion — KDA R3+** | — | RenderingTier; variant tracking | §26.6/§29 reference KDA R3+ rendering. PacketInjectionManifest carries variant tracking per II.1 restoration. OP-A §6.24 tracks. |
| **Companion — PropA R6+** | — | Policy decision schema; source eligibility | §38.3 race-safety. OP-A §6.22. |
| **Companion — DOC72 R6+** | — | Entity graph; NodeKind; OutcomeClass; AuthorityResult; EvidenceRef | All imported types per HH.11.2. OP-A §6.21. |
| **Companion — DOC73 V1.5+** | — | ConsolidatedUnderstanding; authority resolution | §26.5.2 confidence-floor cascade; PBE-lite (HH.7). OP-A §6.21. |
| **Companion — DOC8** | — | Pattern detection; utility ledgers; Shapley | 3-way manifest join (HH.2.7). |
| **Companion — DOC11** | — | Gateway; final-prompt span coverage | HH.2.4. |
| **Companion — DOC15** | — | Cognitive infrastructure | — |
| **Companion — DOC25** | — | Tier negotiation; tokenization | HH.2.9, HH.8.3. |

**Companion artifacts to R3.1** (build-time, generated mechanically, attached as separate files):

| Artifact | Generated from | Owner |
|---|---|---|
| `LintClosureTable` | DD.1 + V1.3 HH.0.2 + V1.4 II.4 | DOC24 |
| `ReviewerFindingIndex` | Original review files (mechanical grep per HH.0.3) | DOC24 |
| `AcceptanceTestRegistry` | V1.1 CC + V1.3 HH.10 + V1.4 II tests | DOC24 |
| `InjectionSlotRegistry` | HH.6.5 + per-section slot definitions | DOC24 |
| `ReasonCodeNamespaceRegistry` | HH.5.3 | DOC24 |
| `DegradedReasonCodeRegistry` | HH.15.2 | DOC24 |
| `DOC24_IMPORTED_TYPES` | HH.11.2 | DOC24 |
| `OPACrossCheckResult` | HH.8.2 mechanical verification against OP-A | DOC24 |

---

## 0. Purpose and scope *(R3.1 — extended)*

DOC24 specifies the unified architecture for ELNOR's knowledge, capability, onboarding, semantic routing, invocation, and delivery layers, including the runtime lifecycle that binds them into one deterministic machine.

DOC24 owns:

- The entity graph world model and persistence model (§5–§7).
- The entity creation, update, and lifecycle pipeline (§8).
- Background indexing and passive learning (§9).
- Memory integration with cards, buckets, and authority constellations (§10).
- Self-learning and reflection cycles (§11).
- Onboarding architecture (§12).
- Semantic routing and action readiness assessment (§13).
- The stable semantic capability registry and tool capability graph (§14, §13.4B).
- Live action state (§15).
- Tool packs and JIT/sticky mounting (§16).
- Invocation architecture (§17).
- Receipts, confirmation, and outcome truth (§18).
- Runtime packet and injection model (§19) — extended in §38.
- UI/operator surfaces (§20).
- EC Core service contract (§21).
- Cross-doc obligations summary (§22) — operationalized in OP-A.
- Generation, CI, and maintenance (§23).
- Validation and acceptance obligations (§24).
- Open questions register (§25).
- Knowledge-to-LLM delivery architecture (§26–§37).
- **Runtime lifecycle, assembly contract, and session continuity (§38).** This is the cross-cutting binding layer.

DOC24 does NOT own (cross-doc; listed for boundary clarity):

- Satisfaction scoring, utility attribution, Shapley computation — **BDSM (Addendum A) and DOC8**.
- Rendering tier internals, neuroplasticity, variant performance — **KDA**.
- Policy decision evaluation, source eligibility internals — **PropA**.
- Entity graph type system, OutcomeClass canonical enum, EvidenceRef — **DOC72**.
- Authority constellation resolution, ConsolidatedUnderstanding, PBE extraction internals — **DOC73**.
- Final-prompt assembly (when DOC11 or OpenClaw is final-prompt owner) — **DOC11 / OpenClaw**.
- Tier negotiation and compressed bundle serialization — **DOC25**.

R3.1 closes V1.x adjudications without reopening V1's 136 cards. Where V1.x patches conflict with R3 prose, V1.x governs per the lineage precedence rule (HH.0.1 + V1.4 II.0.3):

```
0. V1.4 governs missed R3 field restorations and lint closure citations.
1. V1.3 governs structural corrections in Part HH (except where V1.4 II patches HH).
2. V1.2 governs citation hygiene (GG), except where V1.3 HH.13 overrides.
3. V1.1 governs cross-card normalization (X–FF), except where HH or II overrides.
4. V1 governs per-finding rationale where V1.1/V1.2/V1.3/V1.4 are silent.
5. R3 prose governs where all V1.x adjudications are silent.
```

This precedence is normative; R3.1 prose throughout the document is written under it.

---

## 1. Design goals

R3.1 preserves R3's eight design goals and adds three R3.1 goals derived from V1.x adjudication:

**R3 design goals (unchanged):**

1. **Unified knowledge model.** One entity graph; one resolution chain; one rendering pipeline. No parallel "memory systems."
2. **Local-first execution.** OpenClaw is the runtime; EC is the sole durable writer; everything is rebuildable from append-only events.
3. **Domain-agnostic core, domain-aware profiles.** All domain-specific behavior lives in pluggable DomainSignalProfile objects. The core knows nothing about legal or finance or biology; it knows about NodeKind, EntityRef, ConsolidatedUnderstanding.
4. **Attribution-safe feedback.** Outcomes attribute only to cards actually delivered (3-way manifest join, R3.1 HH.2.7). No utility signal from a card the LLM never saw.
5. **Race-safe runtime.** Policy and bundle generations are snapshotted; revalidation catches drift; reassembly restarts cleanly without losing diagnostic trail.
6. **Inspectable assembly.** Every packet has a manifest; every manifest has a trace; every trace has a decision graph. The Packet Inspector can answer "why was card X included/excluded?"
7. **Confidence floors and forced-hedging.** When data is missing, the spec degrades visibly via `degraded_reason_codes` and `hedge_mode`. No silent skew.
8. **Cross-doc obligation discipline.** Every dependency on BDSM/KDA/PropA/DOC72/DOC73/DOC8/DOC11/DOC25 lives in OP-A as a named obligation row, not buried in prose.

**R3.1 additional goals (V1.x derived):**

9. **Buildability over expressiveness.** Every type referenced is defined; every union is exhaustive; every escape hatch (`Record<string, unknown>`) is closed unless explicitly marked non-normative. Coding agents reading R3.1 cannot produce a partial schema by accident (V1.3 HH.6, V1.4 II.1).
10. **R3 field restoration discipline.** No silent field drops between R3 and R3.1. Every R3 §38.2.2 field is either preserved, explicitly renamed with rationale, or absorbed into a discriminated union with restoration of semantics (V1.4 II.1, II.2).
11. **Mechanical audit closure.** Coverage metrics derive from generated `ReviewerFindingIndex` (HH.0.3) and `LintClosureTable` (HH.0.2), not prose counts. Companion artifacts are required, not optional.

---

## 2. Core philosophy

DOC24's design philosophy follows three principles:

**Principle 1: Entity graph is the single source of truth.** Every assertion, every relationship, every authority anchor, every tool capability lives as a node or edge in the DOC72-owned entity graph. There is no parallel "memory store" or "fact database." When a knowledge card is rendered to the LLM, it represents one or more entity graph nodes; when the LLM produces an outcome, the outcome attributes back to those nodes via reconciliation events (§38.9 + HH.3).

This principle has structural consequences:
- Knowledge cards are *projections* of entity graph state at a point in time, not independent records.
- Card identity is `card_stable_key` derived from `node_ref + source_ref_canonical_form + rendering_variant_seed` (HH.6.13). The same underlying entity can produce multiple cards if rendering varies, but each has a stable cross-packet identity.
- "Forgetting" is supersession, not deletion. A retracted entity remains queryable for audit; only its `lifecycle_state` changes (§38).

**Principle 2: The manifest is the contract.** Three manifest artifacts (`CandidateInjectionManifest`, `PacketInjectionManifest`, `FinalPromptInjectionManifest`) form a chain from assembly to delivery. Every downstream consumer (BDSM, KDA, PropA, DOC11 inspector, Packet Inspector UI) reads from these manifests. **No consumer reads from a different source.** The manifest is the boundary between "what DOC24 decided" and "what the world did with it."

The 3-way join contract (HH.2.7) makes this principle operational: BDSM/DOC8 utility attribution joins `packet_manifest × final_prompt_manifest × reconciliation_events`, and signals emit only for cards present in `final_prompt_manifest` with a reconciled outcome.

**Principle 3: Degradation is visible.** When DOC24 cannot deliver its best output — because authority constellation collapsed, because budget overflow forced reference-only rendering, because PBE-lite returned a cached fallback — the manifest carries `degraded_reason_codes`, `card_presence` may shift to `included_reference_only`, and the LLM sees explicit hedging signals (`hedge_mode`, `still_current` flags). DOC24 never silently downgrades; the operator and the LLM both know.

This principle is operationalized via the `DegradedReasonCodeRegistry` (HH.15.2) and the `AuthorityRenderDecision` discriminated union (HH.5.1), which distinguishes `render_inline` (success — including computed authority), `render_diagnostic_reference` (visible degradation), and `exclude_from_packet` (terminal).

---

## 3. Architectural invariants

R3.1 codifies thirteen architectural invariants. The first ten are inherited from R3; the last three are introduced by V1.x adjudications.

**INV-1: EC is the sole durable writer.** No service other than EC Core writes to durable stores. Other services emit events; EC applies them. This is the basis for rebuildability.

**INV-2: OpenClaw is the runtime.** Q Dashboard is the operator surface. EC Core is the durable layer. OpenClaw orchestrates. No other process holds runtime context.

**INV-3: Append-only canonical truth.** Every durable artifact (manifest, lint result, reconciliation event, tool outcome event, policy snapshot) is append-only. Current-view projections are rebuildable.

**INV-4: Policy and bundle generations are race-safe.** Every packet captures `policy_generation_snapshot` (HH.5.4) at assembly start; revalidation at dispatch detects drift; race produces `aborted_for_retry` with fresh snapshot acquisition (II.5.1).

**INV-5: Attribution requires delivery proof.** No utility signal emits for a card absent from `final_prompt_manifest`. The 3-way join contract is mandatory (HH.2.7).

**INV-6: Lint precedes durability.** `CandidateInjectionManifest` is linted before any durable write. Failed lint produces `PacketLintFailureRecord + BlockedPacketManifest` with `candidate_manifest_at_block` preserved (HH.2.2). No durable `PacketInjectionManifest` is written for a failed packet.

**INV-7: Source eligibility is policy-gated.** Every card has `source_eligibility_aggregate`; no card enters a manifest without policy clearance. Source-exclusion (`excluded_workspace_ids`) is enforced at retrieval AND at delivery directive assignment (R3 ADJ-RT-072).

**INV-8: Authority cascade is bounded.** Authority resolution traverses the DOC72/DOC73 constellation with `frontier_cap` and cycle detection. `AuthorityRenderDecision` distinguishes success (`render_inline`), bounded degradation (`render_diagnostic_reference`), and exclusion (`exclude_from_packet`).

**INV-9: Tokenizer alignment is per-packet.** One `tokenizer_ref` per `PacketInjectionManifest`; per-card `tokenizer_ref` must match. Tokenizer drift between assembly and dispatch blocks the packet (HH.2.9).

**INV-10: Cross-doc obligations are named.** Every dependency on a non-DOC24 doc lives as an OBL-* row in OP-A. Prose-only obligations are forbidden.

**INV-11 (R3.1 — V1.3 HH.6.5): Injection slots are registered.** Every span of the final prompt corresponds to a registered `InjectionSlotId` with `owner_doc ∈ InjectionOwnerDoc`. The `InjectionSlotRegistry` is a build-time artifact; references to unregistered slots fail build.

**INV-12 (R3.1 — V1.3 HH.11.1): Schema-version reset is DOC24-local.** `schema_version: 1` markers reset for DOC24-owned types only. Imported types retain their owner-doc schema versions per `DOC24_IMPORTED_TYPES` (HH.11.2).

**INV-13 (R3.1 — V1.4 II.1): R3 field restoration discipline.** No silent drops between R3 and R3.1. Every R3 §38.2.2 field is preserved, renamed with explicit rationale, or absorbed into a discriminated union with full semantic restoration.

---

## 4. Unified layered architecture

R3.1 retains R3's six-layer architecture:

```
┌──────────────────────────────────────────────────────────────────────┐
│ Layer 6 — Delivery (§19, §26–§37, §38)                                │
│   PacketAssemblyOrchestrator + ContextAuthorityHandoff + DOC11/OpenClaw│
│   ↓ PacketInjectionManifest, FinalPromptInjectionManifest             │
├──────────────────────────────────────────────────────────────────────┤
│ Layer 5 — Invocation (§17, §18)                                       │
│   Tool execution; receipts; OutcomeClass routing                       │
├──────────────────────────────────────────────────────────────────────┤
│ Layer 4 — Capability and routing (§13, §14, §15, §16)                 │
│   Semantic routing; tool capability graph; Thompson selection          │
│   ↓ ToolSelectionResult, ToolOutcomeEvidenceEvent                     │
├──────────────────────────────────────────────────────────────────────┤
│ Layer 3 — Onboarding and reflection (§11, §12)                        │
│   DomainSignalProfile; OnboardingCommitSaga; self-learning             │
├──────────────────────────────────────────────────────────────────────┤
│ Layer 2 — Knowledge and memory (§6, §7, §8, §9, §10)                  │
│   Entity graph; persistence; pipeline; indexing; memory integration    │
├──────────────────────────────────────────────────────────────────────┤
│ Layer 1 — Data model (§5)                                              │
│   Spaces; canonical schemas; ActiveContextSlot; MatterContextSlot stack│
└──────────────────────────────────────────────────────────────────────┘

Cross-cutting:
  §38 Runtime lifecycle binds layers 4-6 via:
    - Lifecycle state machine (HH.1)
    - Manifest contracts (HH.2)
    - Reconciliation events (HH.3)
    - Saga concurrency (HH.9)
    - Authority/policy snapshot (HH.5)
```

Layer dependencies are strictly downward: Layer N may depend on Layer N-k for k > 0; layers never depend upward.

Cross-doc consumers of layered output:

```
BDSM V6.5+  ← Layer 6 PacketInjectionManifest (attribution learning)
KDA R3+     ← Layer 6 PacketInjectionManifest.card_records (variant tracking — restored II.1)
PropA R6+   ← Layer 6 policy_generation_id, source_eligibility_aggregate
DOC8        ← Layer 6 reconciliation_events (pattern detection)
DOC11       ← Layer 6 FinalPromptInjectionManifest (gateway annotation)
DOC72 R6+   ← Layer 2 entity graph mutations (EC-applied events)
DOC73 V1.5+ ← Layer 2 ConsolidatedUnderstanding queries; Layer 6 AuthorityRenderDecision
```

---

## 5. Data model

R3.1 preserves R3's data-model structure (§5.1–§5.4A) and incorporates V1.x canonical type additions throughout §5.4.1.

### 5.1 Canonical major categories

```ts
type CanonicalMajorCategory =
  | "entity_graph"            // §6; DOC72-owned types imported
  | "context_slot"            // §5.3, §5.3A, §5.3B
  | "knowledge_card"          // §10, §26
  | "bucket"                  // §10, §16
  | "authority_constellation" // DOC72/DOC73-owned types imported
  | "tool_capability"         // §14, §13.4B
  | "delivery_packet"         // §19, §38
  | "injection_manifest"      // §38.2 — three-manifest chain
  | "reconciliation"          // §38.9, HH.3
  | "policy_snapshot"         // §38.3, HH.5
  | "session_artifact"        // §38.4, §38.5, §38.6
  | "onboarding_artifact"     // §12
  | "audit_artifact";         // build-time companion artifacts
```

### 5.2 Spaces

DOC24 spaces are the top-level partition of entity graph identity. R3.1 preserves R3's space model:

```ts
type SpaceKind =
  | "matter"                  // legal matter or domain-specific work container
  | "personal"                // user-owned, non-shared
  | "firm_shared"             // organization-shared
  | "private"                 // sub-personal; user-encrypted access
  | "system";                 // ELNOR-internal (bootstrap profiles, system seeds)

type Space = {
  space_id: string;
  space_kind: SpaceKind;
  display_name: string;
  owner_principal_id: string;
  scope_inference_basis?: ScopeInferenceBasis;  // per DOC72 R6+ — how scope was determined
  created_at: string;
  schema_version: 1;
};

type ScopeInferenceBasis =
  | "matter_linked"           // entity is matter-scoped because of explicit matter linkage
  | "domain_profile_extracted" // extracted via DomainSignalProfile → firm_shared
  | "user_assigned"           // user explicitly assigned space
  | "system_seed"             // bootstrap or default
  | "inferred_default";       // fallback (rare; logged for audit)
```

Scope inference happens at write time per DOC72 R6+ contract. Matter-linked or domain-profile-extracted nodes → `firm_shared`; everything else → `personal` unless user-assigned otherwise.

### 5.3 Active context slot *(R3 — extended per ADJ-RT-068; R3.1 — unchanged)*

The ActiveContextSlot is the top-of-stack matter or work-context binding for the current operation. R3 introduced the MatterContextSlot stack to support concurrent context across multiple matters (e.g., quick switch from Paramount to Marex without losing Paramount's working state).

```ts
type ActiveContextSlot = {
  slot_id: string;
  matter_context_slot_id?: string;  // present when current context is a matter
  work_context_id: string;
  activated_at: string;
  schema_version: 1;
};

type MatterContextSlot = {
  slot_id: string;
  matter_id: string;
  matter_display_name: string;
  jurisdiction?: { primary: string; appellate_path?: string[] };  // per ADJ-RT-107
  domain_signal_profile_id?: string;  // when matter has a domain profile bound
  pinned_authority_constellations: string[];
  most_recent_activity_at: string;
  schema_version: 1;
};

type MatterContextSlotStack = {
  stack: MatterContextSlot[];           // top of stack = current
  max_depth: number;                    // default 5; LRU eviction beyond
  schema_version: 1;
};
```

### 5.3A Active context mutation rule

Active context mutations are SERIALIZED through EC Core. Concurrent mutation attempts use `expected_context_revision` (HH.9.1) for optimistic concurrency control.

```ts
type ContextMutationRequest = {
  request_id: string;
  expected_context_revision: string;     // ULID; per V1.1 ADJ-RT-R3-122
  mutation_kind:
    | "push_matter_context"
    | "pop_matter_context"
    | "switch_active_context"
    | "suspend_current_context"
    | "resume_suspended_context";
  payload: ContextMutationPayload;
  schema_version: 1;
};

type ContextMutationResult =
  | { outcome: "applied"; new_context_revision: string; schema_version: 1 }
  | { outcome: "concurrency_conflict"; observed_revision: string; schema_version: 1 }
  | { outcome: "rejected"; reason_code: ReasonCode; schema_version: 1 };
```

The OnboardingCommitSaga and other multi-step writers use the saga-step concurrency protocol (HH.9.1) to refresh `expected_context_revision` on retry, not blindly retry with the stale revision.

### 5.3B Context interruption and suspension *(ADJ-74; R3 — extended per ADJ-RT-076)*

When a long-running operation must be interrupted (user switches matters mid-extraction; demonstration mode triggers; disambiguation pauses the flow), the in-flight context is captured in a `SuspendedContextSnapshot` and pushed onto the suspension stack.

```ts
type SuspendedContextSnapshot = {
  snapshot_id: string;
  suspended_at: string;
  suspended_operation_id: string;
  scope_snapshot: ScopeSnapshot;          // per HH.6.6 — captures matter/work context
  in_flight_artifacts: SuspendedArtifactRef[];
  resume_eligibility: ResumeEligibility;
  schema_version: 1;
};

type SuspendedArtifactRef = {
  artifact_kind: "candidate_manifest" | "extraction_buffer" | "saga_state" | "disambig_session" | "demonstration_segment";
  artifact_id: string;
  partial_completion_marker?: string;     // for resume continuation
};

type ResumeEligibility = {
  resumable_until: string;                // ISO-8601 UTC; suspension expires after this
  resume_requires_user_confirmation: boolean;
  resume_blocked_reason_codes?: ReasonCode[];
  schema_version: 1;
};

type SavedContextProfile = {
  profile_id: string;
  matter_context_slot: MatterContextSlot;
  pinned_at: string;
  schema_version: 1;
};
```

The R3 split (per ADJ-RT-076) distinguishes:
- `SavedContextProfile` — durable, user-pinned matter context (e.g., "Marex" can be restored at any time).
- `SuspendedContextSnapshot` — transient, ephemeral in-flight state with a `resumable_until` expiry.

Resumption protocol per §38.6.

### 5.4 Canonical schemas

R3.1 §5.4 contains the data-model type definitions consumed across the document. Per HH.11.1, `schema_version: 1` marks DOC24-owned types; imported types (NodeKind, OutcomeClass, DeliveryDirective, etc.) retain their upstream `schema_version` values per HH.11.2 `DOC24_IMPORTED_TYPES` registry.

#### 5.4.0 EntityKind ↔ node_kind type mapping *(ADJ-01; R3.1 — references DOC72-owned enum)*

`NodeKind` is the DOC72-owned canonical enum (HH.6.2). DOC24 imports it; R3.1 does not redefine.

```ts
// Imported from DOC72 R6+ §2.1:
import type { NodeKind } from "@elnor/doc72-types";

// NodeKind values:
//   "entity" | "concept" | "procedure" | "skill" | "matter_context" |
//   "work_context" | "tool_capability" | "authority_constellation" |
//   "schema_definition" | "source" | "user_directive" | "system_artifact"
```

The R2.5 `EntityKind` mapping is retired; DOC24 R3.1 references `NodeKind` directly.

#### 5.4.1 Core type definitions

R3.1 consolidates V1.x canonical types into §5.4.1 in this canonical order: identity → references → presence → directives → budget → manifest → reconciliation → tool capability → policy → authority → session → audit.

##### 5.4.1.A Identity and references

```ts
type EntityRef = {
  entity_id: string;
  canonical_name: string;
  node_kind: NodeKind;
  schema_version: 1;
};

type WorkContextRef = {
  entity_id: string;
  canonical_name: string;
  node_kind: NodeKind;
  entity_type?: string;
  // R3.1 per ADJ-RT-R3-107: legal-domain matters carry jurisdiction in core (not facet)
  jurisdiction?: {
    primary: string;             // e.g., "C.D. Cal." or "9th Cir." or "S.D.N.Y."
    appellate_path?: string[];   // hierarchy
  };
  schema_version: 1;
};

type SourceRef = {
  source_id: string;
  source_kind: "document" | "url" | "tool_output" | "user_capture" | "system_seed";
  canonical_form: string;        // URI or hash; used for CardStableKeyMaterial
  display_label?: string;
  schema_version: 1;
};

type CardStableKey = string;     // 24-char hash prefix per HH.6.13

type CardStableKeyMaterial = {
  node_ref: EntityRef;
  source_ref_canonical_form: string;
  rendering_variant_seed?: string;
  schema_version: 1;
};

function computeCardStableKey(material: CardStableKeyMaterial): string {
  return sha256(canonicalJSON(material)).slice(0, 24);
}
```

`card_stable_key` is computed ONCE per card at the `candidates_gathered` lifecycle state and is preserved across reassembly (HH.6.13 timing rule).

##### 5.4.1.B Provenance

```ts
type Provenance = {
  primary_source: ProvenancePrimarySource;
  contributing_sources: ProvenanceContributingSource[];
  schema_version: 1;
};

type ProvenancePrimarySource =
  | { kind: "pbe_extraction"; extraction_run_id: string }
  | { kind: "user_capture"; capture_event_id: string }
  | { kind: "tool_invocation"; tool_execution_id: string }
  | { kind: "graph_inference"; inference_run_id: string }
  | { kind: "system_seed"; seed_profile_id: string };

type ProvenanceContributingSource = {
  source_kind: ProvenancePrimarySource["kind"];
  source_id: string;
  contribution_kind: "merge" | "enrichment" | "verification" | "supersession";
};

// PBE-derived detection (HH.7.1):
function isPBEDerivedCard(card: PacketCandidateCard): boolean {
  return card.provenance.primary_source.kind === "pbe_extraction";
}
```

##### 5.4.1.C Card presence (replaces R3 inclusion_decision + rendering_kind)

Per HH.6.4, R3.1 replaces R3's separate `inclusion_decision` and `rendering_kind` fields with a single discriminated union:

```ts
type CardPresence =
  | {
      kind: "included_inline";
      rendering_kind: "full" | "compact";       // R3.1 — drops "inline_full" rendering label per HH.2.5
      rendered_token_count: number;
      delivery_directive: DOC24DeliveryDirective;
      directive_constraints: DirectiveConstraint[];
    }
  | {
      kind: "included_reference_only";
      reference_only_payload: ReferenceOnlyPayload;
      rendered_token_count: number;
      directive_constraints: DirectiveConstraint[];
    }
  | {
      kind: "excluded";
      suppression_kind: SuppressionKind;
      suppressing_decision_ref: string;
    };

type SurfaceKind =
  | "prompt_inline"
  | "prompt_reference_only"
  | "ui_inspector"
  | "ui_human_facing"
  | "audit_only"
  | "tool_param";

type SurfaceRenderingConstraint = {
  surface: SurfaceKind;
  allowed_kinds: ("full" | "compact" | "reference_only")[];
  pii_redaction_required: boolean;
  schema_version: 1;
};

type ReferenceOnlyPayload = {
  reference_label: string;
  reason_code: ReasonCode;
  policy_decision_id: string;
  schema_version: 1;
};
```

##### 5.4.1.D Directive constraints

Per HH.6.8, R3.1 introduces an additive constraint vocabulary on top of R3's scalar `force_level`:

```ts
type ForceLevel = "avoid" | "standard" | "strong" | "hard";

type DirectiveConstraint =
  | { kind: "force_level_floor"; floor: ForceLevel; origin: ConstraintOrigin }
  | { kind: "force_level_ceiling"; ceiling: ForceLevel; origin: ConstraintOrigin }
  | { kind: "reference_only_required"; origin: ConstraintOrigin }
  | { kind: "block_inline_rendering"; origin: ConstraintOrigin }
  | { kind: "redaction_applied"; redaction_kind: RedactionKind; origin: ConstraintOrigin };

type ConstraintOrigin =
  | { kind: "policy"; policy_decision_id: string }
  | { kind: "user_override"; user_directive_ref: string }
  | { kind: "architecture"; rule_ref: string }
  | { kind: "authority"; constellation_ref: string }
  | { kind: "matrix"; matrix_decision_id: string };

type RedactionKind = "partial" | "full" | "metadata_only";

type DOC24DeliveryDirective = {
  // Inherited from BDSM V6.5+ canonical DeliveryDirective; DOC24 wraps:
  force_level: ForceLevel;
  render_mode: "inline" | "reference_only" | "diagnostic_reference";
  tag: DeliveryTag;
  hedge_mode: "off" | "hedged" | "verification_required";
  still_current?: boolean;
  jurisdiction_relationship?: "binding_in_context" | "persuasive_in_context" | "out_of_context";  // ADJ-RT-107
  directive_source: DirectiveSource;
  schema_version: 1;
};

type DeliveryTag =
  | "cite_as_rule"
  | "cite"
  | "anchor"
  | "background"
  | "caution"
  | "draft";

type DirectiveSource =
  | { kind: "policy"; policy_decision_id: string }
  | { kind: "matrix"; matrix_decision_id: string }
  | { kind: "authority"; constellation_ref: string }
  | { kind: "user_override"; user_directive_ref: string }
  | { kind: "default"; default_profile_id: string };
```

Constraint feasibility rule:

```
Effective force_level = max(force_level_floor.floor) bounded by min(force_level_ceiling.ceiling).
If max(floor) > min(ceiling), the constraint set is infeasible:
  - If any floor.origin.kind === "policy", the packet blocks with reason "doc24.constraint_set_infeasible_policy_floor".
  - Otherwise, lower-priority origins yield until feasible.
```

##### 5.4.1.E Directive modification audit trail

Per HH.6.9:

```ts
type AssemblyStageId =
  | "candidates_gathered"
  | "lifecycle_filtered"
  | "policy_evaluated"
  | "confidence_gated"
  | "structurally_relevant"
  | "matrix_boosted"
  | "directives_assigned"
  | "rendering_tier_allocated"
  | "overflow_resolved"
  | "lint_check";

type DirectiveModification = {
  modification_id: string;
  card_id: string;
  field: "force_level" | "render_mode" | "constraints" | "tag" | "rendering_kind";
  prior_value: unknown;
  new_value: unknown;
  modified_at_stage: AssemblyStageId;
  modified_by: "policy_evaluator" | "matrix_apply" | "authority_anchor" | "user_override" | "budget_resolver" | "linter";
  rationale_ref?: string;
  modified_at: string;
  schema_version: 1;
};
```

`ContextAssemblyTrace.directive_modifications: DirectiveModification[]` replaces R2's free-form mutation log.

##### 5.4.1.F Budget and droppability

```ts
type DroppabilityClass = "hard_required" | "standard" | "trim_first";

type HardRequirementOrigin =
  | "policy_required"
  | "architecture_required"
  | "authority_required"
  | "user_pinned"
  | "user_emphasized";

type PriorityLane =
  | "authority_first"
  | "policy_required"
  | "matter_context"
  | "tool_capability"
  | "active_evidence"
  | "background";

type SourceLane =
  | "entity_lane"
  | "bucket_lane"
  | "authority_lane";

type UserOverrideBudgetMapping =
  | "policy_required"
  | "user_pinned"
  | "user_emphasized"
  | "user_force_only"
  | "standard_priority";

type BudgetCell = {
  source_lane: SourceLane;
  priority_lane: PriorityLane;
  allocated_tokens: number;
  used_tokens: number;
  unused_tokens: number;
  included_card_ids: string[];
  overflow_card_ids: string[];
  trim_policy: "never_trim" | "trim_last" | "summarize" | "omit";
  schema_version: 1;
};
```

##### 5.4.1.G Outcome class (DOC72-owned; consumed here)

```ts
// Imported from DOC72 R6+ §5.4.1:
import type { OutcomeClass } from "@elnor/doc72-types";

// OutcomeClass values:
//   "success" | "partial_success" | "unexpected_positive" |
//   "failure" | "partial_failure" | "rejection" | "correction_needed" |
//   "no_outcome_observable" | "user_aborted"
```

V1 ADJ-RT-018, ADJ-RT-122, ADJ-RT-124, ADJ-RT-131 all consume this enum unchanged.

##### 5.4.1.H Recency windows

Per HH.15.1, with explicit nullable invariant:

```ts
type RecencyWindow = {
  band_start_days: number;             // inclusive lower bound
  band_end_days: number | null;        // null = open-ended (last band only)
  weight: number;                       // [0, 1]
};

type RecencyWindowConfig = {
  windows: RecencyWindow[];             // sorted ascending; non-overlapping
  outside_window_weight: number;
  schema_version: 1;
};

function validateRecencyWindowConfig(c: RecencyWindowConfig): void {
  const sorted = [...c.windows].sort((a, b) => a.band_start_days - b.band_start_days);
  let prevEnd = -Infinity;
  let openEndedCount = 0;

  for (let i = 0; i < sorted.length; i++) {
    const w = sorted[i];
    if (w.band_start_days <= prevEnd) {
      throw new RecencyWindowOverlap({ at_index: i });
    }
    if (w.band_end_days === null) {
      openEndedCount++;
      if (i !== sorted.length - 1) {
        throw new RecencyWindowNullableNotLast({ at_index: i });
      }
    }
    if (w.band_end_days !== null) {
      prevEnd = w.band_end_days;
    }
  }

  if (openEndedCount > 1) {
    throw new RecencyWindowMultipleOpenEnded({ count: openEndedCount });
  }
}
```

##### 5.4.1.I Reason codes and degradation

```ts
type ReasonCode = string;  // governed namespace per HH.5.3 registry

type DegradedReasonCode =
  | "dependency.requires_verification"
  | "authority.blocked_cycle_detected"
  | "authority.blocked_frontier_capped"
  | "authority.blocked_input_unavailable"
  | "authority.blocked_missing_essential_set"
  | "authority.blocked_scope_inapplicable"
  | "authority.collapsed_essential_retracted"
  | "pbe.lite_degraded_contract"
  | "rendering.tier_downgraded_by_budget"
  | "source.reference_only"
  | "policy.partial_redaction_applied"
  | "matrix.effective_beta_clamped";

// Note: "authority.computed" is NOT in DegradedReasonCode (HH.5.2 removed it);
// "computed" is success state with kind "render_inline" in AuthorityRenderDecision (HH.5.1).

type DegradedState = {
  reason_codes: DegradedReasonCode[];
  human_message_template_ids: string[];
  schema_version: 1;
};

type SuppressionKind =
  | "policy_excluded"
  | "matrix_below_threshold"
  | "authority_blocked"
  | "scope_filter"
  | "budget_overflow"
  | "duplicate_overlap"
  | "lifecycle_archived"
  | "lifecycle_retracted";

type MatrixExposureClass =
  | "eligible_and_injected"
  | "eligible_reference_only"
  | "ineligible_suppressed"
  | "ineligible_excluded";
```

The reason-code namespace registry (HH.5.3) and DegradedReasonCode registry (HH.15.2) are companion build-time artifacts.

##### 5.4.1.J Authority rendering (per HH.5.1)

```ts
type AuthorityRenderDecision =
  | {
      kind: "render_inline";
      directive: DOC24DeliveryDirective;
      reason_codes: ReasonCode[];
    }
  | {
      kind: "render_diagnostic_reference";
      reference_label: string;
      policy_decision_id: string;
      degraded_reason_codes: DegradedReasonCode[];
    }
  | {
      kind: "exclude_from_packet";
      exclusion_reason_codes: ReasonCode[];
    };

type AuthorityRenderContext = {
  safe_reference_label: string;
  policy_decision_id: string;
};

const AUTHORITY_BLOCK_REASON_CODE: Record<
  Exclude<AuthorityResult["computed_state"], "computed">,
  ReasonCode
> = {
  blocked_cycle_detected:          "authority.blocked_cycle_detected",
  blocked_frontier_capped:         "authority.blocked_frontier_capped",
  blocked_input_unavailable:       "authority.blocked_input_unavailable",
  blocked_missing_essential_set:   "authority.blocked_missing_essential_set",
  blocked_scope_inapplicable:      "authority.blocked_scope_inapplicable",
  collapsed_essential_retracted:   "authority.collapsed_essential_retracted",
} as const;

function decideAuthorityRendering(
  authority: AuthorityResult,
  card: PacketCandidateCard,
  context: AuthorityRenderContext
): AuthorityRenderDecision {
  switch (authority.computed_state) {
    case "computed":
      return {
        kind: "render_inline",
        directive: deriveDirectiveFromNumericAuthority(authority, card),
        reason_codes: ["authority.computed_inline"],
      };

    case "blocked_scope_inapplicable":
    case "collapsed_essential_retracted":
      return {
        kind: "exclude_from_packet",
        exclusion_reason_codes: [AUTHORITY_BLOCK_REASON_CODE[authority.computed_state]],
      };

    case "blocked_input_unavailable":
      return {
        kind: "exclude_from_packet",
        exclusion_reason_codes: ["authority.input_unavailable_retry_later"],
      };

    case "blocked_cycle_detected":
    case "blocked_frontier_capped":
    case "blocked_missing_essential_set":
      return {
        kind: "render_diagnostic_reference",
        reference_label: context.safe_reference_label,
        policy_decision_id: context.policy_decision_id,
        degraded_reason_codes: [AUTHORITY_BLOCK_REASON_CODE[authority.computed_state]],
      };
  }
}
```

##### 5.4.1.K Tokenizer

Per HH.2.9, HH.11.4:

```ts
type TokenizerRef = {
  tokenizer_family: "claude" | "llama" | "gemini" | "gpt" | "qwen";
  tokenizer_version: string;
  tokenizer_lib_ref?: string;
};
```

One `TokenizerRef` per packet; per-card `tokenizer_ref` must match the packet-level value.

##### 5.4.1.L Policy snapshot (per HH.5.4)

```ts
type PolicyGenerationSnapshot = {
  policy_generation_id: string;
  policy_state_hash: string;
  policy_state_ref: string;            // immutable in-memory or durable snapshot ref
  snapshot_id: string;                  // audit ID only, NOT a lock token (renamed from V1.2 read_token)
  opened_at: string;
  schema_version: 1;
};

interface ReadWriteLock {
  read<T>(fn: () => Promise<T>): Promise<T>;
  write<T>(fn: () => Promise<T>): Promise<T>;
}

// Recommended implementation: async-mutex@0.5+ (HH.5.5)
// Starvation-prevention via writer-priority signaling.
```

##### 5.4.1.M Tool capability counters

Per HH.4.1, HH.4.2 — event-sourced raw counters with read-time effective decay:

```ts
type ToolCapabilityExperienceCounters = {
  user_visible_invocation_count: number;
  total_invocation_count: number;
  success_count: number;
  failure_count: number;

  // Raw Beta evidence — append-only; decay computed at READ time (HH.4.2):
  alpha_raw: number;                    // renamed from "alpha" in V1.4 II convention
  beta_raw: number;
  last_evidence_at?: string;

  // Lockout timing (ADJ-097):
  last_failure_at?: string;
  last_success_at?: string;
  failure_lockout_until?: string;

  schema_version: 1;
};

type ToolOutcomeEvidenceEvent = {
  event_id: string;
  tool_capability_node_id: string;
  outcome_class: OutcomeClass;
  evidence_weight: number;              // 1.0 full; 0.5 partial (HH.15.5)

  // V1.4 II.3 — attribution thread from selection to outcome:
  selection_decision_id?: string;
  selection_mode?: "deterministic_match" | "thompson_sample" | "seeded_cold_start_exploration";

  occurred_at: string;
  schema_version: 1;
};

type ToolCapabilityExperienceCurrentView = {
  tool_capability_node_id: string;
  counters: ToolCapabilityExperienceCounters;
  alpha_effective: number;              // computed via computeEffectiveBeta (§13.4B)
  beta_effective: number;
  rebuilt_at: string;
  rebuild_source: "event_stream" | "counter_snapshot";
  schema_version: 1;
};

type BetaEvidenceConfig = {
  prior_alpha: number;
  prior_beta: number;
  max_total_evidence_mass: number;
  min_retained_evidence_mass: number;
  half_life_days: number;
};
```

Decay function `computeEffectiveBeta` and its evidence-mass-preserving algorithm specified in §13.4B.

##### 5.4.1.N Tool selection result

Per HH.4.12, HH.4.4 — bounded Thompson sampling with reliability lower bound:

```ts
type ToolSelectionResult =
  | {
      kind: "selected";
      tool: RegisteredTool;
      selection_mode: "deterministic_match" | "thompson_sample" | "seeded_cold_start_exploration";
      why_this_tool: ReasonCode[];
      selection_decision_id: string;
      rng_seed?: string;
      sampled_value?: number;
    }
  | {
      kind: "needs_user_confirmation";
      candidate: RegisteredTool;
      confirmation_reason:
        | "cold_start_tool"
        | "untested_in_high_stakes"
        | "bootstrap_mapping_uncertain";
      reason_codes: ReasonCode[];
    }
  | {
      kind: "no_suitable_tool";
      suggested_action: "surface_to_user" | "fall_back_to_generic" | "block";
      reason_codes: ReasonCode[];
    };

type ToolHighStakesQualification = {
  minimum_visible_invocations: number;
  minimum_reliability_lower_bound: number;   // e.g., 0.70
  lower_bound_method: "beta_credible_lower_95" | "wilson_95";
};

// operation_pause_state moved from ToolSelectionResult to Operation per HH.15.3:
type Operation = {
  operation_id: string;
  packet_id?: string;
  pause_state?:
    | "tool_confirmation_pending"
    | "disambiguation_pending"
    | "policy_acknowledgement_pending"
    | "user_choice_pending"
    | "manual_review_required";
  pause_reason_ref?: string;
  schema_version: 1;
};
```

##### 5.4.1.O Lifecycle and packet types

Per HH.1.2:

```ts
type PacketLifecycleState =
  // Pre-assembly
  | "created"

  // Assembly stages (R3 — RESTORED in V1.3 where V1.1 silently dropped)
  | "candidates_gathered"
  | "lifecycle_filtered"
  | "policy_evaluated"
  | "confidence_gated"
  | "structurally_relevant"
  | "matrix_boosted"
  | "directives_assigned"
  | "rendering_tier_allocated"
  | "overflow_resolved"

  // Lint and persistence (V1.1 X.4)
  | "lint_check"
  | "lint_passed"
  | "lint_failed"
  | "manifest_written"

  // Dispatch
  | "policy_revalidated"
  | "aborted_for_retry"        // R3 — RESTORED (CRITICAL: ADJ-009 depends)
  | "dispatched"

  // Terminal
  | "blocked"
  | "completed"
  | "reconciled";              // R3 — RESTORED

type DispatchedAckState = {
  state: "dispatched";
  acknowledged_by_receiver: boolean;
  acknowledged_at?: string;
  receiver: "doc11" | "structured_ec" | "openclaw_direct" | "background_processor";
};

type LifecycleTrigger =
  | "assembler"
  | "budget_resolver"
  | "linter"
  | "policy_revalidation"
  | "dispatch"
  // V1.4 II.5.2 — "doc11_handoff" removed (unused; ack via DispatchedAckState substate)
  | "runtime_completion"
  | "reconciliation_complete"
  | "abort"
  | "block";
```

Transition table and `assertAllowedPacketTransition` specified in §38.1.

##### 5.4.1.P Packet assembly scope

Per HH.6.1 — canonical, merges V1.1 X.1 + X.5:

```ts
type PacketAssemblyScope = {
  operation_id: string;
  packet_id: string;
  packet_kind: "assistant_turn" | "structured_ec_action" | "background_extraction";

  matter_context_slot_id?: string;
  work_context_id: string;
  excluded_workspace_ids: string[];

  scope_kind:
    | "user_turn_chat"
    | "user_turn_ec_action"
    | "system_initiated_background"
    | "saga_step_continuation";

  pbe_intent: "none" | "extract" | "reuse_cached";

  tokenizer_ref: TokenizerRef;
  total_budget_tokens: number;

  policy_generation_snapshot: PolicyGenerationSnapshot;
  bundle_generation_id: string;
  matrix_generation_id?: string;       // optional per HH.2.3 / II.2.3 cardinality rule
  matrix_state?:
    | "active"
    | "disabled"
    | "gathering_only"
    | "bundle_unavailable";

  schema_version: 1;
};
```

##### 5.4.1.Q Manifest types (per HH.2 + II.1 + II.2)

R3.1's canonical manifest chain has four types: `CandidateInjectionManifest` (pre-durable, lint input), `PacketInjectionManifest` (durable, DOC24 contribution), `FinalPromptInjectionManifest` (durable, final-prompt owner), `BlockedPacketManifest` (durable, failed packet).

```ts
type InjectionOwnerDoc =
  | "DOC7" | "DOC10" | "DOC11" | "DOC12" | "DOC15"
  | "DOC20" | "DOC24" | "DOC73" | "OpenClaw";

type InjectionSlotId = string;          // format: "{owner_doc}.{slot_name}"

type InjectionSlotManifestEntry = {
  slot_id: InjectionSlotId;
  owner_doc: InjectionOwnerDoc;
  slot_position_index: number;
  contribution_manifest_ref?: string;   // → PacketInjectionManifest when DOC24-contributed
  rendered_token_count: number;
  content_hash: string;
  schema_version: 1;
};

type BucketManifestEntry = {
  bucket_id: string;
  slot_id: InjectionSlotId;
  rendered_token_count: number;
  content_hash: string;
  source_refs: SourceRef[];
  schema_version: 1;
};

type ManifestCardEntry = {
  // Identity
  card_id: string;
  card_stable_key: CardStableKey;
  node_ref: EntityRef;

  // Lane and droppability
  source_lane: SourceLane;
  priority_lane: PriorityLane;
  droppability_class: DroppabilityClass;
  hard_requirement_origin?: HardRequirementOrigin;

  // Exposure
  manifest_exposure_class: MatrixExposureClass;
  source_eligibility_aggregate: CardSourceEligibilityAggregate;

  // Presence (HH.6.4 — replaces inclusion_decision + rendering_kind)
  card_presence: CardPresence;

  // Token accounting (HH.2.9)
  rendered_token_count: number;
  tokenizer_ref: TokenizerRef;
  count_method: "exact" | "estimated";

  // V1.4 II.1 — RESTORED from R3 §38.2.2:
  position_in_packet?: number;                // present iff card_presence.kind === "included_inline"
  rendering_specialization_id?: string;       // KDA neuroplasticity tracking
  render_variant_id?: string;                 // KDA per-render variant
  template_version?: string;                  // KDA template version
  resolution_source?: ResolutionSource;       // from CompactEntityCard
  overlap_decision_id?: string;               // overlap suppression link

  // V1.1/V1.2 additions
  user_override_applied: boolean;
  user_override_budget_mapping?: UserOverrideBudgetMapping;
  authority_render_decision_ref?: string;
  degraded_reason_codes?: DegradedReasonCode[];

  // Provenance (HH.7.2 full structure)
  provenance: Provenance;

  // Audit
  manifested_at: string;
  schema_version: 1;
};

// Storage representation discriminated union (HH.2.5):
type InlineManifestCardRecord = {
  storage_representation: "inline_full";
  entry: ManifestCardEntry;
  schema_version: 1;
};

type PointerManifestCardRecord = {
  storage_representation: "pointer_compacted";
  pointer: ManifestCardEntryPointer;
  schema_version: 1;
};

type ManifestCardRecord = InlineManifestCardRecord | PointerManifestCardRecord;

type ManifestCardEntryPointer = {
  card_id: string;
  card_stable_key: CardStableKey;
  compact_summary: {
    node_ref: EntityRef;
    manifest_exposure_class: MatrixExposureClass;
    rendered_token_count: number;
    delivery_directive_hash: string;
    source_refs_hash: string;
    policy_decision_refs_hash: string;
  };
  full_entry_blob_ref: string;
  full_entry_hash: string;
  schema_version: 1;
};

type CandidateInjectionManifest = {
  candidate_manifest_id: string;
  packet_id: string;
  operation_id: string;
  session_id?: string;

  slot_entries: InjectionSlotManifestEntry[];
  card_records: ManifestCardRecord[];
  bucket_entries?: BucketManifestEntry[];

  policy_generation_snapshot: PolicyGenerationSnapshot;

  candidate_manifest_hash: string;
  durability_state: "candidate_only";   // exact literal; cannot be persisted

  built_at: string;
  schema_version: 1;
};

type PacketInjectionManifest = {
  manifest_kind: "doc24_packet_contribution";

  manifest_id: string;
  packet_id: string;
  operation_id: string;

  slot_entries: InjectionSlotManifestEntry[];
  card_records: ManifestCardRecord[];
  bucket_entries: BucketManifestEntry[];

  policy_generation_id: string;
  bundle_generation_id: string;
  matrix_generation_id?: string;
  matrix_state?: PacketAssemblyScope["matrix_state"];
  trace_ref: string;

  // V1.4 II.2.1 — R3 §38.2.2 timestamp pair
  assembly_completed_at: string;
  written_at: string;

  // V1.4 II.2.2 — R3 prose reassembly link
  superseded_by_packet_id?: string;

  // Aggregate measurements (R3 §38.2.2)
  total_token_count: number;
  total_card_count_included: number;
  total_card_count_excluded: number;

  // Operational state
  degraded_state?: DegradedState;
  warnings: string[];

  // PBE-lite bridge (HH.7.3)
  pbe_lite_effect?: PBELitePacketEffect;

  // Compaction metadata (HH.2.5)
  manifest_size_bytes_uncompressed: number;
  manifest_size_bytes_persisted: number;
  compression_applied: boolean;

  // Packet-level tokenizer (HH.2.9)
  packet_tokenizer_ref: TokenizerRef;

  schema_version: 1;
};

type PromptInjectionSpan = {
  span_id: string;
  operation_id: string;
  final_prompt_manifest_id: string;
  slot_id: InjectionSlotId;
  owner_doc: InjectionOwnerDoc;
  byte_start: number;
  byte_end: number;
  token_count: number;
  content_hash: string;
  source_refs: SourceRef[];
  policy_generation_id: string;
  contribution_manifest_ref?: string;
  context_injection_event_ref?: string;
  schema_version: 1;
};

type FinalPromptInjectionManifest = {
  manifest_kind: "final_prompt";
  manifest_id: string;
  operation_id: string;
  final_prompt_owner: "DOC11" | "OpenClaw" | "DOC24";

  contribution_manifest_refs: string[];
  final_prompt_hash: string;
  spans: PromptInjectionSpan[];          // V1.3 HH.2.4 — REQUIRED; covers every byte
  total_prompt_tokens: number;

  written_at: string;
  schema_version: 1;
};

type BlockedPacketManifest = {
  blocked_manifest_id: string;
  packet_id: string;
  operation_id: string;
  blocking_reason: ReasonCode;
  blocked_at_stage: AssemblyStageId | "lint_check" | "policy_revalidated";

  // V1.3 HH.2.2 — diagnostic preservation
  candidate_manifest_at_block?: {
    slot_entries: InjectionSlotManifestEntry[];
    card_records: ManifestCardRecord[];
    bucket_entries?: BucketManifestEntry[];
    candidate_manifest_hash: string;
  };
  lint_failures?: BlockingFailure[];

  written_at: string;
  schema_version: 1;
};

type CandidateManifestLintResult = {
  lint_result_id: string;
  packet_id: string;
  candidate_manifest_hash: string;
  outcome: "passed" | "failed";
  blocking_failures: BlockingFailure[];
  warnings: LintWarning[];
  manifest_ref?: string;                 // → PacketInjectionManifest when outcome === "passed"
  linted_at: string;
  schema_version: 1;
};

type PacketLintFailureRecord = {
  lint_failure_id: string;
  packet_id: string;
  candidate_manifest_hash: string;
  lint_result_ref: string;
  blocked_manifest_id: string;
  written_at: string;
  schema_version: 1;
};

type BlockingFailure = {
  failure_id: string;
  rule_name: string;
  affected_card_ids?: string[];
  reason_code: ReasonCode;
  message: string;
  severity: "blocking";
};

type LintWarning = {
  warning_id: string;
  rule_name: string;
  affected_card_ids?: string[];
  reason_code: ReasonCode;
  message: string;
  severity: "warning";
};

type PromptPayloadPreflightResult = {
  operation_id: string;
  final_prompt_owner: "DOC11" | "OpenClaw" | "DOC24";
  total_prompt_bytes: number;
  total_covered_bytes: number;
  total_unregistered_bytes: number;       // MUST be 0 to pass
  unregistered_blocks: Array<{
    block_index: number;
    byte_range_hash: string;
    reason: "missing_slot_id" | "missing_context_injection_event" | "byte_range_uncovered";
  }>;
  outcome: "passed" | "blocked";
  schema_version: 1;
};

type ResolutionSource =
  | "direct_match"
  | "alias_resolution"
  | "constellation_anchor"
  | "context_inference"
  | "user_disambiguation"
  | "tool_provided"
  | "extraction_synthesis";

type CardSourceEligibilityAggregate = {
  policy_decision_refs: string[];
  source_eligibility_state: "eligible" | "reference_only" | "ineligible";
  source_refs: SourceRef[];
  aggregated_at: string;
  schema_version: 1;
};

type PBELitePacketEffect = {
  effect_id: string;
  packet_id: string;
  affected_card_ids: string[];

  banner_kind:
    | "pbe_extraction_deferred"
    | "pbe_lite_degraded_contract"
    | "pbe_reuse_cached"
    | "pbe_lite_promotion_pending";
  banner_user_message_template_id: string;

  deferred_extraction_event_refs: string[];
  replay_eligible_after: string;
  replay_attempt_count: number;
  replay_cap: number;

  emitted_at: string;
  schema_version: 1;
};
```

The hash-content rule for `ManifestCardEntryPointer.full_entry_hash` and content-addressable hashes excludes timestamps per HH.2.6:

```ts
function computeFullEntryHash(entry: ManifestCardEntry): string {
  const hashable = {
    card_id:                       entry.card_id,
    card_stable_key:               entry.card_stable_key,
    node_ref:                      entry.node_ref,
    rendering_kind:                entry.card_presence.kind === "included_inline"
                                     ? entry.card_presence.rendering_kind : null,
    source_lane:                   entry.source_lane,
    priority_lane:                 entry.priority_lane,
    droppability_class:            entry.droppability_class,
    manifest_exposure_class:       entry.manifest_exposure_class,
    source_eligibility_aggregate:  entry.source_eligibility_aggregate,
    card_presence:                 entry.card_presence,
    user_override_applied:         entry.user_override_applied,
    user_override_budget_mapping:  entry.user_override_budget_mapping,
    authority_render_decision_ref: entry.authority_render_decision_ref,
    degraded_reason_codes:         entry.degraded_reason_codes,
    rendered_token_count:          entry.rendered_token_count,
    position_in_packet:            entry.position_in_packet,
    rendering_specialization_id:   entry.rendering_specialization_id,
    render_variant_id:             entry.render_variant_id,
    template_version:              entry.template_version,
    resolution_source:             entry.resolution_source,
    overlap_decision_id:           entry.overlap_decision_id,
    provenance:                    entry.provenance,
    // EXCLUDED: manifested_at, schema_version, tokenizer_ref.tokenizer_lib_ref
  };
  return sha256(canonicalJSON(hashable));
}
```

`canonicalJSON` uses RFC 8785 JCS via `@stablelib/json-canonicalize@2.x` per HH.11.4.

##### 5.4.1.R Reconciliation (per HH.3)

```ts
type PacketReconciliationEvent = {
  reconciliation_event_id: string;
  packet_id: string;
  manifest_id: string;
  card_id: string;

  reconciliation_state:
    | "not_observable"
    | "injected_and_used"
    | "injected_and_ignored"
    | "injected_and_corrected"
    | "injected_and_blocked";

  outcome_kind: "used" | "ignored" | "corrected" | "unknown";

  proxy_method?:
    | "exposure_evidence"
    | "source_citation_match"
    | "claim_paraphrase_match"
    | "substring_coverage";

  prompt_span_refs: string[];           // → PromptInjectionSpan[]
  evidence_refs: EvidenceRef[];          // imported from DOC72 R6+
  attribution_confidence: number;

  supersedes_event_id?: string;          // append-only supersession chain

  written_at: string;
  schema_version: 1;
};

type PacketReconciliationCurrentView = {
  packet_id: string;
  manifest_id: string;
  per_card_current: Array<{
    card_id: string;
    effective_reconciliation_state: PacketReconciliationEvent["reconciliation_state"];
    effective_outcome_kind: PacketReconciliationEvent["outcome_kind"];
    latest_event_id: string;
    event_history_refs: string[];
  }>;
  rebuilt_at: string;
  schema_version: 1;
};

// Imported from DOC72 R6+ §16.7:
import type { EvidenceRef } from "@elnor/doc72-types";
```

The current view is rebuilt from events; events are the source of truth. Per HH.3.2, `PacketInjectionManifest` does NOT carry outcome_kind, proxy_method, post-response usage classification, or reconciliation_state — these live only in events and derived views.

##### 5.4.1.S Working context

```ts
type WorkingContextState = {
  context_id: string;
  context_revision: string;             // ULID; monotonic per ADJ-RT-R3-122
  matter_context_slot_id?: string;
  work_context_ref: WorkContextRef;
  active_authorities: string[];
  active_directives: string[];
  suspended_context_stack: SuspendedContextSnapshot[];
  context_continuity_card_ref?: string; // present after suspend/restore (ADJ-RT-R3-136)
  native_fork_mapping?: {                // OPT-IN observability only; not used by DOC24 routinely
    parent_session_id: string;
    fork_session_id: string;
    forked_at: string;
  };
  schema_version: 1;
};
```

Per HH.8.4: `native_fork_mapping` records observation when a user-level workflow uses fork; DOC24 assembly does not use fork by default. The default `sessions_spawn` context is `isolated`, with payload serialized into `task_prompt` explicitly.

##### 5.4.1.T Context authority handoff

Per HH.6.7:

```ts
type ContextAuthorityChainEntry =
  | { kind: "injection_slot"; slot_id: InjectionSlotId; owner_doc: InjectionOwnerDoc }
  | { kind: "routing_facts"; owner_doc: "DOC10" };

type ScopeSnapshot = {
  matter_context_slot_id?: string;
  work_context_id: string;
  excluded_workspace_ids: string[];
  jurisdiction?: { primary: string; appellate_path?: string[] };
  snapshot_at: string;
  schema_version: 1;
};

type ContextAuthorityHandoff = {
  handoff_id: string;
  operation_id: string;
  route_trace_id: string;
  selected_execution_path: "gateway_first_chat" | "structured_ec_action" | "background_job";

  context_authority_chain: ContextAuthorityChainEntry[];

  final_prompt_assembly: {              // V1.3 HH.6.7 — typed field replaces array index
    owner_doc: "DOC11" | "OpenClaw" | "DOC24";
    final_prompt_manifest_ref?: string;
  };

  doc24_packet_id?: string;
  doc11_annotation_ref?: string;
  openclaw_runtime_context_ref?: string;

  scope_snapshot: ScopeSnapshot;

  schema_version: 1;
};
```

##### 5.4.1.U Decision graph (per HH.6.10)

Closes V1.1 DD.1 `Record<string, unknown>` escape hatches:

```ts
type DecisionGraphNodeData =
  | { kind: "candidate_card"; card: PacketCandidateCard }
  | { kind: "policy_decision"; decision: PacketSourceEligibilityDecisionSchema }
  | { kind: "matrix_adjustment"; adjustment: MatrixConfidenceAdjustment }
  | { kind: "directive_modification"; modification: DirectiveModification }
  | { kind: "budget_cell"; cell: BudgetCell }
  | { kind: "lint_finding"; finding: BlockingFailure | LintWarning }
  | { kind: "reconciliation_event"; event: PacketReconciliationEvent };

type PacketDecisionGraphNode = {
  node_id: string;
  stage_id: AssemblyStageId;
  data: DecisionGraphNodeData;
  upstream_node_ids: string[];
  downstream_node_ids: string[];
  schema_version: 1;
};

type PacketDecisionGraphInput = {
  trace: ContextAssemblyTrace;
  manifest: PacketInjectionManifest;
  lint_result?: CandidateManifestLintResult;
  reconciliation_events?: PacketReconciliationEvent[];
  opportunity_set?: PacketOpportunitySet;
  nodes: PacketDecisionGraphNode[];
};
```

##### 5.4.1.V Opportunity set (per HH.15.4)

```ts
type PacketOpportunityCandidate = {
  card_stable_key: CardStableKey;
  card_id: string;
  node_ref: EntityRef;
  source_lane: SourceLane;
  not_retrieved_reason: SuppressionKind | "below_threshold" | "policy_excluded" | "scope_filter";
  candidate_score: number;              // [0,1]
  schema_version: 1;
};

type PacketOpportunitySamplingPolicy = {
  sampling_mode: "top_k_by_relevance" | "uniform_random" | "stratified_by_lane";
  k_cap: number;                         // default 50
  rng_seed?: string;
};

type PacketOpportunitySet = {
  opportunity_set_id: string;
  packet_id: string;
  manifest_id: string;
  total_candidates_considered: number;
  sampled_candidates: PacketOpportunityCandidate[];
  sampling_policy: PacketOpportunitySamplingPolicy;
  written_at: string;
  schema_version: 1;
};
```

##### 5.4.1.W Bulk action

Per HH.6.11 — discriminated union closes V1.2 Y.20's undefined `BulkActionOptionsByKind`:

```ts
type BulkPartialFailurePolicy =
  | "report_only"
  | "continue_non_destructive_only"
  | "rollback_if_supported";

type BulkActionRequest =
  | {
      action_kind: "digest_items.archive";
      item_ids: string[];
      options: { reason?: "obsolete" | "duplicate" | "user_archived" };
      idempotency_key: string;
      per_item_idempotency_keys: Record<string, string>;
      on_partial_failure: BulkPartialFailurePolicy;
      schema_version: 1;
    }
  | {
      action_kind: "digest_items.dismiss";
      item_ids: string[];
      options: { reason: "not_relevant" | "factually_wrong"; evidence_text?: string };
      idempotency_key: string;
      per_item_idempotency_keys: Record<string, string>;
      on_partial_failure: BulkPartialFailurePolicy;
      schema_version: 1;
    }
  | {
      action_kind: "knowledge_nodes.archive";
      item_ids: string[];
      options: { suppress_only: boolean };
      idempotency_key: string;
      per_item_idempotency_keys: Record<string, string>;
      on_partial_failure: BulkPartialFailurePolicy;
      schema_version: 1;
    }
  | {
      action_kind: "knowledge_nodes.retract";
      item_ids: string[];
      options: { retraction_reason: "user_requested" | "policy_required" };
      idempotency_key: string;
      per_item_idempotency_keys: Record<string, string>;
      on_partial_failure: Exclude<BulkPartialFailurePolicy, "continue_non_destructive_only">;
      schema_version: 1;
    }
  | {
      action_kind: "source_filters.remove";
      item_ids: string[];
      options: { confirm_user_acknowledged: true };
      idempotency_key: string;
      per_item_idempotency_keys: Record<string, string>;
      on_partial_failure: Exclude<BulkPartialFailurePolicy, "continue_non_destructive_only">;
      schema_version: 1;
    }
  | {
      action_kind: "tasks.cancel";
      item_ids: string[];
      options: { cancellation_reason: string };
      idempotency_key: string;
      per_item_idempotency_keys: Record<string, string>;
      on_partial_failure: BulkPartialFailurePolicy;
      schema_version: 1;
    };

type BulkActionResponse = {
  bulk_operation_id: string;
  action_kind: BulkActionRequest["action_kind"];
  total_items: number;
  attempted_items_in_order: string[];
  successful_items: string[];
  failed_items: Array<{
    item_id: string;
    error: ReasonCode;
    error_message: string;
    rollback_supported: boolean;
    rollback_result?: "completed" | "failed" | "not_attempted";
  }>;
  terminal_state: "completed" | "partial_failure" | "rolled_back" | "failed";
  schema_version: 1;
};
```

##### 5.4.1.X Blocked message params

Per HH.6.12:

```ts
type PacketBlockedMessageParams =
  | {
      kind: "hard_required_budget_overflow";
      blocking_card_ids: string[];
      requested_tokens: number;
      available_tokens: number;
    }
  | {
      kind: "user_originated_hard_required_count_exceeded";
      affected_card_ids: string[];
      cap_value: number;
    }
  | {
      kind: "lint_failure";
      lint_result_id: string;
      blocking_failures: BlockingFailure[];
    }
  | {
      kind: "policy_tearing_detected";
      packet_policy_generation_id: string;
      current_policy_generation_id: string;
      retry_attempts_exhausted: boolean;
    }
  | {
      kind: "budget_negative";
      computation: PacketBudgetComputationTrace;
    }
  | {
      kind: "tokenizer_mismatch_dispatch";
      packet_tokenizer: TokenizerRef;
      runtime_tokenizer: TokenizerRef;
    };
```

##### 5.4.1.Y Imported type contracts

Per HH.11.2:

```ts
type ImportedTypeContract = {
  type_name: string;
  owner_doc: "DOC72" | "DOC73" | "PropA" | "BDSM" | "KDA" | "DOC8" | "DOC11" | "DOC15";
  owner_doc_min_version: string;
  upstream_schema_version: number;
  upstream_ref: string;
  schema_version: 1;
};

const DOC24_IMPORTED_TYPES: ImportedTypeContract[] = [
  { type_name: "NodeKind",                 owner_doc: "DOC72", owner_doc_min_version: "DOC72 R6",     upstream_schema_version: 1, upstream_ref: "§2.1",      schema_version: 1 },
  { type_name: "OutcomeClass",             owner_doc: "DOC72", owner_doc_min_version: "DOC72 R6",     upstream_schema_version: 1, upstream_ref: "§5.4.1",    schema_version: 1 },
  { type_name: "EvidenceRef",              owner_doc: "DOC72", owner_doc_min_version: "DOC72 R6",     upstream_schema_version: 1, upstream_ref: "§16.7",     schema_version: 1 },
  { type_name: "DeliveryDirective",        owner_doc: "BDSM",  owner_doc_min_version: "BDSM V6.5",    upstream_schema_version: 2, upstream_ref: "§2",        schema_version: 1 },
  { type_name: "RenderingTier",            owner_doc: "KDA",   owner_doc_min_version: "KDA R3",       upstream_schema_version: 1, upstream_ref: "§4.2",      schema_version: 1 },
  { type_name: "PolicyDecisionSchema",     owner_doc: "PropA", owner_doc_min_version: "PropA R6",     upstream_schema_version: 1, upstream_ref: "§Y",        schema_version: 1 },
  { type_name: "ConsolidatedUnderstanding", owner_doc: "DOC73", owner_doc_min_version: "DOC73 V1.5", upstream_schema_version: 1, upstream_ref: "§3.2A",     schema_version: 1 },
  { type_name: "AuthorityResult",          owner_doc: "DOC73", owner_doc_min_version: "DOC73 V1.5",   upstream_schema_version: 1, upstream_ref: "§3.2A.1",   schema_version: 1 },
];
```

CI gate: every DOC24 reference to an imported type must match a row above, with the minimum upstream version satisfied at build time.

##### 5.4.1.Z Watchpoint references *(R3.1 — preserved from V1.1 Y.14)*

V1.1 Y.14 introduced the `WatchpointTriggerParams` discriminated union to close the ADJ-091 `trigger_params: Record<string, unknown>` escape hatch. Per R3.1 INV-13 (R3 field restoration discipline) and design goal 9 (buildability over expressiveness), every escape hatch is closed.

```ts
type WatchpointTriggerParams =
  | { trigger_kind: "entity_mention"; entity_id: string; alias_match: boolean }
  | { trigger_kind: "calendar_proximity"; minutes_before: number; calendar_id: string }
  | { trigger_kind: "deadline_approach"; days_before: number; deadline_ref: string }
  | { trigger_kind: "external_event"; event_id: string; source: string }
  | { trigger_kind: "scheduled_time"; cron_expression: string; timezone: string };

type WatchpointRef = {
  watchpoint_id: string;
  params: WatchpointTriggerParams;
  registered_at: string;
  registered_by: "system_inferred" | "user_pinned";
  schema_version: 1;
};
```

Watchpoints are consumed by §35.2 predictive warming and the deadline cascade intelligence in §37.1.

##### 5.4.1.AA Observed action references *(R3.1 — preserved from V1.1 Y.15)*

`ObservedActionRef` carries demonstration-mode observations from §12.3A capture. The `privacy_classification` and `retention_window` fields make the privacy posture of the observation explicit at the type level.

```ts
type ObservedActionRef = {
  action_id: string;
  observed_at: string;
  action_summary: string;                     // sanitized; see privacy_classification
  privacy_classification: "demonstration_observed";
  retention_window: "ephemeral_volatile" | "user_accepted_durable";
  schema_version: 1;
};
```

ADJ-079's demonstration buffer is the canonical owner; `ObservedActionRef` as used in §38 demonstration segments (§38.7) references through this contract. The `retention_window === "ephemeral_volatile"` default means observations vanish on session boundary unless the user has explicitly committed them via the OnboardingCommitSaga (§12.6A).

##### 5.4.1.BB Session injection manifest *(R3.1 — preserved from V1.1 Y.23; updated for HH.3 event model)*

The `SessionInjectionManifest` provides a session-level rollup over all packet artifacts in a single session, enabling the Session Inspector surface (§20.3 Context Inspector extension) to enumerate every packet, its outcome, and any reconciliation events that referenced it.

```ts
type SessionPacketArtifactRef =
  | { kind: "packet_manifest";       manifest_id: string; packet_id: string }
  | { kind: "blocked_manifest";      blocked_manifest_id: string; packet_id: string }
  | { kind: "reconciliation_event";  reconciliation_event_id: string; packet_id: string }
  | { kind: "final_prompt_manifest"; final_manifest_id: string; operation_id: string };

type SessionInjectionManifest = {
  session_manifest_id: string;
  session_id: string;
  packet_artifact_refs: SessionPacketArtifactRef[];
  scope_trajectory: ScopeSnapshot[];           // chronological scope evolution within the session
  schema_version: 1;
};
```

Note on V1.1 → R3.1 evolution: V1.1 Y.23 originally included a `reconciliation_overlay` variant. R3.1 renames this to `reconciliation_event` since the overlay type was retired by HH.3 (see §38.9). The semantics are equivalent — both point to per-card reconciliation outcomes — but the underlying artifact is now an append-only event, not a mutable overlay.

V1's `packet_manifest_refs: string[]` is fully replaced by `SessionPacketArtifactRef[]`; the union shape captures blocked, retried, and reconciliation-bearing packets that the V1 string array could not represent.

### 5.4A Knowledge consumer contract *(ADJ-09; R3.1 — extended for HH.2.7)*

The Knowledge Consumer Contract specifies how downstream services consume DOC24 outputs. R3.1 extends it for the 3-way join discipline:

```
Consumer            Reads from                            Writes to                       Attribution
BDSM V6.5+          PacketInjectionManifest               (utility updates via EC)        3-way join: packet × final × reconciliation
                    FinalPromptInjectionManifest
                    PacketReconciliationEvent[]

KDA R3+             PacketInjectionManifest.card_records  (variant performance via EC)    Per render_variant_id (II.1)
                    FinalPromptInjectionManifest.spans    
                    PacketReconciliationEvent[]

PropA R6+           policy_generation_id                  (no DOC24 manifest write)       Per policy_decision_id
                    PacketSourceEligibilityDecisionSchema

DOC8                PacketReconciliationEvent[]           (pattern detection via EC)      3-way join (HH.2.7)
                    PacketInjectionManifest
                    FinalPromptInjectionManifest

DOC11               FinalPromptInjectionManifest          (annotations via gateway)       Per slot_id
                    PromptInjectionSpan[]

DOC72 R6+           EntityGraph mutations                 (consumed via EC events)        Per node_id

DOC73 V1.5+         ConsolidatedUnderstanding queries     AuthorityRenderDecision         Per constellation_ref

Packet Inspector    All of the above                      (read-only)                     Per packet_id

DOC25               PacketAssemblyScope.tokenizer_ref     TierAssignmentResult            Per (max_allocated_tokens, target_model_family)
                    + max_allocated_tokens
                    + target_model_family (HH.8.3)
```

Invariant per HH.2.7: NO utility/attribution signal flows from a card absent from `FinalPromptInjectionManifest`. BDSM and DOC8 implement the 3-way join semantic per `OBL-BDSM-NEW-MANIFEST-JOIN-01` and `OBL-D8-NEW-MANIFEST-JOIN-01`.

---

---

## 6. Entity graph / world model

R3.1 preserves R3's entity graph contract. The entity graph is owned by DOC72 R6+; DOC24 consumes it via `NodeKind`, `EntityRef`, and `OutcomeClass` imports (HH.6.2, HH.11.2). This section specifies DOC24's interface to the graph, not the graph's internal structure.

### 6.1 What the graph is

The entity graph is the single source of truth for ELNOR's knowledge model. Every assertion, every relationship, every authority anchor, every tool capability lives as a node or edge owned by DOC72. DOC24's role:

- Read nodes/edges to assemble candidate cards (§19, §27).
- Emit mutation events through EC Core (§7.3 sole-writer rule) when policy and lifecycle gates pass.
- Consume `AuthorityResult` and `ConsolidatedUnderstanding` from DOC73 for the rendering decisions in §26.5.2 and HH.5.1.

R3.1 does not re-specify the graph schema; that lives in DOC72 R6+ §2. The `DOC24_IMPORTED_TYPES` registry (§5.4.1.Y) pins the upstream version and section reference.

### 6.2 What the graph is not

The entity graph is NOT:
- A document store. Source documents live in DOC7 buckets; the graph references them via `SourceRef.source_id`.
- A vector index. Embedding-based retrieval is a derived projection over graph nodes; the graph's canonical lookup is by `entity_id` and indexed alias/name fields.
- A capability registry. Tool capabilities are nodes of `node_kind === "tool_capability"`, but the registry contract (§14, §13.4B) wraps them with selection, experience counters (§5.4.1.M), and JIT-mounting state (§16).
- A memory system. DOC1 governs memory lifecycle (§10.1); the graph stores entities, not user-facing memory blobs.

### 6.3 Entity classes are governed

Every entity has a `node_kind` from DOC72's `NodeKind` enum (§5.4.1.A). DOC24 may not introduce new `node_kind` values; new kinds require a DOC72 R6+ amendment and a corresponding `DOC24_IMPORTED_TYPES` registry update.

### 6.4 Core entity classes

DOC24 consumers reference the following `NodeKind` values:

| `node_kind` | DOC24 sections that read/write |
|---|---|
| `entity` | §8, §9, §10, §26 |
| `concept` | §10, §11 |
| `procedure` | §13, §14 |
| `skill` | §11, §13 |
| `matter_context` | §5.3, §12, §38.4 |
| `work_context` | §5.3, §38.4 |
| `tool_capability` | §13.4B, §14, §16, §17 |
| `authority_constellation` | §26.5.2, §29, HH.5.1 |
| `schema_definition` | §7, §23 |
| `source` | §7, §9, §10 |
| `user_directive` | §10.6, §12 |
| `system_artifact` | §7.2A (companion artifacts) |

### 6.5 Q/EC internal objects are graph-eligible

Internal Q Dashboard and EC Core objects (room states, saga states, packet manifests) are not stored in the graph. They live in their own canonical stores (§7.2). The graph is for domain entities and the cross-doc objects (`tool_capability`, `authority_constellation`) that the system reasons about.

### 6.6 Relationship typing

Relationships in the graph are typed by `relationship_type` (DOC72-owned). DOC24 references the following types in its assembly and routing logic:

- `executes_via` — links a procedure to a tool_capability (§13.4B, §14).
- `cites_as_authority` — links an assertion to an authority_constellation (§26.5.2, §29).
- `belongs_to_matter` — links any entity to a `matter_context` node (§5.3).
- `belongs_to_work_context` — links to a `work_context` node (§5.3).
- `confirms` / `contradicts` — outcome-based relationships emitted via reconciliation events (HH.3).
- `superseded_by` — supersession chain for retracted/replaced nodes (§6.8).

Extensions are tracked via `OBL-D72-NEW-RELATIONSHIP-EXTENSIONS-01`.

### 6.7 Provenance / confidence / freshness

Every node carries provenance (§5.4.1.B). Confidence is computed per DOC73 V1.5+ §6.5 from evidence aggregation; DOC24 reads it but does not mutate it. Freshness uses the `staleness_state` enum from DOC72 R6+ §16.2:

```
staleness_state ∈ {fresh, stale, verification_required, expired, invalidated}
```

DOC24 rendering rule for `verification_required`: card is retrievable but `hedge_mode: "verification_required"` is set on the directive (§26.5.2).

### 6.8 Entity lifecycle

`lifecycle_state` per DOC72 R6+ §2.4:

```
lifecycle_state ∈ {draft, active, archived, retracted, merged_into, superseded}
```

Behaviors:
- `draft` — visible to creator only; not surfaced in retrieval.
- `active` — full retrieval eligibility.
- `archived` — suppressed from retrieval by default; queryable via `inspect_knowledge` (per V1.1 ADJ-RT-118).
- `retracted` — suppressed from retrieval; reconciliation events still emit (audit trail preserved).
- `merged_into` — non-canonical; `merged_into_node_id` points to canonical replacement.
- `superseded` — replaced by a newer assertion; `superseded_by_node_id` points to replacement.

The `lifecycle_filtered` packet stage (§38.1.2) removes `archived`, `retracted`, `merged_into`, and `superseded` from candidate sets unless an `inspect_*` capability is invoked.

### 6.9 Promotion rules

Per V1 ADJ-RT-015, ADJ-RT-016, and ADJ-RT-129, promotion from `draft` to `active` requires:

```ts
type EntityPromotionPolicy = {
  minimum_visible_invocations: number;     // for procedure/tool_capability nodes
  minimum_distinct_context_support: number;
  minimum_user_confirmations: number;      // for user_directive nodes
  schema_version: 1;
};
```

The Thompson sampling for tool selection (§13.4B) uses `user_visible_invocation_count` from the experience counters as the promotion gate input.

### 6.10 Deterministic ID policy and alias normalization

Node IDs are ULIDs. Alias normalization (lowercasing, diacritic stripping, whitespace collapse) is done by DOC72; DOC24 reads canonical names but does NOT re-normalize.

The `NameMatchPolicy` from V1 ADJ-RT-108 governs how name-based resolution (§13 routing) compares against canonical and alias forms; this lives in DOC72 R6+.

### 6.11 Pruning / archive policy

Per V1 ADJ-RT-085 (and HH.13.4 architectural retention), `archive_node` and `archive_candidate` dispositions cause `lifecycle_state` transitions to `archived` without deletion. Retrieval suppression is by lifecycle filter (§38.1.2), not deletion. The retention guarantee preserves all events; only current view is filtered.

---

## 7. Persistence model

### 7.1 File-backed persistence

ELNOR's persistence layer is local-first, file-backed. The canonical paths are pinned in EC Core configuration. R3.1 preserves R3's file-backed model; the only addition is the §7.2A extension for V1.x companion stores (HH.11.5).

```
~/.elnor/
  entity_graph.sqlite                    # canonical durable store (DOC72)
  events/                                # append-only JSONL streams
    graph_events.jsonl
    working_context_events.jsonl
    source_policy_events.jsonl
    onboarding_session_events.jsonl
    pack_mount_events.jsonl
    receipt_events.jsonl
    packet_reconciliation_events.jsonl   # V1.3 HH.3 added
    tool_outcome_evidence_events.jsonl   # V1.3 HH.4.2 added
    candidate_manifest_lint_results.jsonl # V1.3 HH.2.2 added
    packet_lint_failure_records.jsonl    # V1.3 HH.2.2 added
  views/                                 # derived current-view snapshots
    ...
  registries/                            # build-time companion artifacts
    reason_code_namespace_registry.json
    injection_slot_registry.json
    acceptance_test_registry.json
    reviewer_finding_index.json
    lint_closure_table.json
    degraded_reason_code_registry.json
    doc24_imported_types.json
  ELNOR_MEMORY/
    config/
      saved_contexts/*.json              # SavedContextProfile-s (ADJ-42)
```

### 7.2 Store classification matrix *(revised per ADJ-02, ADJ-03; extended in §7.2A)*

| Store / artifact | Durability class | Canonical owner | Rebuild source |
|---|---|---|---|
| `entity_graph.sqlite` | **canonical durable store (SQLite)** | EntityGraphService (DOC72) | n/a — this IS canonical truth |
| `graph_events.jsonl` | **async audit export (JSONL)** | EntityGraphService | written from SQLite mutations; DR rebuild only |
| `suggestion_queue_store` | canonical atomic current view | EntityGraphService | suggestion events / graph writes |
| `working_context_events.jsonl` | canonical append-only event log | WorkingContextService | n/a |
| `working_context_store` | canonical atomic current view | WorkingContextService | `working_context_events.jsonl` |
| `saved_context_profile_store` | canonical atomic current view (JSON config) | WorkingContextService | `ELNOR_MEMORY/config/saved_contexts/*.json` *(ADJ-42)* |
| `user_preference_store` | **derived rebuildable projection** | UserPreferenceService (facade) | filtered read-model of `entity_graph.sqlite` rows where `node_kind = 'user_directive'` *(ADJ-03; R3.1 — terminology aligned)* |
| `source_policy_events.jsonl` | canonical append-only event log | SourcePolicyService | n/a |
| `source_policy_store` | canonical atomic current view | SourcePolicyService | `source_policy_events.jsonl` |
| `onboarding_session_events.jsonl` | canonical append-only event log | OnboardingOrchestrator | n/a |
| `onboarding_session_store` | canonical atomic current view | OnboardingOrchestrator | `onboarding_session_events.jsonl` |
| `pack_mount_events.jsonl` | canonical append-only event log | ToolPackManager | n/a |
| `pack_mount_state_store` | derived rebuildable current view | ToolPackManager | `pack_mount_events.jsonl` + live runtime probe |
| `packet_snapshot_store` | derived rebuildable cache | CapabilityKnowledgePacketAssembler | packet assembly inputs + revision hash |
| `note_context_projection` | derived rebuildable projection | NotesContextService | DOC20 note/current views + graph + working context |
| `resolver_cache_store` | derived rebuildable cache | TargetResolutionService | resolver inputs + current graph/memory/live state |
| `artifact_index_store` | derived rebuildable projection | BackgroundIndexingService | approved source metadata + indexing job events |
| `indexing_job_store` | canonical atomic current view | BackgroundIndexingService | indexing job events |
| `receipt_store` | canonical append-only event log | SharedActionHandlerLayer | n/a *(ADJ-53)* |
| `compaction_trace_store` | derived rebuildable projection | CapabilityKnowledgePacketAssembler | compaction decisions *(ADJ-81)* |

**Persistence truth hierarchy (ADJ-02):** SQLite (`entity_graph.sqlite`) is canonical. JSONL (`graph_events.jsonl`) is the async audit trail. On normal startup, if SQLite is intact, JSONL is irrelevant. If SQLite is corrupt, JSONL enables disaster recovery — but this is DR, not normal startup.

### 7.2A V1.x companion stores *(R3.1 — added per HH.11.5)*

R3.1 adds ten companion stores to support the V1.x runtime contracts:

| Store / artifact | Durability class | Canonical owner | Rebuild source | Introduced in |
|---|---|---|---|---|
| `packet_reconciliation_event_store` | canonical append-only event log | DOC24 (PacketAssemblyOrchestrator) | n/a | HH.3 |
| `packet_reconciliation_current_view_store` | derived rebuildable projection | DOC24 | `packet_reconciliation_event_store` | HH.3 |
| `candidate_manifest_lint_result_store` | canonical append-only | DOC24 (LintRunner) | n/a | HH.2.2 |
| `packet_lint_failure_record_store` | canonical append-only | DOC24 (LintRunner) | n/a | HH.2.2 |
| `tool_outcome_evidence_event_store` | canonical append-only event log | DOC24 (ToolOutcomeLedger) | n/a | HH.4.2 |
| `reason_code_namespace_registry` | build-time static artifact (JSON) | DOC24 (build process) | source-code scan + registry constant | HH.5.3 |
| `injection_slot_registry` | build-time static artifact (JSON) | DOC24 (build process) | source-code scan of registered slots | HH.6.5 |
| `acceptance_test_registry` | build-time static artifact (JSON) | DOC24 (build process) | V1.1 CC + V1.3 HH.10 + V1.4 II tests | HH.10.2 |
| `reviewer_finding_index` | build-time static artifact (JSON) | DOC24 (build process) | mechanical grep over review file | HH.0.3 |
| `lint_closure_table` | build-time static artifact (JSON) | DOC24 (build process) | DD.1 findings + closure references | HH.0.2 |
| `degraded_reason_code_registry` | build-time static artifact (JSON) | DOC24 (build process) | HH.15.2 constant | HH.15.2 |
| `doc24_imported_types` | build-time static artifact (JSON) | DOC24 (build process) | §5.4.1.Y constant | HH.11.2 |

Build-time static artifacts are generated mechanically during the R3.1 build pipeline (§23). They are companion to R3.1, not part of R3.1 itself; their generation is gated by HH.16.3 / II.6.2 R3.1 drafting checklist.

### 7.3 Sole-writer rule

Only EC Core writes to canonical durable stores. All other services emit events; EC applies them. This is enforced architecturally (services do not have write credentials to canonical stores) and at the policy layer (V1 ADJ-RT-117 race-safety check requires EC-mediated writes).

Per HH.5.4, the PolicyDecisionEngine's `applyMutation` follows the persist-then-swap pattern: durable persistence completes BEFORE the in-memory state swap. This guarantees that a crash mid-mutation leaves either the prior consistent state OR the new persisted state; never a torn write.

### 7.4 Recovery model *(revised per ADJ-02)*

On startup:
1. Load `entity_graph.sqlite`. If intact, proceed.
2. If corrupt or missing, replay `graph_events.jsonl` to rebuild SQLite (DR mode; logged with `degraded_reason_codes: ["recovery.disaster_recovery_replay"]`).
3. Verify graph integrity per §7.5; block startup on integrity failure.
4. Rebuild derived current-views from canonical event logs.

The companion stores in §7.2A use the same pattern: canonical event logs are authoritative; current-view snapshots are rebuildable.

### 7.5 Machine-checkable graph integrity

R3 preserves the integrity check suite (referential integrity for `merged_into_node_id`, `superseded_by_node_id`, `parent_id`; lifecycle-state validity; alias uniqueness within namespace). R3.1 adds two checks for HH-introduced stores:

```
manifest_reconciliation_event_invariant_check:
  Every PacketReconciliationEvent.manifest_id MUST point to a PacketInjectionManifest
  in packet_snapshot_store OR a BlockedPacketManifest with matching packet_id.

tool_outcome_event_attribution_invariant_check:
  Every ToolOutcomeEvidenceEvent.selection_decision_id (when present) MUST point to
  a ToolSelectionResult.selected emission recorded in receipt_store.
```

Integrity failures emit `reason_codes: ["doc24.integrity_check_failed"]` and block runtime until resolved.

---

## 8. Entity creation and update pipeline

R3.1 preserves the R3 entity creation pipeline. V1.x additions: provenance structure tightening (HH.7.1), candidate manifest pre-durability discipline (HH.2.1), and the saga concurrency protocol (HH.9.1).

### 8.1 Source gating

Source eligibility is gated per V1 ADJ-RT-072 (source exclusion) and ADJ-RT-118 (inspect_knowledge consent). Workspaces listed in `excluded_workspace_ids` (on the active context) are filtered at retrieval AND at delivery directive assignment — the double-protection invariant.

```ts
type SourceGatingDecision =
  | { outcome: "eligible"; source_refs: SourceRef[] }
  | { outcome: "ineligible"; reason_codes: ReasonCode[]; affected_source_refs: SourceRef[] }
  | { outcome: "reference_only"; source_refs: SourceRef[]; reason_codes: ReasonCode[] };
```

Cross-doc obligation (existing in OP-A): `OBL-PROPA-NEW-SOURCE-EXCLUSION-FILTER-01`.

### 8.2 Candidate extraction

PBE extraction produces candidate entities. R3.1's PBE-derived detection per HH.7.1 uses `provenance.primary_source.kind === "pbe_extraction"`, not the V1.2 `entity_kind === "corpus"` heuristic that over-matched user-captured corpus.

```ts
type ExtractionCandidate = {
  candidate_id: string;
  proposed_entity_class: NodeKind;
  proposed_canonical_name: string;
  proposed_aliases: string[];
  evidence_text: string;
  evidence_source_refs: SourceRef[];
  extraction_run_id: string;
  provenance: Provenance;                  // primary_source.kind === "pbe_extraction"
  confidence: number;
  schema_version: 1;
};
```

Long-chunk extraction uses the order specified in §38.19 (HH.12 — full A-I `ExtractionPromptSectionId` enum; `DEFAULT` order keeps instructions before source; `LONG_CHUNK` order moves instructions early when chunk size triggers).

### 8.3 Candidate classification

Per V1 ADJ-RT-085 (digest disposition split), classification produces a `DigestItemDisposition`:

```ts
type DigestItemDisposition =
  | "confirm_accuracy"
  | "mark_useful"
  | "confirm_accuracy_and_useful"
  | "dismiss_digest_item"
  | "not_relevant_here"
  | "factually_wrong"
  | "archive_candidate"
  | "archive_node"                          // suppression, not deletion
  | "edit_and_save"
  | "defer";

type DigestItemDispositionRequest = {
  digest_item_id: string;
  disposition: DigestItemDisposition;
  evidence?: FactuallyWrongEvidence;        // required iff disposition === "factually_wrong"
  idempotency_key: string;
  schema_version: 1;
};

type FactuallyWrongEvidence = {
  evidence_text: string;
  source_refs?: SourceRef[];
};
```

V1 ADJ-RT-120's `accept`/`useful_to_track` synonyms are retired; ADJ-RT-085's enum is canonical.

### 8.4 Entity linking

Entity linking uses the five-step resolution chain from R3 §13 (preserved unchanged in R3.1):

1. Direct match (canonical name + entity_id).
2. Alias resolution (DOC72 `NameMatchPolicy`).
3. Constellation anchor (DOC73 authority-aware lookup).
4. Context inference (active matter/work context bias).
5. User disambiguation (per §38.8 + HH.14).

The chain's outputs are `ResolutionSource` values (§5.4.1.Q) that populate `ManifestCardEntry.resolution_source` (restored per V1.4 II.1).

### 8.5 Promotion decision

Promotion from `draft` to `active` is gated per §6.9. The promotion sequence:

```
1. Candidate enters `draft` lifecycle_state.
2. Background indexing (§9) accumulates evidence.
3. Promotion gate evaluates:
   - user_visible_invocation_count >= minimum_visible_invocations
   - distinct_context_support >= minimum_distinct_context_support
   - user_confirmations (for user_directive nodes) >= minimum_user_confirmations
4. On gate pass: emit EntityPromotionEvent; EC applies; lifecycle_state → `active`.
```

For tool_capability nodes specifically, promotion also requires reliability lower bound per HH.4.12 (Thompson high-stakes qualification).

### 8.6 Human confirmation rules

Per V1 ADJ-RT-014 and ADJ-RT-038, human confirmation is required for:
- Promotion of high-risk-tier tool capabilities (per §13.4B risk tier).
- Retraction of any `active` entity.
- Disposition of `factually_wrong` digest items (evidence required).
- Source exclusion changes that affect previously-active entities.

Confirmation surfaces are owned by the operator UI (§20); DOC24 emits `pause_state: "user_choice_pending"` on the operation while awaiting confirmation.

### 8.7 Conflict resolution

When two extraction runs produce conflicting assertions about the same entity, R3.1 preserves R3's conflict resolution policy:

```
Conflict cases:
1. Same node_id, different canonical_name → resolve via DOC72 alias normalization;
   if conflict persists, emit ConflictRequiresDisambiguation event.

2. Same canonical_name, different node_id → use constellation anchor (DOC73);
   if no anchor, emit ConflictRequiresDisambiguation.

3. Same node_id, different lifecycle_state (one writer says active, another says retracted) →
   last-write-wins with audit trail; reconciliation event emitted.

4. Conflicting evidence (one source says X, another says ¬X) → preserve both;
   DOC72 confidence aggregation handles the contradiction.
```

### 8.8 Runtime update propagation

When an entity mutates (graph write), downstream caches invalidate per the invalidation rules in HH.5.6 (source eligibility cache key) and § (resolver cache):

```ts
type EntityMutationEvent = {
  event_id: string;
  node_id: string;
  mutation_kind: "created" | "updated" | "lifecycle_transitioned" | "merged" | "retracted";
  prior_revision?: string;
  new_revision: string;
  affected_card_stable_keys: CardStableKey[];   // for cache invalidation
  occurred_at: string;
  schema_version: 1;
};
```

The `affected_card_stable_keys` field allows precise cache invalidation: only cards whose `card_stable_key` was derived from this node's material need to be invalidated.

### 8.9 Consent revocation / retraction propagation

User-initiated retraction propagates per V1 ADJ-RT-118:
1. Entity transitions to `retracted` lifecycle_state.
2. Reconciliation events for packets that included the entity remain (audit trail).
3. Subsequent packets do not include retracted entities; lifecycle filter (§38.1.2) catches them.
4. If the entity was a tool_capability, its `failure_lockout_until` is set to far-future to prevent re-selection.

Cross-doc obligation: `OBL-D72-NEW-RETRACTION-PROPAGATION-01` (existing in OP-A).

---

## 9. Background indexing and passive learning

### 9.1 Purpose

Background indexing processes approved sources (DOC7 buckets, calendar items, file imports) in the background to:
- Extract candidate entities (§8.2).
- Build derived projections (`artifact_index_store`).
- Warm semantic caches (§35 calendar warming).

### 9.2 Scope

Indexing operates only on **approved** sources. Source approval is a user-gated step (operator UI; §20). Unapproved sources are visible to the user as buckets but are NOT indexed.

Approval propagates to:
- `source_policy_store` (current view).
- `source_policy_events.jsonl` (canonical event log).
- Background indexing queue (subject to §9.4 priority ladder).

### 9.3 Two-stage indexing

R3.1 preserves R3's two-stage indexing:

```
Stage 1 — Lightweight metadata indexing (always):
  - File hash, byte size, mime type, modification time
  - Storage path, source_id, source_kind
  - Persisted to artifact_index_store

Stage 2 — Content-aware indexing (gated):
  - Text extraction (mime-aware: pdf, docx, txt, md, …)
  - Entity extraction via PBE pipeline (§8.2)
  - Long-chunk extraction with HH.12 LongChunkMitigation when applicable
  - Persisted to graph_events.jsonl (entities created); artifact_index_store (chunk index)
```

Stage 2 is gated by:
- Source approval.
- DomainSignalProfile presence (or extraction-default policy).
- Available indexing budget (rate limits per §9.4).

### 9.4 Lazy / shadow indexing priority ladder

```
Priority 0 (immediate):
  - Sources directly invoked in current operation (e.g., file dropped into a chat).
  - User-marked-priority items.

Priority 1 (active):
  - Newly approved sources.
  - Sources linked to the active matter context.

Priority 2 (background):
  - Approved sources not linked to active context.
  - Shadow re-indexing of stale entries.

Priority 3 (deferred):
  - Failed or partial extractions awaiting retry.
  - Long-chunk extractions queued for off-peak processing.
```

The PBE-lite packet effect (HH.7.3) emits `replay_eligible_after` timestamps for deferred extractions; the replay scheduler processes them at priority 3.

### 9.5 Source-level consent boundary table

| Source kind | Default approval | User-confirmation required? | Notes |
|---|---|---|---|
| Filesystem path (user-dropped) | requested-at-add | yes | One-time approval |
| DOC7 bucket (firm_shared) | per-bucket policy | per-bucket | Policy in `source_policy_store` |
| Calendar (per-account) | per-account toggle | per-account | §35 |
| Email (per-thread) | thread-scope | per-thread | Cross-doc to DOC16 |
| URL fetch | per-fetch | per-fetch | No persistent approval |
| Tool output | implicit (tool was authorized) | no | Tool registration covers |
| User capture | implicit (user authored) | no | — |

### 9.5A Source-policy ownership and inspectability

`source_policy_store` is owned by SourcePolicyService and is queryable via `inspect_source_policy` capability (per V1 ADJ-RT-118 inspectability rule). The operator can see exactly which sources are approved, suspended, or excluded, and with what justification.

### 9.5B Source exclusion filters *(ADJ-45; R3.1 — references HH.8.4)*

`excluded_workspace_ids` on the active context is enforced at retrieval AND at delivery directive assignment (the double-protection invariant). Per HH.8.4 OpenClaw target normalization, DOC24 sub-agent invocations use `sessions_spawn` with `context: "isolated"` by default; payload context is serialized into `task_prompt` explicitly. Fork context is NOT used by DOC24 assembly logic; `native_fork_mapping` records observation only.

### 9.6 Anti-spooky rule

R3 ADJ-RT-090 anti-spooky rule (preserved): the operator must be able to explain, in plain language, why any indexed content surfaced. Indexing that produces results without a traceable approval path is forbidden.

Operationally:
- Every `ExtractionCandidate.evidence_source_refs` must point to a source with approval recorded in `source_policy_store`.
- Every `PacketCandidateCard` must trace through `source_eligibility_aggregate.policy_decision_refs` to a policy decision.
- The Packet Inspector (§20) renders this trail for any included card on demand.

### 9.7 Indexing progress events and UI *(ADJ-62; R3.1 — preserved)*

Indexing progress emits `IndexingProgressEvent` consumed by the operator UI. R3 preserves the event schema; R3.1 adds the `pbe_lite_effect` linkage when PBE-lite defers extractions:

```ts
type IndexingProgressEvent = {
  event_id: string;
  source_id: string;
  stage: "metadata" | "content_aware" | "deferred" | "failed";
  progress_pct: number;                     // 0-100
  pbe_lite_effect_ref?: string;             // R3.1 — present when deferred via HH.7.3
  occurred_at: string;
  schema_version: 1;
};
```

---

## 10. Memory integration

### 10.1 DOC1 remains the memory lifecycle/governance owner

DOC1 owns the memory governance contract: what is durable user-facing memory, how it's surfaced, how it's deleted on user request. DOC24 does not duplicate this; it consumes DOC1's outputs.

### 10.2 DOC1 feeds the graph, but is not the graph

When DOC1 creates a memory blob (e.g., a user-captured note), the underlying entity (the captured concept or fact) lives in the graph. DOC1 holds the user-facing memory record; the graph holds the entity. Retrieval may surface either, with DOC1-mediated rendering for memory-class items.

### 10.2A Preference dual-write elimination *(ADJ-03; R3.1 — terminology aligned)*

Per V1.2 GG terminology alignment: user preferences are entities of `node_kind === "user_directive"` in the graph. The `user_preference_store` is a derived projection (filtered read-model) of those rows. There is no separate canonical preference store. V1's "memory_directive" naming is retired in R3.1; `user_directive` is canonical per HH.6.2 NodeKind enum.

Migration note for coding agents reading V1 cards: where V1 says `node_kind === "memory_directive"`, R3.1 reads `node_kind === "user_directive"`. Both refer to the same node class.

### 10.3 Memory / graph / capability precedence

When the same query could resolve via memory, graph, or capability:
- **Direct lookup** (e.g., "what's my email") → memory (DOC1).
- **Entity surfacing** (e.g., "tell me about Marex") → graph.
- **Tool invocation** (e.g., "search the Brooge filings") → capability.

The semantic router (§13) makes this choice based on the action readiness assessment.

### 10.3A User-taught preference precedence

User-directive nodes (created via explicit user statements like "I prefer X" or "Never do Y") have highest precedence in directive resolution. They surface as `DirectiveConstraint.origin.kind === "user_override"` and have `hard_requirement_origin === "user_emphasized"` when the user marked the preference critical (HH.4.4).

### 10.4 Coordinated graph / memory write contract

When a user input creates BOTH a graph entity AND a DOC1 memory blob (e.g., "remember that I work at Schall Law" creates a `user_directive` graph node AND a DOC1 memory entry), the write is coordinated through the OnboardingCommitSaga (§12.7) with saga-step concurrency control per HH.9.1.

The saga:
1. Acquires `expected_context_revision` for the active context.
2. Writes graph entity (EC-mediated; new revision).
3. Refreshes captured revision (HH.9.1 retry refresh).
4. Writes DOC1 memory entry (EC-mediated).
5. Emits idempotency receipt.

If step 4 encounters `concurrency_conflict`, the saga re-fetches the latest context revision and retries (HH.9.1 retry budget of 3). Beyond budget → `saga_concurrency_retry_budget_exhausted` and operation pauses with `pause_state: "manual_review_required"`.

### 10.5 Type-level heuristics

R3.1 preserves R3's type-level heuristics that resolve ambiguous user inputs to entity classes:

```
Heuristic           Maps to                    Example
------              -------                    -------
proper noun          node_kind === "entity"     "Marex Group" → entity
verb phrase          node_kind === "procedure"  "file a complaint" → procedure
"I prefer"           node_kind === "user_directive"  "I prefer Helvetica" → user_directive
"never X"            node_kind === "user_directive"  "Never email Bob" → user_directive
quote / claim        node_kind === "concept"    "Force is mass times acceleration" → concept
```

Heuristics are advisory; final classification is determined by the candidate classification step (§8.3).

### 10.6 Negative memories / entity-scoped directives / user preferences

Negative memories ("never X") and entity-scoped directives ("for Marex, always Y") are `user_directive` nodes with relationship edges:

- Negative memory: `user_directive` with `directive_constraint: { kind: "block_inline_rendering", origin: { kind: "user_override", … } }`.
- Entity-scoped directive: `user_directive` with `belongs_to_matter` or `belongs_to_work_context` relationship to the scoping entity.

These surface as `DirectiveConstraint[]` on candidate cards (§5.4.1.D).

### 10.7 Memory/graph operator inspection

The Memory Inspector and the Knowledge Inspector (operator surfaces; §20) read from DOC1 and the graph respectively. Per HH.7.4 cross-doc obligation `OBL-DOC21-NEW-PBE-LITE-BANNER-01`, the Knowledge Inspector surfaces `PBELitePacketEffect.banner_kind` in its header when packets in the inspector list have associated PBE-lite effects.

---

## 11. Self-learning and reflection

### 11.1 DOC8 owns learning quality and probing adaptation

DOC8 is the canonical owner of:
- Utility ledger maintenance (per BDSM V6.4 §12.5 + DOC8 line 3073 Shapley computation).
- Pattern detection across packets and outcomes (3-way join per HH.2.7).
- Probing adaptation (when to ask, what to ask).
- Meta-learning over rejection patterns.

R3.1 does not re-specify DOC8 internals. DOC24's interface is the manifest chain + reconciliation events.

### 11.2 Friction-aware probing

Probing is friction-aware: each probe consumes from a probe budget (per HH.4.8 `ProbeCostRule` discriminated union, Approach B with positive decimal costs; `-Infinity` retired).

```ts
type ProbeBudgetPolicyProfile = "suppressed" | "default" | "high_friction" | "low_friction" | "persistent_curiosity";

type ProbeClassification = "contextual_probe" | "clarifying_question" | "topic_shift_question";

type ProbeCostRule =
  | { kind: "allowed"; cost: number }       // positive decimal; never negative or Infinity
  | { kind: "blocked"; block_reason_code: ReasonCode };

const PROBE_COST_BY_KIND_BY_PROFILE: Record<ProbeBudgetPolicyProfile, Record<ProbeClassification, ProbeCostRule>> = {
  suppressed: {
    contextual_probe:     { kind: "blocked", block_reason_code: "probe.profile_suppressed" },
    clarifying_question:  { kind: "blocked", block_reason_code: "probe.profile_suppressed" },
    topic_shift_question: { kind: "blocked", block_reason_code: "probe.profile_suppressed" },
  },
  default: {
    contextual_probe:     { kind: "allowed", cost: 0.5 },
    clarifying_question:  { kind: "allowed", cost: 1.0 },
    topic_shift_question: { kind: "allowed", cost: 2.0 },
  },
  high_friction: {
    contextual_probe:     { kind: "allowed", cost: 1.0 },
    clarifying_question:  { kind: "allowed", cost: 2.0 },
    topic_shift_question: { kind: "blocked", block_reason_code: "probe.high_friction_topic_shift_blocked" },
  },
  low_friction: {
    contextual_probe:     { kind: "allowed", cost: 0.25 },
    clarifying_question:  { kind: "allowed", cost: 0.5 },
    topic_shift_question: { kind: "allowed", cost: 1.0 },
  },
  persistent_curiosity: {
    contextual_probe:     { kind: "allowed", cost: 0.5 },
    clarifying_question:  { kind: "allowed", cost: 1.0 },
    topic_shift_question: { kind: "allowed", cost: 1.5 },
  },
};
```

The probe budget itself is a numeric counter on the active context; exhausting it shifts the profile to `suppressed` until the budget refills (per §11.5).

### 11.3 Reflection loop

Reflection runs periodically (timer or activity-triggered) and reviews:
- Recent outcome reconciliation events (HH.3).
- Rejection patterns (cards that were `injected_and_ignored` or `injected_and_corrected`).
- Tool selection quality (Thompson sample distributions vs. observed outcomes; HH.4.12 reliability lower bound).
- Probe quality (probe → outcome correlation).

### 11.3A Reflection event contracts *(ADJ-63; R3.1 — event-sourced)*

Per HH.3 reconciliation event model and HH.4.2 event-sourced raw counters, reflection consumes events, not mutable counters:

```ts
type ReflectionRunEvent = {
  event_id: string;
  run_kind: "scheduled" | "manual" | "anomaly_triggered";
  input_event_range: { from: string; to: string };   // ISO-8601 UTC range
  outputs: ReflectionOutputRef[];
  occurred_at: string;
  schema_version: 1;
};

type ReflectionOutputRef = {
  output_kind: "rejection_pattern" | "tool_quality_alert" | "probe_quality_alert" | "candidate_directive_promotion";
  artifact_id: string;
  schema_version: 1;
};
```

Reflection outputs are **proposed** changes (candidate directive promotions, suggested probe profile changes). They route through user confirmation per §8.6, not direct mutation.

### 11.4 Learning outputs

Reflection produces four kinds of learning outputs:

1. **Rejection pattern alerts.** A pattern like "Marex cards are consistently `injected_and_ignored`" surfaces a candidate directive to deprioritize Marex cards in matter-Y context.
2. **Tool quality alerts.** A tool whose Thompson sample mean has drifted below the reliability lower bound (HH.4.12) is flagged for review.
3. **Probe quality alerts.** A probe profile producing high probe-to-no-action ratios is flagged for friction adjustment.
4. **Candidate directive promotions.** User behaviors that consistently produce explicit overrides surface as candidate user_directives for the user to accept.

### 11.5 Question debt / probe budget

The probe budget refill model:

```
Per-context probe budget:
  - Initial budget: 10 units
  - Refill: +1 unit per N hours of activity (configurable per ProbeBudgetPolicyProfile)
  - Hard cap: 20 units
  - Floor: 0 units (no negative budget — guarded per HH.4.8)

Per-context probe history:
  - Last probe timestamp (for dedup)
  - Per-trigger-kind cooldowns per HH.4.9

Profile transitions:
  - default → high_friction: triggered by 3+ user-ignored probes in 24h
  - high_friction → default: triggered by 7+ days of low probe activity
  - any → suppressed: explicit user setting OR profile-driven block
```

### 11.5A First-use restraint policy *(ADJ-89; R3.1 — preserved)*

On a user's first session with a new matter or domain profile, the probe profile is `low_friction` for 24 hours OR until 5 probes have been answered, whichever comes first. This prevents overwhelming the user during initial exploration.

### 11.5B Persistent Onboarding Curiosity integration *(R3 — added per ADJ-RT-074; R3.1 — extended per HH.4.9)*

Persistent Onboarding Curiosity (POC) maintains long-running curiosity threads — questions the system has but doesn't ask immediately. POC timing is governed by the `ProbeTimingPolicy` per HH.4.9:

```ts
type ProbeTriggerKind =
  | "first_contact"
  | "knowledge_density_gap"
  | "dormant_curiosity_reactivation"
  | "clarifying_disambiguation"
  | "resumption_affordance";

type ProbeTimingPolicy = {
  trigger_kind: ProbeTriggerKind;
  per_context_cooldown:
    | { kind: "none" }
    | { kind: "hours"; value: number }
    | { kind: "days"; value: number }
    | { kind: "once_per_context" };
  global_min_interval_hours: number;
  dedup_window_hours: number;
  schema_version: 1;
};
```

Default policies per trigger kind specified in HH.4.9.

---

## 12. Onboarding architecture

R3.1 preserves R3's onboarding architecture and incorporates V1.x changes: provenance-based PBE detection (HH.7.1), saga concurrency for OnboardingCommitSaga (HH.9.1), and ScopeSnapshot for SavedContextProfile / SuspendedContextSnapshot consistency.

### 12.1 Onboarding is optional

Onboarding is opt-in. The user may use ELNOR without onboarding; defaults apply. Onboarding enriches the user's working state with explicit context (matters, preferences, authority anchors) and unlocks domain-specific behaviors.

### 12.2 Three onboarding lanes

Onboarding operates across three lanes that may run in parallel:

```
Lane 1 — Conversational onboarding:
  Free-form chat where the user states facts ("I'm a litigator at Schall…").
  Per §12.5 conversational style; produces user_directive nodes.

Lane 2 — Demonstration mode:
  User demonstrates a workflow; system captures it as a procedure (per ADJ-RT-030).
  Per §12.3A.

Lane 3 — Bucket import:
  User points the system at a folder/bucket; PBE extracts entities.
  Per §8.2 + §9.3.
```

### 12.2A Lane coordination *(R3 — extended per ADJ-RT-025; R3.1 — preserved)*

Lane coordination prevents redundant work. If Lane 3 extracts an entity that Lane 1 is about to suggest the user create, Lane 1's prompt is skipped. The OnboardingCommitSaga coordinates across lanes via `idempotency_key` and entity dedup.

### 12.3 User-invoked onboarding mode

The user may invoke onboarding mode explicitly (UI affordance). In this mode:
- Probe profile is `low_friction`.
- Question debt is suspended.
- Demonstration mode (Lane 2) is offered.

Per V1 ADJ-RT-076, on-boarding mode exit returns to the prior context with full state preservation.

### 12.3A Demonstration mode UX *(R3 — significantly extended per ADJ-RT-030; R3.1 — preserved)*

Demonstration mode captures a workflow as a procedure. The user demonstrates steps; the system records them. After demonstration:
- The captured procedure is shown in review (per §12.6).
- Per V1 ADJ-RT-030 segment classification: `setup`, `core_actions`, `verification`, `teardown` segments are auto-classified.
- The user confirms or edits classifications.
- On commit, the procedure becomes an `active` `node_kind === "procedure"` node.

Segment interruption protocol per §38.7.

### 12.4 Onboarding outputs

Onboarding produces:
- `matter_context` and `work_context` nodes (Lane 1).
- `procedure` nodes (Lane 2).
- `entity` and `concept` nodes (Lane 3 PBE extraction).
- `user_directive` nodes (any lane).

All outputs route through the OnboardingCommitSaga (§12.7).

### 12.4A Conversational onboarding equivalence rule

Per V1 ADJ-RT-026: anything the user could state in conversational onboarding (Lane 1) MUST be expressible via the UI surfaces (operator entity creation, preference setting). The reverse is also true: UI affordances do not extend the conversational onboarding vocabulary.

### 12.4B Capture classification and linking

Captures (from any lane) are classified by:
- `node_kind` (per §10.5 heuristics + classifier).
- Scope inference (`firm_shared` vs `personal`; per §5.2 `ScopeInferenceBasis`).
- Linking to existing entities (per §8.4 five-step resolution).

### 12.4C Transient vs durable classification *(R3 — significantly revised per ADJ-RT-028; R3.1 — preserved)*

Captures are either **transient** (session-scoped; e.g., "for this conversation, assume X") or **durable** (persisted; e.g., "always assume X"). Classification:

```
Default:
  - User says "for now…" / "in this conversation…" → transient.
  - User says "always…" / "remember that…" → durable.
  - Ambiguous → ask (clarifying probe).

Transient captures:
  - Stored in active context; do NOT enter the graph.
  - Expire when context ends.

Durable captures:
  - Route through OnboardingCommitSaga.
  - Persist as graph entities.

Transient promotion (per ADJ-RT-028):
  A transient capture that is re-asserted N times across sessions becomes
  a candidate for promotion to durable (surfaces as candidate directive
  via reflection; §11.4).
```

### 12.4D Gap detection *(ADJ-10; R3.1 — preserved)*

Gap detection identifies missing onboarding inputs:
- Matter without jurisdiction (for legal-domain matters).
- Procedure without `executes_via` edge.
- Authority constellation without `essential_set`.

Gaps surface as `knowledge_density_gap` probes (per §11.5B trigger kind).

### 12.4E Onboarding trigger table *(ADJ-10; R3.1 — preserved)*

Triggers for onboarding prompts:

```
Trigger                          Lane     Profile
-------                          ----     -------
First contact (new user)          1        low_friction
New matter created                1        low_friction
Procedure has gaps                1, 2    default
PBE extracted ambiguous entity   3        clarifying_disambiguation
Workflow repeated 3+ times       2        suggest demonstration capture
```

### 12.5 Conversational style

Per R3 (preserved): conversational onboarding is brief, contextual, and never quizzes. The system asks ONE question at a time, in plain language, with no "let me confirm a few things" preambles. Per V1 ADJ-RT-074, prompts are sourced from the Persistent Onboarding Curiosity backlog when no immediate trigger is firing.

### 12.6 Review and commit

After onboarding inputs accumulate, the user reviews and commits:
1. UI surfaces a review panel (per §20).
2. Items show as proposed entities/directives with provenance.
3. User edits, confirms, or rejects each.
4. On commit, OnboardingCommitSaga executes.

### 12.6A Commit conflict detection *(ADJ-10; R3.1 — saga concurrency per HH.9.1)*

The OnboardingCommitSaga uses the saga-step concurrency protocol from HH.9.1. When the user clicks "commit":

```ts
async function executeOnboardingCommitSaga(input: {
  session_id: string;
  proposed_items: OnboardingProposedItem[];
  expected_context_revisions: Record<string, string>;
}): Promise<OnboardingCommitResult> {
  const saga: SagaState = await startSaga({
    saga_kind: "onboarding_commit",
    expected_touched_context_ids: Object.keys(input.expected_context_revisions),
    steps: buildOnboardingCommitSteps(input.proposed_items),
  });

  for (const step of saga.steps_pending) {
    const result = await executeStep(saga, step, 0);

    if (result.result === "blocked") {
      return {
        outcome: "blocked",
        blocked_reason: result.reason,
        completed_items: saga.steps_completed,
        schema_version: 1,
      };
    }

    if (result.result === "applied") {
      await recordStepCompleted(saga, step, result.new_context_revision);
    }
  }

  return {
    outcome: "committed",
    committed_items: saga.steps_completed,
    schema_version: 1,
  };
}
```

`executeStep` per HH.9.1: late-bound context fetched on-demand; on `concurrency_conflict`, re-fetches current revision and retries (budget 3). Beyond budget → `saga_concurrency_retry_budget_exhausted`.

### 12.7 Onboarding session schema

Per V1 ADJ-RT-029 split:

```ts
type OnboardingSessionState = {
  session_id: string;
  matter_context_slot_id?: string;
  work_context_id: string;

  lane_states: {
    conversational: ConversationalLaneState;
    demonstration?: DemonstrationLaneState;
    bucket_import?: BucketImportLaneState;
  };

  proposed_items: OnboardingProposedItem[];
  committed_item_refs: string[];                 // → entity IDs of committed items
  abandoned_at?: string;                          // ADJ-RT-029 — session abandonment policy

  scope_snapshot: ScopeSnapshot;                  // V1.3 HH.6.6 — captures matter/work context
  schema_version: 1;
};

type OnboardingProposedItem = {
  proposed_item_id: string;
  proposed_node_kind: NodeKind;
  proposed_canonical_name: string;
  proposed_aliases?: string[];
  proposed_relationships?: Array<{ relationship_type: string; target_entity_id: string }>;
  provenance: Provenance;
  user_confirmation_state: "pending" | "confirmed" | "edited" | "rejected";
  schema_version: 1;
};
```

### 12.7A Saved context: profiles and snapshots *(R3 — significantly extended per ADJ-RT-029; R3.1 — ScopeSnapshot consistency)*

R3.1 preserves the V1 ADJ-RT-029 split between durable `SavedContextProfile` and ephemeral `SuspendedContextSnapshot` (per §5.3B). The two artifacts now share `ScopeSnapshot` as their scope-capture mechanism per HH.6.6:

```
SavedContextProfile:
  - Durable; user-pinned.
  - scope_snapshot captured at pin time.
  - Resume-on-demand at any time.
  - Stored in saved_context_profile_store + ELNOR_MEMORY/config/saved_contexts/*.json (ADJ-42).

SuspendedContextSnapshot:
  - Ephemeral; transient.
  - scope_snapshot captured at suspension.
  - resumable_until expiry.
  - Stored in working_context_events.jsonl (suspension events).
```

Restoration protocol per §38.6.

### 12.7B Onboarding and saved-context routes

UI routes for onboarding and saved-context management:

```
/onboarding/start
/onboarding/session/{session_id}
/onboarding/session/{session_id}/commit
/onboarding/abandoned                          # list of abandoned sessions per ADJ-RT-029

/contexts/saved                                # list of SavedContextProfile-s
/contexts/saved/{profile_id}
/contexts/saved/{profile_id}/resume
/contexts/suspended                            # list of SuspendedContextSnapshot-s
/contexts/suspended/{snapshot_id}
/contexts/suspended/{snapshot_id}/resume       # if resumable_until > now
/contexts/active                               # current active context
/contexts/active/switch                        # push new matter; old pushes to suspension stack
```

### 12.7C Entity creation action *(ADJ-85; R3.1 — references II.2 supersession)*

Operator-initiated entity creation routes through the same EntityCreationAction as PBE-extracted entities. Operator-created entities have `provenance.primary_source.kind === "user_capture"` (NOT `"pbe_extraction"`) — this is the distinction HH.7.1 codifies for `isPBEDerivedCard`.

When the operator edits an existing entity (rather than creating new), the prior version's `lifecycle_state` transitions to `superseded`, with `superseded_by_node_id` pointing to the new version. The PacketInjectionManifest field `superseded_by_packet_id` (restored per V1.4 II.2.2) tracks the same supersession pattern at the manifest level.

### 12.8 Onboarding should not be rigid

R3.1 preserves R3's principle: onboarding is a conversation, not a form. The user may skip, defer, ask for examples, or change topic. The system never blocks productive work on incomplete onboarding.

Per HH.7.3 `PBELitePacketEffect.banner_kind === "pbe_lite_promotion_pending"`: when onboarding extraction is deferred but the user proceeds anyway, the packet manifest carries a visible banner indicating that some entities are PBE-lite (cached approximations) pending full extraction.

---

---

## 13. Semantic routing and action readiness

R3.1 preserves R3's hybrid routing architecture and incorporates V1.x corrections in §13.4B (tool selection algorithm — HH.4.1 Beta evidence-mass decay, HH.4.12 reliability lower bound, seeded PRNG threading per Cl.9).

### 13.0 Pre-agent routing / EC intercept loop

EC Core intercepts every gateway-first chat message before agent invocation. The intercept loop:

1. Parse input as natural language or structured action.
2. Apply EC-level rules (kill-switches, source exclusion, policy gates).
3. Route to semantic router (§13.1).
4. Either invoke the agent with the routed context, OR handle directly (e.g., direct EC actions like "save context").

Per V1 ADJ-RT-098 EC intercept rule, the gateway never bypasses the intercept loop. Inputs that fail policy gates produce `BlockedPacketManifest` with appropriate `blocking_reason` before any agent invocation.

### 13.1 Router inputs

```ts
type RouterInput = {
  raw_input: string;
  channel: ChannelKind;
  operation_id: string;
  active_context: ActiveContextSlot;
  active_directives: string[];          // user_directive node IDs
  conversation_history_ref?: string;    // → conversation history service (§13.4A.5)
  session_id?: string;
  schema_version: 1;
};

type ChannelKind =
  | "q"
  | "discord"
  | "teams"
  | "email"
  | "automation"                        // V1.4 II.4.3 — replaces V1 pseudocode "background"
  | "mcp_external";
```

Per V1.4 II.4.3 lint closure: V1 ADJ-131 pseudocode `channel === "background"` is corrected to `channel === "automation"`. System-initiated assembly (background jobs, scheduled extraction, replay) routes via `ChannelKind === "automation"`. The enum itself is unchanged; the V1 pseudocode is the issue.

### 13.2 Hybrid routing

R3.1 preserves R3's hybrid routing: deterministic matchers first (per V1 ADJ-RT-005 verb-family classification), LLM-based router as fallback. The deterministic path covers ~80% of inputs with low latency; the LLM router handles the long tail.

```ts
type RoutingDecisionStrategy =
  | { kind: "deterministic_verb_family"; verb_family: VerbFamily; confidence: number }
  | { kind: "llm_router"; model_ref: string; sampled_confidence: number }
  | { kind: "user_override"; override_directive_ref: string };

type RoutedAction = {
  strategy: RoutingDecisionStrategy;
  action_kind: ActionKind;
  target_resolution_outcome: TargetResolutionOutcome;   // per §13.4A
  scope_snapshot: ScopeSnapshot;                         // captures matter/work at route time
  schema_version: 1;
};
```

### 13.2A Verb-family classification *(ADJ-05; R3.1 — preserved)*

Verb families are deterministic syntactic classifiers:

```
VerbFamily             Example                          Maps to
----------             -------                          -------
"create"               "add Marex to my matters"        EntityCreationAction
"surface"              "tell me about X"                EntitySurfacingAction
"invoke"               "search the SEC filings"         ToolInvocationAction
"capture"              "remember that…"                 UserDirectiveCaptureAction
"transition"           "switch to Paramount"            ContextSwitchAction
"inspect"              "show me what you know about X"  InspectionAction
"acknowledge"          "yes" / "confirm" / "cancel"     ConfirmationAction
```

Verb-family classification is sticky: once classified, subsequent ambiguous inputs in the same session bias toward the same family unless the user changes topic.

### 13.3 Routing decision schema

```ts
type RoutingDecision = {
  decision_id: string;
  raw_input: string;
  strategy: RoutingDecisionStrategy;
  routed_action: RoutedAction;
  rng_seed?: string;                    // R3.1 — present when strategy involves sampling
  decided_at: string;
  schema_version: 1;
};
```

The `rng_seed` field is part of the seeded PRNG discipline (per Cl.9 → HH.4 — R3 §13.4B.2 specifies `rng: RandomSource`; R3.1 codifies that the seed is recorded on the decision for replay).

### 13.4 Target resolution

Per R3 five-step resolution chain (preserved):

1. **Direct match.** Canonical name + entity_id exact match.
2. **Alias resolution.** DOC72 `NameMatchPolicy` over aliases.
3. **Constellation anchor.** DOC73 authority-aware lookup.
4. **Context inference.** Active matter/work context bias.
5. **User disambiguation.** Per §38.8 + HH.14.

Resolution outputs `ResolutionSource` per §5.4.1.Q, which populates `ManifestCardEntry.resolution_source` (restored per II.1).

### 13.4A Project-agnostic entity resolution *(Routing Cascade Fix; R3 — extended per ADJ-RT-011 through ADJ-RT-043; R3.1 — preserved; integrates HH.14)*

R3.1 preserves R3's project-agnostic entity resolution machinery in full. V1.x adjudications add disambiguation state machine improvements (§13.4A.4 + HH.14.1 stable IDs + HH.14.2 recently_removed_candidates), but the core five-step chain and arbitration logic are unchanged.

#### 13.4A.1 Final arbitration: selectResolvedEntity

R3's `selectResolvedEntity` function arbitrates between candidates surviving the five-step chain. R3.1 references the R3 specification unchanged; the only V1.x annotation is that `ResolutionSource` values are pinned per §5.4.1.Q for downstream `ManifestCardEntry` population.

#### 13.4A.2 Slot-aware resolution: ResolvedActionSlot

R3.1 preserves R3's slot-aware resolution. Slot binding emits typed `ResolvedActionSlot` records that carry through to packet assembly.

#### 13.4A.3 Lifecycle and policy eligibility filter

The eligibility filter runs in the `lifecycle_filtered` lifecycle state (§38.1 — restored in HH.1.2). It removes:
- Candidates with `lifecycle_state ∈ {archived, retracted, merged_into, superseded}`.
- Candidates failing `source_eligibility_aggregate.source_eligibility_state` check.
- Candidates from excluded workspaces (per V1 ADJ-RT-072 + HH.8.4 OpenClaw normalization).

#### 13.4A.4 Pronoun handling

Pronoun handling per V1 ADJ-RT-015 + ADJ-RT-120 — uses recent disambiguation context. R3.1 incorporates the HH.14 stable-ID discipline: pronouns referring to candidates from a prior disambiguation use `candidate_stable_key`, not display position.

#### 13.4A.5–13.4A.14 Conversation history, performance, helpers, recency, activity, tiebreakers, batched resolution, handoff schema, WorkContextConstellation

R3.1 preserves R3's §13.4A.5 through §13.4A.14 unchanged. The recency window configuration (§13.4A.8) uses the `RecencyWindowConfig` validated invariant from §5.4.1.H + HH.15.1.

#### 13.4A.15 Acceptance tests

R3.1 preserves R3 §13.4A acceptance tests. Added in R3.1 per HH.14.1 + HH.14.2:

```
ADD_TEST disambig_session_stable_keys:
  Given user sees disambig set [A, B, C] with stable_keys s_A, s_B, s_C
   And user selects via stable_key s_A
   And on next turn the same disambig set is re-rendered
  Then the candidate references resolve via stable_keys, not display position

ADD_TEST disambig_recently_removed_recovery:
  Given candidate D was removed from a disambig session 30 minutes ago
   And recoverable_until is 1 hour from removal
  When user invokes "show removed candidates"
  Then D appears in recently_removed_candidates with recoverable_until intact
```

### 13.4B Experience-informed tool selection *(R3 — significantly revised per ADJ-RT-006, ADJ-RT-009, ADJ-RT-010, ADJ-RT-045, ADJ-RT-046, ADJ-RT-047, ADJ-RT-093, ADJ-RT-115, ADJ-RT-119; R3.1 — corrected per HH.4.1, HH.4.2, HH.4.10, HH.4.12, II.3, II.4.1)*

R3.1's tool selection algorithm corrects four V1.x-identified hazards in R3's Thompson sampling:

1. **R3 used `last_invoked_at` and floor-at-5 decay** — these collapse directional evidence (HH.4.1). R3.1 uses evidence-mass-preserving decay.
2. **R3 stored mutable `last_alpha_decay_at` / `last_beta_decay_at`** — non-rebuildable. R3.1 event-sources raw counters; decay is read-time computed (HH.4.2).
3. **R3 used invocation count alone for high-stakes qualification** — heavily-used unreliable tool qualifies. R3.1 requires reliability lower bound (HH.4.12).
4. **R3's Math.random() / unseeded RNG** — non-replayable. R3.1 threads seeded `RandomSource` end-to-end (II.4.1).

#### 13.4B.1 Tool capability experience counters *(R3 — split per ADJ-RT-009; R3.1 — corrected per HH.4.2)*

Per §5.4.1.M:

```ts
type ToolCapabilityExperienceCounters = {
  user_visible_invocation_count: number;     // promotion gate input
  total_invocation_count: number;
  success_count: number;
  failure_count: number;

  // Raw Beta evidence — append-only; decay computed at READ time per HH.4.2:
  alpha_raw: number;
  beta_raw: number;
  last_evidence_at?: string;

  // Lockout timing (ADJ-097):
  last_failure_at?: string;
  last_success_at?: string;
  failure_lockout_until?: string;

  schema_version: 1;
};
```

`alpha` and `beta` from R3 are renamed `alpha_raw` and `beta_raw` per HH.4.2. The `last_alpha_decay_at` / `last_beta_decay_at` fields are REMOVED — decay is no longer stored; it is computed at every read via `computeEffectiveBeta`.

#### 13.4B.2 Tool selection algorithm *(R3 — Thompson sampling per ADJ-RT-045, ADJ-RT-093; R3.1 — corrected per HH.4.1, HH.4.10, HH.4.12, II.4.1)*

```ts
type ToolExplorationPolicySchema = {
  risk_tier: "low_risk" | "normal" | "high_stakes";
  prior_alpha_by_risk: Record<string, number>;          // default low_risk: 2, normal: 2, high_stakes: 2
  prior_beta_by_risk: Record<string, number>;           // default 2, 2, 2
  minimum_support_for_exploitation: number;             // default 5
  exploration_quota_per_session: number;                // default 3
  recent_failure_lockout_minutes: number;               // default 15
  high_stakes_qualification: ToolHighStakesQualification; // R3.1 — HH.4.12
  beta_evidence_config: BetaEvidenceConfig;             // R3.1 — HH.4.1
  schema_version: 1;
};

const DEFAULT_TOOL_BETA_CONFIG: BetaEvidenceConfig = {
  prior_alpha: 2,
  prior_beta: 2,
  max_total_evidence_mass: 196,
  min_retained_evidence_mass: 6,
  half_life_days: 90,
};

async function selectToolForStep(
  step: ProcedureStep,
  candidates: RegisteredTool[],
  graphDb: DOC72Database,
  rng: RandomSource,                                    // R3.1 — required seeded RandomSource
  policy: ToolExplorationPolicySchema
): Promise<ToolSelectionResult> {

  if (candidates.length === 0) {
    return await bootstrapToolSelection(step, graphDb, rng, policy);
  }
  if (candidates.length === 1) {
    return {
      kind: "selected",
      tool: candidates[0],
      selection_mode: "deterministic_match",
      why_this_tool: ["only_candidate"],
      selection_decision_id: ulid(),
    };
  }

  // R3.1 HH.4.12 — high-stakes qualification: filter by reliability lower bound
  let qualified = candidates;
  if (step.risk_tier === "high_stakes") {
    qualified = candidates.filter(tool => qualifiesForHighStakes(tool, policy.high_stakes_qualification));

    if (qualified.length === 0) {
      // No tool meets the high-stakes reliability lower bound
      return {
        kind: "needs_user_confirmation",
        candidate: rankForConfirmation(candidates)[0],
        confirmation_reason: "untested_in_high_stakes",
        reason_codes: ["tool.untested_high_stakes_requires_confirmation"],
      };
    }
  }

  const enriched = await Promise.all(qualified.map(async tool => {
    const node = await graphDb.findToolCapabilityNode(tool.id);
    const counters = node ? await graphDb.getToolExperienceCounters(node.id) : null;

    // R3.1 HH.4.1 — read-time effective Beta (evidence-mass-preserving decay)
    const beta = counters
      ? computeEffectiveBeta({
          alpha_raw: counters.alpha_raw,
          beta_raw: counters.beta_raw,
          last_evidence_at: counters.last_evidence_at,
          now_ms: Date.now(),
          config: policy.beta_evidence_config,
        })
      : {
          alpha_effective: policy.prior_alpha_by_risk[step.risk_tier ?? "normal"],
          beta_effective:  policy.prior_beta_by_risk[step.risk_tier ?? "normal"],
        };

    // Risk-tier-aware Matrix Beta clamping (R3.1 HH.4.10)
    const matrixAdjusted = applyMatrixConfidenceAdjustmentSafe({
      baseline_alpha: beta.alpha_effective,
      baseline_beta:  beta.beta_effective,
      adjustment: node?.matrix_adjustment,
      apply_matrix: step.matrix_applicable ?? false,
    });

    // Recent-failure lockout
    const now = Date.now();
    const recentFailure = counters?.last_failure_at &&
      (now - parseTime(counters.last_failure_at) < policy.recent_failure_lockout_minutes * 60_000) &&
      counters.failure_count > counters.success_count;
    if (recentFailure) {
      return { tool, sample: -1, why: ["recent_failure_lockout"], reachable: false };
    }

    // R3.1 II.4.1 — Thompson sampling with seeded RNG (replayable)
    const sample = sampleBeta(matrixAdjusted.effective_alpha, matrixAdjusted.effective_beta, rng);

    return {
      tool,
      sample,
      why: [`thompson_sample=${sample.toFixed(3)}`,
            `alpha_eff=${matrixAdjusted.effective_alpha.toFixed(2)}`,
            `beta_eff=${matrixAdjusted.effective_beta.toFixed(2)}`,
            counters ? `support=${counters.user_visible_invocation_count}` : "no_experience"],
      reachable: true,
      matrix_clamped: matrixAdjusted.clamped,
    };
  }));

  const reachable = enriched.filter(e => e.reachable);

  if (reachable.length === 0) {
    return {
      kind: "no_suitable_tool",
      suggested_action: "surface_to_user",
      reason_codes: ["tool.all_candidates_in_lockout"],
    };
  }

  reachable.sort((a, b) => b.sample - a.sample);

  const selected = reachable[0];
  const selection_decision_id = ulid();

  // Record routing decision randomness (R3.1 II.4.1 — replayability)
  await RoutingDecisionLedger.record({
    rng_seed: rng.seed,
    sampled_value: selected.sample,
    selection_policy: "thompson_sampling",
    recorded_at: ECClockService.now(),
    schema_version: 1,
  });

  return {
    kind: "selected",
    tool: selected.tool,
    selection_mode: "thompson_sample",
    why_this_tool: selected.why,
    selection_decision_id,
    rng_seed: rng.seed,
    sampled_value: selected.sample,
  };
}

function qualifiesForHighStakes(
  t: ToolCapability,
  q: ToolHighStakesQualification
): boolean {
  const c = t.experience_counters;

  if (c.user_visible_invocation_count < q.minimum_visible_invocations) {
    return false;
  }

  const beta = computeEffectiveBeta({
    alpha_raw: c.alpha_raw,
    beta_raw: c.beta_raw,
    last_evidence_at: mostRecent(c.last_success_at, c.last_failure_at),
    now_ms: Date.now(),
    config: DEFAULT_TOOL_BETA_CONFIG,
  });

  const lower = q.lower_bound_method === "beta_credible_lower_95"
    ? betaCredibleLower95(beta.alpha_effective, beta.beta_effective)
    : wilsonLowerBound(c.success_count / Math.max(1, c.success_count + c.failure_count),
                       c.success_count + c.failure_count);

  return lower >= q.minimum_reliability_lower_bound;
}
```

Thompson sampling with seeded RNG produces replayable selections. Reliability lower bound (HH.4.12) prevents heavily-used unreliable tools from passing the high-stakes gate by invocation count alone.

The `computeEffectiveBeta` algorithm (paste-ready from §5.4.1.M / HH.4.1):

```ts
function computeEffectiveBeta(input: {
  alpha_raw: number;
  beta_raw: number;
  last_evidence_at?: string;
  now_ms: number;
  config: BetaEvidenceConfig;
}): { alpha_effective: number; beta_effective: number } {
  const { prior_alpha, prior_beta, max_total_evidence_mass,
          min_retained_evidence_mass, half_life_days } = input.config;

  const evidence_alpha = Math.max(0, input.alpha_raw - prior_alpha);
  const evidence_beta  = Math.max(0, input.beta_raw  - prior_beta);
  const evidence_total = evidence_alpha + evidence_beta;

  if (!input.last_evidence_at || evidence_total === 0) {
    return { alpha_effective: prior_alpha, beta_effective: prior_beta };
  }

  const age_days =
    (input.now_ms - Date.parse(input.last_evidence_at)) /
    (1000 * 60 * 60 * 24);

  const decay = Math.exp((-Math.log(2) * age_days) / Math.max(1, half_life_days));

  const retained_mass = Math.min(
    max_total_evidence_mass,
    Math.max(min_retained_evidence_mass, evidence_total * decay)
  );

  const success_ratio = evidence_alpha / evidence_total;

  return {
    alpha_effective: prior_alpha + success_ratio * retained_mass,
    beta_effective:  prior_beta + (1 - success_ratio) * retained_mass,
  };
}
```

This preserves directional evidence under decay: a tool at α=20, β=5 after long inactivity decays to α≈7.14, β≈2.86 (mean 0.714), NOT α=5, β=5 (mean 0.50). The success ratio is preserved; only evidence mass shrinks.

The Matrix clamping function (HH.4.10) — paste-ready:

```ts
const BETA_EPSILON = 0.001;
const EFFECTIVE_BETA_TOTAL_CAP = 200;

function applyMatrixConfidenceAdjustmentSafe(input: {
  baseline_alpha: number;
  baseline_beta: number;
  adjustment?: {
    matrix_alpha_adjustment: number;
    matrix_beta_adjustment: number;
  };
  apply_matrix: boolean;
}): {
  effective_alpha: number;
  effective_beta: number;
  clamped: boolean;
  reason_codes: ReasonCode[];
} {
  if (!input.apply_matrix || !input.adjustment) {
    return {
      effective_alpha: input.baseline_alpha,
      effective_beta: input.baseline_beta,
      clamped: false,
      reason_codes: [],
    };
  }

  let a = input.baseline_alpha + input.adjustment.matrix_alpha_adjustment;
  let b = input.baseline_beta  + input.adjustment.matrix_beta_adjustment;
  let clamped = false;

  if (a < BETA_EPSILON) { a = BETA_EPSILON; clamped = true; }
  if (b < BETA_EPSILON) { b = BETA_EPSILON; clamped = true; }

  const total = a + b;
  if (total > EFFECTIVE_BETA_TOTAL_CAP) {
    const scale = EFFECTIVE_BETA_TOTAL_CAP / total;
    a *= scale; b *= scale;
    clamped = true;
  }

  return {
    effective_alpha: a,
    effective_beta: b,
    clamped,
    reason_codes: clamped ? ["matrix.effective_beta_clamped"] : [],
  };
}
```

The clamping prevents BDSM Matrix adjustments from producing invalid Beta values: `BETA_EPSILON = 0.001` floor guarantees positive parameters; `EFFECTIVE_BETA_TOTAL_CAP = 200` prevents pathological evidence inflation from over-eager Matrix boosts. The `clamped` flag and `matrix.effective_beta_clamped` reason code surface in the manifest's `degraded_reason_codes` when clamping was triggered.

#### 13.4B.3 Outcome classification *(R3 — added per ADJ-RT-047, ADJ-RT-122; R3.1 — preserved + HH.15.5 partial weight)*

Tool outcomes are classified using the canonical `OutcomeClass` enum (§5.4.1.G — DOC72-imported):

```
OutcomeClass             Counter increments         evidence_weight
------------             ------------------         ---------------
success                  success_count++, α_raw++   1.0
partial_success          success_count++, α_raw++   0.5
unexpected_positive      success_count++, α_raw++   0.5
failure                  failure_count++, β_raw++   1.0
partial_failure          failure_count++, β_raw++   0.5
rejection                failure_count++, β_raw++   1.0
correction_needed        failure_count++, β_raw++   1.0
no_outcome_observable    no counter update          0
user_aborted             no counter update          0
```

Per HH.15.5 (ADJ-116 partial-tool filter): partial outcomes count toward `total_invocation_count` but receive half-weight in Beta updates. The `ToolOutcomeEvidenceEvent.evidence_weight` field (§5.4.1.M) carries the weight per event.

#### 13.4B.4 Bootstrap fallback chain

When no candidate has experience (cold start), R3.1 preserves R3's bootstrap fallback chain:

1. Look for `executes_via` edges with `learning_origin: "bootstrap_fallback"` (HH.15.6).
2. Look for tool registration metadata indicating "high-prior" tools (e.g., system-seeded).
3. Surface to user for selection (`needs_user_confirmation`).

Bootstrap fallback paths are event-sourced; re-bootstrapping produces the same edges.

#### 13.4B.5 Tool preference context *(R3 — clarified per ADJ-RT-010; R3.1 — preserved; II.4.1 async signature)*

Per II.4.1 canonical async signature for tool-capability invocation:

```ts
async function invokeToolWithExperienceAttribution(input: {
  tool_capability_node_id: string;
  selection_decision_id: string;
  invocation_payload: ToolInvocationPayload;
  scope: PacketAssemblyScope;
}): Promise<ToolInvocationResult> {
  const result = await ToolExecutor.invoke(input);
  await ToolOutcomeLedger.append({
    event_id: ulid(),
    tool_capability_node_id: input.tool_capability_node_id,
    outcome_class: result.outcome_class,
    evidence_weight: result.is_partial ? 0.5 : 1.0,
    selection_decision_id: input.selection_decision_id,
    selection_mode: result.selection_mode,
    occurred_at: ECClockService.now(),
    schema_version: 1,
  });
  return result;
}
```

The `selection_decision_id` threads from `ToolSelectionResult.selected` through invocation to `ToolOutcomeEvidenceEvent` (per II.3) — the attribution link enables exploration-vs-exploitation analysis.

#### 13.4B.6 Tool experience cache invalidation

Per V1 ADJ-RT-115 + HH.5.6 source eligibility cache key pattern: tool experience counters are read-through with cache invalidation on every `ToolOutcomeEvidenceEvent` append. The `ToolCapabilityExperienceCurrentView` projection is rebuilt incrementally per event.

#### 13.4B.7 Tool capability edge canonicalization

Per V1 ADJ-RT-006 + ADJ-RT-007 (preserved): tool-capability `executes_via` edges have canonical naming. The capability graph CI invariant `tool_edge_canonical_naming` checks every edge has the canonical form.

#### 13.4B.8 Acceptance tests

```
tool_edge_canonical_naming
tool_registration_atomic
retry_inflation_blocked
tool_preference_not_in_packet_default
tool_selection_deterministic_with_seed       # R3 line 4648; substantive fix in HH.4 + II.4.1
unknown_tool_prior_risk_aware
tool_outcome_class_routes_correctly
tool_experience_invalidates_cache
tool_bootstrap_without_executes_via
qualifies_for_high_stakes_requires_lower_bound        # R3.1 — HH.4.12
beta_decay_preserves_success_ratio                     # R3.1 — HH.4.1
tool_outcome_event_threads_selection_decision_id       # R3.1 — II.3.1
tool_invocation_function_is_async                      # R3.1 — II.4.1
```

### 13.5 Readiness assessment

R3.1 preserves R3's readiness assessment unchanged:

```ts
type ActionFieldSpec = {
  field_name: string;
  field_kind: "entity_ref" | "scalar" | "enum" | "datetime" | "string";
  required: boolean;
  resolved_value?: unknown;
  resolution_outcome:
    | "resolved_unambiguously"
    | "needs_user_confirmation"
    | "needs_disambiguation"
    | "unresolved";
};

type ReadinessAssessment = {
  action_kind: ActionKind;
  fields: ActionFieldSpec[];
  overall_readiness: "ready" | "needs_input" | "blocked";
  blocking_reason?: ReasonCode;
  schema_version: 1;
};
```

Readiness assessment runs after target resolution; results determine whether to invoke immediately or pause for user input.

### 13.6 Ask-unless-ready

The principle: do not interrupt the user for input unless the action is genuinely not-ready. If a default is reasonable, use it (with provenance recording the default). If the field is optional, omit it.

Per HH.4.4 + HH.4.5 user-originated hard-required cap: when readiness fails because user-pinned hard-required items exceed the cap, surface a count-exceeded message rather than blocking silently.

### 13.7 No-UI-context assumption

The semantic router operates without UI context (no current page, no selected entity, no mouse position). All resolution comes from active context, conversation history, and the raw input. This is invariant: deterministic-from-context routing must work the same whether the UI is open or not.

### 13.8 Working-context save/promote

Working context save/promote routes through the OnboardingCommitSaga (§12.6A). Saving converts an in-flight context into a `SavedContextProfile` (§5.3B); promoting converts a transient capture into a durable graph entity.

---

## 14. Stable semantic capability registry

R3.1 preserves R3's stable capability registry. V1.x adjudication impact: the registry is referenced as a build-time companion artifact (`acceptance_test_registry` lives next to it; see §7.2A); tool capability graph integration uses the canonical `OutcomeClass` enum and seeded selection.

### 14.1 Registry purpose

The capability registry enumerates the named actions ELNOR can take and the procedures that invoke them. Each registry entry maps a verb-family pattern → action_kind → procedure → tool_capability (via `executes_via` edge).

### 14.2 Classification gate

Every new capability must pass classification gate before registration:
- Has a `node_kind === "procedure"` representation in the graph.
- Has at least one `executes_via` edge to a `node_kind === "tool_capability"` node.
- Has documented `OutcomeClass` mapping.
- Has acceptance test coverage (per `acceptance_test_registry`).

### 14.3 Action registry schema

```ts
type RegistryEntry = {
  entry_id: string;
  action_kind: ActionKind;
  verb_family: VerbFamily;
  procedure_node_id: string;
  required_field_specs: ActionFieldSpec[];
  risk_tier: "low_risk" | "normal" | "high_stakes";
  outcome_class_mapping: OutcomeClassMapping;
  registered_at: string;
  schema_version: 1;
};

type OutcomeClassMapping = {
  on_success: OutcomeClass;
  on_partial: OutcomeClass;
  on_failure: OutcomeClass;
  on_user_abort: OutcomeClass;
};
```

### 14.4 Generated artifact rule

Per V1 ADJ-RT-073 (and HH.10.2): the action registry is a generated artifact, not hand-edited. It is regenerated during build from procedure/tool_capability node metadata. The `acceptance_test_registry` is a companion that maps each registry entry to its acceptance tests.

### 14.5 Registry examples

R3.1 preserves R3's registry examples; no V1.x change to examples themselves.

### 14.6 Tool capability graph integration *(ADJ-91; R3 — extended per ADJ-RT-006, ADJ-RT-007, ADJ-RT-050; R3.1 — preserved + HH.8.1 cross-doc obligation)*

The tool capability graph fuses with BDSM Satisfaction Matrix per V1 ADJ-RT-101 and cross-doc obligation `OBL-BDSM-NEW-04`. R3.1 cross-doc obligation: `OBL-BDSM-NEW-FORCE-LEVEL-CONSTRAINT-01` — BDSM V6.5+ consumes `DirectiveConstraint[]` (force_level emission becomes floor/ceiling pair, not scalar).

The fusion contract:

```
DOC24 supplies:
  - Tool capability graph (executes_via edges; risk tiers).
  - Tool selection algorithm (§13.4B).
  - ToolOutcomeEvidenceEvent stream.

BDSM V6.5+ supplies:
  - Satisfaction matrix bundles (attribution outcomes).
  - DirectiveConstraint emission (force_level floors/ceilings).
  - Utility ledger updates (consumed via reconciliation 3-way join, HH.2.7).

The boundary:
  - DOC24 owns selection.
  - BDSM owns attribution.
  - The 3-way join (PacketInjectionManifest × FinalPromptInjectionManifest × PacketReconciliationEvent)
    is the only path between them.
```

---

## 15. Live action state

### 15.1 Shared envelope

```ts
type LiveActionState = {
  state_id: string;
  operation_id: string;
  action_kind: ActionKind;
  state_kind:
    | "queued"
    | "preflight"
    | "awaiting_confirmation"
    | "executing"
    | "completed"
    | "cancelled"
    | "failed";
  pause_state?:                              // R3.1 — moved to Operation per HH.15.3
    | "tool_confirmation_pending"
    | "disambiguation_pending"
    | "policy_acknowledgement_pending"
    | "user_choice_pending"
    | "manual_review_required";
  preflight_result?: PromptPayloadPreflightResult;
  receipt_id?: string;                        // → receipt_store when state_kind === "completed"
  last_updated_at: string;
  schema_version: 1;
};
```

`pause_state` is canonically on `Operation` (§5.4.1.N) per HH.15.3; the field is duplicated here for convenience but the authoritative source is the operation.

### 15.2 Examples

R3.1 preserves R3 examples; no V1.x change.

### 15.3 Capability state change events

State changes emit `LiveActionStateChangeEvent`. Subscribed UI surfaces (§20) update accordingly.

### 15.4 Settings-backed auto-confirm

Per V1 ADJ-RT-104, user settings may auto-confirm certain `awaiting_confirmation` states (e.g., low-risk tools that the user has marked "auto-approve"). Auto-confirm decisions emit receipts the same as user-confirmed.

---

## 16. Tool packs and JIT / sticky mounting

R3.1 preserves R3's tool pack model. V1.x adjudication impact: tool pack manifests reference the broader `InjectionOwnerDoc` enum (HH.6.5) which now includes DOC7, DOC12, DOC15, DOC20, DOC73.

### 16.1 Pack philosophy

Packs are bundles of tools mounted on demand. The core pack is always available; specialty packs mount when context demands.

### 16.2 Core pack *(revised per ADJ-34; R3.1 — preserved)*

The core pack contains:
- File operations (read, write, list).
- Web fetch / search.
- Calendar / clock.
- Knowledge inspection (`inspect_knowledge`, `inspect_knowledge_summary`).
- Context management (save, switch, suspend).

### 16.3 Pack definition schema

```ts
type ToolPackDefinition = {
  pack_id: string;
  display_name: string;
  pack_kind: "core" | "specialty" | "mcp_external";
  contained_tool_capability_node_ids: string[];
  mount_conditions: PackMountCondition[];
  pack_exposure_manifest: ToolPackExposureManifestSchema;   // per V1 ADJ-RT-090
  schema_version: 1;
};

type PackMountCondition =
  | { kind: "always_mounted" }                       // core pack
  | { kind: "matter_kind"; matter_kinds: string[] }   // mount for legal/finance/medical matters
  | { kind: "domain_signal_profile"; profile_ids: string[] }
  | { kind: "user_explicit"; user_directive_ref: string }
  | { kind: "ec_action_required"; required_for_actions: ActionKind[] };
```

### 16.4 Candidate pack families

R3.1 preserves R3's candidate pack family inventory (legal, finance, medical, dev, ops, comms, knowledge_management). Each family has a default pack definition and is mountable by user opt-in.

### 16.5 Context-aware deterministic mounting

When matter context activates with a bound DomainSignalProfile, the corresponding pack family auto-mounts (per `PackMountCondition`). Deterministic mounting events are recorded in `pack_mount_events.jsonl`.

### 16.6 Sticky pack rule

Once mounted, a pack stays mounted until explicit unmount or matter context exit. This avoids thrash on quick matter-switch operations.

### 16.7 Predictive prefetch

Predictive prefetch warms pack metadata (not contents) for matters the user has interacted with in the last N days. Prefetch reduces mount latency on context switch.

### 16.8 Pack inspectability

Per V1 ADJ-RT-090 anti-spooky rule: every mounted pack is queryable via operator UI. The operator can see exactly which tools are currently mounted and why.

---

## 17. Invocation architecture

R3.1 preserves R3's invocation transport order.

### 17.1 Preferred transport order

```
1. Native tools             (zero-marshaling; lowest latency)
2. MCP gateway              (cross-process; structured tool protocol)
3. ec_action                (in-process EC routine)
4. Skills                   (high-level capability bundles)
5. Generic OpenClaw fallback (everything else)
```

### 17.2 Native tools

Native tools live in-process and are invoked by direct function call. They are the lowest-latency option but require explicit registration.

### 17.3 MCP gateway

MCP (Model Context Protocol) tools route via the gateway. The MCP gateway translates DOC24 invocation envelopes to MCP messages and back.

### 17.4 `ec_action`

EC actions are structured commands handled by EC Core directly (e.g., "save context", "switch matter"). They do not invoke tools; they invoke EC routines.

### 17.5 Skills

Skills bundle higher-level capabilities (e.g., "draft a motion to dismiss"). Each skill maps to a procedure node with multiple `executes_via` edges to underlying tools.

### 17.6 Generic OpenClaw fallback

When no transport-specific path matches, the generic OpenClaw fallback routes the invocation to a generalist sub-agent via `sessions_spawn` with `context: "isolated"` (per HH.8.4 default). Payload is serialized into `task_prompt` explicitly.

Per HH.8.4: DOC24 does NOT use `context: "fork"` by default. `native_fork_mapping` on `WorkingContextState` is observability-only for the rare case where a user-level workflow uses fork.

### 17.7 MCP client profiles

MCP client profiles configure per-server credentials, scopes, and timeouts. Profiles are stored in `pack_mount_state_store` and updated via pack mount events.

### 17.7A Tool pack exposure manifest *(R3 — added per ADJ-RT-090; R3.1 — preserved)*

```ts
type ToolPackExposureManifestSchema = {
  pack_id: string;
  exposed_tools: Array<{
    tool_capability_node_id: string;
    canonical_name: string;
    description: string;
    risk_tier: "low_risk" | "normal" | "high_stakes";
  }>;
  mount_state: "mounted" | "unmounted" | "mounting" | "unmounting";
  schema_version: 1;
};
```

### 17.8 Ambient baseline / packet / deep context coexistence

Per V1 ADJ-RT-099: three context layers coexist:

```
Ambient baseline:
  - System prompt fragments always present.
  - Owner: DOC24 (via injection slots) + DOC15 (cognitive infrastructure).

Packet:
  - Per-operation injection of cards/buckets/authority.
  - Owner: DOC24 (assembly per §38).

Deep context:
  - Long-lived session context (saved contexts, demonstration captures).
  - Owner: DOC24 (working context service) + DOC1 (memory).
```

The three layers compose at final prompt assembly time via `FinalPromptInjectionManifest.spans[]` (HH.2.4 byte-coverage rule).

---

## 18. Receipts, confirmation, and outcome truth

R3.1 preserves R3's receipt subsystem and integrates V1.x ToolOutcomeEvidenceEvent threading (II.3) and 3-way reconciliation (HH.3, HH.2.7).

### 18.1 Action receipt schema

```ts
type ActionReceipt = {
  receipt_id: string;
  operation_id: string;
  action_kind: ActionKind;

  // What was invoked
  procedure_node_id: string;
  tool_capability_node_id?: string;
  selection_decision_id?: string;        // R3.1 II.3 — attribution thread

  // Inputs / outputs
  input_summary: string;
  output_summary?: string;
  output_artifact_refs: string[];

  // Outcome classification (per §13.4B.3)
  outcome_class: OutcomeClass;
  is_partial: boolean;

  // Confirmation state
  confirmation_state: "user_confirmed" | "auto_confirmed" | "user_cancelled" | "system_aborted";

  // Errors
  error_reason_codes?: ReasonCode[];
  error_message?: string;

  // Timing
  invoked_at: string;
  completed_at: string;

  schema_version: 1;
};
```

Receipts persist in `receipt_store` (append-only per V1 ADJ-RT-053).

### 18.2 Confirmation replay protection

Per V1 ADJ-RT-024 + ADJ-RT-066: every confirmation has an `idempotency_key`. Duplicate confirmation events (e.g., from accidental double-clicks) are detected and rejected via idempotency check.

### 18.3 Outcome truth

The receipt is the single source of truth for "what did this operation actually do." Downstream consumers (BDSM attribution, KDA learning, DOC8 pattern detection) read receipts AND the 3-way manifest join (HH.2.7); they do not read alternative outcome representations.

### 18.3A Receipt subsystem *(ADJ-53; R3 — extended per ADJ-RT-024, ADJ-RT-066, ADJ-RT-077, ADJ-RT-034; R3.1 — preserved)*

The receipt subsystem owns:
- Receipt persistence (append-only).
- Idempotency enforcement.
- Receipt replay for state reconstruction.
- Cross-receipt aggregation for usage analytics.

```ts
type ReceiptStoreOps = {
  append(receipt: ActionReceipt): Promise<void>;
  getById(receipt_id: string): Promise<ActionReceipt | null>;
  query(filter: ReceiptQueryFilter): Promise<ActionReceipt[]>;
  rebuildOperationState(operation_id: string): Promise<OperationStateReconstruction>;
};
```

### 18.4 Tool-level outcome tracking *(ADJ-91; R3.1 — corrected per HH.4.2 event-sourcing + II.3 attribution thread)*

Per V1 ADJ-RT-091 and HH.4.2: tool-level outcome tracking is event-sourced. Every tool invocation produces one or more events:

```
Invocation:
  receipt_events.jsonl:                ← receipt (canonical outcome truth)
    ActionReceipt with outcome_class, selection_decision_id

  tool_outcome_evidence_events.jsonl:  ← per HH.4.2 (separate append-only stream)
    ToolOutcomeEvidenceEvent with selection_decision_id thread (II.3)
```

The two streams are co-authoritative:
- `receipt_events.jsonl` is the canonical operation-level outcome.
- `tool_outcome_evidence_events.jsonl` is the canonical tool-capability-level evidence stream (feeds Thompson sampling).

Both streams reference the same `selection_decision_id`, enabling cross-stream join for attribution analysis.

The `OutcomeClass` value in the receipt drives the counter update via `OutcomeClassMapping` (§14.3); the same value populates `ToolOutcomeEvidenceEvent.outcome_class` (§5.4.1.M). `evidence_weight` is 1.0 for full outcomes, 0.5 for partial (HH.15.5).

Acceptance test (II.3.1):

```
ADD_TEST tool_outcome_event_threads_selection_decision_id:
  Given selectToolForStep returns { kind: "selected", selection_decision_id: "dec_X",
        selection_mode: "thompson_sample" }
   And the selected tool executes and produces an outcome
  Then ActionReceipt.selection_decision_id === "dec_X"
   AND ToolOutcomeEvidenceEvent.selection_decision_id === "dec_X"
   AND ToolOutcomeEvidenceEvent.selection_mode === "thompson_sample"
```

The cross-stream `selection_decision_id` join allows BDSM/DOC8 to answer: "of the exploration-mode selections in the last week, what was the success rate?"

---

---

## 19. Runtime packet and injection model

R3.1 preserves R3's runtime packet model and incorporates V1.x corrections in §19.1A (budget waterfall per HH.4.5/HH.4.6/HH.4.7), §19.1B (adaptive budgeting per HH.4.13), §19.2 (compact entity cards per ADJ-RT-001/035/113/114 with HH.6 type updates), and §19.4 (packet versioning per HH.2 manifest restructuring).

### 19.0 Terminology *(ADJ-29; R3.1 — extended)*

```
Packet                      = the unit of LLM context injection for one operation.
Knowledge card               = a rendered projection of one or more entity graph nodes
                                that fits in a packet slot (§5.4.1.Q ManifestCardEntry).
Bucket content               = direct file content (PDFs, docs) injected verbatim
                                (§5.4.1.Q BucketManifestEntry).
Slot                         = a named region of the final prompt with a registered
                                InjectionSlotId (HH.6.5).
Card stable key              = cross-packet card identity (HH.6.13).
PacketInjectionManifest      = DOC24's per-packet contribution truth (§5.4.1.Q).
CandidateInjectionManifest   = pre-durable manifest passed to lint (HH.2.1).
FinalPromptInjectionManifest = final-prompt-owner truth covering every byte (HH.2.4).
BlockedPacketManifest        = durable record of a packet that failed lint or other gate.
```

### 19.1 Packet assembly scope

Per §5.4.1.P (HH.6.1 canonical merge):

```ts
type PacketAssemblyScope = {
  operation_id: string;
  packet_id: string;
  packet_kind: "assistant_turn" | "structured_ec_action" | "background_extraction";

  matter_context_slot_id?: string;
  work_context_id: string;
  excluded_workspace_ids: string[];

  scope_kind: "user_turn_chat" | "user_turn_ec_action" | "system_initiated_background" | "saga_step_continuation";
  pbe_intent: "none" | "extract" | "reuse_cached";

  tokenizer_ref: TokenizerRef;
  total_budget_tokens: number;

  policy_generation_snapshot: PolicyGenerationSnapshot;
  bundle_generation_id: string;
  matrix_generation_id?: string;
  matrix_state?: "active" | "disabled" | "gathering_only" | "bundle_unavailable";

  schema_version: 1;
};
```

The scope is captured once at packet creation (`created` lifecycle state) and threaded through every subsequent stage. It is never mutated.

### 19.1A Budget waterfall *(ADJ-04; R3.1 — extended per HH.4.5, HH.4.6, HH.4.7)*

R3.1 corrects three hazards in R3's budget waterfall:

1. **Negative budgets propagated silently.** Per HH.4.5, `computeTotalBudgetTokens` now returns a discriminated union that distinguishes `available`, `degraded`, and `blocked` outcomes.
2. **Unified context budget could produce negative remaining or non-summing shares.** Per HH.4.6, `computeUnifiedBudget` guards against `tokens_used_before > model_context_window` and normalizes shares to sum to one.
3. **`BudgetCell` lacked card membership for auditability.** Per HH.4.7, each cell carries `included_card_ids[]` and `overflow_card_ids[]`.

```ts
type PacketBudgetComputationResult =
  | { outcome: "available"; total_budget_tokens: number; computation: PacketBudgetComputationTrace; schema_version: 1 }
  | { outcome: "blocked" | "degraded"; computed_budget_tokens: number;
      reason_code: "doc24.extraction_budget_negative" | "doc24.extraction_budget_below_minimum";
      computation: PacketBudgetComputationTrace; schema_version: 1 };

type PacketBudgetComputationTrace = {
  model_context_window: number;
  completion_reserve_tokens: number;
  system_prompt_reserve_tokens: number;
  profile_cap_tokens?: number;
  base_budget_tokens: number;
  capped_budget_tokens: number;
  min_required_budget_tokens: number;
};

function computeTotalBudgetTokens(input: {
  model_context_window: number;
  completion_reserve_tokens: number;
  system_prompt_reserve_tokens: number;
  profile_cap_tokens?: number;
  min_required_budget_tokens: number;
}): PacketBudgetComputationResult {
  const base =
    input.model_context_window -
    input.completion_reserve_tokens -
    input.system_prompt_reserve_tokens;

  const capped = input.profile_cap_tokens !== undefined
    ? Math.min(base, input.profile_cap_tokens)
    : base;

  const trace: PacketBudgetComputationTrace = {
    model_context_window: input.model_context_window,
    completion_reserve_tokens: input.completion_reserve_tokens,
    system_prompt_reserve_tokens: input.system_prompt_reserve_tokens,
    profile_cap_tokens: input.profile_cap_tokens,
    base_budget_tokens: base,
    capped_budget_tokens: capped,
    min_required_budget_tokens: input.min_required_budget_tokens,
  };

  if (capped < 0) {
    return { outcome: "blocked", computed_budget_tokens: 0,
             reason_code: "doc24.extraction_budget_negative",
             computation: trace, schema_version: 1 };
  }
  if (capped < input.min_required_budget_tokens) {
    return { outcome: "degraded", computed_budget_tokens: capped,
             reason_code: "doc24.extraction_budget_below_minimum",
             computation: trace, schema_version: 1 };
  }
  return { outcome: "available", total_budget_tokens: capped,
           computation: trace, schema_version: 1 };
}
```

Per HH.4.5 ownership rule: **DOC24 computes and pins `total_budget_tokens`; DOC73 consumes the pinned value, never recomputes.** If DOC73 receives an extraction-mode invocation without a pinned budget, the run blocks with `"doc73.budget_unpinned_at_extraction"`.

For the unified context budget (knowledge cards + bucket content):

```ts
type UnifiedBudgetResult =
  | { outcome: "available"; total_budget_tokens: number;
      knowledge_card_budget_tokens: number; bucket_content_budget_tokens: number;
      degraded: false; schema_version: 1 }
  | { outcome: "degraded" | "blocked";
      reason_code: "doc24.no_remaining_context_budget" | "doc24.unified_budget_below_minimum";
      total_budget_tokens: number; knowledge_card_budget_tokens: number;
      bucket_content_budget_tokens: number;
      degraded: true; schema_version: 1 };

function computeUnifiedBudget(input: {
  model_context_window: number;
  tokens_used_before: number;
  policy: UnifiedContextBudgetPolicy;
  has_bucket_content: boolean;
  has_knowledge_cards: boolean;
  bucket_is_direct_target: boolean;
}): UnifiedBudgetResult {
  const remaining = Math.max(0, input.model_context_window - input.tokens_used_before);
  const totalBudget = Math.min(
    input.policy.max_injection_tokens_absolute,
    Math.floor(remaining * input.policy.max_injection_tokens_pct_of_context)
  );

  if (totalBudget <= 0) {
    return { outcome: "blocked", reason_code: "doc24.no_remaining_context_budget",
             total_budget_tokens: 0, knowledge_card_budget_tokens: 0,
             bucket_content_budget_tokens: 0, degraded: true, schema_version: 1 };
  }

  const rawShares = input.bucket_is_direct_target
    ? { knowledge: 0.20, bucket: 0.80 }
    : { knowledge: input.policy.knowledge_card_share_default,
        bucket: input.policy.bucket_content_share_default };

  const activeShares = {
    knowledge: input.has_knowledge_cards ? rawShares.knowledge : 0,
    bucket: input.has_bucket_content ? rawShares.bucket : 0,
  };

  const denom = activeShares.knowledge + activeShares.bucket;

  if (denom <= 0) {
    return { outcome: "available", total_budget_tokens: totalBudget,
             knowledge_card_budget_tokens: 0, bucket_content_budget_tokens: 0,
             degraded: false, schema_version: 1 };
  }

  const normalizedKnowledgeShare = activeShares.knowledge / denom;
  const knowledge = Math.floor(totalBudget * normalizedKnowledgeShare);
  const bucket = totalBudget - knowledge;

  return { outcome: "available", total_budget_tokens: totalBudget,
           knowledge_card_budget_tokens: knowledge,
           bucket_content_budget_tokens: bucket,
           degraded: false, schema_version: 1 };
}
```

Per HH.4.7, `BudgetCell` carries auditability fields:

```ts
type BudgetCell = {
  source_lane: SourceLane;
  priority_lane: PriorityLane;
  allocated_tokens: number;
  used_tokens: number;
  unused_tokens: number;
  included_card_ids: string[];
  overflow_card_ids: string[];
  trim_policy: "never_trim" | "trim_last" | "summarize" | "omit";
  schema_version: 1;
};

type PacketBudgetResult = {
  total_budget_tokens: number;
  cells: BudgetCell[];
  total_used_tokens: number;
  total_unused_tokens: number;
  total_overflow_count: number;
  blocked_reason?: ReasonCode;
  schema_version: 1;
};
```

This is what enables the Packet Inspector (§20.3) to answer: "Which lane overflowed, and which cards lost?"

### 19.1B Adaptive packet budgeting *(R3 — added per ADJ-RT-099; R3.1 — extended per HH.4.13)*

Adaptive packet budgeting adjusts lane shares based on observed utility. Per V1 ADJ-RT-099, lane shares can drift over time based on which lanes consistently produce `injected_and_used` cards vs `injected_and_ignored` cards (reconciliation events; HH.3).

Per HH.4.13, adaptive drift is bounded per lane and sum-to-one normalized:

```ts
function applyAdaptiveLaneDeltas(input: {
  baseline: Record<SourceLane, number>;
  proposed_delta: Partial<Record<SourceLane, number>>;
  max_abs_delta_per_lane: number;
}): Record<SourceLane, number> {
  const adjusted: Partial<Record<SourceLane, number>> = {};

  for (const lane of Object.keys(input.baseline) as SourceLane[]) {
    const rawDelta = input.proposed_delta[lane] ?? 0;
    const boundedDelta = Math.max(
      -input.max_abs_delta_per_lane,
      Math.min(input.max_abs_delta_per_lane, rawDelta)
    );
    adjusted[lane] = Math.max(0, input.baseline[lane] + boundedDelta);
  }

  const total = Object.values(adjusted).reduce((a, b) => a + (b ?? 0), 0);
  if (total <= 0) return input.baseline;

  return Object.fromEntries(
    Object.entries(adjusted).map(([k, v]) => [k, (v ?? 0) / total])
  ) as Record<SourceLane, number>;
}
```

Default `max_abs_delta_per_lane = 0.10`. Adaptive drift never sets a lane share to 0 (would silently eliminate the lane); minimum is `max(0, baseline_share - 0.10)`.

### 19.2 Compact entity cards and tool directory *(R3 — modified per ADJ-RT-001, ADJ-RT-035, ADJ-RT-113, ADJ-RT-114; R3.1 — preserved + HH.6 type alignment)*

The compact entity card is the canonical rendered representation of an entity for packet injection. It is generated per the canonical rendering templates (§26.4) and produces a `ManifestCardEntry` (§5.4.1.Q).

R3.1 alignment with HH.6 / II.1: every `ManifestCardEntry` from a compact entity card population carries:
- `card_stable_key` (HH.6.13).
- `card_presence: CardPresence` (HH.6.4 — replaces inclusion_decision + rendering_kind).
- `position_in_packet?` (II.1 restored; required iff `included_inline`).
- KDA variant-tracking fields (`rendering_specialization_id`, `render_variant_id`, `template_version`) when KDA assigned a variant.
- `resolution_source?` (II.1 restored; from CompactEntityCard).
- `overlap_decision_id?` (II.1 restored; if overlap-suppressed against another card).

The tool directory is a per-packet inventory of mounted tool capabilities, rendered as a compact list with risk tiers and recent outcome summary. It populates one or more `InjectionSlotManifestEntry` records with `slot_kind === "tool_capability_block"`.

### 19.3 Packet contents and ambient baseline boundary

R3.1 preserves R3's three-tier coexistence (§17.8): ambient baseline (system prompt fragments; DOC24 + DOC15 owned), packet (per-operation; DOC24 owned), deep context (saved profiles + memory; DOC24 + DOC1 owned). The three compose via `FinalPromptInjectionManifest.spans[]` (HH.2.4 byte coverage).

### 19.4 Packet versioning

R3.1 corrects R3's implicit single-version model. Per HH.2.5/HH.2.8, the manifest type chain has four named types (`CandidateInjectionManifest`, `PacketInjectionManifest`, `FinalPromptInjectionManifest`, `BlockedPacketManifest`); the R2/R3 type name `UnifiedInjectionManifestSchema` is retired as a transitional alias.

Versioning rules:

```
Per-type schema_version (DOC24-owned types per HH.11.1):
  - CandidateInjectionManifest:   schema_version 1 (R3.1)
  - PacketInjectionManifest:      schema_version 1 (R3.1)
  - FinalPromptInjectionManifest: schema_version 1 (R3.1)
  - BlockedPacketManifest:        schema_version 1 (R3.1; extended per HH.2.2)
  - ManifestCardEntry:            schema_version 1 (R3.1; field-restored per II.1)

Manifest parsing per HH.15.7 SchemaVersionedParser:
  - schema_version === 1 → parseV1Manifest
  - schema_version === undefined → ManifestParseError ("missing_schema_version")
  - schema_version > 1 → ManifestParseError ("future_schema_version") — fail closed
```

Supersession across versions: when a packet is reassembled (`aborted_for_retry → created`), the new packet has a new `packet_id` and the prior packet's manifest is updated with `superseded_by_packet_id` (II.2.2). This forms a linked chain queryable for diagnostic timing.

### 19.5 Agent-facing packet rule

The agent sees only the final assembled prompt. It does NOT see:
- The manifest itself.
- The reconciliation overlay.
- The opportunity set.
- The decision graph.

The agent's contract is the prompt text; everything else is inspectable infrastructure visible only to DOC24, the operator UI, and downstream consumers (BDSM, KDA, DOC8).

### 19.6 Debug/inspectability packet

Per V1 ADJ-RT-090: every dispatched packet is queryable via the Packet Inspector (§20.3) using its `packet_id`. The inspector renders:
- The `PacketInjectionManifest`.
- The associated `ContextAssemblyTrace`.
- The associated `FinalPromptInjectionManifest.spans[]`.
- The `PacketReconciliationCurrentView` after dispatch.
- The `PacketDecisionGraph` for "why X / why not Y" queries.
- The `PacketOpportunitySet` (sampled per HH.15.4 with `k_cap: 50` default).

### 19.7 On-demand deep context contracts

Deep context (saved contexts; suspended sessions) is loaded on demand at packet assembly time. The loading contract:

```
At candidates_gathered lifecycle state:
  1. Active context is read from working_context_store.
  2. Suspended contexts are NOT auto-loaded; they require explicit user action.
  3. Saved context profiles MAY be loaded if matter binding requires.
  4. The scope_snapshot captured on PacketAssemblyScope reflects what loaded.
```

---

## 20. UI / operator surfaces

R3.1 preserves R3's UI surface inventory. V1.x adjudication impact: Packet Inspector renders the four-manifest chain plus reconciliation events and PBE-lite banners (HH.7.4).

### 20.1 Onboarding entry points

Per §12: onboarding may be invoked from the chat interface ("/onboard"), Knowledge Manager ("Add entity"), or the operator settings panel.

### 20.2 World Model Browser / Knowledge Manager

The World Model Browser is the operator's view of the entity graph. The Knowledge Manager subset focuses on knowledge cards specifically (vs. tool capabilities or matter contexts).

Per HH.7.4 cross-doc obligation `OBL-DOC21-NEW-PBE-LITE-BANNER-01`: the Knowledge Manager header surfaces `PBELitePacketEffect.banner_kind` for any packets whose card list includes PBE-lite affected cards.

### 20.3 Capability / packet / context inspector

Three inspector surfaces:

```
Capability Inspector:
  - Lists mounted packs.
  - Per pack: ToolPackExposureManifestSchema rendering.
  - Per tool: experience counters with effective Beta (read-time computed; HH.4.1).
  - Per tool: recent ToolOutcomeEvidenceEvent[] stream.

Packet Inspector:
  - Lists dispatched packets (per session, per matter).
  - Per packet: PacketInjectionManifest + ContextAssemblyTrace + FinalPromptInjectionManifest.spans[].
  - "Why was card X included?" → decision graph walk via walkPacketDecisionGraph (II.4.2).
  - "Why was card Y excluded?" → suppression_kind + suppressing_decision_ref.
  - "What did the model do with card Z?" → PacketReconciliationCurrentView per_card_current.
  - Sampled opportunity set (HH.15.4).

Context Inspector:
  - Active context.
  - MatterContextSlotStack.
  - Suspended context snapshots (resumable_until).
  - SavedContextProfile-s.
```

### 20.4 Notes / DOC20 integration

Notes (DOC20) integrate with the entity graph via NoteReadSkill / NoteWriteSkill (DOC3). Per V1 ADJ-RT-049: notes can be promoted to entities; entities can be referenced from notes.

### 20.5 Active pack chips, authority audit, and capability diff

The Active Pack Chips strip (top of operator chrome) shows mounted packs with one-click unmount. Authority Audit surfaces the constellation cascade for any authority-rendered card. Capability Diff shows changes to the tool registry between sessions.

### 20.6 Review, correction, and mutation surfaces *(R3 — extended per ADJ-RT-049; R3.1 — preserved)*

Review surfaces for:
- Onboarding session commits (§12.6).
- Digest item dispositions (§8.3 + ADJ-085).
- Demonstration capture review (§12.3A).
- Authority constellation review (DOC73).

Correction surfaces apply patches to graph state via the EntityCreationAction route (§12.7C).

### 20.7 Inspectability surface contracts

Every operator surface has a contract: query schema, result schema, latency budget. R3.1 references R3's contract list unchanged. Per V1 ADJ-RT-079/080: surfaces that read across multiple stores use the "derived projection" pattern, not direct multi-store queries.

### 20.7A Route contract requirements *(ADJ-51; R3 — extended per ADJ-RT-063, ADJ-RT-065, ADJ-RT-079, ADJ-RT-080; R3.1 — preserved)*

Route contracts pin URL patterns, query parameters, and result schemas. R3.1 preserves R3's route list and adds the V1.3 HH.8.1 cross-doc routes for OP-A new rows (no new DOC24-local routes).

### 20.7B Result schemas for named surfaces

R3.1 preserves R3's result schemas. New result schemas added in R3.1:

```ts
type PacketInspectorResult = {
  packet_id: string;
  packet_manifest: PacketInjectionManifest;
  trace: ContextAssemblyTrace;
  final_prompt_manifest?: FinalPromptInjectionManifest;   // present when DOC11/OpenClaw final-prompt-owner has emitted
  reconciliation_current_view?: PacketReconciliationCurrentView;
  decision_graph?: PacketDecisionGraph;
  opportunity_set?: PacketOpportunitySet;                   // sampled per HH.15.4
  pbe_lite_effect?: PBELitePacketEffect;
  blocked_manifest?: BlockedPacketManifest;                 // present iff packet was blocked
  schema_version: 1;
};

type CapabilityInspectorResult = {
  tool_capability_node_id: string;
  experience_counters: ToolCapabilityExperienceCounters;
  experience_current_view: ToolCapabilityExperienceCurrentView;   // includes alpha_effective, beta_effective
  recent_outcome_events: ToolOutcomeEvidenceEvent[];               // last 50 by default
  recent_selection_decisions: Array<{ selection_decision_id: string; sampled_value?: number; rng_seed?: string }>;
  high_stakes_qualification_status: { qualified: boolean; reliability_lower_bound: number };
  schema_version: 1;
};
```

The Packet Inspector and Capability Inspector are the two surfaces that surface the full V1.x inspectability work.

---

## 21. EC Core service changes

### 21.1 SharedActionHandlerLayer rule

Per V1 ADJ-RT-053: every action passes through `SharedActionHandlerLayer`, which:
1. Validates the action envelope (per §15).
2. Applies idempotency check.
3. Persists the receipt (post-execution).
4. Emits state change events.

R3.1 preserves this rule. The handler also writes to the canonical companion stores (HH.5.5 persist-then-swap pattern for policy snapshots, HH.2.2 lint-result-then-manifest sequence for lint passes).

### 21.2 Minimum event catalog *(expanded per ADJ-78; R3.1 — extended)*

R3.1 extends R3's minimum event catalog with V1.x events:

| Event | Producer | Consumer | Stream |
|---|---|---|---|
| `entity.created`, `entity.updated`, `entity.retracted` | DOC24/DOC72 | All | `graph_events.jsonl` |
| `working_context.mutated` | DOC24 | All | `working_context_events.jsonl` |
| `source_policy.changed` | DOC24 | All | `source_policy_events.jsonl` |
| `onboarding_session.event` | DOC24 | All | `onboarding_session_events.jsonl` |
| `pack.mounted`, `pack.unmounted` | DOC24 | All | `pack_mount_events.jsonl` |
| `receipt.appended` | DOC24 | All | `receipt_events.jsonl` |
| `packet.lifecycle.transitioned` | DOC24 | All | (in-memory + telemetry) |
| `packet.assembly.stage_completed` | DOC24 | Inspector | (in-memory + telemetry) |
| `packet.blocked` | DOC24 | All | (in-memory + telemetry; manifest in BlockedPacketManifest store) |
| `packet.lint_failed` | DOC24 | All | `packet_lint_failure_record_store` (per HH.2.2) |
| `manifest.written` | DOC24 | BDSM, KDA, Inspector | (manifest itself is the artifact) |
| `packet.reconciliation_event` | DOC24 | BDSM, DOC8 | `packet_reconciliation_event_store` (per HH.3) |
| `tool.outcome_evidence` | DOC24 | DOC24 (Thompson) | `tool_outcome_evidence_event_store` (per HH.4.2) |
| `pbe_lite.packet_effect_emitted` | DOC24 | DOC21 (Knowledge Inspector banner) | (on PacketInjectionManifest; HH.7.3) |

All events use `monotonicFactory` for ULIDs and `ECClockService.now()` for timestamps (HH.9.3 monotonic guard).

---

## 22. Cross-doc obligations *(R3 — replaced per ADJ-RT-073; R3.1 — extended per HH.8.1)*

R3.1 maintains the cross-doc obligation summary as an OP-A pointer:

```
Cross-doc obligations are tracked in OP-A V3.7+ (cross-doc obligation tracker).
R3.1 does not enumerate obligations inline; the OP-A document is canonical.

R3.1-introduced OBL rows (per HH.8.1 + II.1.3):

| Row ID | Target |
|---|---|
| OBL-BDSM-NEW-MANIFEST-JOIN-01 | BDSM V6.5+ |
| OBL-D8-NEW-MANIFEST-JOIN-01 | DOC8 |
| OBL-BDSM-NEW-EMPTY-CONTEXT-CRASH-01 | BDSM V6.5+ |
| OBL-BDSM-NEW-RELEVANCE-NORMALIZATION-01 | BDSM V6.5+ |
| OBL-BDSM-NEW-RECONCILIATION-EVENT-01 | BDSM V6.5+ |
| OBL-BDSM-NEW-MANIFEST-RENAME-01 | BDSM V6.5+ |
| OBL-KDA-NEW-MANIFEST-RENAME-01 | KDA R3+ |
| OBL-D72-NEW-NODEKIND-EXPORT-01 | DOC72 R6+ |
| OBL-D11-NEW-FINAL-PROMPT-SPAN-01 | DOC11 |
| OBL-OPENCLAW-NEW-FINAL-PROMPT-SPAN-01 | OpenClaw |
| OBL-DOC21-NEW-PBE-LITE-BANNER-01 | DOC21 R3+ |
| OBL-BDSM-NEW-FORCE-LEVEL-CONSTRAINT-01 | BDSM V6.5+ |
| OBL-KDA-NEW-VARIANT-TRACKING-FIELDS-RESTORED-01 | KDA R3+ |

R3.1-updated OBL rows:

| Row ID | Update |
|---|---|
| OBL-D25-NEW-MAX-TOKENS-PARAM-01 | Add target_model_family and tokenizer_ref requirements (HH.2.9, HH.8.3) |
| OBL-D25-NEW-V15-01 | Add tokenizer_ref consumer note (HH.2.9) |
```

R3.1 drafting gate (II.6.2 #7): OP-A cross-check `missing_rows.length === 0` MUST pass before R3.1 publication.

---

## 23. Generation, CI, and maintenance

R3.1 preserves R3's generation/CI architecture. V1.x adjudication impact: build-time companion artifacts (HH.11.5 §7.2A) are generated as part of the build pipeline; CI gates verify presence and validity.

### 23.1 Generated artifacts

```
Generated at build time:
  - registries/reason_code_namespace_registry.json   (HH.5.3)
  - registries/injection_slot_registry.json          (HH.6.5)
  - registries/acceptance_test_registry.json         (HH.10.2)
  - registries/reviewer_finding_index.json           (HH.0.3 — mechanical grep)
  - registries/lint_closure_table.json               (HH.0.2 — per DD.1 status)
  - registries/degraded_reason_code_registry.json    (HH.15.2)
  - registries/doc24_imported_types.json             (HH.11.2)
  - action_registry.json                             (R3 §14.4 + HH.10.2)
```

Each generator is a script under `tools/r31_generation/`; CI invokes them and checksums the outputs against committed reference values.

### 23.2 CI rules

```
CI gates that must pass for R3.1 build:
  1. All InjectionSlotManifestEntry.slot_id references resolve in InjectionSlotRegistry.
  2. All reason codes emitted resolve in ReasonCodeNamespaceRegistry.
  3. All DegradedReasonCode values emitted resolve in DegradedReasonCodeRegistry.
  4. All imported types referenced (NodeKind, OutcomeClass, etc.) match DOC24_IMPORTED_TYPES.
  5. LintClosureTable.open_count === 0 (HH.0.2).
  6. OP-A cross-check: missing_rows.length === 0 (HH.8.2).
  7. Phantom audit script clean (HH.13.3).
  8. AcceptanceTestRegistry covers every spec-section anchor referenced.
  9. canonicalJSON library version pinned: @stablelib/json-canonicalize@2.x (HH.11.4).
 10. Monotonic ULID factory pinned globally (HH.9.3).
```

CI failures emit `reason_codes: ["doc24.ci_gate_failed"]` and block deployment.

### 23.2A Schema evolution discipline *(R3 — added per ADJ-RT-032, ADJ-RT-033, ADJ-RT-081; R3.1 — extended per HH.11.1, HH.11.2)*

Schema evolution rules:

```
1. schema_version: 1 in V1+V1.1+V1.2+V1.3+V1.4+R3.1 marks DOC24-OWNED types only.
2. Imported types retain owner-doc schema_version values per DOC24_IMPORTED_TYPES.
3. Field additions are non-breaking (existing readers ignore unknown fields).
4. Field removals require schema_version bump and migration path.
5. Field renames require both old and new names accepted for one schema_version cycle.
6. Discriminated union extensions (new kind variants) require schema_version bump.
```

V1.4 II.1 ManifestCardEntry field restoration is treated as a bug-fix restoration (not a schema evolution), since the fields existed in R3 and were silently dropped by V1.1 Y.5.

### 23.2B Review process discipline *(R3 — added per ADJ-RT-108 through ADJ-RT-112; R3.1 — preserved + V1.x lineage)*

The review process is locked to:
1. Multi-LLM red team workflow (ChatGPT, Grok, Gemini, Claude in fresh chat windows).
2. Self-contained spec documents (full context per review).
3. Reviewer prompt → adjudication card → audit prompt → incorporation cycle.
4. Self-audit after every review.
5. Two-prompt system (primary review + button-to-route enumeration) as standard for catching phantom features.

R3.1 lineage:
- V1 (136 adjudication cards) covers R3 review.
- V1.1 (cross-card normalization) covers V1 review.
- V1.2 (citation hygiene) covers V1.1 review.
- V1.3 (HH preflight closure) covers V1.2 review.
- V1.4 (II missed restorations + lint citations) covers V1.3 review.

R3.1 reviewer outputs (post-publication) follow the same pattern.

### 23.3 Machine-checkable obligation registry

OP-A V3.7+ is queryable as a structured artifact. CI gates verify:
- Every `OBL-*` row in OP-A has a target doc/version.
- Every OBL row referenced in R3.1 prose exists in OP-A.
- Every R3.1-introduced OBL row appears in OP-A (HH.8.2).

### 23.4 Maintenance rules

R3.1 preserves R3's maintenance discipline. Standing rules:
- Never reuse filenames; every modification gets a new version number.
- Single consolidated documents over amendment stacks (V1.4 ends the V1.x amendment chain; future patches start V2.x).
- Specs are not ready for implementation until red-team cycles are complete.
- Discuss before file creation or modification.

---

## 24. Validation and acceptance obligations

### 24.1 Latency governance

R3.1 preserves R3's latency budgets. Per-stage targets:

```
Stage                              Budget (p95)
-----                              ------------
created                            5ms
candidates_gathered                100ms
lifecycle_filtered                 20ms
policy_evaluated                   50ms
confidence_gated                   20ms
structurally_relevant              80ms (BDSM Matrix dependent)
matrix_boosted                     50ms
directives_assigned                30ms
rendering_tier_allocated           30ms
overflow_resolved                  20ms
lint_check                         15ms
lint_passed / lint_failed          5ms
manifest_written                   20ms
policy_revalidated                 10ms
dispatched                         (handoff; bounded by downstream)
```

End-to-end packet assembly target: 350ms p95 from `created` to `manifest_written`.

### 24.2 Adversarial acceptance test suite *(R3 — new per ADJ-RT-125; R3.1 — extended)*

The adversarial suite is the canonical test set for R3.1 readiness. R3 introduced 79 tests; R3.1 adds (estimated counts):

```
+ 11 from V1.3 HH (state machine, manifest contracts, reconciliation, algorithms, etc.)
+ 8 from V1.4 II (R3 field restorations, lint citations, attribution thread)

Total: ~98 adversarial acceptance tests in R3.1.
```

Per HH.10.2, the `AcceptanceTestRegistry` companion artifact is the authoritative test inventory. Test names referenced throughout R3.1 prose MUST appear in the generated registry.

R3.1-added tests (highlights, from various sections):

```
HH.1 state machine restoration:
  packet_lifecycle_aborted_for_retry_path_exists
  packet_lifecycle_reconciliation_closure
  packet_lifecycle_rejects_illegal_transitions

HH.2 manifest contracts:
  bdsm_attribution_skips_cards_dropped_by_final_prompt_owner
  lint_runs_on_candidate_before_manifest_persisted
  manifest_persists_lint_failure_triple

HH.4 algorithm corrections:
  beta_decay_preserves_success_ratio
  within_band_allocator_preserves_high_value_large_card
  within_band_allocator_avoids_large_low_value_displacement
  within_band_allocator_does_not_mutate_candidate_order
  qualifies_for_high_stakes_requires_lower_bound

HH.5 authority and policy:
  authority_computed_renders_inline_not_diagnostic
  policy_decision_engine_persist_before_swap
  read_write_lock_starvation_free

II.1 manifest card entry:
  manifest_card_entry_restores_r3_kda_variant_fields
  manifest_card_entry_position_in_packet_optional_for_reference_only
  manifest_card_entry_position_in_packet_required_for_inline

II.2 manifest:
  packet_manifest_preserves_r3_assembly_completed_at
  packet_manifest_supersession_link_set_on_reassembly
  packet_manifest_matrix_state_required_when_id_absent

II.3 attribution:
  tool_outcome_event_threads_selection_decision_id

II.4 lint closure:
  tool_invocation_function_is_async
  decision_graph_walk_is_async
  adj_131_uses_automation_channel

HH.14 disambig:
  disambig_session_stable_keys
  disambig_recently_removed_recovery
```

---

## 25. Open questions *(updated per ADJ-48; R3 — extended; R3.1 — final)*

R3.1's open-question register is shorter than R3's because V1.x adjudications closed most:

```
Closed in V1.x (no longer open):
  - Lifecycle state machine completeness (HH.1)
  - Manifest field set (HH.2 + II.2)
  - Reconciliation overlay vs. event model (HH.3)
  - Beta decay direction loss (HH.4.1)
  - Within-band sort relevance loss (HH.4.3)
  - Authority.computed render kind (HH.5.1)
  - PBE-lite over-inclusion (HH.7.1)
  - Saga retry refresh (HH.9.1)
  - Test registry concreteness (HH.10.2)
  - ManifestCardEntry KDA fields (II.1)
  - Schema-version reset scope (HH.11.1)
  - assembly_completed_at field (II.2.1)

Remaining open questions for R3.x (post-R3.1):
  Q1. R3.1 reviewer task (II.6.2) — verify zero-open after R3.1 publication.
  Q2. PBE-lite promotion-pending UX — banner kind needs DOC21 R3+ design refinement.
  Q3. Adaptive lane drift convergence dynamics — long-term observability question.
  Q4. Cross-tenant matter context isolation in OpenClaw native fork scenarios — deferred to R3.x (HH.8.4 inoculating note suffices for R3.1).
  Q5. AcceptanceTestRegistry coverage holes — to be discovered during generation.
```

---

## 26. Knowledge-to-LLM Delivery Architecture — Overview *(R3 — note updated; R3.1 — extended)*

R3.1 preserves R3's three-level delivery model and incorporates V1.x corrections in §26.5 (DeliveryDirective assignment per HH.5.1 AuthorityRenderDecision; HH.6.8 DirectiveConstraint; HH.6.9 DirectiveModification audit trail) and §26.6 (relevance + experience refinement per HH.4.3 within-band sort).

### 26.1 Three-Level Delivery Model

```
Level 1 — Resource (the underlying entity):
  - Lives in entity graph (DOC72-owned).
  - Has lifecycle_state, confidence, freshness.
  - Per §6.

Level 2 — Card (the rendered projection):
  - PacketCandidateCard during assembly.
  - ManifestCardEntry in the persisted manifest.
  - Per §5.4.1.Q.

Level 3 — Delivery (the LLM-facing payload):
  - Span of bytes in the final prompt.
  - PromptInjectionSpan in FinalPromptInjectionManifest.
  - Per HH.2.4.
```

The three levels are connected by:
- Resource → Card: rendering templates (§26.4) produce ManifestCardEntry from entity graph nodes.
- Card → Delivery: final-prompt assembly (DOC11/OpenClaw/DOC24-owned) materializes spans from manifest entries.

### 26.2 XML Injection Format

R3.1 preserves R3's XML injection format (per V1 ADJ-RT-001). Cards render with `<card>`, `<authority>`, `<bucket>` envelopes carrying `slot_id`, `card_id`, `card_stable_key`, and rendering attributes.

### 26.3 The Rendering Contract

```
For every ManifestCardEntry with card_presence.kind === "included_inline":
  - Rendering template per §26.4 produces:
    - byte span (becomes PromptInjectionSpan.byte_start..byte_end)
    - token_count (becomes PromptInjectionSpan.token_count)
    - content_hash (becomes PromptInjectionSpan.content_hash)
  - Per HH.2.9 tokenizer consistency: card.tokenizer_ref MUST match
    manifest.packet_tokenizer_ref.

For every ManifestCardEntry with card_presence.kind === "included_reference_only":
  - Rendering produces a brief reference label only.
  - Reference rendering uses card_presence.reference_only_payload.reference_label.
  - No bucket content is rendered for reference-only cards.

For every ManifestCardEntry with card_presence.kind === "excluded":
  - No rendering. Card does not appear in any PromptInjectionSpan.
```

### 26.4 Canonical Rendering Templates *(revised per ADJ-25; R3.1 — preserved + HH.5.1 authority render decision)*

R3.1 preserves R3's canonical templates. New rule per HH.5.1: when an `AuthorityRenderDecision.kind === "render_diagnostic_reference"`, the template renders the reference label with explicit degradation framing ("Although controlling authority on this point was not retrievable, prior work referenced…") — never rendering the assertion as inline fact.

When `AuthorityRenderDecision.kind === "render_inline"` (including `computed` authority — per HH.5.2 the success state, NOT degraded), the template renders inline with the directive's `jurisdiction_relationship` framing per V1 ADJ-RT-107:

```
[cite_as_rule] + binding_in_context    → "The law requires X. *Citation*."
[cite_as_rule] + persuasive_in_context → "The 9th Circuit has held that X. *Citation*."
[cite_as_rule] + out_of_context        → "Although not directly controlling here, the 9th Circuit has held that X. *Citation*."
[cite] + any                           → unchanged.
```

#### 26.4A Concrete rendering examples *(ADJ-57; R3.1 — preserved)*

R3.1 preserves R3 §26.4A examples; V1.x adjudication does not change example content.

### 26.4B Tool capability in procedure cards *(ADJ-91; R3.1 — preserved)*

Procedure cards render their `executes_via` tool_capability edges via inline tool references in the procedure body. Per §13.4B, tool selection happens at invocation time; the rendered procedure card identifies available tools but does not commit to one.

### 26.5 Deterministic Delivery Directive Assignment *(R3 — major revision per ADJ-RT-001, ADJ-RT-002, ADJ-RT-023, ADJ-RT-117; R3.1 — extended per HH.5.1, HH.6.8, HH.6.9)*

#### 26.5.1 Canonical DeliveryDirective vocabulary

R3.1 preserves R3 §26.5.1 vocabulary. The `DOC24DeliveryDirective` (§5.4.1.D) wraps the BDSM V6.5+ canonical type and adds:
- `directive_source: DirectiveSource` (per HH.6.8 ConstraintOrigin parallels).
- `jurisdiction_relationship?` (per V1 ADJ-RT-107).

#### 26.5.2 determineDeliveryDirective algorithm *(R3 — replaces R2.5 determine_tag; R3.1 — corrected per HH.5.1)*

R3.1's `determineDeliveryDirective` integrates the corrected AuthorityRenderDecision (HH.5.1) and per-card DirectiveConstraint accumulation (HH.6.8):

```ts
function determineDeliveryDirective(input: {
  card: PacketCandidateCard;
  authority: AuthorityResult;                          // from DOC73
  policy_decisions: PolicyDecisionSchema[];
  matrix_decision?: MatrixConfidenceAdjustment;        // from BDSM
  user_overrides: UserDirective[];
  render_context: AuthorityRenderContext;
}): {
  directive: DOC24DeliveryDirective | null;            // null iff card excluded
  card_presence: CardPresence;
  modifications: DirectiveModification[];
} {
  const modifications: DirectiveModification[] = [];

  // Step 0 — Authority anchor protection per ADJ-RT-002
  const authorityDecision = decideAuthorityRendering(
    input.authority, input.card, input.render_context
  );

  if (authorityDecision.kind === "exclude_from_packet") {
    return {
      directive: null,
      card_presence: {
        kind: "excluded",
        suppression_kind: "authority_blocked",
        suppressing_decision_ref: authorityDecision.exclusion_reason_codes[0],
      },
      modifications: [],
    };
  }

  if (authorityDecision.kind === "render_diagnostic_reference") {
    return {
      directive: null,
      card_presence: {
        kind: "included_reference_only",
        reference_only_payload: {
          reference_label: authorityDecision.reference_label,
          reason_code: authorityDecision.degraded_reason_codes[0],
          policy_decision_id: authorityDecision.policy_decision_id,
          schema_version: 1,
        },
        rendered_token_count: estimateReferenceTokens(authorityDecision.reference_label),
        directive_constraints: [],
      },
      modifications: [],
    };
  }

  // authorityDecision.kind === "render_inline"
  let directive = authorityDecision.directive;

  // Accumulate constraints from policy decisions
  const constraints: DirectiveConstraint[] = [];

  for (const p of input.policy_decisions) {
    constraints.push(...derivePolicyConstraints(p, modifications));
  }

  // Apply matrix adjustment (BDSM)
  if (input.matrix_decision) {
    const prior = directive.force_level;
    directive = applyMatrixForceAdjustment(directive, input.matrix_decision);
    if (prior !== directive.force_level) {
      modifications.push({
        modification_id: ulid(),
        card_id: input.card.card_id,
        field: "force_level",
        prior_value: prior,
        new_value: directive.force_level,
        modified_at_stage: "matrix_boosted",
        modified_by: "matrix_apply",
        rationale_ref: input.matrix_decision.decision_id,
        modified_at: ECClockService.now(),
        schema_version: 1,
      });
    }
  }

  // Apply user overrides
  for (const u of input.user_overrides) {
    constraints.push(...deriveUserOverrideConstraints(u, modifications, input.card));
  }

  // Validate constraint feasibility
  const feasibility = checkConstraintFeasibility(constraints, directive.force_level);
  if (feasibility.kind === "infeasible_policy_floor") {
    return {
      directive: null,
      card_presence: {
        kind: "excluded",
        suppression_kind: "policy_excluded",
        suppressing_decision_ref: feasibility.blocking_policy_decision_id,
      },
      modifications,
    };
  }

  // Apply effective force_level
  directive.force_level = feasibility.effective_force_level;

  return {
    directive,
    card_presence: {
      kind: "included_inline",
      rendering_kind: directive.render_mode === "reference_only" ? "compact" : "full",
      rendered_token_count: 0,  // populated at rendering tier allocation
      delivery_directive: directive,
      directive_constraints: constraints,
    },
    modifications,
  };
}
```

The returned `modifications: DirectiveModification[]` populates `ContextAssemblyTrace.directive_modifications` (HH.6.9).

#### 26.5.3 Migration from R2.5 flat tags

R3.1 preserves R3's migration guidance. R2.5's flat `tag` field is fully replaced by `DOC24DeliveryDirective` (§5.4.1.D).

### 26.6 Injection Selection: Relevance-First, Experience-Refined *(R3.1 — corrected per HH.4.3)*

R3.1 corrects R2's selection algorithm. The R2/R3 sort by `force_level` only erased BDSM relevance signal; HH.4.3 specifies a three-tier sort with relevance as primary within-band:

```ts
function rankCandidatesWithinPriorityBand(
  cards: readonly PacketCandidateCard[]
): readonly PacketCandidateCard[] {
  // Non-mutating: copy before sort
  return [...cards].sort((a, b) => {
    // 1. Force level (band — primary)
    const r = forceLevelRank(b.delivery_directive.force_level) -
              forceLevelRank(a.delivery_directive.force_level);
    if (r !== 0) return r;

    // 2. BDSM relevance — highest wins (within-band primary scoring)
    const relA = a.source_eligibility_aggregate?.normalized_relevance ?? 0;
    const relB = b.source_eligibility_aggregate?.normalized_relevance ?? 0;
    if (relB !== relA) return relB - relA;

    // 3. Token cost — tie-breaker only (cheaper wins when relevance ties)
    if (a.compact_token_cost !== b.compact_token_cost) {
      return a.compact_token_cost - b.compact_token_cost;
    }

    // 4. Stable: card_id lexicographic
    return a.card_id.localeCompare(b.card_id);
  });
}
```

`normalized_relevance` is in [0,1]. Per cross-doc obligation `OBL-BDSM-NEW-RELEVANCE-NORMALIZATION-01` (HH.8.1), BDSM delivers normalized scores at the BDSM→DOC24 boundary. DOC24 does not own normalization.

Tests (added in R3.1):

```
within_band_allocator_preserves_high_value_large_card
within_band_allocator_avoids_large_low_value_displacement
within_band_allocator_does_not_mutate_candidate_order
```

### 26.8 Proportional Context Injection *(revised per ADJ-NEW-02; R3.1 — preserved)*

R3.1 preserves R3 §26.8 proportional injection. The proportional split between knowledge cards and bucket content is governed by `computeUnifiedBudget` (§19.1A) with the share-normalization fix per HH.4.6.

### 26.9 Post-Generation Compliance Verifier

The post-generation compliance verifier runs after LLM generation to check whether outputs comply with packet constraints (e.g., did the LLM cite the required authority? Did it avoid the user-excluded sources?). Compliance results feed reconciliation events (HH.3).

### 26.9A Caution card format *(ADJ-73; R3.1 — preserved)*

Caution cards (with `tag === "caution"`) render with explicit visual framing per R3 examples. R3.1 alignment: caution cards use the same `DOC24DeliveryDirective` structure as other cards; the `tag` field discriminates.

### 26.10 Prompt Caching for Stable Context

Stable context (ambient baseline, long-lived system instructions) is prompt-cached at the LLM provider when possible. Per V1 ADJ-RT-110 prompt caching contract: the cache key is `(model_ref + system_prompt_hash + ambient_baseline_hash)`. Per-packet content is never cached (rotates every operation).

### 26.11 Ownership Split for Delivery

```
DOC24 owns:
  - PacketAssemblyOrchestrator (§38).
  - determineDeliveryDirective (§26.5.2).
  - rankCandidatesWithinPriorityBand (§26.6 + HH.4.3).
  - computeUnifiedBudget (§19.1A + HH.4.6).
  - PacketInjectionManifest emission.

BDSM V6.5+ owns:
  - DeliveryDirective canonical schema.
  - Satisfaction scoring + Shapley computation.
  - Matrix bundles + matrix_generation_id production.
  - normalized_relevance emission at the BDSM→DOC24 boundary.

KDA R3+ owns:
  - RenderingTier internals.
  - Variant tracking (rendering_specialization_id, render_variant_id, template_version).
  - Per-render performance attribution.

DOC73 V1.5+ owns:
  - ConsolidatedUnderstanding + AuthorityResult.
  - Authority constellation cascade.
  - PBE extraction internals.

DOC11/OpenClaw own:
  - FinalPromptInjectionManifest assembly.
  - PromptInjectionSpan byte-coverage emission (HH.2.4).
```

Per HH.8.4: when DOC11 or OpenClaw owns final-prompt assembly, the cross-doc obligation `OBL-D11-NEW-FINAL-PROMPT-SPAN-01` / `OBL-OPENCLAW-NEW-FINAL-PROMPT-SPAN-01` requires byte-coverage emission. The "hash + summary" escape hatch from V1.1 Y.6 is retired.

---

## 27. Three-Lane Retrieval for Packet Assembly *(revised per Routing Cascade Fix; R3.1 — preserved)*

R3.1 preserves R3's three-lane retrieval. V1.x adjudication impact: the hard-required overflow protocol (§27.0A) is corrected per HH.4.4 (user-originated cap scoping).

```
Three retrieval lanes (per SourceLane):
  - entity_lane:     knowledge cards from entity graph.
  - bucket_lane:     bucket content (direct file material).
  - authority_lane:  authority constellation cards.

Each lane has its own retrieval logic, but all lanes produce PacketCandidateCard
records that flow into the same assembly pipeline at candidates_gathered.
```

### 27.0A Hard-required overflow protocol *(R3 — significantly revised per ADJ-RT-019; R3.1 — corrected per HH.4.4)*

R3.1 refines R3's overflow protocol with the user-originated cap distinction:

```ts
type DroppabilityClass =
  | "hard_required"      // Cannot be dropped; if budget cannot accommodate, block
  | "policy_required"    // Required by policy; treated as hard_required for budget
  | "pinned_requested"   // User explicitly pinned; preserved over ordinary cards
  | "ordinary"           // Default; subject to standard trimming
  | "optional";          // Lowest priority; first to trim

const MAX_USER_ORIGINATED_HARD_REQUIRED_CARDS_PER_PACKET = 8;

function enforceHardRequiredCountCap(cards: PacketCandidateCard[]): void {
  const userOriginatedHard = cards.filter(c =>
    c.droppability_class === "hard_required" &&
    (c.hard_requirement_origin === "user_pinned" ||
     c.hard_requirement_origin === "user_emphasized")
  );

  if (userOriginatedHard.length > MAX_USER_ORIGINATED_HARD_REQUIRED_CARDS_PER_PACKET) {
    throw new PacketBlocked({
      blocking_reason: "doc24.user_originated_hard_required_count_exceeded",
      affected_card_ids: userOriginatedHard.map(c => c.card_id),
      user_visible_message_template_id:
        "user_originated_hard_required_count_exceeded_user_facing",
    });
  }
}
```

Per HH.4.4: **count caps apply ONLY to user-originated hard-required cards** (`hard_requirement_origin ∈ {"user_pinned", "user_emphasized"}`). Policy-required, architecture-required, and authority-required cards are NEVER count-capped. If they collectively exceed `total_budget_tokens`, the packet blocks with `blocking_reason: "hard_required_budget_overflow"` (per R3 OVERCONSTRAINED_BUDGET state), not by count.

Overflow algorithm (corrected):

```
1. Validate user-originated hard-required count cap (enforceHardRequiredCountCap).
2. Compute total token cost H of hard_required + policy_required cards.
3. If H > total_budget_tokens: emit PacketBlocked with blocking_reason
   "hard_required_budget_overflow"; do NOT silently drop hard-required content.
4. If H ≤ total_budget_tokens: reserve hard-required cards at full size.
5. Compact pinned_requested cards if budget pressure exists.
6. Trim ordinary cards by priority.
7. Drop optional cards.
8. If after step 7 the packet still exceeds budget but H fits: trim pinned_requested
   if necessary. Emit packet.degraded event with reason "pinned_trimmed_for_hard_required".
```

OVERCONSTRAINED_BUDGET state and `OBL-D15-NEW-01` (DOC15 budget governance) preserved from R3 unchanged.

### 27.0B Source policy and exclusion pre-filter *(R3 — significantly revised per ADJ-RT-003, ADJ-RT-004, ADJ-RT-053; R3.1 — preserved)*

The source policy pre-filter runs at the `policy_evaluated` lifecycle state. Per HH.4.4 cap rule: the pre-filter does not enforce count caps; counts are enforced at `overflow_resolved` via the user-originated cap.

---

## 28. Context Editing and Compaction Before Injection *(revised per ADJ-33, ADJ-67; R3.1 — preserved + HH.2.5 storage representation)*

R3.1 preserves R3's compaction logic. V1.x adjudication impact: storage representation (HH.2.5) is now a discriminated union (`InlineManifestCardRecord` vs `PointerManifestCardRecord`) — compaction produces pointer-compacted records that reference full payloads in the append-only blob store.

```
Compaction sequence:
  1. Build PacketInjectionManifest with card_records: ManifestCardRecord[].
  2. Initially, all records are InlineManifestCardRecord (storage_representation: "inline_full").
  3. If manifest_size_bytes_uncompressed > compaction_threshold:
     - Convert low-priority cards to PointerManifestCardRecord (storage_representation: "pointer_compacted").
     - Compute full_entry_hash per computeFullEntryHash (HH.2.6 — excludes timestamps).
     - Write full ManifestCardEntry blob to append-only blob store.
     - Replace inline record with pointer.
  4. Set compression_applied: true on the manifest.

CI gate: when compression_applied === true, every pointer record's full_entry_blob_ref
resolves AND full_entry_hash matches the dereferenced blob.
```

Content hashing uses RFC 8785 JCS via `@stablelib/json-canonicalize@2.x` (HH.11.4); the library version is pinned to prevent variant-output drift across coding agents.

---

---

## 29. Injection primary_tag mapping by AuthorialVoice and AssertionType *(revised per ADJ-26; R3 — note per ADJ-RT-001; R3.1 — extended per HH.5.1)*

R3.1 preserves R3's primary tag mapping with one V1.x correction: when authority renders inline (HH.5.1 `AuthorityRenderDecision.kind === "render_inline"`, including computed authority — per HH.5.2 the success state), the primary tag is determined by the standard mapping. When authority renders as a diagnostic reference (`kind === "render_diagnostic_reference"`), the card is `included_reference_only` (HH.6.4) and bypasses the tag mapping; rendering produces a reference label only.

### 29.1 Primary mapping table

```
AuthorialVoice × AssertionType → primary_tag

Voice               AssertionType         Primary tag
-----               -------------         -----------
court               binding_holding       cite_as_rule
court               persuasive_holding    cite_as_rule (with jurisdiction_relationship: "persuasive_in_context")
court               dicta                 cite
court               other                 cite
statute             text                  cite_as_rule
statute             commentary            cite
regulation          text                  cite_as_rule
regulation          interpretive          cite
secondary_authority any                   cite
treatise            any                   cite
user_directive      any                   anchor
system_directive    any                   anchor
news / fact         any                   background
note_or_draft       any                   draft
warning_or_risk     any                   caution
```

Per V1 ADJ-RT-107, `jurisdiction_relationship` on the directive distinguishes binding vs. persuasive vs. out-of-context rendering for `cite_as_rule` tagged authorities.

### 29.2 Fallback mapping tables *(ADJ-26; R3.1 — preserved)*

When voice or assertion type is undetermined:

```
Undetermined voice → default to "secondary_authority" → tag: "cite"
Undetermined assertion_type → default to "other" → tag: "cite"
Ambiguous user_directive vs note → ask (clarifying probe); default "anchor" with hedge_mode: "hedged"
```

The fallback never produces `cite_as_rule` from undetermined inputs — silently elevating uncertain authority to rule-citation is forbidden.

### 29.3 Non-legal domain mapping

For non-legal domains (finance, medicine, software, ops), the mapping table is adjusted by the active `DomainSignalProfile`. R3.1 preserves R3's pluggable mapping. The `AuthorityRenderContext` (§5.4.1.J) supplies the domain-appropriate `safe_reference_label` when authority renders as diagnostic reference.

Per V1.x cross-doc obligation `OBL-D72-NEW-DOMAIN-PROFILE-MAPPING-01` (existing): DomainSignalProfile owns the per-domain mapping; R3.1 consumes via the profile.

---

## 30. Tension-Aware Injection Policy

R3.1 preserves R3's tension-aware injection policy. V1.x adjudication impact: tension surfaces use the same `DegradedReasonCode` registry (HH.15.2) and `DirectiveConstraint` accumulation (HH.6.8).

### 30.1 Contradiction Injection Rules

When two retrieved cards make contradictory assertions about the same entity, R3.1 preserves R3's rules:

```
1. Both cards are retrieved if both have eligibility.
2. The DeliveryDirective on each carries hedge_mode: "hedged".
3. The packet manifest emits warnings: ["contradiction.detected_between_cards"]
   with the card_ids involved.
4. The LLM-facing rendering presents both with explicit framing:
   "Sources disagree. Source A says X; Source B says ¬X."
5. The packet does NOT silently choose one side.
```

Per HH.6.8 DirectiveConstraint: contradictory cards may receive
`{ kind: "force_level_ceiling", ceiling: "standard", origin: { kind: "architecture", rule_ref: "§30.1" } }`
to prevent either side rendering as "hard" while the contradiction stands.

### 30.2 Conflict Set Injection

Conflict sets group multiple contradictory cards (more than two) into a single rendered block. Per V1 ADJ-RT-126: conflict sets are bounded (default ≤ 5 cards per set; beyond, summary-only rendering) to prevent the conflict block from consuming most of the packet budget.

The conflict set is rendered as one logical unit; budget allocation per HH.4.7 BudgetCell treats the set as a single membership.

---

## 31. Legal Citation Rendering

R3.1 preserves R3's legal citation rendering. V1.x adjudication impact: the citation infrastructure references the V1 ADJ-RT-107 jurisdiction-in-core decision (rejected ChatGPT 2 #14 reviewer proposal to move to domain facet per HH.0.5).

### 31.1 AuthorityRef Schema

```ts
type AuthorityRef = {
  authority_id: string;
  citation: string;                        // human-readable Bluebook-style citation
  authority_kind: "case" | "statute" | "regulation" | "treatise" | "secondary" | "rule";
  jurisdiction: { primary: string; appellate_path?: string[] };
  precedential_status: "binding" | "persuasive" | "non_precedential" | "abrogated";
  pinpoint?: string;                       // page/paragraph anchor
  schema_version: 1;
};
```

The `jurisdiction` field on `AuthorityRef` parallels the `jurisdiction` field on `WorkContextRef` (§5.4.1.A) — both live in core types, not facets, per V1 ADJ-RT-107.

### 31.1A Authority availability refresh *(ADJ-23; R3.1 — preserved)*

Authority availability is checked at packet assembly time. If an authority constellation has `lifecycle_state: invalidated` (e.g., subsequent history overrules), the rendering uses `AuthorityRenderDecision.kind === "render_diagnostic_reference"` with `degraded_reason_codes: ["authority.collapsed_essential_retracted"]`.

### 31.2 LegalSupportBundle

A LegalSupportBundle is a curated set of `AuthorityRef[]` supporting a single proposition. R3.1 preserves R3's bundle structure:

```ts
type LegalSupportBundle = {
  bundle_id: string;
  proposition: string;
  authority_refs: AuthorityRef[];
  jurisdiction_relationship_summary: "fully_binding_in_context" | "mixed" | "all_persuasive" | "all_out_of_context";
  built_at: string;
  schema_version: 1;
};
```

Per V1 ADJ-RT-107 + R3.1 §26.4: bundles render with `jurisdiction_relationship` framing — binding authorities render with neutral framing; persuasive authorities with explicit "X court has held" framing; out-of-context authorities with explicit "although not directly controlling here" framing.

### 31.3 Authority-Backed Response Requirement

For high-stakes legal queries (per `ProcedureStep.risk_tier === "high_stakes"`), R3.1 preserves R3's requirement: the LLM response MUST be backed by at least one authority from a `LegalSupportBundle`. If no bundle is available, the operation pauses with `pause_state: "user_choice_pending"` and the user is prompted to either approve unsupported response, defer, or rescope.

### 31.4 Rendering Modes

```
RenderingMode             What the LLM sees
-------------             -----------------
authority_inline          "The Supreme Court held that X. *Cite v Cite (2020).*"
authority_reference_only  "[Bryant v. Acme Corp., 542 U.S. 1 (2020) — reference only]"
authority_excluded        (card absent from prompt entirely)
```

The mode is determined by `AuthorityRenderDecision` (HH.5.1):

```
render_inline                  → authority_inline
render_diagnostic_reference    → authority_reference_only
exclude_from_packet            → authority_excluded
```

This is the operational consequence of HH.5.1's three-kind discriminated union: rendering mode follows directly from the decision kind.

---

## 32. Provenance Display Policy

R3.1 preserves R3's provenance display policy. V1.x adjudication impact: provenance uses the HH.7.1/HH.7.2 full `Provenance` structure (`primary_source: ProvenancePrimarySource` + `contributing_sources: ProvenanceContributingSource[]`).

The provenance display rules:

```
Inline cards (card_presence.kind === "included_inline"):
  - Source citations are inline with the rendered content.
  - Per V1 ADJ-RT-032 source citation discipline: every assertion has a SourceRef.
  - PBE-derived cards (provenance.primary_source.kind === "pbe_extraction") render
    with explicit "extracted from <source>" framing.
  - User-captured cards (provenance.primary_source.kind === "user_capture") render
    with "you noted" framing.

Reference-only cards (card_presence.kind === "included_reference_only"):
  - Reference label includes minimal citation.
  - Bucket/document content is not rendered; only the reference label.

Excluded cards:
  - Not rendered at all.
  - Listed in PacketOpportunitySet for inspector visibility (HH.15.4).
```

PBE-lite cards (cached approximations awaiting full extraction) carry the `PBELitePacketEffect.banner_kind === "pbe_lite_degraded_contract"` banner per HH.7.3. The packet manifest's `pbe_lite_effect` field is non-null; the Packet Inspector surfaces the banner per HH.7.4 cross-doc obligation `OBL-DOC21-NEW-PBE-LITE-BANNER-01`.

---

## 33. Injection Kill-Switch Behavior

R3.1 preserves R3's kill-switch behavior. V1.x adjudication impact: kill-switch produces `BlockedPacketManifest` per HH.2.2 with the appropriate `blocking_reason`.

### 33.0 Kill-switch UX *(ADJ-15; R3.1 — preserved)*

The kill-switch is a system-wide toggle that suspends LLM-facing injection. When enabled:

```
1. PacketAssemblyOrchestrator continues to assemble candidate manifests.
2. The CandidateInjectionManifest passes through lint as normal.
3. Instead of persisting PacketInjectionManifest at manifest_written state, the
   pipeline emits BlockedPacketManifest with:
     blocking_reason: "doc24.kill_switch_engaged"
     blocked_at_stage: "manifest_written"
4. No dispatch occurs; the agent receives no injection from DOC24 for this operation.
5. The user is notified via operator UI that the kill-switch is engaged.
```

The kill-switch state is durable in `working_context_store`. It survives restarts. Engaging the kill-switch emits a `KillSwitchStateChangedEvent`; disengaging emits the inverse.

Per HH.2.2 diagnostic preservation: the `BlockedPacketManifest.candidate_manifest_at_block` preserves what WOULD have been delivered, enabling post-hoc inspection of "what was withheld" during kill-switch periods.

---

## 34. Prior Context Cards *(revised per ADJ-39, ADJ-68; R3.1 — preserved + HH.6.4 CardPresence)*

R3.1 preserves R3's PriorContextCard contract. V1.x adjudication impact: PriorContextCard now produces a `ManifestCardEntry` with `card_presence` populated per HH.6.4 (no separate inclusion_decision + rendering_kind fields).

### 34.1 Continuity detection policy *(ADJ-39; R3.1 — preserved)*

Continuity detection identifies when the current operation continues a prior thread (e.g., user mid-conversation asks about something from yesterday's session). When continuity is detected:

```
1. Relevant PriorContextCard-s are retrieved from prior session manifests.
2. The cards enter the assembly pipeline at candidates_gathered with
   source_lane: "entity_lane" and provenance.primary_source pointing to
   the prior session.
3. PriorContextCards have droppability_class: "ordinary" by default —
   they CAN be trimmed under budget pressure.
4. The user_override_applied flag is false unless the user explicitly pinned
   prior context.
```

### 34.2 PriorContextCard precedence with active context *(ADJ-68; R3.1 — preserved)*

When PriorContextCard conflicts with active context state (e.g., prior context says X; active context says ¬X because of a recent user mutation), active context wins. The PriorContextCard is either:

- Suppressed (if the divergence is total): `card_presence.kind === "excluded"` with `suppression_kind === "duplicate_overlap"`.
- Re-rendered with reduced force (if partial overlap): `force_level_ceiling: "standard"` constraint applied.
- Rendered as diagnostic reference (if archived in active context): `card_presence.kind === "included_reference_only"`.

The choice depends on the divergence severity, computed via `comparePriorContextWithActive` (per V1 ADJ-RT-068 multi-matter slot interaction).

---

## 35. Semantic Cache Warming via Calendar *(R3 — extended per ADJ-RT-056, ADJ-RT-103; R3.1 — preserved)*

R3.1 preserves R3's calendar-based cache warming. V1.x adjudication impact: warming cache invalidation aligns with HH.5.6 source eligibility cache key (multiple invalidation triggers).

The warming pipeline:

```
1. Read upcoming calendar events from connected calendar sources (M365, Google).
2. Per V1 ADJ-RT-056: identify event types likely to trigger ELNOR usage
   (meetings with prep notes, deadlines, court appearances).
3. Prefetch relevant entities and bundle them as warmed cache entries.
4. Cache entries have TTL; expire after the calendar event's window.
```

### 35.1 Cache invalidation triggers *(R3 — added per ADJ-RT-056; R3.1 — extended per HH.5.6)*

Cache invalidation triggers per HH.5.6 `SourceEligibilityCacheKey`:

```
- source_policy_generation_id changes (PropA policy mutation).
- source_lifecycle_revision changes (entity mutation, retraction).
- temporary_grant_generation_id changes (temporary grants issued/revoked).
- entity_lineage_revision changes (entity merge/split affecting lineage).
- exposure_context_hash changes (matter context switch, excluded_workspace_ids update).
- source exclusion filter changes (operator-initiated).
- calendar event passes (warmed cache TTL expiry).
```

Multi-trigger invalidation: a single warmed cache entry may be invalidated by any of the triggers above; whichever fires first.

### 35.2 Predictive warming via patterns *(R3 — added per ADJ-RT-103; R3.1 — preserved)*

Predictive warming uses observed user patterns to warm cache for likely-next operations. Per V1 ADJ-RT-103: the prediction model is a derived projection over `receipt_store` and `tool_outcome_evidence_event_store`; it does not emit recommendations to the user, only pre-warms cache silently.

The prediction is bounded: maximum 50 warmed entries at any time (per `PacketOpportunitySamplingPolicy.k_cap` default — HH.15.4 — sharing the same bounding discipline).

---

## 36. Knowledge Digest *(restructured per ADJ-08; R3 — extended per ADJ-RT-026, ADJ-RT-027, ADJ-RT-084; R3.1 — preserved + DigestItemDisposition canonical)*

R3.1 preserves R3's three-tier digest model. V1.x adjudication impact: `DigestItemDisposition` is the canonical enum per ADJ-RT-085 (Y.8 in V1.1; ADJ-RT-120's `accept`/`useful_to_track` synonyms retired).

### 36.1 Three-tier digest model

```
Tier 1 — Pending review:
  - New extractions awaiting user disposition.
  - PBE-derived candidates not yet confirmed.
  - Per V1 ADJ-RT-026: cap on tier-1 backlog (default 50 items; older items roll
    into tier 2 if not reviewed).

Tier 2 — Aged:
  - Items not reviewed within tier-1 window.
  - Surface less prominently; subject to bulk disposition (per HH.6.11 BulkActionRequest).

Tier 3 — Archived:
  - Items dismissed or marked not-relevant.
  - Queryable via `inspect_knowledge` (per V1 ADJ-RT-118).
  - Lifecycle_state may be `archived` or `retracted` depending on disposition.
```

### 36.2 Digest item disposition *(R3 — significantly revised per ADJ-RT-026, ADJ-RT-084; R3.1 — canonical per V1.1 Y.8)*

```ts
type DigestItemDisposition =
  | "confirm_accuracy"
  | "mark_useful"
  | "confirm_accuracy_and_useful"
  | "dismiss_digest_item"
  | "not_relevant_here"
  | "factually_wrong"
  | "archive_candidate"
  | "archive_node"
  | "edit_and_save"
  | "defer";

type DigestItemDispositionRequest = {
  digest_item_id: string;
  disposition: DigestItemDisposition;
  evidence?: FactuallyWrongEvidence;        // REQUIRED iff disposition === "factually_wrong"
  idempotency_key: string;
  schema_version: 1;
};

function validateDigestDisposition(req: DigestItemDispositionRequest): void {
  if (req.disposition === "factually_wrong" && !req.evidence?.evidence_text?.trim()) {
    throw new ValidationError({
      code: "doc24.factually_wrong_evidence_required",
      message: "factually_wrong disposition requires non-empty evidence_text",
    });
  }
}
```

The dispositions route to:

```
Disposition                     Effect on graph
-----------                     ---------------
confirm_accuracy                lifecycle_state: draft → active (no other change)
mark_useful                     adds utility signal (consumed by BDSM via 3-way join)
confirm_accuracy_and_useful     both of the above
dismiss_digest_item             item removed from digest; entity unchanged
not_relevant_here               matter-scoped suppression; emit user_directive
factually_wrong                 emit factually_wrong evidence; entity may transition to draft
                                pending review; reconciliation event recorded
archive_candidate               extraction candidate suppressed; no entity created
archive_node                    extant entity transitions to archived
edit_and_save                   route to entity creation/edit UI (§12.7C)
defer                           item moves to tier 2 (aged)
```

### 36.2A Routes for digest item dispositions

```
POST /digest/items/{item_id}/disposition         (single)
POST /digest/items/bulk/disposition              (BulkActionRequest with action_kind:
                                                  "digest_items.archive" | "digest_items.dismiss")
```

The bulk endpoint uses the typed `BulkActionRequest` discriminated union from HH.6.11. Destructive bulk operations (e.g., archive of items linked to active entities) are subject to the destructive partial-failure policy narrowing.

### 36.3 Delivery channel preference *(R3 — added per ADJ-RT-027; R3.1 — preserved)*

Per V1 ADJ-RT-027: digest delivery uses the user's preferred channel:

```
DigestDeliveryChannel:
  - "q_inline"          — digest appears as Q Dashboard side panel
  - "q_modal"           — digest opens as Q modal on entry
  - "discord_dm"        — digest pushed to Discord direct message
  - "teams_chat"        — digest pushed to Teams personal chat
  - "email_daily"       — digest emailed daily at user-configured time
  - "automation"        — digest emitted to automation channel (per §13.1 ChannelKind)
  - "off"               — no digest delivery
```

Channel preference is a `user_directive` node (`node_kind === "user_directive"`). Per HH.15.2 DegradedReasonCodeRegistry: delivery failures emit human-readable messages from the registry's `human_message_template_id`.

---

## 37. Extended Delivery Intelligence

R3.1 preserves R3's extended delivery intelligence. V1.x adjudication impact: cross-work-context retrieval uses the `excluded_workspace_ids` source exclusion (V1 ADJ-RT-072 + HH.8.4 OpenClaw normalization).

### 37.1 Deadline Cascade Intelligence

When upcoming deadlines (from calendar or matter context) are within a configurable window, the system surfaces related work proactively. Per R3 §35 calendar warming + §37.1: deadline-adjacent entities receive a small `force_level` boost via the BDSM Matrix (per `OBL-BDSM-NEW-DEADLINE-BOOST-01` existing in OP-A).

### 37.2 Cross-Work-Context Prior Work Product Retrieval *(R3 — extended per ADJ-RT-072; R3.1 — preserved)*

When the user's current matter has a query that resembles prior work in a different matter, the system MAY surface the prior work as a reference-only card. The cross-matter retrieval is governed by:

```
1. Active matter is matter_A.
2. Query embedding matches prior work product in matter_B.
3. matter_B is NOT in excluded_workspace_ids of active context.
4. Prior work product is retrieved as a candidate with:
   - source_lane: "entity_lane"
   - card_presence.kind: "included_reference_only" (default; user pin required for inline)
   - reference_only_payload.reference_label: "Prior work in <matter_B canonical_name>"
5. The reference does NOT include matter_B's full content unless the user clicks through.
6. Click-through is policy-gated per source_policy_store.
```

Per HH.8.4: cross-matter retrieval routes via `sessions_spawn` with `context: "isolated"`; payload is serialized into `task_prompt` explicitly. Fork context is not used.

### 37.3 Cross-Reference: What DOC72 Sends to DOC24

DOC72 surfaces (for DOC24 consumption):
- Entity nodes and their relationships.
- Authority constellations (with frontier_cap; per HH.5.1 cycle/frontier handling).
- `OutcomeClass` enum (DOC72-owned per HH.6.2/HH.11.2).
- `NodeKind` enum.
- `EvidenceRef` records (from reconciliation; HH.3).
- `AuthorityResult` (with `computed_state` per HH.5.1).

### 37.4 What DOC24 Sends Back to DOC72 *(ADJ-55; R3.1 — preserved)*

DOC24 emits (for DOC72 consumption):
- Entity mutation events (created/updated/retracted; § 8.8).
- `PacketReconciliationEvent` per HH.3 (consumed by DOC72 for confidence aggregation).
- Tool outcome events (consumed by DOC72 for procedure node experience updates).
- User directive captures (consumed by DOC72 to update `user_directive` nodes).

All emissions route through EC Core sole-writer rule (§7.3). The 3-way join discipline (HH.2.7) applies to all DOC72-consumed events: only cards present in `FinalPromptInjectionManifest` produce confidence-affecting signals.

---

---

## 38. Runtime lifecycle, assembly contract, and session continuity *(R3 — new section; R3.1 — significantly extended per V1.x adjudications)*

§38 owns the cross-cutting runtime contract that binds packet assembly, policy gating, rendering, attribution feedback, and session evolution into one deterministic machine. It does not replace or govern existing sections — §13 (routing), §19 (packet model), §26 (delivery), §27 (retrieval lanes), §28 (compaction), §29 (tags) retain ownership of their domain logic. §38 specifies the cross-cutting invariants those sections must collectively satisfy.

R3.1 extends §38 with V1.x adjudication closures:
- §38.1 — full 20-state machine restored per HH.1 (V1.1 Y.7 silent state drops corrected).
- §38.2 — four-manifest chain per HH.2 (Candidate, Packet, FinalPrompt, Blocked); 3-way join contract (HH.2.7).
- §38.3 — race-safety integrates HH.5.4 PolicyDecisionEngine snapshot/lock model.
- §38.6 — resume uses HH.9.1 saga concurrency protocol.
- §38.8 — disambiguation uses HH.14 stable IDs + recently_removed_candidates.
- §38.9 — reconciliation as append-only events + derived current view per HH.3.
- §38.11 — linter operates on CandidateInjectionManifest per HH.2.1; failure produces durable triple per HH.2.2.
- §38.16 (R3.1 new) — InjectionSlotRegistry contract per HH.6.5.
- §38.17 (R3.1 new) — PBELitePacketEffect machinery per HH.7.3.
- §38.18 (R3.1 new) — Saga concurrency contract per HH.9.

### 38.0 Purpose and scope *(ADJ-RT-088; R3.1 — preserved)*

§38 codifies the runtime invariants that span:
- Lifecycle ordering (§38.1, §38.12).
- Persistence boundaries (§38.2, §38.11).
- Race conditions (§38.3, §38.18).
- Concurrent context (§38.4, §38.5, §38.6).
- User-facing interruption (§38.7, §38.8).
- Attribution and learning feedback (§38.9).
- Inspectability (§38.10).
- Cross-doc obligations (§38.14).

§38 is the destination for V1.x normalization-layer corrections that don't belong to any single domain section. The HH.1 state machine restoration lives here, not in §19, because it's a runtime invariant across the pipeline.

### 38.1 Packet lifecycle states and invariants *(ADJ-RT-088; R3.1 — restored per HH.1)*

R3.1 restores the full 20-state lifecycle. V1.1 Y.7 silently dropped five R3 states (`lifecycle_filtered`, `confidence_gated`, `structurally_relevant`, `aborted_for_retry`, `reconciled`) and silently renamed two (`candidates_gathered` → `candidates_resolved`; `rendering_tier_allocated` → `tier_allocated`); HH.1 caught and reverted these. V1.1 X.4's additions (`lint_check`, `lint_failed`) are retained; V1.1 Y.7's undefined additions (`handed_off`, `failed`) are dropped — substate modeling (DispatchedAckState) handles those without new enum values.

#### 38.1.1 PacketAssemblyScope (canonical per HH.6.1)

`PacketAssemblyScope` (§5.4.1.P) is captured at the `created` lifecycle state and threaded through every subsequent stage without mutation. It carries the packet's identity, scope, tokenizer ref, total budget, and three generation snapshots (policy, bundle, matrix).

#### 38.1.2 Lifecycle state enum

```ts
type PacketLifecycleState =
  // Pre-assembly
  | "created"

  // Assembly stages (R3 — RESTORED in R3.1; V1.1 Y.7 had silently dropped 5)
  | "candidates_gathered"
  | "lifecycle_filtered"
  | "policy_evaluated"
  | "confidence_gated"
  | "structurally_relevant"
  | "matrix_boosted"
  | "directives_assigned"
  | "rendering_tier_allocated"
  | "overflow_resolved"

  // Lint and persistence (V1.1 X.4)
  | "lint_check"
  | "lint_passed"
  | "lint_failed"
  | "manifest_written"

  // Dispatch
  | "policy_revalidated"
  | "aborted_for_retry"        // R3 — RESTORED (CRITICAL: ADJ-009 depends)
  | "dispatched"

  // Terminal
  | "blocked"
  | "completed"
  | "reconciled";              // R3 — RESTORED (CRITICAL: reconciliation closure)
```

#### 38.1.3 Allowed transitions

```ts
type LifecycleTrigger =
  | "assembler"
  | "budget_resolver"
  | "linter"
  | "policy_revalidation"
  | "dispatch"
  | "runtime_completion"
  | "reconciliation_complete"
  | "abort"
  | "block";

type PacketLifecycleTransitionRule = {
  from: PacketLifecycleState;
  to: PacketLifecycleState;
  trigger: LifecycleTrigger;
  terminal?: boolean;
};

const PACKET_LIFECYCLE_TRANSITIONS: PacketLifecycleTransitionRule[] = [
  // Assembly forward path (linear)
  { from: "created",                     to: "candidates_gathered",      trigger: "assembler" },
  { from: "candidates_gathered",         to: "lifecycle_filtered",       trigger: "assembler" },
  { from: "lifecycle_filtered",          to: "policy_evaluated",         trigger: "assembler" },
  { from: "policy_evaluated",            to: "confidence_gated",         trigger: "assembler" },
  { from: "confidence_gated",            to: "structurally_relevant",    trigger: "assembler" },
  { from: "structurally_relevant",       to: "matrix_boosted",           trigger: "assembler" },
  { from: "matrix_boosted",              to: "directives_assigned",      trigger: "assembler" },
  { from: "directives_assigned",         to: "rendering_tier_allocated", trigger: "assembler" },
  { from: "rendering_tier_allocated",    to: "overflow_resolved",        trigger: "budget_resolver" },

  // Overflow can block
  { from: "overflow_resolved",           to: "blocked",                  trigger: "block",   terminal: true },
  { from: "overflow_resolved",           to: "lint_check",               trigger: "linter" },

  // Lint
  { from: "lint_check",                  to: "lint_passed",              trigger: "linter" },
  { from: "lint_check",                  to: "lint_failed",              trigger: "linter" },
  { from: "lint_failed",                 to: "blocked",                  trigger: "block",   terminal: true },

  // Post-lint
  { from: "lint_passed",                 to: "manifest_written",         trigger: "assembler" },
  { from: "manifest_written",            to: "policy_revalidated",       trigger: "policy_revalidation" },

  // Race detection (ADJ-009)
  { from: "policy_revalidated",          to: "aborted_for_retry",        trigger: "abort" },
  { from: "aborted_for_retry",           to: "created",                  trigger: "assembler" },

  // Dispatch
  { from: "policy_revalidated",          to: "dispatched",               trigger: "dispatch" },

  // Runtime completion
  { from: "dispatched",                  to: "completed",                trigger: "runtime_completion" },

  // Reconciliation closure
  { from: "completed",                   to: "reconciled",               trigger: "reconciliation_complete", terminal: true },
];

function assertAllowedPacketTransition(
  from: PacketLifecycleState,
  to: PacketLifecycleState
): void {
  if (!PACKET_LIFECYCLE_TRANSITIONS.some(t => t.from === from && t.to === to)) {
    throw new InvalidPacketLifecycleTransition({ from, to });
  }
}
```

#### 38.1.4 Substate modeling (in place of dropped enum additions)

V1.1 Y.7 added `handed_off` and `failed` as undefined enum values; R3.1 drops both. The conditions they tried to model use substate flags instead:

```ts
// In place of V1.1 Y.7's "handed_off" enum value:
type DispatchedAckState = {
  state: "dispatched";
  acknowledged_by_receiver: boolean;
  acknowledged_at?: string;
  receiver: "doc11" | "structured_ec" | "openclaw_direct" | "background_processor";
};

// In place of V1.1 Y.7's "failed" enum value:
//   - Pre-dispatch terminal failure: lifecycle = "blocked" with reason on BlockedPacketManifest.
//   - Runtime failure during/after dispatch: lifecycle = "completed" with outcome_class === "failure"
//     on the ActionReceipt; reconciliation event records the runtime outcome.
```

DOC11 acknowledgement is queryable on the dispatched packet without a separate lifecycle transition.

#### 38.1.5 Lifecycle invariants

```ts
type PacketLifecycleInvariants = {
  every_transition_emits_event: true;          // packet.lifecycle.transitioned
  every_stage_emits_telemetry: true;            // packet.assembly.stage_completed
  manifest_immutable_post_write: true;          // per HH.3.2
  no_durable_manifest_for_failed_lint: true;    // per HH.2.2
  reassembly_acquires_fresh_snapshot: true;     // per II.5.1
  reconciliation_appends_events: true;          // per HH.3.1 (no overlay mutation)
};
```

#### 38.1.6 Acceptance tests

```
packet_lifecycle_aborted_for_retry_path_exists
packet_lifecycle_reconciliation_closure
packet_lifecycle_rejects_illegal_transitions
packet_lifecycle_aborted_for_retry_acquires_fresh_snapshot   # II.5.1
```

### 38.2 Manifest chain contracts *(R3.1 — extended per HH.2)*

R3.1 replaces R3's single `UnifiedInjectionManifestSchema` with a four-manifest chain. The R3 type name is retired as a transitional alias per HH.2.8.

#### 38.2.1 ContextAssemblyTraceSchema (preserved from R3)

```ts
type ContextAssemblyTrace = {
  trace_id: string;
  packet_id: string;
  scope: PacketAssemblyScope;
  stage_records: PacketAssemblyStageRecord[];
  directive_modifications: DirectiveModification[];    // R3.1 — per HH.6.9
  emitted_at: string;
  schema_version: 1;
};

type PacketAssemblyStageRecord = {
  stage_id: AssemblyStageId;
  stage_index: number;                                  // 1-N positional
  started_at: string;
  completed_at: string;
  duration_ms: number;
  records_produced: number;                              // candidate cards / lint findings / etc.
  schema_version: 1;
};
```

The trace persists in `packet_snapshot_store` as a derived cache (rebuildable from assembly inputs + revision hash per §7.2 table).

#### 38.2.2 CandidateInjectionManifest (pre-durable; per HH.2.1)

```ts
type CandidateInjectionManifest = {
  candidate_manifest_id: string;
  packet_id: string;
  operation_id: string;
  session_id?: string;

  slot_entries: InjectionSlotManifestEntry[];
  card_records: ManifestCardRecord[];
  bucket_entries?: BucketManifestEntry[];

  policy_generation_snapshot: PolicyGenerationSnapshot;

  candidate_manifest_hash: string;
  durability_state: "candidate_only";       // exact literal; CANNOT be persisted as PacketInjectionManifest

  built_at: string;
  schema_version: 1;
};
```

Rule: a `CandidateInjectionManifest` flows into the linter (§38.11). On lint pass, it materializes a `PacketInjectionManifest` (content-equivalent in cards/slots, plus durable identity and timestamps). On lint fail, it produces `PacketLintFailureRecord + BlockedPacketManifest` instead — no durable `PacketInjectionManifest` is written.

#### 38.2.3 PacketInjectionManifest (durable; per HH.2.3 + II.2)

R3 §38.2.2's `UnifiedInjectionManifestSchema` field set is restored per II.2.1/II.2.2 (V1.1 Y.6 had silently dropped them):

```ts
type PacketInjectionManifest = {
  manifest_kind: "doc24_packet_contribution";

  manifest_id: string;
  packet_id: string;
  operation_id: string;

  slot_entries: InjectionSlotManifestEntry[];
  card_records: ManifestCardRecord[];
  bucket_entries: BucketManifestEntry[];

  // R3 §38.2.2 fields restored per V1.4 II.2.1:
  policy_generation_id: string;
  bundle_generation_id: string;
  matrix_generation_id?: string;
  matrix_state?: "active" | "disabled" | "gathering_only" | "bundle_unavailable";   // II.2.3
  trace_ref: string;
  assembly_completed_at: string;            // R3 — RESTORED in V1.4
  written_at: string;                        // durable persistence timestamp
  superseded_by_packet_id?: string;          // R3 prose — RESTORED in V1.4

  total_token_count: number;
  total_card_count_included: number;
  total_card_count_excluded: number;

  degraded_state?: DegradedState;
  warnings: string[];

  pbe_lite_effect?: PBELitePacketEffect;     // R3.1 — per HH.7.3

  manifest_size_bytes_uncompressed: number;
  manifest_size_bytes_persisted: number;
  compression_applied: boolean;

  packet_tokenizer_ref: TokenizerRef;        // R3.1 — per HH.2.9

  schema_version: 1;
};
```

Distinction rule (II.2.1):

```
assembly_completed_at — when the assembly pipeline completed the packet payload
                       (lifecycle transition into manifest_written).
written_at            — when the manifest was durably persisted to storage.

The two MAY differ when manifest write is queued or batched. They MUST NOT be collapsed.
```

Supersession (II.2.2): on `aborted_for_retry → created`, the prior packet's manifest is updated with `superseded_by_packet_id = new_packet_id`; the prior packet's lifecycle state remains `aborted_for_retry` (terminal); the new packet proceeds independently with a fresh `policy_generation_snapshot`.

#### 38.2.4 FinalPromptInjectionManifest (final-prompt-owner-emitted; per HH.2.4)

```ts
type FinalPromptInjectionManifest = {
  manifest_kind: "final_prompt";
  manifest_id: string;
  operation_id: string;
  final_prompt_owner: "DOC11" | "OpenClaw" | "DOC24";

  contribution_manifest_refs: string[];      // → PacketInjectionManifest[]
  final_prompt_hash: string;
  spans: PromptInjectionSpan[];               // R3.1 — REQUIRED; covers every byte
  total_prompt_tokens: number;

  written_at: string;
  schema_version: 1;
};

type PromptInjectionSpan = {
  span_id: string;
  operation_id: string;
  final_prompt_manifest_id: string;

  slot_id: InjectionSlotId;
  owner_doc: InjectionOwnerDoc;

  byte_start: number;
  byte_end: number;
  token_count: number;
  content_hash: string;

  source_refs: SourceRef[];
  policy_generation_id: string;

  contribution_manifest_ref?: string;
  context_injection_event_ref?: string;
  schema_version: 1;
};
```

Per HH.2.4, the byte-coverage rule retires V1.1 Y.6's "minimum hash + slot summary" escape hatch:

```
Spec text (replaces V1.1 Y.6 escape hatch language):

A final-prompt owner MUST emit FinalPromptInjectionManifest with PromptInjectionSpan[]
covering every byte of the final prompt. Each span maps to a registered InjectionSlotId
(per §38.16). Runtime preflight (per §38.11) passes ONLY when every byte range belongs
to exactly one PromptInjectionSpan with a registered slot_id and policy_generation_id.

The hash + summary form is insufficient for dispatch. The cross-doc obligations
OBL-D11-NEW-FINAL-PROMPT-SPAN-01 and OBL-OPENCLAW-NEW-FINAL-PROMPT-SPAN-01 codify this.
```

#### 38.2.5 BlockedPacketManifest (durable; per HH.2.2)

```ts
type BlockedPacketManifest = {
  blocked_manifest_id: string;
  packet_id: string;
  operation_id: string;
  blocking_reason: ReasonCode;
  blocked_at_stage: AssemblyStageId | "lint_check" | "policy_revalidated";

  // V1.3 HH.2.2 — diagnostic preservation
  candidate_manifest_at_block?: {
    slot_entries: InjectionSlotManifestEntry[];
    card_records: ManifestCardRecord[];
    bucket_entries?: BucketManifestEntry[];
    candidate_manifest_hash: string;
  };
  lint_failures?: BlockingFailure[];

  written_at: string;
  schema_version: 1;
};

type CandidateManifestLintResult = {
  lint_result_id: string;
  packet_id: string;
  candidate_manifest_hash: string;
  outcome: "passed" | "failed";
  blocking_failures: BlockingFailure[];
  warnings: LintWarning[];
  manifest_ref?: string;                     // → PacketInjectionManifest when outcome === "passed"
  linted_at: string;
  schema_version: 1;
};

type PacketLintFailureRecord = {
  lint_failure_id: string;
  packet_id: string;
  candidate_manifest_hash: string;
  lint_result_ref: string;
  blocked_manifest_id: string;
  written_at: string;
  schema_version: 1;
};
```

Persistence rule (HH.2.2): every failed candidate produces a durable triple `(CandidateManifestLintResult, BlockedPacketManifest, PacketLintFailureRecord)`. No durable `PacketInjectionManifest` is written for failed packets.

#### 38.2.6 BDSM/DOC8 manifest read contract (3-way join per HH.2.7)

This is the critical correctness contract that prevents attribution from cards the LLM never saw:

```ts
type BDSMAttributionInput = {
  packet_manifest: PacketInjectionManifest;            // assembly truth
  final_prompt_manifest: FinalPromptInjectionManifest; // delivery truth
  reconciliation_events: PacketReconciliationEvent[];  // runtime outcomes
};

type CardAttributionDecision =
  | { kind: "credit_or_penalize"; signal: "positive" | "ignored" | "corrected" }
  | { kind: "dropped_by_final_prompt_owner"; reason: "budget_at_final_assembly" | "policy_block_at_final" }
  | { kind: "no_observable_outcome" };

function decideCardAttribution(input: {
  card_id: string;
  in_packet_manifest: boolean;
  in_final_prompt_manifest: boolean;
  reconciliation_outcome?: "used" | "ignored" | "corrected" | "unknown";
}): CardAttributionDecision {
  if (!input.in_packet_manifest) {
    throw new InvalidAttribution({ reason: "card_not_in_packet" });
  }

  if (!input.in_final_prompt_manifest) {
    return {
      kind: "dropped_by_final_prompt_owner",
      reason: "budget_at_final_assembly",
    };
  }

  if (input.reconciliation_outcome === "used")      return { kind: "credit_or_penalize", signal: "positive" };
  if (input.reconciliation_outcome === "ignored")   return { kind: "credit_or_penalize", signal: "ignored" };
  if (input.reconciliation_outcome === "corrected") return { kind: "credit_or_penalize", signal: "corrected" };

  return { kind: "no_observable_outcome" };
}
```

**Invariant**: NO utility signal flows from a card absent from `FinalPromptInjectionManifest`. The cross-doc obligations `OBL-BDSM-NEW-MANIFEST-JOIN-01` and `OBL-D8-NEW-MANIFEST-JOIN-01` codify this for BDSM V6.5+ and DOC8 respectively.

Acceptance test:

```
ADD_TEST bdsm_attribution_skips_cards_dropped_by_final_prompt_owner:
  Given packet_manifest has 12 cards
   And final_prompt_manifest has 10 cards (2 dropped during final assembly)
   And reconciliation_events show outcome for all 10 delivered cards
  When BDSM computes attribution
  Then signals emit for exactly the 10 delivered cards
   AND no signal emits for the 2 dropped cards
```

#### 38.2.7 ContextAuthorityHandoff (per HH.6.7; per V1.1 BB.2 anchor reassignment)

```ts
type ContextAuthorityHandoff = {
  handoff_id: string;
  operation_id: string;
  route_trace_id: string;
  selected_execution_path: "gateway_first_chat" | "structured_ec_action" | "background_job";

  context_authority_chain: ContextAuthorityChainEntry[];

  final_prompt_assembly: {
    owner_doc: "DOC11" | "OpenClaw" | "DOC24";
    final_prompt_manifest_ref?: string;
  };

  doc24_packet_id?: string;
  doc11_annotation_ref?: string;
  openclaw_runtime_context_ref?: string;

  scope_snapshot: ScopeSnapshot;            // V1.3 HH.6.6

  schema_version: 1;
};
```

The R3 location for `ContextAuthorityHandoff` is §38.2.7 per V1.1 BB.2 anchor reassignment (originally collided with `PacketReconciliationOverlay` at §38.2.3).

### 38.3 Race-safety: policy_generation_id snapshot and DOC11 handoff revalidation *(ADJ-RT-004, ADJ-RT-117; R3.1 — extended per HH.5.4, HH.5.5)*

R3.1 integrates HH.5.4 `PolicyDecisionEngine` as the snapshot/lock owner. R3's `policy_generation_id` race-safety is preserved; the lock mechanism is now explicitly specified.

#### 38.3.1 PolicyGenerationSnapshot

```ts
type PolicyGenerationSnapshot = {
  policy_generation_id: string;
  policy_state_hash: string;
  policy_state_ref: string;
  snapshot_id: string;                       // audit ID only — NOT a lock token (HH.5.4 rename)
  opened_at: string;
  schema_version: 1;
};
```

The `snapshot_id` (renamed from V1.2 `read_token`) is an audit ID; releasing it does not "release a read lock." The actual concurrency control is in `PolicyDecisionEngine`'s `ReadWriteLock`.

#### 38.3.2 PolicyDecisionEngine snapshot/apply pattern

```ts
class PolicyDecisionEngine {
  private readonly rwLock: ReadWriteLock;     // per HH.5.5 (async-mutex@0.5+)
  private policyState: ImmutablePolicyState;
  private currentGenerationId: string;

  async openSnapshot(): Promise<PolicyGenerationSnapshot> {
    return this.rwLock.read(async () => {
      const stateRef = this.snapshotStore.retain(this.policyState);
      return {
        policy_generation_id: this.currentGenerationId,
        policy_state_hash: stableHash("policy_state", this.policyState),
        policy_state_ref: stateRef,
        snapshot_id: ulid(),
        opened_at: ECClockService.now(),
        schema_version: 1,
      };
    });
  }

  async applyMutation(mutation: PolicyMutation): Promise<string> {
    return this.rwLock.write(async () => {
      const priorState = this.policyState;
      const nextState = applyMutationToImmutableState(priorState, mutation);
      const nextGenerationId = nextMonotonicPolicyGenerationId();
      const nextHash = stableHash("policy_state", nextState);

      // Persist FIRST; swap live memory only after durable commit succeeds
      await this.persistGenerationAtomically({
        policy_generation_id: nextGenerationId,
        policy_state_hash: nextHash,
        policy_state: nextState,
      });

      // Atomicity: swap live state only after durable commit
      this.policyState = nextState;
      this.currentGenerationId = nextGenerationId;

      return nextGenerationId;
    });
  }

  async evaluateAtSnapshot(
    input: PolicyEvaluationInput,
    snapshot: PolicyGenerationSnapshot
  ): Promise<PacketSourceEligibilityDecisionSchema> {
    const state = this.snapshotStore.get(snapshot.policy_state_ref);

    if (stableHash("policy_state", state) !== snapshot.policy_state_hash) {
      throw new PolicyTearingDetected({
        expected: snapshot.policy_state_hash,
        observed: stableHash("policy_state", state),
      });
    }

    return this.evaluateAgainstState(input, state, snapshot.policy_generation_id);
  }
}
```

Per HH.5.4 atomicity rule: durable persistence completes BEFORE the in-memory state swap. A crash mid-mutation leaves either the prior consistent state OR the new persisted state; never a torn write.

#### 38.3.3 Revalidation at policy_revalidated state

```
At lifecycle state policy_revalidated (per §38.1.2):
  1. Read current PolicyDecisionEngine.currentGenerationId.
  2. Compare against PacketInjectionManifest.policy_generation_id.
  3. If mismatch → throw PolicyTearingDetected → packet → aborted_for_retry.
  4. If match → packet proceeds to dispatched.
```

On `aborted_for_retry → created` (II.5.1): the new packet acquires a FRESH `policy_generation_snapshot`; the prior snapshot is discarded. The new packet's `policy_generation_id` may or may not equal the prior packet's (depending on whether policy mutated in flight); the snapshot reset is unconditional.

#### 38.3.4 ReadWriteLock contract (per HH.5.5)

```ts
interface ReadWriteLock {
  read<T>(fn: () => Promise<T>): Promise<T>;
  write<T>(fn: () => Promise<T>): Promise<T>;
}
```

Recommended implementation: `async-mutex@0.5+` with starvation-prevention via writer-priority signaling. Acceptance test: starvation-freedom under contention. Writers eventually acquire; readers don't get scheduled past a pending writer.

Acceptance tests:

```
policy_decision_engine_persist_before_swap
read_write_lock_starvation_free
policy_revalidated_detects_generation_drift
aborted_for_retry_acquires_fresh_snapshot
```

### 38.4 Multi-matter concurrent context: MatterContextSlot stack *(ADJ-RT-068; R3.1 — preserved + scope_snapshot)*

R3.1 preserves R3's matter context slot stack (§5.3). The `ScopeSnapshot` (HH.6.6) is captured at each push/pop and persisted with the stack mutation event.

```
Stack operations:
  push_matter_context(matter_id, ...):
    1. Acquire context_revision via expected_context_revision concurrency control.
    2. Compute fresh ScopeSnapshot for new top.
    3. If stack at max_depth: evict bottom (LRU; suspended_context_snapshot emitted for evictee).
    4. Push new MatterContextSlot.
    5. Emit working_context.mutated event.

  pop_matter_context():
    1. Acquire context_revision.
    2. Top slot becomes a SuspendedContextSnapshot (if not explicitly closed) with
       resumable_until = now + default_resumable_window.
    3. Pop.
    4. New top becomes active; recompute ScopeSnapshot.
    5. Emit working_context.mutated event.

  switch_active_context(target_slot_id):
    1. Acquire context_revision.
    2. Reorder stack so target_slot_id is top.
    3. Original top moves to suspension (per pop_matter_context behavior).
    4. Emit working_context.mutated event.
```

Concurrent mutations use the saga concurrency protocol (HH.9.1 → §38.18) for multi-step writers (e.g., OnboardingCommitSaga that touches multiple context slots).

### 38.5 Session boundary affordances *(ADJ-RT-069, ADJ-RT-070; R3.1 — preserved)*

R3.1 preserves R3's session boundary affordances. Session boundaries are:

```
Hard boundaries (auto-detected):
  - System sleep/wake.
  - Q Dashboard restart.
  - OpenClaw daemon restart.
  - Long idle (default 4h with no activity).

Soft boundaries (user-invoked):
  - Explicit "end session" UI action.
  - Matter switch to a context that triggers a soft boundary policy.
```

On hard boundary: active operations are checkpointed; in-flight packets transition through `aborted_for_retry → blocked` if not yet dispatched. Working context is preserved; on resume, the user sees the resumption affordance.

Per HH.4.9 `ProbeTriggerKind === "resumption_affordance"`: resumption probes are bounded to once per 24-hour per-context window.

### 38.6 Suspension and resume semantics *(ADJ-RT-029, ADJ-RT-076, ADJ-RT-100; R3.1 — extended per HH.9.1 saga concurrency for resume)*

R3.1 preserves R3's suspension/resume model and integrates HH.9.1 saga concurrency for multi-step resume operations.

#### 38.6.1 SuspendedContextSnapshot (per §5.3B)

```ts
type SuspendedContextSnapshot = {
  snapshot_id: string;
  suspended_at: string;
  suspended_operation_id: string;
  scope_snapshot: ScopeSnapshot;
  in_flight_artifacts: SuspendedArtifactRef[];
  resume_eligibility: ResumeEligibility;
  schema_version: 1;
};

type SuspendedArtifactRef = {
  artifact_kind: "candidate_manifest" | "extraction_buffer" | "saga_state" | "disambig_session" | "demonstration_segment";
  artifact_id: string;
  partial_completion_marker?: string;
};

type ResumeEligibility = {
  resumable_until: string;
  resume_requires_user_confirmation: boolean;
  resume_blocked_reason_codes?: ReasonCode[];
  schema_version: 1;
};
```

#### 38.6.2 SavedContextProfile (per §5.3B)

```ts
type SavedContextProfile = {
  profile_id: string;
  matter_context_slot: MatterContextSlot;
  pinned_at: string;
  schema_version: 1;
};
```

#### 38.6.3 Resume protocol (multi-step; uses saga concurrency)

```
Resume saga (per HH.9.1):
  1. acquired_context_revisions captured at saga start.
  2. Restore working_context_store fields from SuspendedContextSnapshot.
  3. Rehydrate in_flight_artifacts per kind:
     - candidate_manifest → re-enter pipeline at the captured lifecycle state.
     - extraction_buffer → resume PBE chunk processing.
     - saga_state → re-enter the saga at saved step.
     - disambig_session → restore current_candidates + recently_removed_candidates.
     - demonstration_segment → resume capture at saved segment boundary.
  4. On step concurrency_conflict → re-fetch revision, retry (budget 3).
  5. Beyond retry budget → resume blocks with reason "saga_concurrency_retry_budget_exhausted".

Per ADJ-RT-100: no new node kind is introduced for suspension/resume; existing primitives
(SavedContextProfile, SuspendedContextSnapshot, WorkContextConstellation, PriorContextCard) suffice.
```

API surface (preserved from R3):

```
POST   /api/runtime/saved-contexts          # Create profile
GET    /api/runtime/saved-contexts          # List
GET    /api/runtime/saved-contexts/{id}     # Read
DELETE /api/runtime/saved-contexts/{id}
POST   /api/runtime/saved-contexts/{id}/apply

POST   /api/runtime/saved-snapshots         # Create snapshot
GET    /api/runtime/saved-snapshots
GET    /api/runtime/saved-snapshots/{id}
DELETE /api/runtime/saved-snapshots/{id}
POST   /api/runtime/saved-snapshots/{id}/restore
```

UI: when the user invokes "save context," the UI offers both profile and snapshot options with explanation of the difference. Default selection is snapshot if any suspended contexts exist; profile otherwise.

Acceptance tests:

```
saved_context_distinguishes_profile_from_snapshot
suspended_restoration_state_explicit
resume_saga_refreshes_state_on_concurrency_conflict
resume_saga_retry_budget_exhausted_terminates_cleanly
```

### 38.7 Demonstration mode segments and interruption protocol *(ADJ-RT-030; R3.1 — preserved)*

R3.1 preserves R3's demonstration segment model (per §12.3A reference). Segments are classified at capture:

```
SegmentKind:
  - "setup"          # context preparation (matter switch, file open)
  - "core_actions"   # the meat of the workflow being demonstrated
  - "verification"   # confirmation steps (review, sign-off)
  - "teardown"       # cleanup (close artifacts, clear context)
  - "interruption"   # unrelated activity (Teams call, browser tab, email)
```

Interruption protocol: when an interruption is detected (window focus change, idle threshold, explicit user signal), the current segment is closed and a new segment opens with `SegmentKind === "interruption"`. Interruption segments are excluded from procedure capture; they exist for the capture record but do not become procedure node content.

Per V1 ADJ-RT-030: on resume from interruption, the user sees a re-entry confirmation ("Resume capturing from <last core_actions segment>?").

### 38.8 Disambiguation state machine *(ADJ-RT-016, ADJ-RT-018, ADJ-RT-120; R3.1 — extended per HH.14)*

R3.1 integrates HH.14.1 stable IDs and HH.14.2 recently_removed_candidates for user recovery.

#### 38.8.1 DisambiguationStateSchema

```ts
type DisambiguationStateSchema = {
  disambig_session_id: string;
  current_candidates: OfferedResolutionCandidate[];
  recently_removed_candidates: Array<{
    candidate: OfferedResolutionCandidate;
    removal_reason: ReasonCode;
    removed_at: string;
    recoverable_until: string;
  }>;
  schema_version: 1;
};

type OfferedResolutionCandidate = {
  candidate_id: string;
  candidate_stable_key: string;            // material-derived; reproducible
  display_label: string;
  reason_for_candidacy: ReasonCode[];
  schema_version: 1;
};

function computeCandidateStableKey(material: {
  node_ref: EntityRef;
  candidacy_method: "name_match" | "alias_match" | "context_inference" | "user_history";
  disambig_session_id: string;
}): string {
  return sha256(canonicalJSON(material)).slice(0, 16);
}
```

User selections reference `candidate_stable_key`, not display position. This prevents the V1.x-identified hazard where filtered candidates silently renumber.

#### 38.8.2 State transitions

```
Disambig session state machine:
  open       → awaiting_user_choice
  awaiting_user_choice → choice_made                       (user picks candidate)
  awaiting_user_choice → recovery_requested                (user clicks "show removed")
  recovery_requested → awaiting_user_choice                (candidates restored)
  awaiting_user_choice → cancelled                         (user cancels)
  choice_made → closed                                     (terminal)
  cancelled → closed                                       (terminal)
```

#### 38.8.3 Recovery semantics (per HH.14.2)

When a candidate is removed (filtered, archived, scoped out), it enters `recently_removed_candidates` with `recoverable_until = now + 1h` (default). User invokes recovery via UI; candidates restored before `recoverable_until` move back to `current_candidates`. After expiry, recovery requires full re-resolution.

Acceptance tests:

```
disambig_session_stable_keys
disambig_recently_removed_recovery
disambig_recoverable_until_expiry_purges_recovery_buffer
```

### 38.9 Attribution-safe feedback semantics *(ADJ-RT-021, ADJ-RT-054, ADJ-RT-118, ADJ-RT-124; R3.1 — rewritten per HH.3 event model)*

R3.1 replaces V1.2 Y.22's `PacketReconciliationOverlay` (singular per packet; mutable feel) with HH.3's `PacketReconciliationEvent` (append-only) plus `PacketReconciliationCurrentView` (derived projection).

#### 38.9.1 PacketReconciliationEvent

```ts
type PacketReconciliationEvent = {
  reconciliation_event_id: string;
  packet_id: string;
  manifest_id: string;                        // → PacketInjectionManifest
  card_id: string;

  reconciliation_state:
    | "not_observable"
    | "injected_and_used"
    | "injected_and_ignored"
    | "injected_and_corrected"
    | "injected_and_blocked";

  outcome_kind: "used" | "ignored" | "corrected" | "unknown";

  proxy_method?:                              // REQUIRED iff outcome_kind ∈ {used, ignored, corrected}
    | "exposure_evidence"
    | "source_citation_match"
    | "claim_paraphrase_match"
    | "substring_coverage";

  prompt_span_refs: string[];                 // → PromptInjectionSpan[]
  evidence_refs: EvidenceRef[];                // V1 ADJ-004 RESTORED
  attribution_confidence: number;              // V1 ADJ-004 RESTORED

  supersedes_event_id?: string;                // append-only supersession

  written_at: string;
  schema_version: 1;
};
```

#### 38.9.2 PacketReconciliationCurrentView

```ts
type PacketReconciliationCurrentView = {
  packet_id: string;
  manifest_id: string;
  per_card_current: Array<{
    card_id: string;
    effective_reconciliation_state: PacketReconciliationEvent["reconciliation_state"];
    effective_outcome_kind: PacketReconciliationEvent["outcome_kind"];
    latest_event_id: string;
    event_history_refs: string[];
  }>;
  rebuilt_at: string;
  schema_version: 1;
};
```

The current view is rebuilt from events; events are the source of truth (per HH.3 model).

#### 38.9.3 Immutable manifest invariant (per HH.3.2)

```
The immutable PacketInjectionManifest MAY record that a card was rendered and exposed
(via card_presence and rendered_token_count). It MUST NOT store:
  - outcome_kind
  - proxy_method
  - post-response usage classification
  - reconciliation_state

These live ONLY in PacketReconciliationEvent and derived views.
```

R3 prose that suggested otherwise is read in R3.1 as a draft error; the canonical model is event + view. The manifest is immutable post-write per HH.3.2.

#### 38.9.4 Reconciliation rules

```
proxy_method is REQUIRED when outcome_kind ∈ {used, ignored, corrected}.
proxy_method is OPTIONAL when outcome_kind === "unknown".
evidence_refs MAY be empty only when reconciliation_state === "not_observable".

To revise a prior reconciliation, write a new event with supersedes_event_id pointing
to the prior. The current view tracks supersession chains.
```

Acceptance tests:

```
manifest_does_not_store_post_response_outcome_classification
reconciliation_event_supersession_chain_resolves_to_latest
reconciliation_event_evidence_refs_present_when_observable
```

### 38.10 UI / read-model obligations *(ADJ-RT-088, ADJ-RT-102, ADJ-RT-114; R3.1 — preserved + Packet Inspector extensions)*

R3.1 preserves R3's UI/read-model obligations and extends the Packet Inspector contract per §20.3.

The Packet Inspector renders:
- `PacketInjectionManifest` with card-by-card breakdown.
- `ContextAssemblyTrace` with stage-by-stage timing.
- `FinalPromptInjectionManifest.spans[]` (when DOC11/OpenClaw emitted).
- `PacketReconciliationCurrentView` after runtime completion.
- `BlockedPacketManifest.candidate_manifest_at_block` when blocked (HH.2.2 diagnostic).
- `PBELitePacketEffect` banner when present (HH.7.3).
- `PacketDecisionGraph` via `walkPacketDecisionGraph` (II.4.2).
- `PacketOpportunitySet` sampled to `k_cap: 50` (HH.15.4).

Per HH.7.4 cross-doc obligation `OBL-DOC21-NEW-PBE-LITE-BANNER-01`: the Knowledge Manager header surfaces `PBELitePacketEffect.banner_kind` for any packets with PBE-lite effects.

#### 38.10.1 Decision graph walk *(R3.1 — per II.4.2)*

The Packet Inspector's "Why was card X included?" / "Why was card Y excluded?" answers are produced by walking the decision graph from the manifest's `card_records` back through the `directive_modifications`, policy decisions, matrix adjustments, and reconciliation events recorded during assembly.

```ts
type PacketDecisionGraphVisitor = {
  visit(
    node: PacketDecisionGraphNode,
    currentView: PacketReconciliationCurrentView | undefined
  ): Promise<void>;
};

type PacketDecisionGraphTraversalResult = {
  nodes_visited: number;
};

async function walkPacketDecisionGraph(input: {
  packet_id: string;
  decision_graph_input: PacketDecisionGraphInput;     // V1.3 HH.6.10 typed
  visitor: PacketDecisionGraphVisitor;
}): Promise<PacketDecisionGraphTraversalResult> {
  // ADJ-073 + II.4.2 — async-correct; awaits event-stream loads
  const events = await ReconciliationEventStore.getByPacketId(input.packet_id);
  const currentView = buildCurrentView(events);       // HH.3.1 derived projection

  for (const node of input.decision_graph_input.nodes) {
    await input.visitor.visit(node, currentView);
  }

  return { nodes_visited: input.decision_graph_input.nodes.length };
}
```

The walk is read-only over append-only event streams; concurrent inspector sessions never conflict. The visitor pattern lets the Inspector UI render incrementally as the walk emits nodes, rather than blocking until traversal completes.

### 38.11 Pre-dispatch packet linter *(ADJ-RT-121; R3.1 — operates on CandidateInjectionManifest per HH.2.1; failure produces durable triple per HH.2.2)*

R3.1 corrects R3's implicit linter input (`UnifiedInjectionManifestSchema` either pre-durable or durable — V1.1 X.4 ambiguity). The linter operates on `CandidateInjectionManifest` (pre-durable; explicit type per HH.2.1).

#### 38.11.1 Linter input

```ts
type PacketPreDispatchLintInput = {
  packet_id: string;
  candidate_manifest: CandidateInjectionManifest;
  schema_version: 1;
};
```

#### 38.11.2 Linter rules

```
The linter checks (canonical list; tracked in LintClosureTable):
  - Tokenizer consistency (HH.2.9): packet_tokenizer_ref matches every card tokenizer_ref.
  - InjectionSlotId registration (§38.16): every slot_id resolves in InjectionSlotRegistry.
  - Card presence validity: position_in_packet present iff card_presence.kind === "included_inline".
  - Hard-required count cap: user-originated count ≤ MAX_USER_ORIGINATED_HARD_REQUIRED_CARDS_PER_PACKET (HH.4.4).
  - Budget consistency: BudgetCell.used_tokens ≤ BudgetCell.allocated_tokens.
  - Authority rendering consistency: AuthorityRenderDecision kind matches card_presence.kind.
  - DirectiveConstraint feasibility: per-card constraints feasible (no policy-floor > policy-ceiling).
  - Provenance present: every ManifestCardEntry.provenance.primary_source is well-formed.
  - Schema version consistency: every schema_version === 1 for DOC24-owned types (HH.11.1).
  - Reason code namespace registry resolution: every emitted reason code resolves (HH.5.3).
```

#### 38.11.3 Linter lifecycle and outcomes

```
Stage 10 (lint_check):
  Input: CandidateInjectionManifest (in-memory; not durable).
  Output: CandidateManifestLintResult.

  outcome === "passed":
    1. Write CandidateManifestLintResult (outcome: "passed") to candidate_manifest_lint_result_store.
    2. Lifecycle: lint_check → lint_passed.
    3. Stage 11 (manifest_written): materialize PacketInjectionManifest from candidate.
       The PacketInjectionManifest is content-equivalent to the candidate plus durable identity,
       written_at timestamp, and reference back to the lint result.
    4. CandidateManifestLintResult.manifest_ref ← PacketInjectionManifest.manifest_id.

  outcome === "failed":
    1. Write CandidateManifestLintResult (outcome: "failed", with blocking_failures).
    2. Lifecycle: lint_check → lint_failed.
    3. Persist BlockedPacketManifest with candidate_manifest_at_block preserved (HH.2.2).
    4. Write PacketLintFailureRecord linking lint_result → blocked_manifest.
    5. DO NOT write PacketInjectionManifest.
    6. Lifecycle: lint_failed → blocked (terminal).
```

#### 38.11.4 Runtime preflight (final-prompt-side)

The runtime preflight runs at dispatch when DOC11/OpenClaw owns final-prompt assembly. It validates that every byte of the final prompt belongs to a registered `PromptInjectionSpan`:

```ts
type PromptPayloadPreflightResult = {
  operation_id: string;
  final_prompt_owner: "DOC11" | "OpenClaw" | "DOC24";
  total_prompt_bytes: number;
  total_covered_bytes: number;
  total_unregistered_bytes: number;          // MUST be 0 to pass
  unregistered_blocks: Array<{
    block_index: number;
    byte_range_hash: string;
    reason: "missing_slot_id" | "missing_context_injection_event" | "byte_range_uncovered";
  }>;
  outcome: "passed" | "blocked";
  schema_version: 1;
};
```

`total_unregistered_bytes === 0` is the positive-coverage gate (HH.2.4). The cross-doc obligations `OBL-D11-NEW-FINAL-PROMPT-SPAN-01` and `OBL-OPENCLAW-NEW-FINAL-PROMPT-SPAN-01` require the final-prompt owner to emit `FinalPromptInjectionManifest.spans[]` covering every byte.

#### 38.11.5 Token-count write-time rule and tokenizer drift detection *(HH.15.8)*

Per HH.15.8, the relationship between DOC24-measured token counts and final-prompt-owner-measured token counts is governed by:

```
RULE (HH.15.8):
  Token counts on ManifestCardEntry.rendered_token_count are written at
  manifest_written lifecycle state, measured against
  PacketInjectionManifest.packet_tokenizer_ref.

  If the final-prompt owner re-tokenizes during final-prompt assembly with a
  different tokenizer, the FinalPromptInjectionManifest.spans[].token_count
  may differ from the PacketInjectionManifest values. This is observed
  (reconciliation handles via diff), not invalid.

  If the differences exceed acceptable_drift_pct (default 5%), the final-prompt
  owner emits a "tokenizer_drift" reason code on PromptPayloadPreflightResult
  for manual investigation.
```

```ts
type TokenizerDriftCheckPolicy = {
  acceptable_drift_pct: number;          // default 0.05 (5%)
  drift_reason_code: "tokenizer_drift";
  emit_threshold_kind: "per_card" | "per_packet_aggregate";
  schema_version: 1;
};

type TokenizerDriftObservation = {
  observation_id: string;
  packet_id: string;
  doc24_packet_tokenizer_ref: TokenizerRef;
  final_prompt_tokenizer_ref: TokenizerRef;
  per_card_drift_pct: number[];          // |delta| / doc24_count for each card
  aggregate_drift_pct: number;
  exceeded_threshold: boolean;
  schema_version: 1;
};
```

Acceptance tests:

```
manifest_token_count_written_at_manifest_written_stage
tokenizer_drift_observation_emits_when_exceeded
tokenizer_drift_within_threshold_does_not_emit
```

Acceptance tests:

```
lint_runs_on_candidate_before_manifest_persisted
manifest_persists_lint_failure_triple
preflight_blocks_on_unregistered_byte_range
preflight_passes_when_every_byte_covered
```

### 38.12 Policy / budget / utility ordering invariant *(ADJ-RT-117, ADJ-RT-123; R3.1 — corrected per HH.1 ordering)*

R3.1 codifies the ordering invariant per HH.1.3 transition table. The assembly pipeline runs in exactly this order:

```
1. created
2. candidates_gathered                  # lane retrieval complete
3. lifecycle_filtered                   # archived/retracted/merged removed (R3 restored)
4. policy_evaluated                     # EC PolicyDecisionEngine snapshot evaluation
5. confidence_gated                     # below-threshold dropped or hedged (R3 restored)
6. structurally_relevant                # lane assignment + relevance scoring (R3 restored)
7. matrix_boosted                       # BDSM utility influence applied
8. directives_assigned                  # DeliveryDirective + DirectiveConstraint per card
9. rendering_tier_allocated             # KDA RenderingTier per card (R3 restored)
10. overflow_resolved                   # §27.0A overflow protocol applied (user-cap per HH.4.4)
11. lint_check                          # operates on candidate (in-memory; HH.2.1)
12. lint_passed | lint_failed           # outcome determines next step
13. manifest_written                    # candidate → durable PacketInjectionManifest (iff lint_passed)
14. policy_revalidated                  # final policy_generation_id check (HH.5.4)
15. dispatched | aborted_for_retry      # race detected → restart with fresh snapshot
16. completed                           # runtime delivered; reconciliation pending
17. reconciled                          # reconciliation closed (R3 restored)
```

Out-of-order execution is forbidden. The `assertAllowedPacketTransition` function (§38.1.3) enforces this at runtime.

### 38.13 Acceptance tests for §38 *(R3.1 — extended)*

The §38 acceptance test inventory in R3.1 (canonical list — generated mechanically into `acceptance_test_registry` per HH.10.2):

```
Lifecycle:
  packet_lifecycle_aborted_for_retry_path_exists
  packet_lifecycle_reconciliation_closure
  packet_lifecycle_rejects_illegal_transitions
  packet_lifecycle_aborted_for_retry_acquires_fresh_snapshot

Manifest contracts:
  candidate_manifest_distinct_durability_state
  packet_manifest_restores_r3_assembly_completed_at
  packet_manifest_preserves_r3_assembly_completed_at
  packet_manifest_supersession_link_set_on_reassembly
  packet_manifest_matrix_state_required_when_id_absent
  final_prompt_manifest_spans_cover_every_byte
  bdsm_attribution_skips_cards_dropped_by_final_prompt_owner
  manifest_card_entry_restores_r3_kda_variant_fields
  manifest_card_entry_position_in_packet_required_for_inline
  manifest_card_entry_position_in_packet_optional_for_reference_only
  unified_manifest_complete_for_every_packet
  render_variant_attribution_distinguishable

Race-safety:
  policy_decision_engine_persist_before_swap
  read_write_lock_starvation_free
  policy_revalidated_detects_generation_drift
  policy_tearing_blocks_dispatch

Reconciliation:
  manifest_does_not_store_post_response_outcome_classification
  reconciliation_event_supersession_chain_resolves_to_latest
  reconciliation_event_evidence_refs_present_when_observable

Linter and preflight:
  lint_runs_on_candidate_before_manifest_persisted
  manifest_persists_lint_failure_triple
  preflight_blocks_on_unregistered_byte_range
  preflight_passes_when_every_byte_covered

Suspension and resume:
  saved_context_distinguishes_profile_from_snapshot
  suspended_restoration_state_explicit
  resume_saga_refreshes_state_on_concurrency_conflict
  resume_saga_retry_budget_exhausted_terminates_cleanly

Disambiguation:
  disambig_session_stable_keys
  disambig_recently_removed_recovery
  disambig_recoverable_until_expiry_purges_recovery_buffer

Authority rendering:
  authority_computed_renders_inline_not_diagnostic
  authority_blocked_input_unavailable_excludes_from_packet
  authority_blocked_cycle_renders_diagnostic_reference

Hard-required overflow:
  hard_required_budget_overflow_blocks_dispatch
  user_originated_hard_required_count_exceeded_blocks
  policy_required_count_not_capped

Within-band sort:
  within_band_allocator_preserves_high_value_large_card
  within_band_allocator_avoids_large_low_value_displacement
  within_band_allocator_does_not_mutate_candidate_order

Tool selection (Thompson):
  beta_decay_preserves_success_ratio
  qualifies_for_high_stakes_requires_lower_bound
  tool_selection_deterministic_with_seed
  tool_outcome_event_threads_selection_decision_id

Attribution thread:
  tool_outcome_event_threads_selection_decision_id

Async correctness:
  tool_invocation_function_is_async
  decision_graph_walk_is_async

Channel enum:
  adj_131_uses_automation_channel
```

### 38.14 Cross-doc obligations summary *(R3.1 — extended per HH.8.1)*

Cross-doc obligations referenced from §38 (canonical inventory in OP-A V3.7+; HH.8.1 inventory):

```
BDSM V6.5+:
  OBL-BDSM-NEW-MANIFEST-JOIN-01           (3-way join per HH.2.7)
  OBL-BDSM-NEW-EMPTY-CONTEXT-CRASH-01     (Shapley empty-context guard per HH.0.5)
  OBL-BDSM-NEW-RELEVANCE-NORMALIZATION-01 (normalized_relevance ∈ [0,1] at boundary)
  OBL-BDSM-NEW-RECONCILIATION-EVENT-01    (consume events not overlay per HH.3)
  OBL-BDSM-NEW-MANIFEST-RENAME-01         (PacketInjectionManifest naming per HH.2.8)
  OBL-BDSM-NEW-FORCE-LEVEL-CONSTRAINT-01  (DirectiveConstraint emission per HH.6.8)
  OBL-BDSM-NEW-DEADLINE-BOOST-01          (existing; per §37.1)

DOC8:
  OBL-D8-NEW-MANIFEST-JOIN-01             (3-way join per HH.2.7)

KDA R3+:
  OBL-KDA-NEW-MANIFEST-RENAME-01          (PacketInjectionManifest naming per HH.2.8)
  OBL-KDA-NEW-VARIANT-TRACKING-FIELDS-RESTORED-01  (per V1.4 II.1)

DOC72 R6+:
  OBL-D72-NEW-NODEKIND-EXPORT-01          (NodeKind enum export per HH.6.2)

DOC11:
  OBL-D11-NEW-FINAL-PROMPT-SPAN-01        (byte-coverage emission per HH.2.4)

OpenClaw:
  OBL-OPENCLAW-NEW-FINAL-PROMPT-SPAN-01   (byte-coverage emission per HH.2.4)

DOC21 R3+:
  OBL-DOC21-NEW-PBE-LITE-BANNER-01        (banner surfacing per HH.7.4)

DOC25:
  OBL-D25-NEW-MAX-TOKENS-PARAM-01         (UPDATED: target_model_family + tokenizer_ref per HH.2.9, HH.8.3)
  OBL-D25-NEW-PREFLIGHT-LEASE-01          (existing)
  OBL-D25-NEW-V15-01                      (UPDATED: tokenizer_ref consumer note per HH.2.9)

DOC15:
  OBL-D15-NEW-01                          (budget governance recognizes DroppabilityClass; per §27.0A)
```

### 38.15 Open questions and §38 future work *(R3.1 — narrowed)*

V1.x closed most R3 §38 open questions. Remaining for R3.x:

```
Q1. PBE-lite promotion-pending UX — banner kind needs DOC21 R3+ design refinement.
Q2. Adaptive lane drift convergence dynamics — long-term observability question.
Q3. AcceptanceTestRegistry coverage holes — to be discovered during R3.1 build generation.
Q4. Final-prompt owner negotiation when both DOC24 and OpenClaw are eligible — out of R3.1 scope; deferred to R3.x.
```

### 38.16 InjectionSlotRegistry contract *(R3.1 — new per HH.6.5)*

R3.1 introduces the `InjectionSlotRegistry` as a build-time companion artifact (§7.2A).

#### 38.16.1 Slot definition

```ts
type InjectionOwnerDoc =
  | "DOC7" | "DOC10" | "DOC11" | "DOC12" | "DOC15"
  | "DOC20" | "DOC24" | "DOC73" | "OpenClaw";

type InjectionSlotId = string;            // format: "{owner_doc}.{slot_name}"

type InjectionSlotRegistryEntry = {
  slot_id: InjectionSlotId;
  owner_doc: InjectionOwnerDoc;
  slot_name: string;                       // unique within owner_doc
  slot_kind:
    | "system_prompt_segment"
    | "knowledge_card_block"
    | "bucket_content_block"
    | "tool_capability_block"
    | "authority_constellation_block"
    | "ec_directive_block"
    | "router_annotation_block"
    | "session_continuity_block";
  expected_rendering_constraints: SurfaceRenderingConstraint[];
  is_required_for_packet_kind: PacketKindRequirement[];
  schema_version: 1;
};

type PacketKindRequirement = {
  packet_kind: "assistant_turn" | "structured_ec_action" | "background_extraction";
  required: boolean;
  rationale: string;
};

type InjectionSlotRegistry = {
  generated_at: string;
  entries: InjectionSlotRegistryEntry[];
  schema_version: 1;
};
```

#### 38.16.2 Slot registration discipline

CI gates:
- Every `InjectionSlotManifestEntry.slot_id` MUST appear in the registry.
- Every `PromptInjectionSpan.slot_id` MUST appear in the registry.
- Build fails if any slot referenced in code is unregistered.

The registry is generated mechanically from source-code scan + an authoritative registry constant in DOC24 build pipeline (§23.1). Each owner_doc maintains its slot definitions; DOC24 build aggregates.

### 38.17 PBELitePacketEffect machinery *(R3.1 — new per HH.7.3)*

When PBE-lite produces side effects on a packet, `PBELitePacketEffect` records them on the manifest:

```ts
type PBELitePacketEffect = {
  effect_id: string;
  packet_id: string;
  affected_card_ids: string[];

  banner_kind:
    | "pbe_extraction_deferred"
    | "pbe_lite_degraded_contract"
    | "pbe_reuse_cached"
    | "pbe_lite_promotion_pending";
  banner_user_message_template_id: string;

  deferred_extraction_event_refs: string[];
  replay_eligible_after: string;
  replay_attempt_count: number;
  replay_cap: number;

  emitted_at: string;
  schema_version: 1;
};
```

Effect lifecycle:

```
1. Packet assembly produces PBELitePacketEffect when:
   - Hard-budget squeeze caused PBE extraction to defer.
   - PBE-lite mode produced a degraded contract (cached card).
   - Cached PBE card was reused without re-extraction.
   - PBE-lite is promotion-pending awaiting full extraction window.

2. Effect is persisted on the manifest at manifest_written state
   (PacketInjectionManifest.pbe_lite_effect).

3. Replay scheduler reads deferred_extraction_event_refs and re-attempts
   when replay_eligible_after passes (background indexing priority 3 per §9.4).

4. After replay succeeds or replay_attempt_count reaches replay_cap, effect is terminal.

5. The Knowledge Manager header (DOC21 R3+) surfaces the banner via
   OBL-DOC21-NEW-PBE-LITE-BANNER-01.
```

### 38.18 Saga concurrency contract *(R3.1 — new per HH.9)*

The saga concurrency contract governs all multi-step writers (OnboardingCommitSaga, resume sagas, multi-context mutations).

#### 38.18.1 SagaState

```ts
type SagaState = {
  saga_id: string;
  saga_kind: string;
  started_at: string;
  steps_completed: SagaStepResult[];
  steps_pending: SagaStep[];
  acquired_context_revisions: Record<string, string>;
  state: "running" | "blocked" | "completed" | "manual_review_required";
  schema_version: 1;
};
```

#### 38.18.2 executeStep with retry refresh (per HH.9.1)

```ts
async function executeStep(
  saga: SagaState,
  step: SagaStep,
  retry_attempt: number = 0
): Promise<SagaStepResult> {
  const RETRY_BUDGET = 3;

  if (step.touches_working_context_id) {
    let expected = saga.acquired_context_revisions[step.touches_working_context_id];

    if (!expected) {
      // R3.1 HH.9.2 — late-bound context: read current revision and capture
      const current = await WorkingContextStore.getByContextId(step.touches_working_context_id);
      expected = current.context_revision;
      saga.acquired_context_revisions[step.touches_working_context_id] = expected;
    }

    try {
      const result = await step.execute({ expected_context_revision: expected });
      return result;
    } catch (err) {
      if (err.reason_code === "concurrency_conflict" && retry_attempt < RETRY_BUDGET) {
        // R3.1 HH.9.1 — re-fetch current revision before retry (NOT stale expected)
        const refreshed = await WorkingContextStore.getByContextId(
          step.touches_working_context_id
        );
        saga.acquired_context_revisions[step.touches_working_context_id] =
          refreshed.context_revision;

        const refreshed_step = await rebindSagaStep(saga, step, refreshed);

        return executeStep(saga, refreshed_step, retry_attempt + 1);
      }
      if (err.reason_code === "concurrency_conflict") {
        return {
          result: "blocked",
          reason: "saga_concurrency_retry_budget_exhausted",
          saga_id: saga.saga_id,
        };
      }
      throw err;
    }
  }

  return await step.execute();
}
```

#### 38.18.3 Monotonic ULID factory (per HH.9.3)

```ts
import { monotonicFactory } from "ulid";

const ulid = monotonicFactory();          // pinned globally per HH.9.3

class ECClockService {
  private static last_emitted_ms = 0;

  static now(): string {
    const wall = Date.now();
    const safe = Math.max(wall, ECClockService.last_emitted_ms + 1);
    ECClockService.last_emitted_ms = safe;
    return new Date(safe).toISOString();
  }
}
```

All DOC24 ULID emission MUST use the monotonic factory; all timestamp emission MUST use `ECClockService.now()`. Backward clock movement produces synthetic timestamps that preserve ordering.

Acceptance tests:

```
saga_concurrency_retry_refreshes_state
saga_concurrency_retry_budget_exhausted
late_bound_context_captured_on_first_touch
monotonic_ulid_factory_pinned_globally
ec_clock_service_handles_backward_clock_movement
```

### 38.19 Long-chunk extraction prompt order *(R3.1 — new per HH.12)*

PBE extraction (§8.2 + §9.3 stage 2) renders an extraction prompt composed of nine canonical sections. The section order matters: for short source chunks, `DEFAULT` order places extraction instructions before source so the LLM scans source with intent in mind. For long source chunks that approach the model's effective attention window, `LONG_CHUNK` order moves instructions earlier to ensure they remain in attention when source rendering completes.

```ts
type ExtractionPromptSectionId =
  | "A_corpus_context"
  | "B_authority_anchor"
  | "C_user_context_snapshot"
  | "D_existing_knowledge_summary"
  | "E_source_excerpt"
  | "F_extraction_instructions"
  | "G_response_format_template"
  | "H_safety_constraints"
  | "I_metadata_envelope";

type ExtractionPromptSectionOrder = {
  order_kind: "DEFAULT" | "LONG_CHUNK";
  sections: ExtractionPromptSectionId[];
  rationale: string;
  schema_version: 1;
};

const DEFAULT_EXTRACTION_PROMPT_ORDER: ExtractionPromptSectionOrder = {
  order_kind: "DEFAULT",
  sections: [
    "A_corpus_context",
    "B_authority_anchor",
    "C_user_context_snapshot",
    "D_existing_knowledge_summary",
    "F_extraction_instructions",
    "G_response_format_template",
    "E_source_excerpt",
    "H_safety_constraints",
    "I_metadata_envelope",
  ],
  rationale: "Standard order: instructions before source so LLM scans source with intent in mind.",
  schema_version: 1,
};

const LONG_CHUNK_EXTRACTION_PROMPT_ORDER: ExtractionPromptSectionOrder = {
  order_kind: "LONG_CHUNK",
  sections: [
    "A_corpus_context",
    "B_authority_anchor",
    "F_extraction_instructions",
    "G_response_format_template",
    "C_user_context_snapshot",
    "D_existing_knowledge_summary",
    "E_source_excerpt",
    "H_safety_constraints",
    "I_metadata_envelope",
  ],
  rationale: "Long-chunk source can exceed attention window for instructions; place instructions early; safety still at end.",
  schema_version: 1,
};

type LongChunkMitigation = {
  trigger_chunk_token_count: number;
  switch_to_order: "LONG_CHUNK";
  add_extraction_checkpoint_intervals_tokens: number;
  hash_extracted_facts_for_dedup: true;
  schema_version: 1;
};
```

Denominator guard (HH.12 evaluable-observation rule):

```
RULE: When extracting from long chunks, confidence aggregation uses
evaluable observations only. Chunks producing no extractable signal
do not count toward the denominator; otherwise confidence converges
to zero for long chunks of irrelevant text.
```

Acceptance test:

```
long_chunk_order_switches_when_trigger_exceeded
long_chunk_denominator_excludes_unevaluable_chunks
```

---

# Appendices

## Appendix A — Profiles

Saved context profiles are durable user-pinned matter contexts. They are restorable at any time.

API surface:

```
POST   /api/runtime/saved-contexts          # Create profile
GET    /api/runtime/saved-contexts          # List
GET    /api/runtime/saved-contexts/{id}     # Read
DELETE /api/runtime/saved-contexts/{id}
POST   /api/runtime/saved-contexts/{id}/apply
```

Storage: `saved_context_profile_store` (current view) + `ELNOR_MEMORY/config/saved_contexts/*.json` (file-backed canonical per ADJ-42).

Per HH.6.6 `ScopeSnapshot`: each profile carries a captured scope snapshot for restore.

## Appendix B — Snapshots

Suspended context snapshots are transient, ephemeral, in-flight captures with a `resumable_until` expiry.

API surface:

```
POST   /api/runtime/saved-snapshots         # Create snapshot (typically auto-created on suspension)
GET    /api/runtime/saved-snapshots
GET    /api/runtime/saved-snapshots/{id}
DELETE /api/runtime/saved-snapshots/{id}
POST   /api/runtime/saved-snapshots/{id}/restore
```

Storage: events in `working_context_events.jsonl`; current view derived. Restore uses the resume saga protocol (§38.6.3 + §38.18).

Per HH.9.1: resume restoration uses saga concurrency for multi-step rehydration; on `concurrency_conflict` during restore, the saga re-fetches and retries (budget 3); beyond budget → `saga_concurrency_retry_budget_exhausted`.

## Appendix C — Companion artifact references

R3.1 references the following build-time companion artifacts (generated under `~/.elnor/registries/`):

| File | Generator | Spec section |
|---|---|---|
| `reason_code_namespace_registry.json` | source-code scan + constant | §5.4.1.I, HH.5.3 |
| `injection_slot_registry.json` | per-owner-doc slot scan + constant | §38.16, HH.6.5 |
| `acceptance_test_registry.json` | test name mechanical aggregation | §24.2, HH.10.2 |
| `reviewer_finding_index.json` | mechanical grep over review file | HH.0.3 |
| `lint_closure_table.json` | DD.1 + V1.3 HH.0.2 + V1.4 II.4 | HH.0.2 |
| `degraded_reason_code_registry.json` | HH.15.2 constant | HH.15.2 |
| `doc24_imported_types.json` | §5.4.1.Y constant | HH.11.2 |
| `action_registry.json` | procedure/tool_capability metadata | §14.4 |
| `opa_cross_check_result.json` | mechanical grep over OP-A | HH.8.2 |
| `phantom_audit_v1_3.log` | bash script HH.13.3 | HH.13.3 |

R3.1 drafting gate (II.6.2 — 10 checks): all artifacts above MUST exist and pass validation before R3.1 publication.

## Appendix D — Lineage and reviewer closure

R3.1 lineage:
- R3 base → V1 (136 cards) → V1.1 (X-FF normalization) → V1.2 (GG citation hygiene) → V1.3 (HH preflight closure) → V1.4 (II missed restorations) → R3.1.

R3.1 reviewer closure (post-publication task per V1.4 II.6.2 #10):
- ChatGPT, Claude, Gemini, Grok each produce a closure verification record against R3.1.
- Reviewer task: verify every CRITICAL finding from V1.x has a corresponding R3.1 section that addresses it; flag any unaddressed as `OPEN_BLOCKS_R3_2`.
- All four reviewer records zero-open → R3.1 is the canonical reference; R3.2 follow-ups (if any) start V2.x adjudication cycle.

---

*End of DOC24 R3.1.*

*This document supersedes DOC24 R3 as the canonical reference for ELNOR's knowledge, capability, onboarding, routing, invocation, and delivery architecture.*

*For per-finding adjudication rationale, see V1 + V1.1 + V1.2 + V1.3 + V1.4 (the adjudication card chain).*

*For build-time companion artifacts, see Appendix C inventory.*

*For cross-doc obligations, see OP-A V3.7+ (canonical cross-doc obligation tracker).*