ELNOR REPO READER TEXT MIRROR Original path: Memory Rebuild Docs/Stage_6_Charters/E1_E2_DOC81_Scope_Policy/Reviews/RT-0001_CHATGPT_CHATGPT_CONSOLIDATED_REVIEW.md Source repo: /Users/OpenClaw1/Elnor/Elnor Specs Git branch: main Git commit: dbaa25962edc11ab30e8d4ca1715f9ae5bf77331 Generated: 2026-06-09T01:23:58.539Z --- Repo access status: GitHub read access confirmed; writeback failed twice via `create_file` safety wrapper. Intended path: `Memory Rebuild Docs/Stage_6_Charters/E1_E2_DOC81_Scope_Policy/Reviews/E1_E2_R2_Design_Review_GPT-5.5-Pro.md`. Bottom line: **DESIGN_REVISION_NEEDED** — R2 is close, but not ratifiable as-is until three focused fixes land: R-1 empty-applicable internal-use blocking, floor/sticky disclosure-vector composition, and incomplete typed obligation union. I could not save the review to the repo after two identical write failures, so here is the completed review text inline: ````md # DOC81 E1/E2 R2 Design Review — GPT-5.5 Pro Repo access status: CONFIRMED via GitHub connector on `wbrody/Elnor-Specs@main`; writeback failed twice via connector safety wrapper. Bottom line: DESIGN_REVISION_NEEDED — R2 is close and structurally strong, but not ratifiable as-is. ## Blocking BUG findings 1. B1 — R-1 internal-use posture is violated by the empty-applicable-decision path. DOC81 §3.2 lines ~773-781 returns `blockedBottomPolicy(policy.no_applicable_decision)` before domain contribution is included. DOC81 §4.0 lines ~1193-1200 says internal use may hard-block only for four bases. This creates a fifth block basis. 2. B2 — Scope floors and sticky prior policies do not meet disclosure vectors before final scalar derivation. DOC81 §3.2 lines ~778-802 applies floor/prior via `meetPoints` only, while §4.6.1 lines ~1496-1510 defines `max_disclosure_vector`. A more-open vector can later derive a more-open scalar. 3. B3 — `PolicyObligation` typed union omits declared obligation kinds. DOC81 §3.3 lines ~846-882 lists kinds including `hide_existence` and `show_generic_existence_only`, but the union lacks concrete members for them. The §3.3 vector-ceiling table at ~923-940 therefore cannot type-check the required hide-existence case cleanly. ## Substantive findings - S1 — `deriveDisclosureClass` can under-class vectors that disclose source title, exact counts, or reason details. Anchor: §4.2 lines ~1240-1277. - S2 — `same_machine_local -> local_file_export` correctly treats local file export as egress, but the default `reference_only_candidate` floor is too restrictive for ordinary local matter export. Anchor: §4.6.4 lines ~1549-1567; §7.4 lines ~2021-2030. - S3 — `contamination_ceiling = 0.0` is too blunt as an operational seed; every finite score trips the veto. Anchor: §4.6.3 lines ~1530-1547. - S4 — DOC11 external-agent delegation needs a concrete policy envelope. - S5 — Policy-history/time-travel audit query remains implicit. ## CONFIRMED - Lattice/rank maps are explicit and coherent; `same_firewall_only` is correctly placed. - Egress with only destinationless decisions is blocked. - CODEX R2-B1 obligation-vector fix is present. - Restamps compare to root ceiling, not prior restamp. - R-2 firewalled-only human authority is coherent. - Legal hold default-block registry is structural. - Suppression ambiguity fail-closes without prompting. - Relation traversal has closed registry, budget, trace, and default-deny. - DOC81→DOC86 export is downward-only and facts-only. ## Walked cases ### Privileged source + conflicting decisions + obligation The meet correctly performs per-axis MIN and obligation resolution, but `hide_existence` cannot be represented cleanly because the typed union omits that obligation kind. Result: BUG B3. ### `hide_existence` + more-open vector The CODEX fix works for obligations: obligation vector is met before scalar derivation and coherence. Result: confirmed, subject to B3. ### Floor/sticky analog of the same leak Replacing the obligation with a scope floor or prior effective not-disclosable policy exposes B2: floor/prior scalar caps can be overwritten because their vectors are not met before final derivation. ### Egress with only destinationless decisions Step 0b requires a destination-specific applicable decision. Result: confirmed. ### Internal retrieve with no object-specific decision Same-principal local internal retrieval with no wall, revocation, PO-outside-matter use, or malformed record still blocks on `policy.no_applicable_decision`. Result: BUG B1. ## Paste-ready fixes ### Fix B1 Change §3.2 empty-applicable handling: ```text if applicable is empty: if isInternalUseAction(request.action) and isSamePrincipalOrAuthorizedScope(request) and !hasInternalUseQualifyingBlockBasis(scopeResolution, request): eff = domainContribution(request) contributing_decision_refs = [] reason_codes += ['policy.domain_profile_only_internal_baseline'] goto step_3_floor else: return blockedBottomPolicy('policy.no_applicable_decision') ```` Keep fail-closed no-applicable behavior for egress, write, learn, delegate, carryover, and principal/destination crossings. ### Fix B2 Patch §3.2 steps 3-4: ```text floor_effect = FLOOR_EFFECT(scopeResolution.minimum_conservatism_floor) eff = meetPoints(eff, floorEffectToPolicyPoint(floor_effect)) eff.disclosure_vector = meetDisclosureVectors([ eff.disclosure_vector, floor_effect.max_disclosure_vector ]) if priorEffective: eff = meetPoints(eff, toPolicyPoint(priorEffective)) eff.disclosure_vector = meetDisclosureVectors([ eff.disclosure_vector, priorEffective.effective_disclosure_vector ]) ``` Then derive scalar and cohere. ### Fix B3 Either add concrete interfaces for every `PolicyObligationKind`, or use a parameter map: ```typescript type PolicyObligationParametersByKind = { hide_existence: { disclosure_ceiling: 'bottomDisclosureVector' }; show_generic_existence_only: { count_disclosure_mode: 'none' | 'bucketed'; source_title_allowed: false }; require_migration_plan: { schema_migration_plan_ref?: string; if_missing: 'route_manual_review_only' }; // all remaining kinds... }; type TypedPolicyObligation = PolicyObligationBase & { obligation_kind: K; parameters: PolicyObligationParametersByKind[K]; }; type PolicyObligation = { [K in PolicyObligationKind]: TypedPolicyObligation }[PolicyObligationKind]; ``` ## §13.6 seed recommendations * Confirm `same_firewall_only` position. * Confirm floor cells after B2 vector fix. * Confirm `show_generic_existence_only.count_disclosure_mode = none`. * Change `same_machine_local -> local_file_export` floor to `normal_policy_check` while keeping egress attestation. * Keep unknown destination block. * Confirm `scope_confidence_floor = 1.0` as fallback only; add domain-profile seeds. * Change `contamination_ceiling = 0.0` operational default. * Change traversal default depth from 16 to 8, allow 16 by profile/budget override. * Confirm `source_and_derived_artifacts` exclusion closure. * Confirm action closure minima with explicit learning sealed/firewalled predicates. ## A/A+ gap Current grade: A- / strong A after B1-B3. Path to A+: 1. Fix B1-B3. 2. Centralize policy operations over point + disclosure vector. 3. Make §13.6 constants executable registry rows. 4. Add delegation envelope and policy-history query. 5. Add securities-litigation domain seed pack. Final verdict: DESIGN_REVISION_NEEDED. No ARCHITECT_STOP; no family rearchitecture; apply the focused fixes and rerun a short delta review. ``` ``` I went back through the R2 charter harder, with the “no phantom wiring / no under-specified helper / no pretty-but-unbuildable seam” lens from the ELNOR carryover. **Updated bottom line:** I would keep the verdict at **DESIGN_REVISION_NEEDED**, and I would strengthen the critique. My earlier review caught real issues, but it under-counted the number of R2 seams. The charter is architecturally right, but the executable layer still has several places where a table/schema exists without being fully wired into the algorithm that is supposed to consume it. ## Revised top-level judgment I now see **five ratification-gating or near-gating issues**, not just three: 1. **Destination crosswalk floors/attestation flags are defined but not actually composed into `meet_v2`.** 2. **Sticky-restrictive cache keys do not include the sticky prior effective policy.** 3. **The R-1 internal-use empty-applicable path still creates a fifth internal block basis.** 4. **Disclosure-vector handling is still split across point/vector paths, creating several leak or drift seams.** 5. **The lifecycle / derived-object split is asserted in lineage but not consistently implemented in the schemas.** The first two are the biggest new findings from the deeper pass. --- # 1. BUG — `DestinationPolicyCrosswalk` is defined, but `meet_v2` does not actually apply its `policy_floor` or attestation requirement This is the most important new issue I missed. R2 defines `DestinationPolicyCrosswalk` as a constraint table: `relation_to_destination` maps to allowed E0 outbound classes, a `policy_floor`, attestation flags, lookup requirement, and disposition. The seed rules explicitly say, for example, external destinations get `fail_closed_candidate`, local file export requires attestation, unknown destinations block until resolved, and terminal bytes leaving same-machine runtime carry an egress attestation. But the actual `meet_v2` algorithm only does an egress precondition: destination present, E0 class recognized, and at least one destination-specific decision. It then proceeds to applicable filtering, per-axis meet, scope floor, sticky prior, obligations, scalar derivation, and coherence. There is no step that resolves the `DestinationPolicyCrosswalk` row and meets its `policy_floor` into the policy, nor a step that emits or requires the crosswalk’s attestation flag. That means the crosswalk can become decorative: a destination-specific decision may authorize an egress action, while the crosswalk’s intended floor and attestation requirements are not part of the executable meet. **Why this matters:** This is exactly the kind of “looks specified, not wired” bug the R2 pass was supposed to eliminate. The charter closes the destinationless-egress hole, but it does not yet make the crosswalk’s substantive constraints load-bearing. **Fix:** Add a step after 0b and before floor application: ```text # 0c. Destination crosswalk constraint. if request.action in {export, delegate, carryover} or renderMayLeaveRuntime(request): crosswalk = resolveDestinationPolicyCrosswalk( scopeResolution.relation_to_destination, request.destination ) if crosswalk.disposition in {'block', 'block_until_terminal_destination_resolved'}: return blockedBottomPolicy('policy.destination_crosswalk_block', crosswalk.reason_codes) require request.destination ∈ crosswalk.allowed_outbound_destination_classes required_egress_attestation = crosswalk.egress_attestation_required destination_floor = crosswalk.policy_floor ``` Then in the floor step: ```text combined_floor = meetFloors([ scopeResolution.minimum_conservatism_floor, destination_floor ?? 'normal_policy_check' ]) floor_effect = FLOOR_EFFECT(combined_floor) eff = meetPolicyState(eff, floor_effect) ``` And add fields to `EffectiveMemoryPolicy`: ```typescript destination_crosswalk_ref?: DestinationPolicyCrosswalkRef; egress_attestation_required: boolean; egress_attestation_ref?: E0EgressAttestationRef; ``` --- # 2. BUG — Sticky-restrictive cache keys omit the sticky prior effective policy This is a second major issue I missed. `meet_v2` step 4 says `priorEffective` is met in so input removal cannot widen policy without restamp. That is the key sticky-restrictive law. But the `EffectivePolicyCacheKey` includes object/action/destination, evaluation context, scope resolution, generation/effective state/evaluator hash, contributing-decision hash, obligation-set hash, and registry versions. It does **not** include `priorEffective` or any hash/ref of the prior effective policy. So two evaluations can have the same current decision set and freshness key but different sticky priors. If the cache key ignores the prior, EC can reuse a wider cached policy where the sticky prior should have kept it narrower. **Why this matters:** This undercuts the very law R2 says it is enforcing: “input removal cannot widen without restamp.” It also makes the NI-1 monotone-down property fixture incomplete unless the fixture model includes cache reuse. **Fix:** Add sticky prior to both the result and the cache key: ```typescript interface EffectiveMemoryPolicy extends E0DurableRecord { // existing fields... sticky_prior_effective_policy_ref?: EffectiveMemoryPolicyRef; sticky_prior_effective_policy_hash?: ContentHash; } interface EffectivePolicyCacheKey { // existing fields... sticky_prior_effective_policy_ref?: EffectiveMemoryPolicyRef; sticky_prior_effective_policy_hash?: ContentHash; } ``` Add lint/fixture: ```text cache.effective_policy_reused_across_different_sticky_prior fixture.cache.sticky_prior_effective_policy_changes_cache_key ``` I would treat this as **Blocking** because it is a cache/freshness seam that can silently violate policy monotonicity. --- # 3. BUG — R-1 internal-use posture is still violated by the empty-applicable path I still stand by this finding. R-1 says internal use of the user’s own memories defaults to permitted, and only four bases may hard-block internal use: ethical wall, revoked/clawed-back source, sealed/protective-order material used outside its own matter, and malformed-record conservative containment. But `meet_v2` still blocks if `applicable` is empty before the always-present domain contribution is used. That means “no object-specific policy decision generated yet” becomes a fifth internal-use block basis. **Deeper refinement:** The fix should not simply allow all empty-applicable internal use. The safe pattern is “domain-profile-only baseline allowed for same-principal internal actions unless one of the R-1 four bases applies.” ```text if applicable is empty: if isSamePrincipalInternalUse(request) and !hasR1InternalUseBlockBasis(scopeResolution, request): eff = domainContribution(request) reason_codes += ['policy.domain_profile_only_internal_baseline'] goto floor_step else: return blockedBottomPolicy('policy.no_applicable_decision') ``` Keep the hard block for egress, delegation, carryover, durable write, learning, cross-principal delivery, and any destination-bearing action. --- # 4. BUG — Scope floor and sticky prior still update `PolicyPoint`, not `DisclosurePermissionVector` The CODEX fix repaired the obligation path: obligation-induced disclosure ceilings now tighten the vector, derive scalar, then cohere. That part is visible in `meet_v2` and §3.3. But the same pattern is not applied to **scope floors** or **sticky prior effective policies**. §4.6.1 defines `ConservatismFloorEffect` with both scalar and `max_disclosure_vector`, but `meet_v2` step 3 only calls `meetPoints`. That means a floor can cap `disclosure_class` in the point, while the existing vector remains more open; step 6 derives scalar from the more-open vector and can undo the scalar floor. Same issue for `priorEffective`. **Fix:** Introduce a single state object that cannot update point and vector separately: ```typescript interface PolicyEffectiveState { point: PolicyPoint; disclosure_vector: DisclosurePermissionVector; } function meetFloorEffect( state: PolicyEffectiveState, floor: ConservatismFloorEffect ): PolicyEffectiveState { return { point: meetPoints(state.point, floorEffectToPolicyPoint(floor)), disclosure_vector: meetDisclosureVectors([ state.disclosure_vector, floor.max_disclosure_vector ]) }; } function meetPriorEffective( state: PolicyEffectiveState, prior: EffectiveMemoryPolicy ): PolicyEffectiveState { return { point: meetPoints(state.point, toPolicyPoint(prior)), disclosure_vector: meetDisclosureVectors([ state.disclosure_vector, prior.effective_disclosure_vector ]) }; } function finalizePolicyState(state: PolicyEffectiveState): PolicyPoint { const point = { ...state.point, disclosure_class: deriveDisclosureClass(state.disclosure_vector) }; return makeCoherent(point); } ``` This is the single highest-value simplification: it prevents the whole scalar/vector family of bugs. --- # 5. BUG — Input decision scalar/vector mismatch is not runtime-failed closed `MemoryPolicyDecision` carries both `disclosure_class` and `disclosure_vector`, while the prose says scalar is always derived from the vector. But `meet_v2` uses `toPolicyPoint` over the scalar and separately meets vectors, then derives the final scalar from the final vector. If an applicable decision is malformed in this specific way—scalar says `not_disclosable`, vector says existence/source/reason may be disclosed—F1 only catches unknown chain values. It does not catch scalar/vector inconsistency. The final derivation can trust the more-open vector and raise disclosure. **Fix:** Treat scalar/vector inconsistency as a malformed disclosure axis: ```text for d in decisions: expected = deriveDisclosureClass(d.disclosure_vector) if d.disclosure_class != expected: forceBottom['disclosure_class'] = true forceBottomDisclosureVector = true excluded.note(d, 'malformed_disclosure_vector') ``` Add lints: ```text policy.decision_scalar_vector_mismatch_not_failed_closed policy.disclosure_vector_exceeds_declared_scalar ``` This is separate from Stage-9 linting; it needs to be a runtime meet gate. --- # 6. BUG — Lifecycle / derived-object split is asserted but not implemented consistently This is another important miss in my first pass. §14.3 says the derived/per-request objects should “drop `E0DurableRecord` or mark `derived_projection`,” and it explicitly names `ScopeResolutionResult`, `ScopeBoundary`, the meet result, `PolicyMembraneDecision`, `PolicyCappedDAMSInput`, and `PolicyEvaluationContext`. But several of those schemas still extend `E0DurableRecord` without a visible `derived_projection` marker: * `ScopeBoundary extends E0DurableRecord`, even though the lifecycle prose says it is derived per resolution and not independently mutated except where cached. * `ScopeResolutionResult extends E0DurableRecord`. * `EffectiveMemoryPolicy extends E0DurableRecord`. * `PolicyMembraneDecision extends E0DurableRecord`. * `PolicyCappedDAMSInput extends E0DurableRecord`. * `PolicyEvaluationContext extends E0DurableRecord`. **Why this matters:** If Stage-7 implementers read the TypeScript schemas literally, they will durable-write request artifacts that are supposed to be derived/cacheable/rebuildable. That bloats durable truth and blurs canonical vs derived state. **Fix:** Add an explicit base: ```typescript interface E0DerivedProjection { schema_version: SchemaVersionRef; created_at: string; projection_kind: 'derived_projection'; canonical_source_refs: string[]; rebuild_key: ContentHash; expires_at?: string; } ``` Then make derived records extend `E0DerivedProjection`, not `E0DurableRecord`, unless the architecture truly wants an audit-durable event. If some of these are intended to be durable audit records, say so explicitly and remove them from the derived split list. --- # 7. BUG — Scope protection-state derivation and `active_scope_protection_state` disagree `ScopeResolutionResult.active_scope_protection_state` allows only: ```text ordinary | matter_or_project_sensitive | firewalled | sealed | personal_private | unknown_sensitive ``` But `ScopeProtectionStateDerivation.resolved_protection_state` allows: ```text ordinary | matter_or_project_sensitive | personal_private | client_confidential | firewalled | privileged | sealed | unknown_sensitive ``` and the flags include `client_confidential`, `privileged`, and `classification_unknown`. So a derivation can resolve to `privileged` or `client_confidential`, but the main result field cannot carry that value. **Why this matters:** The first target domain is securities litigation. Privilege and client confidentiality are not decorative. If implementers collapse those to `matter_or_project_sensitive`, egress gates lose specificity. If they collapse a combined `firewalled + privileged` object to whichever rank wins, they may hide the wall flag unless they remember to inspect `active_flags`. **Fix:** Make the result field use the derivation type exactly: ```typescript active_scope_protection_state: ScopeProtectionStateDerivation['resolved_protection_state']; ``` And add a rule: ```text Internal-use blocking must be computed from preserved active_flags, not solely from resolved_protection_state. ``` This avoids a dominant-rank summary suppressing a firewalled flag. --- # 8. BUG — `ConservatismFloorEffect` is referenced by `floor_effect_ref` but has no ID field `EffectiveMemoryPolicy` has `floor_effect_ref?: ConservatismFloorEffectRef`. But `ConservatismFloorEffect` itself has `floor` and `schema_owner`, not an `effect_id` or `effect_ref`. That makes the reference dangling. There is a similar smell in `DomainPolicyThresholds`: `thresholds_id` is typed as `DomainProfileId`, but the schema owner is DOC81. That makes an E0-owned domain profile ID double as a DOC81 primary ID. **Fix:** ```typescript type DomainPolicyThresholdsRef = Brand; interface ConservatismFloorEffect extends E0DurableRecord { effect_id: ConservatismFloorEffectRef; floor: ScopeConservatismFloor; schema_owner: 'DOC81'; // ... } interface DomainPolicyThresholds extends E0DurableRecord { thresholds_id: DomainPolicyThresholdsRef; domain_profile_ref: DomainProfileId; schema_owner: 'DOC81'; // ... } ``` This is not conceptual, but it is exactly the kind of schema gap that causes implementation drift. --- # 9. BUG — `CollectionMode` has an empty-input contradiction The helper says: ```typescript if (modes.length === 0) return 'suppress'; ``` because empty governance is fail-closed. But the evaluation rule says `no_topic_match => admit_collect`, not suppress-everything. Those are both reasonable rules, but they describe two different empty cases: 1. No matching topic exists: ordinary admission. 2. A match/governance is unresolved or unavailable: fail-closed suppress. The current helper cannot distinguish them. **Fix:** Replace `meetCollectionMode(modes)` with context-aware evaluation: ```typescript function evaluateCollectionMode(input: { ambiguity_state: 'unambiguous' | 'ambiguous' | 'no_topic_match'; matched_modes: CollectionMode[]; governance_resolution_state: 'resolved' | 'unavailable' | 'stale'; ec_surface_collection_enabled: boolean; ec_incognito_active: boolean; }): CollectionSuppressionEvaluation['disposition'] { if (!input.ec_surface_collection_enabled || input.ec_incognito_active) return 'refuse_suppress'; if (input.ambiguity_state === 'no_topic_match') return 'admit_collect'; if (input.governance_resolution_state !== 'resolved') return 'defer_review_fail_closed'; if (input.ambiguity_state === 'ambiguous' && input.matched_modes.some(m => m === 'suppress' || m === 'exclude')) { return 'defer_review_fail_closed'; } const mode = meetCollectionMode(input.matched_modes); if (mode === 'exclude') return 'refuse_exclude_and_backfill'; if (mode === 'suppress') return 'refuse_suppress'; return 'admit_collect'; } ``` --- # 10. BUG — Restamp decision requires the MFC it produces `PolicyStampRestampBase` requires `memory_flow_certificate_ref`, while the prose says the restamp decision contract “produces an E0 RestampMFC.” That creates a phase-order ambiguity: the decision object appears to require a certificate that cannot exist until after the decision is accepted and EC issues the certificate. **Fix:** Split decision and issuance: ```typescript interface PolicyStampRestampDecisionBase extends E0DurableRecord { restamp_id: PolicyStampRestampRef; // no MFC yet } interface PolicyStampRestampIssuance extends E0DurableRecord { restamp_ref: PolicyStampRestampRef; memory_flow_certificate_ref: MemoryFlowCertificateId; issued_at: string; ceiling_compliance_attested: true; } ``` Or make the field explicitly post-issuance: ```typescript issued_memory_flow_certificate_ref?: MemoryFlowCertificateId; ``` with an invariant that it is required only once `restamp_disposition` is committed. --- # 11. BUG — Restamp ceiling comparison over `disclosure_vector` is not formally defined `PolicyCeilingSnapshot` stores both scalar and vector ceilings. `PolicyAxis` includes `disclosure_vector`, but `PolicyAxisCeilingComparison` uses string `prior_value`, `new_value`, and `ceiling_value`. The prose says restamp validity requires `new <= MIN(root ceiling, current scope floor)` per axis. That is well-defined for scalar chains, but not for a composite vector unless the vector partial order is defined. **Fix:** ```typescript interface DisclosureVectorCeilingComparison { axis: 'disclosure_vector'; prior_vector: DisclosurePermissionVector; new_vector: DisclosurePermissionVector; ceiling_vector: DisclosurePermissionVector; comparison_by_field: Array<{ field: keyof DisclosurePermissionVector; comparison: 'within_ceiling' | 'downgraded' | 'exceeds_ceiling'; }>; aggregate_comparison: 'within_ceiling' | 'downgraded' | 'exceeds_ceiling'; } ``` And define: ```text v1 <= v2 iff every boolean in v1 implies the corresponding boolean in v2, count mode rank(v1) <= rank(v2), summary fidelity rank(v1) <= rank(v2), and every required template/ref in v1 is authorized by v2. ``` --- # 12. GAP — Action predicate table exists, but `meet_v2` does not call it §4.7 defines action closure and `ActionPermissionPredicate`. It says terminal actions imply prerequisite checks, and predicate minima are architect-confirmable seeds. But `meet_v2` returns `OK(assemble(...))` after coherence. It does not run `evaluateActionPermissionPredicate(eff, request.action)`. That means an effective policy can be produced without an explicit final action disposition. **Fix:** Add a final gate after coherence: ```text # 7. Action permission predicate. closure = ACTION_CLOSURE[request.action] require all closure.required_policy_actions have effective_policy_refs predicate = ACTION_PERMISSION_PREDICATE[request.action] permission = evaluatePredicate(eff, predicate) if permission.blocked: return blockedBottomPolicy('policy.action_predicate_failed', permission.reason_codes) return OK(assemble(eff, obligations, conflicts, request, permission)) ``` Add to `EffectiveMemoryPolicy`: ```typescript action_permission_disposition: | 'permitted' | 'blocked_by_axis_minimum' | 'blocked_missing_prerequisite_action' | 'blocked_missing_egress_attestation' | 'requires_obligation_discharge' | 'requires_disambiguation'; ``` This would make DOC84/DOC86 less likely to invent their own action interpretation. --- # 13. GAP — Principal keys remain optional in some Phase-2-sensitive places R-5 says internal use means same principal or principals authorized for the scope, not any user of a shared database. The charter correctly requires `principal_ref` on `PolicyEvaluationContext`. But `ScopeResolutionResult.principal_ref` is optional, and `ScopeResolutionCacheKey.principal_ref` is optional. That creates an avoidable Phase-2 bleed risk: a scope resolution result or cache key can be reused across principals if the principal is omitted. **Fix:** Make principal explicit with a sentinel, not optional: ```typescript type PrincipalScope = | { principal_scope_kind: 'principal_scoped'; principal_ref: PrincipalRef } | { principal_scope_kind: 'system_global'; reason_code: ReasonCodeId }; principal_scope: PrincipalScope; ``` Use that in `ScopeResolutionResult`, `ScopeResolutionCacheKey`, and any export/portability bundle. --- # 14. GAP — Safe-label vocabulary version is optional even when safe labels are used `PolicyRuntimeFreshnessKey.safe_label_vocabulary_version` is optional. That is fine for artifacts that provably do not use safe labels. But many DOC81 artifacts do use safe-label policy, including `PolicyUIExport`, which carries `safe_label_policy_ref`, constraints, and label refs. **Fix:** Add a conditional invariant: ```text If disclosure_class ∈ {generic_safe_label_only, redacted_summary, full} or any safe_label_policy_ref/default_label_ref/inspector_label_ref is present, then freshness_key.safe_label_vocabulary_version is REQUIRED. ``` Add lint: ```text cache.safe_label_artifact_missing_vocab_version ``` --- # 15. SUGGESTION — Build the “policy state machine” as the simplification layer The theme of the deeper review is that R2 added the right objects, but several are connected by prose rather than a single executable state pipeline. I would add one tiny §3.0/§3.2 helper module: ```typescript interface PolicyEffectiveState { point: PolicyPoint; disclosure_vector: DisclosurePermissionVector; obligations: PolicyObligationRef[]; required_attestations: Array<'egress' | 'mfc' | 'obligation_discharge'>; applied_floor_refs: ConservatismFloorEffectRef[]; destination_crosswalk_ref?: DestinationPolicyCrosswalkRef; sticky_prior_effective_policy_ref?: EffectiveMemoryPolicyRef; } function meetDecisionInputs(...): PolicyEffectiveState; function applyScopeFloor(...): PolicyEffectiveState; function applyDestinationCrosswalk(...): PolicyEffectiveState; function applyStickyPrior(...): PolicyEffectiveState; function applyObligations(...): PolicyEffectiveState; function finalizeDisclosureAndCoherence(...): PolicyEffectiveState; function evaluateActionPredicate(...): ActionPermissionResult; ``` This would collapse a lot of R2’s scattered mechanisms into one implementable pipeline. --- ## Updated severity list ### I would now mark these Blocking 1. **Destination crosswalk policy floor/attestation not wired into `meet_v2`.** 2. **Sticky prior omitted from effective-policy cache key.** 3. **R-1 empty-applicable internal-use path creates a fifth hard-block basis.** 4. **Floor/prior disclosure vectors not met before scalar derivation.** 5. **Typed obligation union incomplete for declared obligation kinds.** ### I would mark these Substantive 6. Derived-object split contradicted by schemas still extending `E0DurableRecord`. 7. Scope protection-state enum mismatch. 8. `ConservatismFloorEffect` referenced without primary ID; `DomainPolicyThresholds` uses foreign ID as primary. 9. Input decision scalar/vector mismatch not runtime-failed closed. 10. Collection no-topic-match vs empty-mode contradiction. 11. Restamp decision/MFC phase-order ambiguity. 12. Disclosure-vector ceiling comparison undefined. 13. Action predicates defined but not called by `meet_v2`. 14. Principal fields optional in Phase-2-sensitive scope result/cache contexts. 15. Safe-label vocabulary version optional on safe-label-bearing artifacts. ## Revised A/A+ view My earlier “A- / strong A after three fixes” was too generous. I would now say: **Current R2: B+ / A- architecture, not yet A-grade engineering.** **After the five blocking fixes: A-.** **After the substantive cleanup: solid A.** **A+ requires the policy-state-machine simplification, registry-backed seed constants, delegation envelope, and policy-history query.** The architecture is not wrong. The problem is that R2 doubled the charter and introduced many correct objects faster than it fully integrated them into one executable pipeline. The best next pass should not add many more schemas. It should **tighten the pipeline**: crosswalk → floor → sticky → obligations → vector/scalar finalization → action predicate → stamp/restamp/cache. I did a third pass with a more adversarial “spec-functions and helper-functions are probably where the bugs hide” lens. That was the right lens: the remaining risk is less about the high-level architecture and more about **tables, thresholds, hashes, state transitions, and helper functions not being fully wired into the runtime pipeline**. That is exactly the failure mode the stable ELNOR carryover warns against: under-specified schemas/routes/helpers and attractive architecture that is not actually executable. My revised view is harsher than the prior one: **Updated verdict: DESIGN_REVISION_NEEDED, with a required quantitative/function pass.** I would not just patch the three or five earlier blockers. I would add a dedicated “R2 executable-functions repair pass” before ratification. The main new theme: **R2 defines many policy facts, but not every policy fact is consumed by the final decision pipeline.** That is a classic spec drift source. --- ## 1. New or sharpened blocking issues ### BUG Q1 — Domain profile disclosure can be overwritten because `DomainProfilePolicyContribution` has only a scalar `PolicyPoint`, not a disclosure vector This is the most important mathematical issue I found in the third pass. `meet_v2` first meets `domainContribution(request)` into the scalar `PolicyPoint`, then separately sets `eff.disclosure_vector = meetDisclosureVectors(applicable.map(d => d.disclosure_vector))`. That means any disclosure restriction contributed by the domain profile’s scalar `disclosure_class` can be overwritten later when step 6 derives the final scalar from the applicable decisions’ vector. The domain contribution schema itself only has `contribution: PolicyPoint`; it has no `contribution_disclosure_vector`. **Concrete failure case:** securities-litigation conservative domain profile contributes `disclosure_class = generic_safe_label_only` or `not_disclosable`; the only applicable decision vector is `full`. Step 2 scalar meet restricts disclosure, but step 6 derives scalar from the full vector and raises disclosure again. **Patch:** ```typescript interface DomainProfilePolicyContribution extends E0DurableRecord { contribution_id: DomainProfilePolicyContributionRef; schema_owner: 'DOC81'; domain_profile_id: DomainProfileId; action: MemoryPolicyAction; contribution: PolicyPoint; contribution_disclosure_vector: DisclosurePermissionVector; // NEW reason_codes: ReasonCodeId[]; } ``` And in `meet_v2`: ```text domain = domainContribution(request) eff = meetAllPoints([domain.contribution, ...applicable.map(toPolicyPoint)]) eff.disclosure_vector = meetDisclosureVectors([ domain.contribution_disclosure_vector, ...applicable.map(d => d.disclosure_vector) ]) ``` This should be treated as **Blocking**, because it can silently neutralize the always-present domain profile contribution. --- ### BUG Q2 — `DestinationPolicyCrosswalk` floors and attestation requirements are defined but not wired into `meet_v2` R2 defines `DestinationPolicyCrosswalk` with `policy_floor`, `egress_attestation_required`, `policy_lookup_required`, and a disposition. The seed rules say same-machine local export requires attestation, external destinations get a fail-closed floor, unknown destinations block, and terminal bytes leaving same-machine runtime require attestation. But `meet_v2` only checks that an egress destination is present, typed to E0, and supported by at least one destination-specific decision. It does not resolve the crosswalk row, apply the crosswalk `policy_floor`, or carry the crosswalk’s attestation requirement into the result. **Patch:** ```text # 0c. Destination crosswalk constraint. destination_floor = 'normal_policy_check' egress_attestation_required = false if request.action in {export, delegate, carryover} or renderMayLeaveRuntime(request): crosswalk = resolveDestinationPolicyCrosswalk( scopeResolution.relation_to_destination, request.destination ) if crosswalk.disposition in {'block', 'block_until_terminal_destination_resolved'}: return blockedBottomPolicy('policy.destination_crosswalk_block', crosswalk.reason_codes) require request.destination ∈ crosswalk.allowed_outbound_destination_classes destination_floor = crosswalk.policy_floor egress_attestation_required = crosswalk.egress_attestation_required ``` Then combine floors: ```text combined_floor = meetFloors([ scopeResolution.minimum_conservatism_floor, destination_floor ]) ``` And add to `EffectiveMemoryPolicy`: ```typescript destination_crosswalk_ref?: DestinationPolicyCrosswalkRef; egress_attestation_required: boolean; egress_attestation_ref?: E0EgressAttestationRef; ``` Without this, the crosswalk is partly decorative. --- ### BUG Q3 — Sticky-restrictive cache keys omit the sticky prior effective policy `meet_v2` step 4 meets `priorEffective` into the new result to prevent widening when inputs are removed. But `EffectivePolicyCacheKey` does not include `priorEffective` or a hash/ref of it. It includes object/action/destination, context, scope resolution, generation/effective-state/evaluator hash, contributing decision hash, obligation-set hash, and registry versions. So the cache can reuse a policy computed with no sticky prior in a context where a sticky prior should keep the result narrower. **Patch:** ```typescript interface EffectivePolicyCacheKey { object_ref: MemoryObjectRef; action: MemoryPolicyAction; destination?: E0OutboundDestinationClass; policy_evaluation_context_ref: PolicyEvaluationContextRef; scope_resolution_ref?: ScopeResolutionResultRef; policy_generation_id: PolicyGenerationId; effective_state_generation_id: EffectiveStateGenerationId; compiled_policy_evaluator_hash: ContentHash; contributing_decision_hash: ContentHash; obligation_set_hash: ContentHash; sticky_prior_effective_policy_ref?: EffectiveMemoryPolicyRef; // NEW sticky_prior_effective_policy_hash?: ContentHash; // NEW reason_code_registry_version: SchemaVersionRef; domain_profile_registry_version: SchemaVersionRef; safe_label_vocabulary_version?: SchemaVersionRef; } ``` Add: ```text cache.effective_policy_reused_across_different_sticky_prior fixture.cache.sticky_prior_effective_policy_changes_cache_key ``` This is a **policy monotonicity** bug, not just a cache hygiene bug. --- ### BUG Q4 — The same scalar/vector overwrite class applies to floors, prior effective policy, malformed disclosure inputs, and domain contribution The CODEX R2-B1 fix correctly repaired obligations: obligations now tighten the vector, derive scalar, then cohere. But the same pattern remains in other paths: * Domain profile contribution has scalar only. * Scope floor has `max_disclosure_vector`, but step 3 only calls `meetPoints`. * Prior effective policy has vector, but step 4 only calls `meetPoints`. * Malformed scalar disclosure can bottom the scalar while leaving a more-open vector intact. * Crosswalk floor is not applied at all. The root cause is that `PolicyPoint` and `DisclosurePermissionVector` are manipulated separately. **Patch pattern:** ```typescript interface PolicyEffectiveState { point: PolicyPoint; disclosure_vector: DisclosurePermissionVector; } function meetPolicyState(a: PolicyEffectiveState, b: PolicyEffectiveState): PolicyEffectiveState { return { point: meetPoints(a.point, b.point), disclosure_vector: meetDisclosureVectors([a.disclosure_vector, b.disclosure_vector]), }; } function finalizePolicyState(s: PolicyEffectiveState): PolicyPoint { const point = { ...s.point, disclosure_class: deriveDisclosureClass(s.disclosure_vector), }; return makeCoherent(point); } ``` Then every contribution must become a `PolicyEffectiveState`, not a bare `PolicyPoint`. --- ### BUG Q5 — `PolicyObligationKind` and `PolicyObligation` union are still inconsistent The charter lists many obligation kinds, including disclosure obligations, restamp obligations, learning obligations, sealed/firewalled fixture obligations, DSPy target eligibility, critique policy gates, and counterfactual/additive synthesis gates. But `PolicyObligation` only unions a subset of concrete interfaces. This matters because the `OBLIGATION_DISCLOSURE_VECTOR_CEILING` table references `hide_existence` and `show_generic_existence_only`, but those are not concrete union members. **Patch:** Use a parameter map so future kinds cannot drift: ```typescript type PolicyObligationParametersByKind = { hide_existence: { disclosure_ceiling: 'bottomDisclosureVector'; }; show_generic_existence_only: { count_disclosure_mode: 'none' | 'bucketed'; source_title_allowed: false; }; require_user_confirmation: { confirmation_context: 'egress' | 'wall_crossing' | 'policy_disambiguation'; prompt_must_be_safe_label: true; }; require_restamp_before_action: { required_before_actions: MemoryPolicyAction[]; }; require_migration_plan: { schema_migration_plan_ref?: string; if_missing: 'route_manual_review_only'; }; // every remaining PolicyObligationKind must appear here }; type TypedPolicyObligation = PolicyObligationBase & { obligation_kind: K; parameters: PolicyObligationParametersByKind[K]; }; type PolicyObligation = { [K in PolicyObligationKind]: TypedPolicyObligation }[PolicyObligationKind]; ``` Add lint: ```text policy.obligation_kind_without_typed_parameter_contract ``` --- ## 2. Additional structural issues ### BUG — `deriveDisclosureClass` is not conservative enough for source-title, exact-count, and reason-summary cases The current function returns `generic_safe_label_only` if `max_summary_fidelity === 'none'`, even if `may_disclose_source_title` is true or `count_disclosure_mode` is exact. **Patch:** ```typescript function deriveDisclosureClass(v: DisclosurePermissionVector): DisclosureClass { if (!v.may_disclose_existence) return 'not_disclosable'; const onlyExistence = !v.may_disclose_container_type && !v.may_disclose_topic_label && !v.may_disclose_source_title && v.count_disclosure_mode === 'none' && !v.may_disclose_reason_summary && v.max_summary_fidelity === 'none'; if (onlyExistence) return 'existence_only'; if ( v.may_disclose_source_title || v.count_disclosure_mode === 'exact' || v.max_summary_fidelity === 'full_reason' ) { return 'full'; } if (v.may_disclose_reason_summary || v.max_summary_fidelity === 'redacted_reason') { return 'redacted_summary'; } return 'generic_safe_label_only'; } ``` Add property test: for every vector, all true granular flags must be allowed by `DISCLOSURE_ALLOWS[deriveDisclosureClass(v)]`. --- ### BUG — `meetDisclosureVectors` drops `count_bucket_policy_ref` and `reason_summary_template_ref` `DisclosurePermissionVector` has optional `count_bucket_policy_ref` and `reason_summary_template_ref`, but `meetDisclosureVectors` returns only booleans, count mode, reason-summary boolean, and max summary fidelity. It drops the refs. That means DOC86 may have permission to render a bucket or reason summary but no policy/template reference to render it safely. **Patch:** ```typescript function meetOptionalRef( refs: Array, onConflict: 'drop' | 'conflict' ): T | undefined { const present = Array.from(new Set(refs.filter(Boolean))); if (present.length === 0) return undefined; if (present.length === 1) return present[0] as T; if (onConflict === 'drop') return undefined; throw new PolicyLatticeError('incompatible disclosure rendering refs'); } function meetDisclosureVectors(vs: DisclosurePermissionVector[]): DisclosurePermissionVector { if (vs.length === 0) return bottomDisclosureVector(); const countMode = meetCountModes(vs.map(v => v.count_disclosure_mode)); const mayReason = vs.every(v => v.may_disclose_reason_summary); const summary = meetSummaryFidelity(vs.map(v => v.max_summary_fidelity)); return { may_disclose_existence: vs.every(v => v.may_disclose_existence), may_disclose_container_type: vs.every(v => v.may_disclose_container_type), may_disclose_topic_label: vs.every(v => v.may_disclose_topic_label), may_disclose_source_title: vs.every(v => v.may_disclose_source_title), count_disclosure_mode: countMode, count_bucket_policy_ref: countMode === 'bucketed' ? meetOptionalRef(vs.map(v => v.count_bucket_policy_ref), 'conflict') : undefined, may_disclose_reason_summary: mayReason, reason_summary_template_ref: mayReason ? meetOptionalRef(vs.map(v => v.reason_summary_template_ref), 'conflict') : undefined, max_summary_fidelity: summary, }; } ``` If refs conflict, either fail closed or drop to `none`. --- ### BUG — `user_disambiguation_candidate` can leak wall existence unless floor is parameterized by ambiguity kind The floor seed says `user_disambiguation_candidate` has `disclosure_class = existence_only` and must raise §3.5 disambiguation. But R-3 says firewalled material produces no notice: no existence, no count, nothing. So the floor cannot be a single unparameterized “existence-only and disambiguate” value. It must know **why** disambiguation is needed. **Patch:** ```typescript type DisambiguationCause = | 'ordinary_scope_ambiguity' | 'destination_ambiguity' | 'privacy_topic_ambiguity' | 'firewall_boundary_ambiguity' | 'sealed_or_protective_order_ambiguity' | 'malformed_record_containment'; interface ConservatismFloorEffect extends E0DurableRecord { effect_id: ConservatismFloorEffectRef; floor: ScopeConservatismFloor; cause?: DisambiguationCause; max_content_fidelity: ContentFidelityLevel; max_locality: LocalityLevel; max_learning_scope: LearningScopeLevel; max_mutation_authority: MutationAuthorityLevel; max_disclosure_class: DisclosureClass; max_disclosure_vector: DisclosurePermissionVector; movement_allowed: boolean; disambiguation_required: boolean; disambiguation_prompt_allowed: boolean; // NEW allowed_actions_before_disambiguation: MemoryPolicyAction[]; reason_codes: ReasonCodeId[]; } ``` Rule: ```text if cause in {'firewall_boundary_ambiguity', 'sealed_or_protective_order_ambiguity'}: max_disclosure_vector = bottomDisclosureVector() disambiguation_prompt_allowed = false unless a safe generic prompt is independently permitted ``` --- ### BUG — Lifecycle / derived-object split is asserted but not actually represented consistently §14.3 says derived objects should either drop `E0DurableRecord` or mark `derived_projection`, and it lists `ScopeResolutionResult`, `ScopeBoundary`, the meet result, `PolicyMembraneDecision`, `PolicyCappedDAMSInput`, and `PolicyEvaluationContext`. But the schemas still extend `E0DurableRecord` in several of those cases: `ScopeBoundary`, `ScopeResolutionResult`, `EffectiveMemoryPolicy`, `PolicyMembraneDecision`, `PolicyCappedDAMSInput`, and `PolicyEvaluationContext`. **Patch:** ```typescript interface E0DerivedProjection { schema_version: SchemaVersionRef; created_at: string; projection_kind: 'derived_projection'; canonical_source_refs: string[]; rebuild_key: ContentHash; expires_at?: string; } ``` Then either: ```typescript interface ScopeResolutionResult extends E0DerivedProjection { ... } ``` or explicitly mark as audit-durable: ```typescript persistence_kind: 'audit_durable_runtime_record'; ``` The charter must choose. Right now it says both. --- ### BUG — Protection-state enum mismatch and rank conflation `ScopeResolutionResult.active_scope_protection_state` allows only `ordinary`, `matter_or_project_sensitive`, `firewalled`, `sealed`, `personal_private`, and `unknown_sensitive`. But `ScopeProtectionStateDerivation.resolved_protection_state` also includes `client_confidential` and `privileged`. That means the derivation can output a state the main result cannot carry. There is also a deeper policy issue: one max-restrictiveness rank conflates **internal-use block bases** with **egress sensitivity**. R-1 says privilege is egress-only and does not block internal use, while firewalls can block internal use. A single rank like `firewalled < privileged < sealed` can mislead implementers if they use the resolved state rather than preserved flags. **Patch:** ```typescript type ScopeProtectionResolvedState = | 'ordinary' | 'matter_or_project_sensitive' | 'personal_private' | 'client_confidential' | 'firewalled' | 'privileged' | 'sealed' | 'unknown_sensitive'; interface ScopeResolutionResult extends E0DerivedProjection { active_scope_protection_state: ScopeProtectionResolvedState; protection_derivation: ScopeProtectionStateDerivation; } ``` Add: ```typescript interface ProtectionPolicyEffects { internal_use_block_basis: | 'none' | 'ethical_wall' | 'revoked_or_clawed_back' | 'sealed_or_po_outside_matter' | 'malformed_record_containment'; egress_sensitivity_floor: ScopeConservatismFloor; learning_floor: LearningScopeLevel; notice_posture: 'no_notice' | 'generic_notice' | 'specific_in_app_notice'; } ``` Compute effects from preserved flags, not just max rank. --- ### BUG — `ConservatismFloorEffectRef` is referenced but the schema has no primary ID `EffectiveMemoryPolicy` carries `floor_effect_ref?: ConservatismFloorEffectRef`. But `ConservatismFloorEffect` has no `effect_id`; it only has `floor` and `schema_owner`. **Patch:** ```typescript interface ConservatismFloorEffect extends E0DurableRecord { effect_id: ConservatismFloorEffectRef; floor: ScopeConservatismFloor; schema_owner: 'DOC81'; // existing fields... } ``` Related: `DomainPolicyThresholds.thresholds_id` is typed as `DomainProfileId`, a foreign/E0-owned ID, while the schema is DOC81-owned. Patch: ```typescript type DomainPolicyThresholdsRef = Brand; interface DomainPolicyThresholds extends E0DurableRecord { thresholds_id: DomainPolicyThresholdsRef; domain_profile_ref: DomainProfileId; schema_owner: 'DOC81'; // existing fields... } ``` --- ### BUG — `PolicyStampInvalidation.user_visible_summary_ref` is required even when disclosure forbids a notice `PolicyStampInvalidation` requires `user_visible_summary_ref`, described as safe label only. But R-3 says firewalled material produces no notice: no existence, no count, nothing. **Patch:** ```typescript interface PolicyStampInvalidation extends E0DurableRecord { invalidation_id: PolicyStampInvalidationRef; schema_owner: 'DOC81'; invalidated_stamp_ref: PolicyStampRef; object_ref: MemoryObjectRef; prior_freshness_key: PolicyRuntimeFreshnessKey; new_freshness_key: PolicyRuntimeFreshnessKey; affected_scope_items: PolicyStampInvalidationScopeItem[]; notice_disposition: | 'no_notice' | 'internal_suppressed_manifest_only' | 'generic_safe_notice' | 'specific_in_app_notice'; user_visible_summary_ref?: SafeReferenceLabelRef; // optional, forbidden when no_notice reason_codes: ReasonCodeId[]; } ``` Add lint: ```text policy.invalidation_notice_emitted_when_disclosure_not_disclosable ``` --- ## 3. Separate quantitative / spec-function / technical-function audit This is the dedicated function-level audit you asked for. ### QF1 — Scope confidence aggregation is underspecified for required-component selection `ScopeResolutionConfidenceBreakdown` says aggregate confidence is the minimum of required components, with missing required components equal to zero. That formula is safe, but the spec does not define how `required_components` is selected. If an egress action omits `destination_relation_confidence`, or a protected source omits `sensitivity_classification_confidence`, the min can look high while a relevant dimension is absent. **Patch:** ```typescript function requiredScopeConfidenceComponents(input: { action: MemoryPolicyAction; destination?: E0OutboundDestinationClass; boundary_kind: ScopeBoundaryKind; sensitive_tag_summary: ScopeResolutionResult['sensitive_tag_summary']; relation_to_destination: ScopeResolutionResult['relation_to_destination']; }): ScopeResolutionConfidenceBreakdown['required_components'] { const req = new Set([ 'identity_confidence', 'boundary_confidence', 'population_freshness_confidence', ]); if (input.action in ['export', 'delegate', 'carryover'] || input.destination) { req.add('destination_relation_confidence'); } if ( input.boundary_kind !== 'same_scope' || input.sensitive_tag_summary !== 'none' ) { req.add('sensitivity_classification_confidence'); } return [...req].sort(); } ``` Also require: ```text required_components must be non-empty, duplicate-free, action-derived, and auditable. ``` --- ### QF2 — ScopeSearchCoverageProof numerator/denominator math allows bad ratios unless uniqueness/disjointness is enforced `ScopeSearchCoverageProof` has numerator, denominator, ratio, searched scopes, and not-searched scopes; `may_emit_not_found` is true iff denominator > 0, numerator == denominator, and not-searched is empty. But it does not explicitly require uniqueness, subset relations, or disjoint searched/not-searched sets. **Patch formula:** ```typescript function computeCoverage(required: ScopeRef[], searched: ScopeRef[]): { coverage_numerator: number; coverage_denominator: number; coverage_ratio: number; not_searched_scope_refs: ScopeRef[]; may_emit_not_found: boolean; } { const req = unique(required); const got = unique(searched).filter(s => req.includes(s)); const not = req.filter(s => !got.includes(s)); const denominator = req.length; const numerator = got.length; return { coverage_numerator: numerator, coverage_denominator: denominator, coverage_ratio: denominator === 0 ? 0 : numerator / denominator, not_searched_scope_refs: not, may_emit_not_found: denominator > 0 && numerator === denominator && not.length === 0, }; } ``` Add lints: ```text scope_search.required_scope_refs_not_unique scope_search.searched_scope_not_subset_of_required scope_search.searched_and_not_searched_overlap scope_search.zero_denominator_reported_as_full_coverage ``` --- ### QF3 — Threshold semantics need polarity-specific functions, not generic comparator/equality fields `ThresholdEvaluation` has `comparator` values like `gte_pass` and an `equality_rule` of `pass_on_equal` or `block_on_equal`. This is easy to misuse because some metrics are “higher is safer” and others are “higher is riskier.” **Patch:** ```typescript type ThresholdPolarity = | 'higher_is_safer' | 'lower_is_safer' | 'range_is_safer'; interface ThresholdRule { metric_kind: | 'scope_confidence' | 'scope_equivalence_confidence' | 'contamination_risk' | 'topic_match_confidence' | 'population_freshness'; polarity: ThresholdPolarity; threshold_value: number; equality_disposition: 'pass_on_equal' | 'fail_on_equal'; missing_or_nan_disposition: 'fail_closed' | 'requires_review'; } ``` Evaluation: ```typescript function evaluateThreshold(observed: number | undefined, r: ThresholdRule): 'pass' | 'fail_closed' | 'requires_review' { if (observed === undefined || Number.isNaN(observed)) { return r.missing_or_nan_disposition; } if (r.polarity === 'higher_is_safer') { if (observed > r.threshold_value) return 'pass'; if (observed === r.threshold_value) { return r.equality_disposition === 'pass_on_equal' ? 'pass' : 'fail_closed'; } return 'fail_closed'; } if (r.polarity === 'lower_is_safer') { if (observed < r.threshold_value) return 'pass'; if (observed === r.threshold_value) { return r.equality_disposition === 'pass_on_equal' ? 'pass' : 'fail_closed'; } return 'fail_closed'; } throw new Error('range threshold not implemented without bounds'); } ``` For defaults: ```typescript const THRESHOLD_DEFAULTS = { scope_confidence_floor: { polarity: 'higher_is_safer', threshold_value: 1.0, equality_disposition: 'pass_on_equal', missing_or_nan_disposition: 'fail_closed', }, contamination_risk: { polarity: 'lower_is_safer', threshold_value: 'domain_profile_required', equality_disposition: 'fail_on_equal', missing_or_nan_disposition: 'fail_closed', }, } as const; ``` Do **not** use `contamination_ceiling = 0.0` as an operational numeric default. Use it only as “profile missing means fail closed.” --- ### QF4 — Contamination-risk formula ignores confidence `ContaminationRiskMeasurement` has both `risk_score` and `confidence`. But `ContaminationRiskThresholdRule` compares only the score to the threshold. A high-risk/low-confidence score and a low-risk/low-confidence score should not be treated the same as high-confidence measurements. Low confidence should fail closed or route review. **Patch:** ```typescript interface ContaminationRiskThresholdRule extends E0DurableRecord { rule_id: ContaminationRiskThresholdRuleRef; schema_owner: 'DOC81'; domain_profile_ref: DomainProfileId; risk_model_ref: string; risk_model_generation_id: string; threshold_value: number; minimum_measurement_confidence: number; // NEW comparator: 'gte'; equality_rule: 'block_on_equal'; low_confidence_disposition: | 'fail_closed' | 'route_manual_review_only' | 'reroute_reference_only_notice'; over_threshold_disposition: | 'reroute_warning_constraint' | 'reroute_blocked_scope_notice' | 'reroute_reference_only_notice' | 'suppress'; reason_codes: ReasonCodeId[]; } ``` Evaluation: ```typescript function evaluateContamination(m: ContaminationRiskMeasurement, r: ContaminationRiskThresholdRule) { if ( m.risk_score === undefined || Number.isNaN(m.risk_score) || m.confidence === undefined || Number.isNaN(m.confidence) ) return r.low_confidence_disposition; if (m.confidence < r.minimum_measurement_confidence) { return r.low_confidence_disposition; } return m.risk_score >= r.threshold_value ? r.over_threshold_disposition : 'within_policy_ceiling'; } ``` --- ### QF5 — Equivalence cluster confidence depends on unspecified spanning-binding selection `ScopeEquivalenceCluster` says `cluster_confidence = MIN over spanning bindings`. But it does not define which spanning bindings are selected. Different spanning trees can produce different bottleneck confidences. **Patch:** define the proof tree. ```typescript interface ScopeEquivalenceCluster extends E0DurableRecord { cluster_id: ScopeEquivalenceClusterRef; schema_owner: 'DOC81'; member_scope_refs: ScopeRef[]; spanning_binding_refs: ScopeEquivalenceBindingRef[]; spanning_selection_algorithm: | 'maximum_bottleneck_spanning_tree' | 'explicit_user_binding_only' | 'strict_full_pairwise_profile'; cluster_confidence: number; // min confidence over selected proof tree cluster_confidence_formula: 'min_edge_confidence_over_selected_spanning_tree'; contradictory_evidence_refs: string[]; contradiction_disposition: | 'none' | 'do_not_collapse' | 'requires_user_confirmation'; // existing fields... } ``` Formula: ```text For inferred clusters: choose the maximum-bottleneck spanning tree over confirmed positive bindings; cluster_confidence = min(edge.confidence) over that selected tree; if contradictory evidence exists above domain threshold, disposition = do_not_collapse or requires_user_confirmation. ``` This preserves the F2 settlement while making the math deterministic. --- ### QF6 — `ScopeResolutionConfidenceBreakdown` needs range validation and finite-number guards Every confidence field is optional `number`; the schema says 0..1 in comments but not as a type. **Patch:** ```typescript type UnitInterval = number & { readonly __brand: 'UnitInterval_0_1_finite' }; function asUnitInterval(n: number): UnitInterval { if (!Number.isFinite(n) || n < 0 || n > 1) { throw new PolicyLatticeError('confidence outside [0,1]'); } return n as UnitInterval; } ``` Use `UnitInterval` for confidence, risk score, coverage ratio, threshold values where appropriate. --- ### QF7 — Count bucketing exposes exact zero unless tied to search coverage proof The default bucket scheme includes `0 -> "none"`. But `not_found` may be emitted only with full search coverage proof. A bucketed count of “none” can therefore leak an exact absence without satisfying the not-found proof. **Patch:** ```typescript interface CountDisclosurePolicy { mode: CountDisclosureMode; bucket_scheme_ref?: string; exact_count_allowed_only_if_disclosure_class: 'full'; zero_bucket_requires_scope_search_coverage_proof: true; // NEW zero_bucket_fallback_label: 'not_disclosed' | 'not_searched' | 'not_available'; } ``` Rendering rule: ```text If count = 0 and disclosure mode is bucketed: may render "none" only if ScopeSearchCoverageProof.may_emit_not_found = true. Otherwise render no count or "not available". ``` --- ### QF8 — Collection match confidence has max/min fields but no ambiguity formula `CollectionSuppressionEvaluation` has `topic_match_confidence_max`, `topic_match_confidence_min`, and an `ambiguity_state`, but no formula for when a match is ambiguous. **Patch:** ```typescript interface TopicMatchAmbiguityRule extends E0DurableRecord { rule_id: Brand; schema_owner: 'DOC81'; enter_match_threshold: UnitInterval; ambiguity_margin: UnitInterval; // e.g., top1 - top2 < margin suppress_or_exclude_candidate_threshold: UnitInterval; equality_rule: 'ambiguous_on_equal'; reason_codes: ReasonCodeId[]; } ``` Evaluation: ```typescript function classifyTopicMatch(scores: Array<{ topic_ref: TopicRef; score: UnitInterval; mode?: CollectionMode }>, rule: TopicMatchAmbiguityRule) { const sorted = [...scores].sort((a, b) => b.score - a.score); const top = sorted[0]; const second = sorted[1]; if (!top || top.score < rule.enter_match_threshold) return 'no_topic_match'; const suppressOrExcludeCandidate = sorted.some(s => s.score >= rule.suppress_or_exclude_candidate_threshold && (s.mode === 'suppress' || s.mode === 'exclude') ); if ( second && top.score - second.score < rule.ambiguity_margin && suppressOrExcludeCandidate ) return 'ambiguous'; return 'unambiguous'; } ``` --- ### QF9 — `meetCollectionMode([]) = suppress` conflicts with `no_topic_match = admit_collect` The helper returns suppress on an empty mode list. But the evaluation rule says no-topic-match is ordinary admission, not suppress-everything. Those are both valid but are different cases. Empty because no topic matched is not the same as empty because governance was unavailable. **Patch:** ```typescript type GovernanceResolutionState = 'resolved' | 'stale' | 'unavailable'; function evaluateCollectionDisposition(input: { ambiguity_state: 'unambiguous' | 'ambiguous' | 'no_topic_match'; governance_resolution_state: GovernanceResolutionState; matched_modes: CollectionMode[]; ec_surface_collection_enabled: boolean; ec_incognito_active: boolean; }): CollectionSuppressionEvaluation['disposition'] { if (!input.ec_surface_collection_enabled || input.ec_incognito_active) { return 'refuse_suppress'; } if (input.ambiguity_state === 'no_topic_match') { return 'admit_collect'; } if (input.governance_resolution_state !== 'resolved') { return 'defer_review_fail_closed'; } const mode = meetCollectionMode(input.matched_modes); if (mode === 'exclude') return 'refuse_exclude_and_backfill'; if (mode === 'suppress') return 'refuse_suppress'; return 'admit_collect'; } ``` --- ### QF10 — Quota envelope has estimates but no estimator, denominator, or chunk invariants `DOC81BatchOperationQuotaEnvelope` carries estimated object/decision/resolution/plane counts, maxima, optional wall-clock/cost caps, on-quota-exceeded behavior, idempotency key, and progress ref. But it does not define how estimates are computed, how progress denominators are preserved, or what happens to affected items when chunking pauses. **Patch:** ```typescript interface DOC81BatchOperationQuotaEnvelope extends E0DurableRecord { quota_envelope_id: BatchOperationQuotaEnvelopeRef; schema_owner: 'DOC81'; operation_kind: | 'policy_generation_restamp_batch' | 'source_revocation_cascade' | 'collection_exclude_backfill' | 'relation_traversal_scope_check' | 'legal_hold_destructive_job_scan'; estimate_method: | 'manifest_exact_count' | 'sampled_upper_bound' | 'prior_run_regression' | 'manual_bound'; estimate_confidence: UnitInterval; estimate_upper_bound_multiplier: number; estimated_object_count: number; estimated_policy_decision_count: number; estimated_scope_resolution_count: number; estimated_plane_updates: number; progress_denominator: number; processed_count: number; pending_count: number; failed_count: number; chunk_size: number; chunk_sequence: number; total_chunks: number; affected_items_disposition_while_pending: | 'quarantined_fail_closed' | 'previous_policy_sticky_until_restamped' | 'blocked_until_processed'; max_object_count: number; max_policy_decision_count: number; max_wall_clock_ms?: number; max_cost_usd?: number; on_quota_exceeded: | 'chunk_and_continue' | 'pause_and_surface_review' | 'fail_closed_for_affected_items' | 'manual_resource_approval_required'; idempotency_key: string; progress_ref?: string; reason_codes: ReasonCodeId[]; } ``` Important R-2 guard: ```text For operation_kind = policy_generation_restamp_batch, manual_resource_approval_required may approve resource usage only. It MUST NOT become human approval of individual routine restamps. ``` R-2 says routine restamps are autonomous, with human approval only for firewalled crossings. --- ### QF11 — Cascade completion boolean needs a denominator formula `CascadingSourceInvalidationRun` has `all_required_planes_complete: boolean`, and per-plane statuses. But the formula for “all required” is not specified. **Patch:** ```typescript function cascadeComplete(statuses: CascadingSourceInvalidationRun['plane_statuses']): boolean { const required = [ statuses.doc82_support_edges, statuses.doc87_memberships, statuses.doc84_delivery_artifacts, statuses.doc84_published_views, statuses.doc85_learning_signals, statuses.doc86_surfaces, ]; return required.every(s => s.status === 'completed' || (s.status === 'not_applicable' && s.receipt_refs.length > 0) ); } ``` If `doc84_published_views` remains a row inside DOC84, represent it as nested under `doc84`, not as a sibling plane. --- ### QF12 — Last-active-support-edge denominator proof needs lawful-edge predicate `LastActiveSupportEdgeEvaluation` carries remaining-edge count, refs, and a boolean. But it does not define the predicate for “lawful support edge.” **Patch:** ```typescript type SupportEdgeStatus = | 'active_lawful' | 'revoked_source' | 'excluded_source' | 'held_suppressed' | 'out_of_scope' | 'stale_generation' | 'unsearched_unknown'; interface LastActiveSupportEdgeEvaluation extends E0DurableRecord { evaluation_id: LastActiveSupportEdgeEvaluationRef; schema_owner: 'DOC81'; source_ref: SourceRef; affected_set_manifest_ref: string; evaluated_variant_refs: MemoryObjectRef[]; support_edge_evaluations: Array<{ support_edge_ref: string; status: SupportEdgeStatus; source_ref: SourceRef; policy_generation_id: PolicyGenerationId; effective_policy_ref?: EffectiveMemoryPolicyRef; reason_codes: ReasonCodeId[]; }>; lawful_support_edges_remaining_count: number; remaining_support_edge_refs: string[]; unsearched_support_edge_count: number; last_active_support_edge_lost: | true | false | 'unknown_due_to_unsearched_edges'; polarity_recompute_trace_ref?: string; evaluated_under_freshness_key: PolicyRuntimeFreshnessKey; reason_codes: ReasonCodeId[]; } ``` Rule: ```text last_active_support_edge_lost may be true only if unsearched_support_edge_count = 0 and lawful_support_edges_remaining_count = 0. ``` --- ### QF13 — Restamp vector ceiling comparison is not defined `PolicyCeilingSnapshot` stores `max_disclosure_vector`, and `PolicyAxis` includes `disclosure_vector`, but `PolicyAxisCeilingComparison` uses string values. Scalar axes have total order. Vectors need a partial order. **Patch:** ```typescript interface DisclosureVectorCeilingComparison { axis: 'disclosure_vector'; prior_vector: DisclosurePermissionVector; new_vector: DisclosurePermissionVector; ceiling_vector: DisclosurePermissionVector; comparison_by_field: Array<{ field: keyof DisclosurePermissionVector; comparison: 'within_ceiling' | 'downgraded' | 'exceeds_ceiling'; }>; aggregate_comparison: 'within_ceiling' | 'downgraded' | 'exceeds_ceiling'; } ``` Define vector order: ```text A <= B iff: for each boolean field, A[field] implies B[field]; rank(A.count_disclosure_mode) <= rank(B.count_disclosure_mode); rank(A.max_summary_fidelity) <= rank(B.max_summary_fidelity); any A template/ref is allowed by B's corresponding ref policy. ``` --- ### QF14 — Restamp decision requires the MFC it produces `PolicyStampRestampBase` requires `memory_flow_certificate_ref`, while the prose says the restamp decision produces an E0 RestampMFC. That creates phase-order ambiguity. **Patch:** ```typescript interface PolicyStampRestampDecisionBase extends E0DurableRecord { restamp_id: PolicyStampRestampRef; schema_owner: 'DOC81'; object_ref: MemoryObjectRef; prior_stamp_ref: PolicyStampRef; root_stamp_ref: PolicyStampRef; prior_freshness_key: PolicyRuntimeFreshnessKey; new_freshness_key: PolicyRuntimeFreshnessKey; original_ceiling_ref: PolicyCeilingRef; authority: RestampAuthority; ceiling_comparisons: Array; reason_codes: ReasonCodeId[]; } interface PolicyStampRestampIssuance extends E0DurableRecord { issuance_id: Brand; schema_owner: 'DOC81'; restamp_ref: PolicyStampRestampRef; memory_flow_certificate_ref: MemoryFlowCertificateId; issued_at: string; ceiling_compliance_attested: true; reason_codes: ReasonCodeId[]; } ``` --- ### QF15 — Cache hashes need canonicalization rules The cache key names `contributing_decision_hash` and `obligation_set_hash`, but not how to compute them. **Patch:** ```typescript interface CanonicalHashSpec { hash_algorithm: 'sha256'; serialization: 'json_canonical_sorted_keys_utf8'; array_ordering: | 'sort_by_branded_ref_then_content_hash' | 'preserve_semantic_order'; include_schema_version: true; include_freshness_key: true; } ``` For decisions: ```text contributing_decision_hash = sha256(canonical_json(sort_by(decision_id, [ decision_id, object_ref, action, destination, policy_evaluation_context_ref, valid_from, valid_to, all five axes, disclosure_vector, obligations, freshness_key, reason_codes ]))) ``` For obligations: ```text obligation_set_hash = sha256(canonical_json(sort_by(obligation_id, [ obligation_id, obligation_kind, applies_to_actions, enforcement_owner, blocking, typed parameters, freshness_key, reason_code ]))) ``` Add: ```text cache.hash_without_canonical_serialization cache.hash_excluded_disclosure_vector cache.hash_excluded_valid_time_window ``` --- ### QF16 — Safe-label vocabulary version should be conditionally required `PolicyRuntimeFreshnessKey.safe_label_vocabulary_version` is optional. But safe-label-bearing artifacts like `PolicyUIExport` carry `safe_label_policy_ref`, label refs, and safe-label constraints. **Patch:** ```text If an artifact carries any of: safe_label_policy_ref, default_label_ref, inspector_label_ref, internal_suppressed_manifest_label_ref, reason_summary_template_ref, disclosure_class ∈ {generic_safe_label_only, redacted_summary} then freshness_key.safe_label_vocabulary_version is REQUIRED. ``` Add lint: ```text cache.safe_label_artifact_missing_vocab_version ``` --- ### QF17 — Action predicates are defined but not called by `meet_v2` §4.7 defines action closure and `ActionPermissionPredicate`, but `meet_v2` returns after assemble without an explicit action-predicate evaluation. **Patch:** ```text # 7. Action predicate and closure gate. closure = ACTION_CLOSURE[request.action] require all closure.required_policy_actions have effective_policy_refs predicate = ACTION_PERMISSION_PREDICATE[request.action] permission = evaluateActionPredicate(eff, predicate) if permission.disposition != 'permitted': return blockedBottomPolicy(permission.reason_code) return OK(assemble(eff, obligations, conflicts, request, permission)) ``` Schema: ```typescript interface ActionPermissionResult { action: MemoryPolicyAction; disposition: | 'permitted' | 'blocked_axis_below_required_minimum' | 'blocked_forbidden_axis_value' | 'blocked_missing_prerequisite_policy_action' | 'blocked_missing_egress_attestation' | 'requires_obligation_discharge' | 'requires_disambiguation'; predicate_ref: ActionPermissionPredicateRef; reason_codes: ReasonCodeId[]; } ``` Add to `EffectiveMemoryPolicy`: ```typescript action_permission_result_ref?: Brand; ``` --- ## 4. More under-specification issues worth fixing ### GAP — `contextCompatible` needs exact compatibility rules `meet_v2` depends on `contextCompatible(d, request)`, but the function is not defined. It must define equality/compatibility for principal, surface, exposure context, model class, client kind, interaction mode, destination, scope resolution, freshness key, and user-instruction policy bounds. **Patch:** ```typescript interface PolicyContextCompatibilityResult { compatible: boolean; exclusion_reason?: | 'wrong_object' | 'wrong_action' | 'wrong_destination' | 'wrong_principal' | 'wrong_surface' | 'wrong_exposure_context' | 'wrong_model_class' | 'wrong_client_kind' | 'wrong_interaction_mode' | 'stale_freshness_key' | 'wrong_scope_resolution' | 'instruction_policy_bound_mismatch'; reason_codes: ReasonCodeId[]; } ``` Rule: exact match unless a domain profile explicitly declares a safe widening relation. Unknown model/client context should never widen compatibility. --- ### GAP — Principal scoping remains optional in cache/scope places `ScopeResolutionResult.principal_ref` is optional, and `ScopeResolutionCacheKey.principal_ref` is optional. R-5 says internal use means same principal or principals authorized for the scope, not any user in a shared database. The contracts should not allow omission by accident. **Patch:** ```typescript type PrincipalScope = | { principal_scope_kind: 'principal_scoped'; principal_ref: PrincipalRef } | { principal_scope_kind: 'system_global'; reason_code: ReasonCodeId }; principal_scope: PrincipalScope; ``` Use it in `ScopeResolutionResult`, `ScopeResolutionCacheKey`, `EffectivePolicyCacheKey`, exports, and portability bundles. --- ### GAP — Legal hold state should be a discriminated union `LegalHoldState.state` is `'held' | 'released'`, with `release_clearance_ref?` optional. A released hold should always have release authorization. **Patch:** ```typescript type LegalHoldStateRecord = LegalHoldHeld | LegalHoldReleased; interface LegalHoldBase extends E0DurableRecord { hold_id: LegalHoldStateRef; schema_owner: 'DOC81'; selector: LegalHoldSelector; placed_at: string; placed_by: PrincipalRef; reason_codes: ReasonCodeId[]; } interface LegalHoldHeld extends LegalHoldBase { state: 'held'; release_clearance_ref?: never; released_at?: never; } interface LegalHoldReleased extends LegalHoldBase { state: 'released'; release_clearance_ref: LegalHoldClearanceRef; released_at: string; } ``` Also change `LegalHoldClearance.hold_ref: string` to `hold_ref: LegalHoldStateRef`. --- ### BUG — Legal-hold destructive-effect enum may overblock non-destructive suppression/archive `DestructiveJobLegalHoldGate.destructive_effect` includes `archive_or_suppress`. But R-1 says legal holds block destruction only, never use. The cascade text also says held objects are invalidated/suppressed but not destroyed. If “archive_or_suppress” is treated as destructive, a hold may block non-destructive suppression that is supposed to remain allowed. **Patch:** ```typescript type DestructiveEffect = | 'hard_destruction' | 'redaction' | 'retention_expiry' | 'materialization_clearing' | 'semantic_folding'; type NonDestructiveVisibilityEffect = | 'archive' | 'suppress' | 'safe_label' | 'tombstone'; interface DestructiveJobLegalHoldGate extends E0DurableRecord { destructive_effect?: DestructiveEffect; non_destructive_visibility_effect?: NonDestructiveVisibilityEffect; legal_hold_query_required: true; } ``` Rule: legal hold clearance is required only for destructive effects, not for suppression/safe-label/tombstone visibility changes. --- ## 5. Revised severity list ### Blocking 1. Domain profile disclosure scalar can be overwritten because it lacks a vector. 2. Destination crosswalk floor/attestation not wired into `meet_v2`. 3. Sticky prior omitted from cache key. 4. Floor/prior/malformed/crosswalk vector paths not centralized with scalar paths. 5. R-1 empty-applicable internal-use path remains a fifth hard block. 6. Typed obligation union incomplete. ### Substantive 7. `deriveDisclosureClass` under-classifies source-title/exact-count/reason cases. 8. `meetDisclosureVectors` drops rendering refs. 9. `user_disambiguation_candidate` can leak wall existence unless cause-parameterized. 10. Lifecycle/derived split contradicted by schemas. 11. Protection-state enum mismatch and rank conflation. 12. Floor effect ref and domain threshold primary IDs are malformed. 13. Stamp invalidation notice required even when no notice is permitted. 14. Scope confidence required-component selection undefined. 15. Coverage proof denominator/uniqueness math incomplete. 16. Threshold comparator/equality semantics too generic. 17. Contamination-risk confidence unused. 18. Equivalence spanning-binding confidence selection undefined. 19. Collection ambiguity math undefined and empty-mode contradiction exists. 20. Quota estimates/progress/chunking not mathematically specified. 21. Cascade completion boolean lacks formula. 22. Last-active-support-edge lawful denominator predicate undefined. 23. Restamp disclosure-vector ceiling comparison undefined. 24. Restamp decision/certificate phase-order ambiguity. 25. Cache hash canonicalization undefined. 26. Safe-label vocabulary version optional where it should be conditionally required. 27. Action predicates defined but not called by `meet_v2`. 28. Context compatibility helper undefined. 29. Principal scoping optional in Phase-2-sensitive cache/scope objects. 30. Legal-hold release/clearance should be discriminated. 31. Legal-hold destructive-effect enum may overblock non-destructive suppression/archive. --- ## 6. What I would do next I would not add more broad schemas first. I would do a **DOC81 R2.1 executable-functions patch** with four edit clusters: 1. **PolicyEffectiveState pipeline:** domain contribution, decisions, crosswalk, floors, prior, obligations, vector/scalar finalization, action predicates. 2. **Quantitative helper pack:** confidence, coverage, thresholds, risk, equivalence confidence, collection ambiguity, cache canonical hashes. 3. **Lifecycle/state transition pack:** derived projections, legal hold, restamp issuance, cascade completion, quota progress. 4. **Seed confirmation pack:** local-file export floor, contamination thresholds, traversal depth, protection-rank effects, topic ambiguity thresholds. After that, I would run a narrower re-review with a fixed checklist: “every table has a consumer, every metric has a formula, every hash has canonicalization, every state has transition rules, every optional field has a condition.”