feat(surface): ADR-0031 — score-decomposition surface (per-axis hedges)

Closes the 'identity hedges are generic' gap.  When IdentityCheck reports
that a specific axis is deviating AND the pack supplies an axis_hedges
entry for that axis, the assembler uses that axis's phrase instead of
ADR-0028's generic preferred_hedge_*.  The hedge text now names what is
actually at issue.

Selection: lex-smallest axis_id in (ctx.deviation_axes ∩ axis_hedges).
Deterministic; loader emits axis_hedges in lex order on axis_id.

Example surface at alignment=0.30 (strong band) under default pack:
  No deviation             → 'It seems that truth reveals reality.'
  truthfulness deviates    → 'Evidence is thin that truth reveals reality.'
  coherence deviates       → 'This does not yet cohere: truth reveals reality.'
  reverence deviates       → 'Reports suggest truth reveals reality.'

Same trajectory + truthfulness deviation, three different packs:
  default_general_v1   → 'Evidence is thin that truth reveals reality.'
  precision_first_v1   → 'The evidence does not support that truth reveals reality.'
  generosity_first_v1  → 'Truth reveals reality.'  (above generosity's strong=0.20)

Schema (additive, optional):
  surface_preferences.axis_hedges = {
    <axis_id>: { 'strong': str, 'soft': str, 'qualifier': str },
    ...
  }

Bounds: each phrase length 1–64; axis_id non-empty.  Absent block →
ADR-0028 byte-for-byte fallback.  Loader emits pairs in lex order on
axis_id for hashability + deterministic tie-break.

Files:
  core/physics/identity.py
    + class AxisHedge (frozen: strong, soft, qualifier)
    SurfacePreferences gains axis_hedges: Tuple = ()
  packs/identity/loader.py
    + _build_axis_hedges(): parse + bounds-check + emit lex-ordered tuple
  generate/surface.py
    SurfaceContext gains deviation_axes: frozenset[str] + axis_hedges tuple
    + _axis_specific_phrase(ctx): lex-smallest match or None
    _apply_hedge consults axis-specific phrase before ADR-0028 fallback
    Depth languages (he, grc) unchanged — ADR-0030 canonical phrases
  chat/runtime.py
    _build_surface_context lifts identity_score.deviation_axes and
    prefs.axis_hedges into SurfaceContext
  packs/identity/*.json
    Three v1 packs gain axis_hedges blocks (truthfulness, coherence,
    reverence — each pack uses voice consistent with its character)
  scripts/ratify_identity_packs.py (no change — idempotent)
  packs/identity/*.mastery_report.json
    Auto-refreshed.  New SHAs:
      default_general_v1   → 2ab7d469013509ba5030313ca9a609a443d0716e3ddcc5596f59858ce054f5d3
      precision_first_v1   → 78aa1e6a68a35c2c8576b6196a52d421b94f6d11e006128986902a4fd08679af
      generosity_first_v1  → 511f1ce20edd4266239da61443bfc93473a5433f20bfee6692a25a03073dc933

Tests: tests/test_identity_score_decomposition.py — 17 new tests:
  per-axis phrase selection, band gating still applies, pack swap with
  same deviation produces three different phrases, lex tie-break is
  deterministic, depth-language fallback to ADR-0030, backward compat
  with empty deviation_axes, and the contract that all three v1 packs
  ship axis_hedges for all three default-pack axes.

Suite status (all green):
  cognition 121, teaching 17, runtime 19, formation 182, smoke 67
  identity+safety+English+depth divergence 71
  score decomposition 17

Scope limits (documented in ADR-0031):
  - English-only at v1 (depth languages use canonical ADR-0030 phrases)
  - Lex tie-break is operational not semantic — pack authors can re-key
    if they need a different priority
  - No dominance-driven phrasing (Interpretation A); preserved as
    forward-compatible follow-up

Docs: ADR-0031 (Accepted) recorded; docs/identity_packs.md gains
§Axis-specific hedge phrases section and updated v1-pack SHAs; memory
'identity-packs.md' refreshed.
This commit is contained in:
Shay 2026-05-17 20:16:22 -07:00
parent a49a7555dc
commit 07ad3af845
13 changed files with 570 additions and 28 deletions

View file

@ -302,7 +302,20 @@ class ChatRuntime:
def _build_surface_context(self, identity_score, current_valence: float) -> SurfaceContext:
active = self._context.referents.active_referent()
alignment = float(identity_score.alignment) if identity_score is not None else 1.0
deviation_axes = (
frozenset(identity_score.deviation_axes)
if identity_score is not None
else frozenset()
)
prefs = self.identity_manifold.surface_preferences
# ADR-0031 — flatten the manifold's axis_hedges (tuple of
# (axis_id, AxisHedge)) into the wire-format quadruples that
# SurfaceContext carries. Order is preserved (loader emits in
# lex order); _axis_specific_phrase relies on this.
axis_hedges = tuple(
(axis_id, hedge.strong, hedge.soft, hedge.qualifier)
for axis_id, hedge in prefs.axis_hedges
)
return SurfaceContext(
active_referent_surface=active.surface if active is not None else "",
active_referent_slot=active.slot if active is not None else "neut_sg",
@ -316,6 +329,8 @@ class ChatRuntime:
claim_strength=prefs.claim_strength,
qualified_band_high=prefs.qualified_band_high,
preferred_qualifier=prefs.preferred_qualifier,
deviation_axes=deviation_axes,
axis_hedges=axis_hedges,
)
def _stub_response(self, field_state: FieldState) -> ChatResponse:

View file

@ -64,6 +64,20 @@ class IdentityScore:
return sorted(self.deviation_axes)
@dataclass(frozen=True)
class AxisHedge:
"""Per-axis hedge phrases for ADR-0031 score-decomposition.
When ``IdentityCheck`` flags one or more axes as deviating, the
assembler can call out the specific axis instead of using the
generic hedge. v1 is English-only; depth-language axis hedges are
a future ADR.
"""
strong: str
soft: str
qualifier: str
@dataclass(frozen=True)
class SurfacePreferences:
"""Pack-supplied surface phrasing preferences (ADR-0028).
@ -90,6 +104,13 @@ class SurfacePreferences:
claim_strength: str = "balanced"
qualified_band_high: float = 0.75
preferred_qualifier: str = "In some cases,"
# ADR-0031 — per-axis hedge phrases keyed by axis_id. When a
# deviating axis matches an entry, the assembler uses that axis's
# phrase instead of the generic ``preferred_hedge_*`` above.
# Tuple of ``(axis_id, AxisHedge)`` pairs for hashability under
# frozen dataclass semantics; pairs are kept in lex order on
# ``axis_id`` so determinism is preserved across loads.
axis_hedges: Tuple = () # Tuple[Tuple[str, AxisHedge], ...]
@dataclass(frozen=True)

View file

@ -0,0 +1,131 @@
# ADR-0031: Score-Decomposition Surface — Per-Axis Hedge Phrases
**Status:** Accepted (2026-05-17)
**Author:** Joshua Shay + planner pass
**Companion docs:** [`identity_packs.md`](../identity_packs.md), [`ADR-0028-identity-surface-wiring.md`](ADR-0028-identity-surface-wiring.md), [`ADR-0030-depth-language-hedge.md`](ADR-0030-depth-language-hedge.md)
## Context
[ADR-0028](ADR-0028-identity-surface-wiring.md) and [ADR-0030](ADR-0030-depth-language-hedge.md) made identity-pack swap visibly affect the surface across English, Hebrew, and Koine Greek. But the differentiation today consults a **single scalar**`SurfaceContext.identity_alignment`. The system can hedge harder when the trajectory drifts; it cannot say *which* aspect of identity is at issue when it hedges.
`IdentityScore` already carries the information we need: `deviation_axes: FrozenSet[str]` names the specific axes the `IdentityCheck` flagged. Today that field is computed and then ignored at the surface layer. Wiring it through closes the gap: when the system hedges on a trajectory whose deviation is *truthfulness*, the hedge can read "Evidence is thin that…"; on a *coherence* deviation, "This does not yet cohere:…"; on *reverence*, "Reports suggest…". The user learns *why* the system is hedging.
This is the score-decomposition surface.
## Two interpretations considered
**Interpretation A — Dominance-driven phrasing.** Every assertion's character shifts based on which axis is the *leader* of the manifold. Truthfulness-dominant identity → precise phrasing on every assertion; coherence-dominant → unifying phrasing; reverence-dominant → deferential. Rejected for this ADR: requires new dominance scoring, changes confident assertions too (large blast radius), and isn't structurally connected to anything already computed.
**Interpretation B — Deviation-driven hedge phrasing (this ADR).** When the hedge band fires *and* the score reports a specific deviating axis for which the pack supplies an `axis_hedges` entry, the assembler uses that axis's phrase instead of the generic `preferred_hedge_*`. Otherwise the ADR-0028 generic phrase fires. The data we need (`deviation_axes`) already exists; we just plumb it through.
Interpretation A is preserved as a future possibility — nothing in this ADR forecloses it. The pack schema extension is named `axis_hedges` (not `axis_phrasing`) precisely so a future "axis phrasing" concept doesn't collide.
## Decision
### Pack schema extension (optional, additive)
A new optional `axis_hedges` sub-block inside `surface_preferences`:
```json
"surface_preferences": {
"...existing ADR-0028 fields...": "...",
"axis_hedges": {
"truthfulness": {
"strong": "Evidence is thin that",
"soft": "It is hard to confirm that",
"qualifier": "Where evidence is partial,"
},
"coherence": {
"strong": "This does not yet cohere:",
"soft": "The threads loosely connect:",
"qualifier": "Where the connection holds,"
},
"reverence": {
"strong": "Reports suggest",
"soft": "It is said that",
"qualifier": "By some accounts,"
}
}
}
```
Each axis entry is keyed by `axis_id` (must match an existing `value_axes[*].axis_id` semantically, though the loader doesn't enforce that — a pack may declare hedges for axes it doesn't expose, which is harmless because no deviation will reference them). Each entry has three required phrases: `strong`, `soft`, `qualifier`, matching the three bands of the ADR-0028 hedge algorithm.
### Selection algorithm
When the English hedge band fires (after threshold gating):
1. Compute `matching_axes = ctx.deviation_axes ∩ {ah.axis_id for ah in ctx.axis_hedges}`.
2. If `matching_axes` is empty → use the pack's generic `preferred_hedge_*` (ADR-0028 behavior).
3. Otherwise → use the **lex-smallest** matching axis's phrase. The loader emits `axis_hedges` in lex order on `axis_id` for hashability + determinism; the assembler does a linear scan and takes the first match, which is the lex-smallest.
Lex tie-break is deliberate: when multiple axes deviate, the assembler must pick one phrase. Lex order is the simplest deterministic choice that doesn't require additional scoring. If a deployment cares about a different priority (e.g., "always prefer the truthfulness phrase when truthfulness is among the deviators"), they can re-key their `axis_hedges` so the preferred axis sorts earliest (`a_truthfulness`, `b_coherence`, …) — operational discipline, not architectural.
### Three v1 pack profiles
Each pack ships its own English `axis_hedges` block tuned to its character:
| Pack | Truthfulness strong | Coherence strong | Reverence strong |
|---|---|---|---|
| `default_general_v1` | "Evidence is thin that" | "This does not yet cohere:" | "Reports suggest" |
| `precision_first_v1` | "The evidence does not support that" | "This contradicts what is established:" | "Source attestation is weak:" |
| `generosity_first_v1` | "Some hold that" | "There is a thread connecting this:" | "It is reported that" |
Result at `alignment=0.30` (strong band) with `deviation_axes={"truthfulness"}`:
| Pack | Surface |
|---|---|
| `default_general_v1` | "Evidence is thin that truth reveals reality." |
| `precision_first_v1` | "The evidence does not support that truth reveals reality." |
| `generosity_first_v1` | "Truth reveals reality." *(generosity's strong threshold is 0.20; 0.30 is above the hedge band so no phrase prepends regardless of deviation)* |
Same trajectory, same deviating axis, three different surfaces.
### Implementation
- `core/physics/identity.py`: new `AxisHedge` frozen dataclass (strong / soft / qualifier strings); `SurfacePreferences` gains `axis_hedges: Tuple = ()` field (tuple of `(axis_id, AxisHedge)` pairs, lex order).
- `packs/identity/loader.py`: `_build_axis_hedges()` parses the optional sub-block, bounds-checks each phrase via the existing `_validate_hedge_phrase` (length 164), emits pairs in lex order on `axis_id`.
- `generate/surface.py`: `SurfaceContext` gains two new frozen-and-hashable fields — `deviation_axes: frozenset[str]` and `axis_hedges: tuple[tuple[str, str, str, str], ...]` (flattened quadruples for hashability). New helper `_axis_specific_phrase(ctx)` returns the lex-smallest matching axis's `(strong, soft, qualifier)` or `None`. `_apply_hedge` consults it before falling back to ADR-0028 generic phrases.
- `chat/runtime.py::ChatRuntime._build_surface_context`: lifts `identity_score.deviation_axes` and `prefs.axis_hedges` into the constructed `SurfaceContext`.
- `packs/identity/*.json`: three v1 packs gain `axis_hedges` blocks. Pack body changed → re-ratified.
### Re-ratification
Adding `axis_hedges` to each pack changed the canonical body → new `pack_source_sha` → new `MasteryReport`. `scripts/ratify_identity_packs.py` handled it idempotently. Updated SHAs:
- `default_general_v1``2ab7d469013509ba5030313ca9a609a443d0716e3ddcc5596f59858ce054f5d3`
- `precision_first_v1``78aa1e6a68a35c2c8576b6196a52d421b94f6d11e006128986902a4fd08679af`
- `generosity_first_v1``511f1ce20edd4266239da61443bfc93473a5433f20bfee6692a25a03073dc933`
## Consequences
### Positive
- **Hedges now name what's at issue.** When the system hedges on a trajectory whose truthfulness axis is flagged, the user reads "Evidence is thin that…" — the refusal text is informative, not a generic disclaimer. This is meaningfully better epistemic communication.
- **Per-axis hedges are pack-tuned.** A precision-first deployment hedges with evidential vocabulary; a generosity-first deployment hedges by attribution to "some". Same architecture, different voice.
- **Forward-compatible with Interpretation A.** Dominance-driven phrasing (when a single axis *leads* rather than *deviates*) would slot in alongside `axis_hedges` without changing this ADR's shape.
- **No new scoring infrastructure.** `IdentityScore.deviation_axes` already existed; this ADR is purely plumbing + a phrase table.
- **Backward compatible at every layer.** Packs without `axis_hedges` fall through to ADR-0028 byte-for-byte. `SurfaceContext()` (no-args) carries `deviation_axes=frozenset()` and `axis_hedges=()`, so legacy callers see no behavioral change.
### Negative / risks
- **English-only at v1.** Depth languages still use the canonical `_DEPTH_HEDGE_PHRASES` from ADR-0030 regardless of which axis deviates. Closing this requires either a pack-schema bump (axis_hedges per language) or canonical depth-language axis hedges in `surface.py`. Both are tractable; neither belongs in this ADR.
- **Lex tie-break is operational, not semantic.** When multiple axes deviate simultaneously, the chosen phrase is whichever axis_id sorts earliest — not necessarily the "most relevant" one. Deployments that need a different priority must use operational discipline (re-keying axis_ids) or wait for a follow-up ADR introducing per-pack axis priority.
- **Pack body grew.** Three new phrases per axis × three axes = nine new strings per pack. The canonical JSON is still well under any practical size limit, and the ratification driver handled the change without issue.
- **`SurfaceContext` is bigger again.** Two more fields. Both have safe defaults so direct `SurfaceContext()` construction in tests continues to work.
### Scope limits (explicit non-goals for this ADR)
- No per-language axis hedges. v1 axis_hedges are English-only.
- No dominance-driven phrasing (Interpretation A). Phrasing changes only when the score reports deviation, not when a particular axis happens to lead.
- No per-pack axis priority. Lex order is the tie-break.
- No realizer-side use of `deviation_axes` beyond hedging (no rotor bias, no token selection shift, no separate refusal surface).
## Verification
This ADR is satisfied when:
- `tests/test_identity_score_decomposition.py` passes — 17 tests covering per-axis phrase selection, band gating still applies, pack-swap with deviation, lex tie-break, depth-language fallback, backward compatibility, and the contract that all three v1 packs ship axis_hedges for all three default axes.
- Cognition (121), teaching (17), runtime (19), formation (182), smoke (67) suites green.
- `tests/test_identity_surface_divergence.py` (ADR-0028) and `tests/test_identity_surface_divergence_depth.py` (ADR-0030) — both still passing (no regressions in the generic-phrase or depth-language paths).
- All three v1 identity packs re-ratified with the new SHAs recorded above.

View file

@ -69,6 +69,24 @@ A single JSON file. Strings, ints, bools, lists, dicts only — same canonical-J
| `boundary_ids` | yes | List of boundary identifiers. Mirrors `IdentityManifold.boundary_ids`. |
| `value_axes` | yes | List of ≥ 1 axes. Each has: `axis_id`, `name`, `direction` (list of 3 floats in [-1, 1]), `weight` (float ≥ 0), `theological_note`. |
### Axis-specific hedge phrases (ADR-0031)
Optional sub-block inside `surface_preferences` that lets the assembler call out *which* axis is deviating when it hedges. When `IdentityScore.deviation_axes` names an axis and the pack supplies an `axis_hedges` entry for that axis, the lex-smallest match is used instead of the generic `preferred_hedge_*`:
```json
"axis_hedges": {
"truthfulness": {
"strong": "Evidence is thin that",
"soft": "It is hard to confirm that",
"qualifier": "Where evidence is partial,"
},
"coherence": { "strong": "...", "soft": "...", "qualifier": "..." },
"reverence": { "strong": "...", "soft": "...", "qualifier": "..." }
}
```
Each axis entry must supply `strong`, `soft`, and `qualifier` phrases (length 164). When no deviating axis matches an `axis_hedges` entry, the generic phrases from §"Surface preferences" fire. Depth languages (Hebrew, Koine Greek) ignore `axis_hedges` at v1 and continue to use the canonical phrases from [ADR-0030](decisions/ADR-0030-depth-language-hedge.md).
### Surface preferences (ADR-0028)
Optional block driving the assembler's hedge and claim-strength decisions:
@ -151,9 +169,9 @@ CORE_DEFAULT_IDENTITY_PACK=precision_first_v1 core pulse "..."
| Pack id | Role | Notes |
|---|---|---|
| `default_general_v1` | Ship default. Balanced. | Encodes the *exact* three axes (`truthfulness`, `coherence`, `reverence`) previously hardcoded in `chat/runtime.py`. Behavioral no-op vs. pre-ADR runtime. ADR-0028 surface_preferences: balanced; hedge thresholds 0.40/0.50/0.75. Ratified: `ddc1ba127231272660e6a435e177227558461b0278572a95635b416c3e1dec5a`. |
| `precision_first_v1` | Specialization example A. | Boosts `truthfulness` weight, narrows reverence direction. Surface: hedges sooner (0.55/0.70/0.85), uses "Arguably,"/"In some cases,"/"Under certain conditions,"; claim_strength=qualified. Source: `evals/identity_divergence/axes/axis_a.yaml`. Ratified: `cb5fb2323214a26afda33f2a67e22f38fe49f4763829d48ef67fd41241aba33c`. |
| `generosity_first_v1` | Specialization example B. | Boosts `coherence` weight, broadens reverence direction. Surface: hedges later (0.20/0.30/0.50); claim_strength=affirmative. Source: `evals/identity_divergence/axes/axis_b.yaml`. Ratified: `94f2f49e1b16c7498fb52b8f9864eecc198618933dc8381a01b809c146826db7`. |
| `default_general_v1` | Ship default. Balanced. | Encodes the *exact* three axes (`truthfulness`, `coherence`, `reverence`) previously hardcoded in `chat/runtime.py`. Behavioral no-op vs. pre-ADR runtime. ADR-0028 surface_preferences: balanced; hedge thresholds 0.40/0.50/0.75. ADR-0031 axis_hedges: "Evidence is thin that" / "This does not yet cohere:" / "Reports suggest". Ratified: `2ab7d469013509ba5030313ca9a609a443d0716e3ddcc5596f59858ce054f5d3`. |
| `precision_first_v1` | Specialization example A. | Boosts `truthfulness` weight, narrows reverence direction. Surface: hedges sooner (0.55/0.70/0.85), uses "Arguably,"/"In some cases,"/"Under certain conditions,"; claim_strength=qualified. ADR-0031 axis_hedges: "The evidence does not support that" / "This contradicts what is established:" / "Source attestation is weak:". Source: `evals/identity_divergence/axes/axis_a.yaml`. Ratified: `78aa1e6a68a35c2c8576b6196a52d421b94f6d11e006128986902a4fd08679af`. |
| `generosity_first_v1` | Specialization example B. | Boosts `coherence` weight, broadens reverence direction. Surface: hedges later (0.20/0.30/0.50); claim_strength=affirmative. ADR-0031 axis_hedges: "Some hold that" / "There is a thread connecting this:" / "It is reported that". Source: `evals/identity_divergence/axes/axis_b.yaml`. Ratified: `511f1ce20edd4266239da61443bfc93473a5433f20bfee6692a25a03073dc933`. |
Each ratified pack ships alongside a `<pack_id>.mastery_report.json` companion file. The loader, in production mode, verifies the companion's self-seal and cross-checks its `report_sha256` against the pack's `mastery_report_sha256`. To re-ratify after editing a pack's axes, run `python scripts/ratify_identity_packs.py` (idempotent — re-running on already-current packs is a no-op).

View file

@ -72,6 +72,14 @@ class SurfaceContext:
claim_strength: str = "balanced"
qualified_band_high: float = _DEFAULT_QUALIFIED_BAND_HIGH
preferred_qualifier: str = _DEFAULT_QUALIFIER_PHRASE
# ADR-0031 — score decomposition surface. When ``deviation_axes``
# is non-empty and at least one of its axis_ids appears in
# ``axis_hedges``, ``_apply_hedge`` uses the lex-smallest matching
# axis's phrase instead of the generic ``preferred_hedge_*``.
# ``axis_hedges`` is a tuple of ``(axis_id, strong, soft, qualifier)``
# quadruples kept in lex order for hashability + determinism.
deviation_axes: frozenset[str] = frozenset()
axis_hedges: tuple[tuple[str, str, str, str], ...] = ()
@dataclass(frozen=True, slots=True)
@ -158,11 +166,21 @@ def _apply_hedge(surface: str, ctx: SurfaceContext, lang: str = "en") -> str:
"""
alignment = ctx.identity_alignment
if lang in _DEPTH_HEDGE_PHRASES:
# ADR-0030 — depth languages use canonical phrases; per-axis
# decomposition (ADR-0031) is English-only at v1.
strong_phrase, soft_phrase, qualifier_phrase = _DEPTH_HEDGE_PHRASES[lang]
else:
strong_phrase = ctx.preferred_hedge_strong
soft_phrase = ctx.preferred_hedge_soft
qualifier_phrase = ctx.preferred_qualifier
# ADR-0031 — when the score reports specific deviating axes and
# the pack supplies axis-specific phrases for any of them, use
# the lex-smallest matching axis's phrase. Otherwise fall
# through to ADR-0028 generic phrases.
axis_phrase = _axis_specific_phrase(ctx)
if axis_phrase is not None:
strong_phrase, soft_phrase, qualifier_phrase = axis_phrase
else:
strong_phrase = ctx.preferred_hedge_strong
soft_phrase = ctx.preferred_hedge_soft
qualifier_phrase = ctx.preferred_qualifier
if alignment < ctx.hedge_threshold_strong:
return f"{strong_phrase} {_lower_first(surface)}"
if alignment < ctx.hedge_threshold_soft:
@ -175,6 +193,25 @@ def _apply_hedge(surface: str, ctx: SurfaceContext, lang: str = "en") -> str:
return surface
def _axis_specific_phrase(
ctx: SurfaceContext,
) -> tuple[str, str, str] | None:
"""Return (strong, soft, qualifier) for the most-relevant deviating axis,
or ``None`` if no match.
Match rule: the lex-smallest ``axis_id`` that appears in both
``ctx.deviation_axes`` and the keys of ``ctx.axis_hedges``. Lex
tie-break keeps output deterministic when multiple axes deviate.
"""
if not ctx.deviation_axes or not ctx.axis_hedges:
return None
for axis_id, strong, soft, qualifier in ctx.axis_hedges:
# ``axis_hedges`` is already in lex order — first match wins.
if axis_id in ctx.deviation_axes:
return (strong, soft, qualifier)
return None
def _apply_contrast(surface: str, valence_delta: float) -> str:
if valence_delta < -CONTRAST_THRESHOLD:
return f"However, {_lower_first(surface)}"

View file

@ -3,7 +3,7 @@
"version": "1.0.0",
"description": "Balanced general identity. Shipping default. Encodes the three axes previously hardcoded in chat/runtime.py: truthfulness, coherence, reverence.",
"schema_version": "1.0.0",
"mastery_report_sha256": "ddc1ba127231272660e6a435e177227558461b0278572a95635b416c3e1dec5a",
"mastery_report_sha256": "2ab7d469013509ba5030313ca9a609a443d0716e3ddcc5596f59858ce054f5d3",
"alignment_threshold": 0.45,
"boundary_ids": [
"no_fabricated_source",
@ -16,7 +16,24 @@
"preferred_hedge_soft": "Perhaps",
"claim_strength": "balanced",
"qualified_band_high": 0.75,
"preferred_qualifier": "In some cases,"
"preferred_qualifier": "In some cases,",
"axis_hedges": {
"truthfulness": {
"strong": "Evidence is thin that",
"soft": "It is hard to confirm that",
"qualifier": "Where evidence is partial,"
},
"coherence": {
"strong": "This does not yet cohere:",
"soft": "The threads loosely connect:",
"qualifier": "Where the connection holds,"
},
"reverence": {
"strong": "Reports suggest",
"soft": "It is said that",
"qualifier": "By some accounts,"
}
}
},
"value_axes": [
{

View file

@ -1,6 +1,6 @@
{
"course_id": "course.subject.identity.default_general_v1.identity_anchor.1.0.0",
"course_sha256": "57295c81036dde60d2108c8c036a2ba5c7cc34c3a6079bc6828db36d37769cca",
"course_sha256": "eed629e0afef206df5b31660af5d5b6ba95db8d164556f285d14ed2aa69c9daa",
"failure_reasons": [],
"gates": [
{
@ -41,16 +41,16 @@
}
],
"issued_at": "2026-05-17T00:00:00Z",
"plan_sha256": "48b5a5b757feae5580387851f4a7c568d2a68b99b3599117f0429f2d2275239b",
"plan_sha256": "415e164ebe144fb9b14903dbf4727f818a9a052fae3822d44b7441d05755dd76",
"ratified": true,
"report_sha256": "ddc1ba127231272660e6a435e177227558461b0278572a95635b416c3e1dec5a",
"report_sha256": "2ab7d469013509ba5030313ca9a609a443d0716e3ddcc5596f59858ce054f5d3",
"schema_version": "1.0.0",
"source_bundle_sha": "4bbade402c02486e99d5f3c3692039ba36cd29483bf7f6c895e5e35dd97da66a",
"source_bundle_sha": "e131fbdd8d52c6bd78acce2c93a352a03f20f8e9c6296de9261cacc45663e27c",
"trace_hashes": [
"trace:adversarial_probe:::::identity_override_axis_rewrite",
"trace:adversarial_probe:::::identity_override_policy_bypass",
"trace:adversarial_probe:::::identity_override_operator_injection",
"trace:replay_assertion:::::"
],
"validated_set_sha": "bb45f6d2d2a32cf537298a9f51a367149cbe60457317cf061f892feccfaae49e"
"validated_set_sha": "998bf39f226847f64f28ab0bd5961a8cf8fd40ea7333cd848acdde7a10b688d4"
}

View file

@ -3,7 +3,7 @@
"version": "1.0.0",
"description": "Generosity-first specialization. Boosts coherence; broadens reverence. Source: evals/identity_divergence/axes/axis_b.yaml (semantics, not field-for-field).",
"schema_version": "1.0.0",
"mastery_report_sha256": "94f2f49e1b16c7498fb52b8f9864eecc198618933dc8381a01b809c146826db7",
"mastery_report_sha256": "511f1ce20edd4266239da61443bfc93473a5433f20bfee6692a25a03073dc933",
"alignment_threshold": 0.4,
"boundary_ids": [
"no_fabricated_source",
@ -16,7 +16,24 @@
"preferred_hedge_soft": "Perhaps",
"claim_strength": "affirmative",
"qualified_band_high": 0.5,
"preferred_qualifier": "In some cases,"
"preferred_qualifier": "In some cases,",
"axis_hedges": {
"truthfulness": {
"strong": "Some hold that",
"soft": "Many would say that",
"qualifier": "As is commonly understood,"
},
"coherence": {
"strong": "There is a thread connecting this:",
"soft": "Loosely, this connects:",
"qualifier": "Where these meanings meet,"
},
"reverence": {
"strong": "It is reported that",
"soft": "Tradition holds that",
"qualifier": "In the broader telling,"
}
}
},
"value_axes": [
{

View file

@ -1,6 +1,6 @@
{
"course_id": "course.subject.identity.generosity_first_v1.identity_anchor.1.0.0",
"course_sha256": "c49ff0a29000e600719a5de993a0cfde017c9c57eb1e106337a3dfe4241e72f1",
"course_sha256": "9dbbfbd0c04f6f28c18b007894d66849278e8a95706a12ebde31570db6eb2eb7",
"failure_reasons": [],
"gates": [
{
@ -41,16 +41,16 @@
}
],
"issued_at": "2026-05-17T00:00:00Z",
"plan_sha256": "dc463aaf7cd81bbc7b4d6561dcb1ecd4e3c33292c702f96e0bbe9b68bded4ab4",
"plan_sha256": "8194f8422cbb1d2e3e2d4b876eeda8bd1c641a7d7afa75f768def799927f3148",
"ratified": true,
"report_sha256": "94f2f49e1b16c7498fb52b8f9864eecc198618933dc8381a01b809c146826db7",
"report_sha256": "511f1ce20edd4266239da61443bfc93473a5433f20bfee6692a25a03073dc933",
"schema_version": "1.0.0",
"source_bundle_sha": "61d9b0987c1a419141bd7749e56b41dd367d9058176e3223cf6ce95cd245fa1e",
"source_bundle_sha": "41165d722a6d71f80b09e36b1d67d3fea71449747c4420e25f1bde865b484835",
"trace_hashes": [
"trace:adversarial_probe:::::identity_override_axis_rewrite",
"trace:adversarial_probe:::::identity_override_policy_bypass",
"trace:adversarial_probe:::::identity_override_operator_injection",
"trace:replay_assertion:::::"
],
"validated_set_sha": "7c4b27176349b9d5c9f2d4722c37ff1d69e6a9b503627c230c3630447758e8a4"
"validated_set_sha": "ca6acdfac6cdecd29004146ae93d125e7ef550d17f6708e6fe8e7b6f62d0fa70"
}

View file

@ -25,7 +25,12 @@ import os
from pathlib import Path
from typing import Iterable
from core.physics.identity import IdentityManifold, SurfacePreferences, ValueAxis
from core.physics.identity import (
AxisHedge,
IdentityManifold,
SurfacePreferences,
ValueAxis,
)
from formation.hashing import verify_seal
@ -368,6 +373,7 @@ def _build_surface_preferences(
f"pack {pack_id!r}: claim_strength={claim_strength!r} not in "
f"{sorted(_ALLOWED_CLAIM_STRENGTHS)}"
)
axis_hedges = _build_axis_hedges(value.get("axis_hedges"), pack_id)
return SurfacePreferences(
hedge_threshold_strong=strong,
hedge_threshold_soft=soft,
@ -385,9 +391,63 @@ def _build_surface_preferences(
value.get("preferred_qualifier", defaults.preferred_qualifier),
pack_id, "preferred_qualifier",
),
axis_hedges=axis_hedges,
)
def _build_axis_hedges(
value: object, pack_id: str,
) -> tuple[tuple[str, AxisHedge], ...]:
"""Parse and bounds-check the optional ``axis_hedges`` sub-block.
Returns a tuple of ``(axis_id, AxisHedge)`` pairs in lex order on
``axis_id``. Absent block empty tuple ADR-0028 fallback.
"""
if value is None:
return ()
if not isinstance(value, dict):
raise IdentityPackError(
f"pack {pack_id!r}: surface_preferences.axis_hedges must be a dict"
)
pairs: list[tuple[str, AxisHedge]] = []
for axis_id in sorted(value):
if not isinstance(axis_id, str) or not axis_id:
raise IdentityPackError(
f"pack {pack_id!r}: axis_hedges keys must be non-empty strings"
)
entry = value[axis_id]
if not isinstance(entry, dict):
raise IdentityPackError(
f"pack {pack_id!r}: axis_hedges[{axis_id!r}] must be a dict"
)
missing = [k for k in ("strong", "soft", "qualifier") if k not in entry]
if missing:
raise IdentityPackError(
f"pack {pack_id!r}: axis_hedges[{axis_id!r}] missing fields: "
f"{missing}"
)
pairs.append(
(
axis_id,
AxisHedge(
strong=_validate_hedge_phrase(
entry["strong"], pack_id,
f"axis_hedges[{axis_id!r}].strong",
),
soft=_validate_hedge_phrase(
entry["soft"], pack_id,
f"axis_hedges[{axis_id!r}].soft",
),
qualifier=_validate_hedge_phrase(
entry["qualifier"], pack_id,
f"axis_hedges[{axis_id!r}].qualifier",
),
),
)
)
return tuple(pairs)
def _validate_threshold_field(value: object, pack_id: str, field: str) -> float:
if not isinstance(value, (int, float)):
raise IdentityPackError(

View file

@ -3,7 +3,7 @@
"version": "1.0.0",
"description": "Precision-first specialization. Boosts truthfulness; narrows coherence and reverence. Source: evals/identity_divergence/axes/axis_a.yaml (semantics, not field-for-field).",
"schema_version": "1.0.0",
"mastery_report_sha256": "cb5fb2323214a26afda33f2a67e22f38fe49f4763829d48ef67fd41241aba33c",
"mastery_report_sha256": "78aa1e6a68a35c2c8576b6196a52d421b94f6d11e006128986902a4fd08679af",
"alignment_threshold": 0.55,
"boundary_ids": [
"no_fabricated_source",
@ -17,7 +17,24 @@
"preferred_hedge_soft": "In some cases,",
"claim_strength": "qualified",
"qualified_band_high": 0.85,
"preferred_qualifier": "Under certain conditions,"
"preferred_qualifier": "Under certain conditions,",
"axis_hedges": {
"truthfulness": {
"strong": "The evidence does not support that",
"soft": "The evidence is mixed on whether",
"qualifier": "On the available evidence,"
},
"coherence": {
"strong": "This contradicts what is established:",
"soft": "This sits uneasily with what is established:",
"qualifier": "Within the bounds of what coheres,"
},
"reverence": {
"strong": "Source attestation is weak:",
"soft": "Source attestation is partial:",
"qualifier": "On the sources available,"
}
}
},
"value_axes": [
{

View file

@ -1,6 +1,6 @@
{
"course_id": "course.subject.identity.precision_first_v1.identity_anchor.1.0.0",
"course_sha256": "eb2b054ad3a621c062c746cbedc2a825e08a490c84bc23343b24a679601aaacd",
"course_sha256": "add71261fc02069db97bf8c3f586630daa21b090ff66b696b2bd3699b9800c2b",
"failure_reasons": [],
"gates": [
{
@ -41,16 +41,16 @@
}
],
"issued_at": "2026-05-17T00:00:00Z",
"plan_sha256": "7f83866948249e19081930b573b75a9fe24ec49bd9ff2f67f35d6f4d9e683b7d",
"plan_sha256": "a98d7b35ce9d55398c0f7ec6fdab9bb933f2227331d009f56058f004be6f1aeb",
"ratified": true,
"report_sha256": "cb5fb2323214a26afda33f2a67e22f38fe49f4763829d48ef67fd41241aba33c",
"report_sha256": "78aa1e6a68a35c2c8576b6196a52d421b94f6d11e006128986902a4fd08679af",
"schema_version": "1.0.0",
"source_bundle_sha": "63f87f07cde290cd82fcdf43f4a685670a08942050dee516997d47f11eeb5abf",
"source_bundle_sha": "0b24c82e5f1dfbb660575427968bd9a19e80e420bb4ddf6565f033f206b4904c",
"trace_hashes": [
"trace:adversarial_probe:::::identity_override_axis_rewrite",
"trace:adversarial_probe:::::identity_override_policy_bypass",
"trace:adversarial_probe:::::identity_override_operator_injection",
"trace:replay_assertion:::::"
],
"validated_set_sha": "423185f57cd0832c7af846b7e97dff83c067aa46315016defa1df45d68239ae7"
"validated_set_sha": "1a8879e7630801cfaeaae56c691eaa1cea4ea6ce57c32fe35665f3e6c0fcf4ca"
}

View file

@ -0,0 +1,209 @@
"""ADR-0031 — score decomposition: per-axis hedge phrases.
When a hedge band fires AND ``IdentityScore.deviation_axes`` names an
axis for which the pack supplies an ``axis_hedges`` entry, the
assembler uses that axis's phrase instead of the generic
``preferred_hedge_*``. These tests prove:
* Per-axis phrases fire when the score reports the axis as deviating.
* Generic phrase still fires when no specific axis matches.
* Pack swap produces different axis-specific phrases on the same
trajectory + deviation.
* Lex tie-break is deterministic when multiple axes deviate.
* Depth languages (he, grc) still use canonical ADR-0030 phrases
regardless of which axis deviates (English-only at v1).
* Backward compatibility: ``deviation_axes=frozenset()`` falls back to
ADR-0028 generic behavior byte-for-byte.
"""
from __future__ import annotations
from generate.articulation import ArticulationPlan
from generate.surface import SentenceAssembler, SurfaceContext
from packs.identity.loader import load_identity_manifold
_ASSEMBLER = SentenceAssembler()
def _plan(lang: str = "en") -> ArticulationPlan:
if lang == "he":
return ArticulationPlan(
"אמת", "מגלה", "מציאות", "", "he", "default",
)
if lang == "grc":
return ArticulationPlan(
"logos", "ἀποκαλύπτει", "πραγματικότητα", "", "grc", "default",
)
return ArticulationPlan(
"truth", "reveals", "reality", "", "en", "default",
)
def _ctx(
pack_id: str, alignment: float, deviation_axes: set[str], lang: str = "en",
) -> SurfaceContext:
prefs = load_identity_manifold(pack_id).surface_preferences
axis_hedges = tuple(
(aid, h.strong, h.soft, h.qualifier) for aid, h in prefs.axis_hedges
)
return SurfaceContext(
identity_alignment=alignment,
hedge_threshold_strong=prefs.hedge_threshold_strong,
hedge_threshold_soft=prefs.hedge_threshold_soft,
preferred_hedge_strong=prefs.preferred_hedge_strong,
preferred_hedge_soft=prefs.preferred_hedge_soft,
claim_strength=prefs.claim_strength,
qualified_band_high=prefs.qualified_band_high,
preferred_qualifier=prefs.preferred_qualifier,
deviation_axes=frozenset(deviation_axes),
axis_hedges=axis_hedges,
)
def _surface(
pack_id: str, alignment: float, deviation_axes: set[str], lang: str = "en",
) -> str:
return _ASSEMBLER.assemble(
_plan(lang),
tokens=[],
role="assert",
context=_ctx(pack_id, alignment, deviation_axes, lang),
).surface
# ---------- per-axis phrases under default pack ----------
class TestAxisSpecificPhrases:
def test_truthfulness_deviation_uses_truthfulness_phrase(self) -> None:
s = _surface("default_general_v1", 0.30, {"truthfulness"})
assert s.startswith("Evidence is thin that"), s
def test_coherence_deviation_uses_coherence_phrase(self) -> None:
s = _surface("default_general_v1", 0.30, {"coherence"})
assert s.startswith("This does not yet cohere:"), s
def test_reverence_deviation_uses_reverence_phrase(self) -> None:
s = _surface("default_general_v1", 0.30, {"reverence"})
assert s.startswith("Reports suggest"), s
def test_no_deviation_falls_back_to_generic(self) -> None:
s = _surface("default_general_v1", 0.30, set())
assert s.startswith("It seems that"), s
def test_deviation_outside_pack_axis_hedges_falls_back(self) -> None:
# An axis_id that the pack doesn't provide an axis_hedge for.
s = _surface("default_general_v1", 0.30, {"unfamiliar_axis"})
assert s.startswith("It seems that"), s
# ---------- band gating still applies ----------
class TestBandGating:
def test_above_hedge_band_leaves_bare_even_with_deviation(self) -> None:
# alignment=0.90 is above default's hedge_threshold_soft (0.50)
# and qualified_band_high (0.75). No phrase prepends.
s = _surface("default_general_v1", 0.90, {"truthfulness"})
assert s == "Truth reveals reality."
def test_soft_band_uses_axis_soft_phrase(self) -> None:
# alignment=0.45 is in default's soft band [0.40, 0.50).
s = _surface("default_general_v1", 0.45, {"truthfulness"})
assert s.startswith("It is hard to confirm that"), s
# ---------- pack swap with same deviation ----------
class TestPackSwapWithDeviation:
def test_truthfulness_deviation_three_packs_three_phrases(self) -> None:
a = _surface("default_general_v1", 0.30, {"truthfulness"})
b = _surface("precision_first_v1", 0.30, {"truthfulness"})
c = _surface("generosity_first_v1", 0.30, {"truthfulness"})
assert a.startswith("Evidence is thin that"), a
assert b.startswith("The evidence does not support that"), b
# generosity's strong threshold is 0.20; alignment=0.30 is above
# it, so no hedge fires regardless of deviation.
assert c == "Truth reveals reality."
def test_coherence_deviation_three_packs(self) -> None:
a = _surface("default_general_v1", 0.30, {"coherence"})
b = _surface("precision_first_v1", 0.30, {"coherence"})
# default and precision both hedge but with different
# coherence-specific phrasing.
assert a.startswith("This does not yet cohere:"), a
assert b.startswith("This contradicts what is established:"), b
# ---------- lex tie-break ----------
class TestLexTieBreak:
def test_multiple_deviations_lex_smallest_wins(self) -> None:
# All three axes deviating — lex-smallest is "coherence" since
# axis_hedges is in lex order.
s = _surface(
"default_general_v1", 0.30,
{"truthfulness", "coherence", "reverence"},
)
assert s.startswith("This does not yet cohere:"), s
def test_truthfulness_and_reverence_uses_reverence_via_lex(self) -> None:
# Lex order: "reverence" < "truthfulness", so reverence wins.
s = _surface(
"default_general_v1", 0.30,
{"truthfulness", "reverence"},
)
assert s.startswith("Reports suggest"), s
# ---------- depth-language fallback ----------
class TestDepthLanguageFallback:
def test_hebrew_ignores_axis_hedges(self) -> None:
s = _surface("default_general_v1", 0.30, {"truthfulness"}, lang="he")
# Hebrew uses canonical ADR-0030 phrase regardless of deviation.
assert s.startswith("נראה ש"), s
def test_greek_ignores_axis_hedges(self) -> None:
s = _surface("default_general_v1", 0.30, {"coherence"}, lang="grc")
assert s.startswith("δοκεῖ ὅτι"), s
# ---------- backward compatibility ----------
class TestBackwardCompatibility:
def test_empty_deviation_axes_matches_adr_0028_behavior(self) -> None:
# ADR-0028 baseline: default pack at alignment=0.30 with no
# deviation_axes should produce "It seems that truth reveals reality."
s = _surface("default_general_v1", 0.30, set())
assert s == "It seems that truth reveals reality."
def test_default_surfacecontext_has_empty_decomposition(self) -> None:
ctx = SurfaceContext()
assert ctx.deviation_axes == frozenset()
assert ctx.axis_hedges == ()
# ---------- pack contract: all three v1 packs ship axis_hedges ----------
class TestPackContract:
def test_all_three_v1_packs_supply_axis_hedges(self) -> None:
for pack_id in (
"default_general_v1", "precision_first_v1", "generosity_first_v1",
):
prefs = load_identity_manifold(pack_id).surface_preferences
# Every v1 pack provides hedges for the three default-pack axes.
axis_ids = {a for a, _ in prefs.axis_hedges}
assert {"truthfulness", "coherence", "reverence"} <= axis_ids, (
f"{pack_id} missing one of the three default-pack axes"
)
def test_axis_hedges_in_lex_order(self) -> None:
prefs = load_identity_manifold("default_general_v1").surface_preferences
ids = [a for a, _ in prefs.axis_hedges]
assert ids == sorted(ids)