* docs: consolidate governance anchors and clean up test registries * refactor(cli): decompose cli into dedicated modules * test: fix broken test baselines and formatting * docs: add domain boundary READMEs for governance anchors * test: update baseline for determination lane * test: fix capability_pass expectation * test: fix CORE_SHOWCASE_SKIP_BUDGET enforcement * chore: cleanup CLI extraction and unreachable code
10 KiB
ADR-0028: Identity Surface Wiring — Pack-Driven Hedge & Claim Strength
Status: Accepted (2026-05-17)
Author: Joshua Shay + planner pass
Companion docs: identity_packs.md, ADR-0027-identity-packs.md, runtime_contracts.md
Context
ADR-0027 landed swappable identity packs and wired them into the runtime through chat/runtime.py::ChatRuntime. The identity manifold is now load-bearing in two ways: PersonaMotor.from_identity_manifold biases every field walk, and IdentityCheck.check() produces an IdentityScore whose alignment field is passed to the surface assembler as SurfaceContext.identity_alignment.
But the surface effect of swapping packs is invisible. The pre-ADR-0028 _apply_hedge consults only the alignment scalar:
if alignment < 0.4: return f"It seems that {surface}"
if alignment < 0.5: return f"Perhaps {surface}"
return surface
So default_general_v1 and precision_first_v1 on the same prompt produce the same hedge decision — both manifolds get hit with similar alignment scores by ordinary input. The pack swap changes upstream scoring and motor bias but does not change the assembled string, which is the user-visible artifact.
This is the "known limit 1" recorded in identity_packs.md: "Identity does not yet visibly differentiate articulation at the realizer." ADR-0028 closes that gap.
Decision
The pack carries surface-shaping preferences alongside its axes. The assembler consults those preferences at the same site it currently consults alignment. No upstream pipeline code changes. No motor / field-bias / generation-walk code changes. No CLAUDE.md invariant is touched: the change is deterministic, surface-only, contains no sampling, no normalization, no hot-path repair.
Schema extension (pack v1)
A new optional surface_preferences block on the identity pack:
"surface_preferences": {
"hedge_threshold_strong": 0.40,
"hedge_threshold_soft": 0.50,
"preferred_hedge_strong": "It seems that",
"preferred_hedge_soft": "Perhaps",
"claim_strength": "balanced",
"qualified_band_high": 0.75,
"preferred_qualifier": "In some cases,"
}
The block is optional — absent it, the loader supplies defaults that reproduce pre-ADR-0028 behavior byte-for-byte. This keeps the schema bump backwards-compatible at the pack format level even though every pack's canonical SHA changes when the block is added.
Surface algorithm
Three nested bands ordered by descending hedge strength. Given an alignment scalar a and pack preferences prefs:
- Strong hedge:
a < prefs.hedge_threshold_strong→ prependprefs.preferred_hedge_strong. - Soft hedge:
a < prefs.hedge_threshold_soft→ prependprefs.preferred_hedge_soft. - Marginal band:
prefs.hedge_threshold_soft <= a < prefs.qualified_band_high. Behavior depends onclaim_strength:"qualified"→ prependprefs.preferred_qualifier."affirmative"→ leave bare."balanced"→ leave bare.
- Above marginal band:
a >= prefs.qualified_band_high→ leave bare regardless ofclaim_strength.
The thresholds must satisfy hedge_threshold_strong <= hedge_threshold_soft <= qualified_band_high; the loader enforces this ordering.
Three shipping pack profiles
| Pack | strong | soft | qual_high | claim_strength | Hedge phrases |
|---|---|---|---|---|---|
default_general_v1 |
0.40 | 0.50 | 0.75 | balanced | "It seems that" / "Perhaps" / qualifier unused |
precision_first_v1 |
0.55 | 0.70 | 0.85 | qualified | "Arguably," / "In some cases," / "Under certain conditions," |
generosity_first_v1 |
0.20 | 0.30 | 0.50 | affirmative | "It seems that" / "Perhaps" / qualifier unused |
Result: at alignment = 0.45, default and precision both hedge but with different phrases; generosity leaves bare. At alignment = 0.80, precision qualifies; default and generosity leave bare. Visible divergence on identical trajectories — proven by tests/test_identity_surface_divergence.py.
Implementation
core/physics/identity.py: newSurfacePreferencesdataclass;IdentityManifoldgains asurface_preferences: SurfacePreferences = SurfacePreferences()field with defaults that reproduce pre-ADR behavior.packs/identity/loader.py:_build_surface_preferences()parses and bounds-checks the new block; missing block uses defaults; threshold ordering enforced;claim_strengthconstrained to{"balanced", "qualified", "affirmative"}.generate/surface.py:SurfaceContextgains seven new fields (defaults preserve pre-ADR behavior);_apply_hedgetakes the full context, not just a float, and implements the four-band algorithm above; the legacy module-levelHEDGE_STRONG_THRESHOLD/HEDGE_SOFT_THRESHOLDconstants are retained as the default values forSurfaceContextso existing imports still resolve.chat/runtime.py::ChatRuntime._build_surface_context: liftsself.identity_manifold.surface_preferencesinto the constructedSurfaceContext.packs/identity/*.json: three v1 packs gainsurface_preferencesblocks tuned to their roles.scripts/ratify_identity_packs.py: no change needed; runs again idempotently. Pack body changes →pack_source_shachanges → MasteryReport regenerated → companion.mastery_report.jsonrewritten → embeddedmastery_report_sha256updated.
Backward compatibility
- Pack format. Packs without
surface_preferencescontinue to load and produce pre-ADR behavior. - In-code
SurfaceContext()construction. Callers who instantiateSurfaceContext()directly (without going through_build_surface_context) get default values that reproduce pre-ADR behavior. The legacy module-level constantsHEDGE_STRONG_THRESHOLD = 0.4andHEDGE_SOFT_THRESHOLD = 0.5are preserved as the defaults for those fields, so any test or code that imports those names continues to work. - Re-ratification cost. The three v1 packs are re-ratified once when the new block is added. Their MasteryReport SHAs change (this is expected). The previous SHAs are recorded in ADR-0027 §"Phase 5"; the new SHAs are recorded in
docs/identity_packs.md§"Shipping packs (v1)" and in the ADR-0027 phase table.
Consequences
Positive
- The identity claim is now visibly load-bearing at the surface layer. Pack swap → different assembled string on the same prompt. The
tests/test_identity_surface_divergence.py::TestPackSwapDivergence::test_same_alignment_different_surfacesasserts this explicitly. - The realizer remains deterministic. No new operator, no normalization, no sampling, no clock, no PID, no hash-randomization. Same
(prompt, alignment, pack)triple → same surface bytes. - The pack format remains optional-block backward-compatible. Authors writing new packs may omit
surface_preferencesif defaults suit them. - The divergence test makes regressions loud. Any future change that re-routes
_apply_hedgeor strips the SurfaceContext fields will failtest_same_alignment_different_surfacesimmediately.
Negative / risks
- Schema bump invalidates the three Phase-5 MasteryReports. Cost paid once; re-ratification handled by the existing idempotent script. The previously-ratified SHAs from 2026-05-17 are superseded.
- English-only differentiation.
_assemble_heand_assemble_grcdo not currently consultSurfaceContextfor hedge/claim-strength shaping; they call neither_apply_hedgenor anything sensitive to the new fields. Per-language hedging is a future concern; identity packs are language-neutral, so the same preferences will eventually drive the same logic in_assemble_heand_assemble_grconce those gain hedge support. - The marginal-band qualifier only fires for
claim_strength="qualified". A future axis profile that wants to expand claims in the marginal band (e.g., add "Indeed," before a confident assertion) would require either a newclaim_strengthvalue or a separate field. Out of scope for ADR-0028. SurfaceContextis now bigger. Seven new fields. The dataclass remains frozen+slots so the cost is small, but every construction site (including tests that build aSurfaceContextdirectly) must accept that the defaults are non-trivial. We rely on the defaults reproducing pre-ADR behavior; the cognition / runtime / smoke suites verify this.
Scope limits (explicit non-goals for this ADR)
- No depth-language (Hebrew, Koine Greek) hedging. Future work; new ADR.
- No
scope,qualification_level,modal_style, orhedge_preferences(list) fields from the eval-layer YAMLs. Future work. - No surface-side differentiation by axis-id (e.g., "the truthfulness axis is dominant → use evidential phrasing"). Future work, and likely requires a richer score-decomposition surface than today's scalar alignment.
- No new CLI verb.
core chat --identity <pack_id>already exists.
Verification
This ADR is satisfied when:
tests/test_identity_surface_divergence.pypasses, in particularTestPackSwapDivergence::test_same_alignment_different_surfaces.- All three v1 packs ratify cleanly under the v1 schema with the
surface_preferencesblock and load in production mode (require_ratified=None). - The cognition / runtime / smoke / formation / teaching suites are green at the same revision.
tests/test_identity_packs.pycontinues to pass (loader bounds checks, runtime wiring, ratification-script idempotency, tamper detection).
Governance Cross-Reference (ADR-0225)
This identity surface wiring ADR is governed by ADR-0225:
- Safety boundaries: surface context shaping (
chat/surface.py) is deterministic and immutable under user interaction. - Versor closure: surface shaping transforms do not violate geometric or manifold invariants (
versor_condition(F) < 1e-6). - Reconstruction-over-storage: surface preferences are derived at runtime from pack manifests.
- Replay-equivalence: exact surface string generation is reproducible across identical execution traces.
- Mutation standing: surface preference blocks are proposal-only until ratified.