core/docs/decisions/ADR-0148-vault-promotion-policy-wiring.md
Shay 0b940674c0
feat(W-003): wire VaultPromotionPolicy into turn boundary (ADR-0148) (#272)
* feat(W-003): wire VaultPromotionPolicy into turn boundary (ADR-0148)

VaultPromotionPolicy had zero callers; vault entries never crystallized
from SPECULATIVE to COHERENT.  This PR wires the policy at the turn
boundary so settled entries can promote automatically.

Changes:
- core/config.py: add vault_promotion_enabled flag (default False, null-drop)
- vault/store.py: add promote_eligible_entries(policy) — metadata-only scan,
  versors unchanged, _matrix_cache not invalidated
- session/context.py: persist energy_raw/energy_class/coherence_residual in
  vault payload inside finalize_turn so the policy has data to decide on
- chat/runtime.py: call promote_eligible_entries after each finalize_turn,
  gated on vault_promotion_enabled; import VaultPromotionPolicy
- docs/decisions/ADR-0148-vault-promotion-policy-wiring.md: decision record
- tests/test_adr_0148_vault_promotion.py: 6 tests, all green

Unlocks W-007 (DerivedRecognizer derivation from COHERENT vault entries).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(W-003): resolve Pyright errors on vault promotion wiring

- vault/store.py: add TYPE_CHECKING guard to import VaultPromotionPolicy
  only at type-check time, avoiding circular import at runtime while
  making the name resolvable to Pyright.
- session/context.py:262: suppress union-attr false positive — self.state
  is guarded non-None by the raise at line 256 when input_versor is also
  None, but Pyright cannot narrow through the nested ternary structure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:57:00 -07:00

3.8 KiB

ADR-0148 — Wire VaultPromotionPolicy into turn boundary

Status: Accepted Date: 2026-05-25 Work item: W-003


Context

VaultPromotionPolicy (introduced in ADR-0014, implemented at core/physics/learning.py) decides whether a stored vault entry should be promoted from SPECULATIVE to COHERENT based on its energy profile.

Prior to this ADR, the policy had zero callers. Every vault entry written by session/context.py remained SPECULATIVE indefinitely, regardless of how settled or coherent the underlying field region was.

This blocked W-007 (DerivedRecognizer derivation), which requires COHERENT vault entries to serve as valid recognition anchors.


Decision

1. Flag in RuntimeConfig (core/config.py)

vault_promotion_enabled: bool = False

Default False enforces the null-drop invariant: zero behavior change when disabled. Operators opt in explicitly.

2. Energy metadata persisted at store time (session/context.py)

In finalize_turn(), after _anchor_pull() resolves oriented_state, the energy fields are written into the vault payload before vault.store():

if oriented_state.energy is not None:
    payload["energy_raw"] = float(oriented_state.energy.raw)
    payload["energy_class"] = oriented_state.energy.energy_class.value
    payload["coherence_residual"] = float(oriented_state.energy.coherence_residual)

Storing raw scalars (not the EnergyProfile object) keeps the payload JSON-serializable and avoids coupling the vault to the energy dataclass.

3. VaultStore.promote_eligible_entries(policy) (vault/store.py)

New method scans all SPECULATIVE entries. For each entry:

  1. Parses the stored epistemic_status string.
  2. If SPECULATIVE, reconstructs a minimal EnergyProfile from the stored energy_raw, energy_class, coherence_residual fields.
  3. Calls policy.decide(energy).
  4. If decision.promote, updates epistemic_status to COHERENT in-place.

Versors are not touched. _matrix_cache is not invalidated because no versor changes — only metadata mutates. Deterministic recall is unaffected.

4. Promotion fires post-finalize in chat/runtime.py

After each finalize_turn() call in chat():

if self.config.vault_promotion_enabled:
    self._context.vault.promote_eligible_entries(VaultPromotionPolicy())

Why post-finalize, not at store time?

A freshly stored entry is always E2+ (new activation, high recency). The VaultPromotionPolicy promotes only E0/E1 entries (vault_candidate=True). A just-written entry will not promote on the same turn it was written — it needs to cool across subsequent turns. This is the correct multi-turn crystallization behavior described in ADR-0014.


Consequences

Positive

  • Vault entries can now crystallize: SPECULATIVE regions that settle over multiple turns become COHERENT, making them admissible as evidence under min_status=EpistemicStatus.COHERENT recall.
  • W-007 (DerivedRecognizer derivation from promoted entries) is now unblocked.
  • Zero coupling change when vault_promotion_enabled=False (default).

Constraints preserved

  • versor_condition invariant: no versor is modified during promotion. promote_eligible_entries mutates only _metadata dicts.
  • No normalization: vault/store.py is a forbidden normalization site per CLAUDE.md. Promotion is a metadata-only operation — it does not repair, reproject, or normalize any field.
  • No approximate recall: CGA inner-product scoring is unchanged.
  • Reviewed learning path: promotion upgrades epistemic_status on already-stored entries; it does not inject new content or bypass the teaching review gate.

Unlocks

  • W-007 — DerivedRecognizer can now query the vault at min_status=EpistemicStatus.COHERENT and receive crystallized entries as recognition anchors.