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:
parent
a49a7555dc
commit
07ad3af845
13 changed files with 570 additions and 28 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
131
docs/decisions/ADR-0031-score-decomposition-surface.md
Normal file
131
docs/decisions/ADR-0031-score-decomposition-surface.md
Normal 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 1–64), 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.
|
||||
|
|
@ -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 1–64). 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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}"
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
209
tests/test_identity_score_decomposition.py
Normal file
209
tests/test_identity_score_decomposition.py
Normal 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)
|
||||
Loading…
Reference in a new issue