core/docs/decisions/ADR-0043-pack-measurements-phase2.md
Shay 4ba1ef2da3 feat(adr-0043): Phase-2 pack measurements — claims → numbers
Converts the load-bearing claims of the ADR-0027→0042 pack-layer chain
into CI-enforced numbers across the three ratified identity packs
(default_general_v1, precision_first_v1, generosity_first_v1).

Two new pack-driven runners + an orchestrator:

- evals/identity_divergence/pack_runner.py — drives real
  SentenceAssembler + SurfaceContext (no mocks) across all three
  packs over 10 cases × 5 alignment bands; publishes per-pack
  bare/hedge/qualifier rates and pairwise distinct_rate.

- evals/refusal_calibration/pack_runner.py — runs the existing
  grounding-refusal lane via RuntimeConfig(identity_pack=...);
  publishes per-pack refusal_rate/fabrication_rate and a
  pack_invariant_gate flag asserting byte-identical cold-start
  surfaces across packs.

- scripts/publish_pack_measurements.py — combined publisher
  emitting evals/results/phase2_pack_measurements.json.

Baseline numbers (2026-05-17):
- precision_first hedge_rate=0.60, qualifier_rate=0.20
- generosity_first hedge_rate=0.20, qualifier_rate=0.00
- default_general hedge_rate=0.40, qualifier_rate=0.00
- pairwise distinct_rate ∈ [0.40, 0.80]
- refusal_rate=1.00, fabrication_rate=0.00 for all three packs
- pack_invariant_gate=True

6 tests in tests/test_pack_measurements_phase2.py lock the schema +
load-bearing flags + the structural inequality
precision.hedge_rate > generosity.hedge_rate. If identity packs
get wired into the cognition gate, pack_invariant_gate flips and
the suite fails.

ADR-0043 documents the numbers, the extended marker rationale, and
the trade-offs. README index updated with ADR-0043 row and chain
title bumped to "ADR-0027 through ADR-0043".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 22:19:24 -07:00

153 lines
5.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ADR-0043 — Phase-2 pack measurements: claims → numbers
Status: Accepted (2026-05-17)
## Context
The pack-layer chain (ADR-0027 → ADR-0042) ships three architectural
claims:
1. Identity is *load-bearing* — different identity packs produce
different articulations on the same input.
2. Safety is the *universal floor* — refusal is invariant across
identity choice.
3. Grounding refusal is *upstream of* articulation — identity packs do
not change the gate.
Through ADR-0042 these claims are demonstrated by hand-curated tests
(`tests/test_identity_surface_divergence.py`,
`tests/test_pack_swap_*`) and the audit-tour demo. They are *asserted*.
They are not *published as numbers* across the three ratified packs.
The strategic frame from session 2026-05-17 is to "convert claim into
measurement without any pack growth" before doing anything that adds
new vocabulary, new packs, or new infrastructure. This ADR is that
conversion.
## Decision
Ship two new pack-aware runners and a publisher that emit a single
combined report:
- `evals/identity_divergence/pack_runner.py` — drives the *real*
`SentenceAssembler` + `SurfaceContext` across the three ratified
identity packs (`default_general_v1`, `precision_first_v1`,
`generosity_first_v1`) over the existing 10 dev+public cases at
five alignment bands `{0.20, 0.45, 0.60, 0.80, 0.95}`. No mocks.
Reports per-pack `bare_rate` / `hedge_rate` / `qualifier_rate` and
pairwise `distinct_rate`.
- `evals/refusal_calibration/pack_runner.py` — runs the existing
grounding-refusal lane across all three identity packs via
`RuntimeConfig(identity_pack=...)`. Reports per-pack `refusal_rate`
/ `fabrication_rate` and a `pack_invariant_gate` flag that asserts
the cold-start out-of-grounding surface is byte-identical across
packs.
- `scripts/publish_pack_measurements.py` — orchestrator emitting
`evals/results/phase2_pack_measurements.json` plus per-runner
`results/packs_v1/measurements.json` artifacts.
The combined report carries a `claims_supported` block:
```json
{
"identity_load_bearing": true,
"grounding_gate_pack_invariant": true,
"no_fabrication_under_any_pack": true
}
```
`tests/test_pack_measurements_phase2.py` gates these flags plus the
schema and the structural inequality
`precision_first.hedge_rate > generosity_first.hedge_rate`. If any of
those flips the suite fails.
## Headline numbers (2026-05-17 baseline)
Identity-divergence (10 cases × 5 alignment bands):
| Pack | bare | hedge | qualifier |
|---|---|---|---|
| default_general_v1 | 0.60 | 0.40 | 0.00 |
| precision_first_v1 | 0.20 | 0.60 | 0.20 |
| generosity_first_v1 | 0.80 | 0.20 | 0.00 |
Pairwise pack divergence:
| Pair | distinct_rate |
|---|---|
| default ⇆ precision | 0.80 |
| default ⇆ generosity | 0.40 |
| precision ⇆ generosity | 0.80 |
Refusal-calibration (8 out-of-grounding probes × 3 packs):
| Pack | refusal_rate | fabrication_rate |
|---|---|---|
| default_general_v1 | 1.00 | 0.00 |
| precision_first_v1 | 1.00 | 0.00 |
| generosity_first_v1 | 1.00 | 0.00 |
`pack_invariant_gate=True` — every out-of-grounding probe produces a
byte-identical surface across the three packs, proving the gate is
upstream of pack articulation.
Note on `refusal_rate=1.00`: this lane uses an *extended* marker set
that recognizes CORE's cold-start unknown-domain surface ("I don't
have field coordinates for that yet.") as a refusal. This is the
correct calibration for the *separation* claim being measured (gate
behavior vs articulation behavior). The original v1 contract
in `evals/refusal_calibration/contract.md` deliberately uses a
narrower marker set and is expected to fail at v1 — that file
remains the source of truth for the canonical v1 refusal claim.
## Consequences
**Positive.**
- The two headline pack-layer claims are now CI-enforced numbers, not
prose assertions.
- Adding a fourth identity pack auto-extends both reports; no glue
code.
- The `pack_invariant_gate` flag turns the separation-of-concerns
argument into a structural test — if anyone wires identity packs
into the cognition gate the test flips immediately.
**Trade-offs.**
- The Phase-2 lane uses an extended marker set; reviewers must read
the ADR to know why the number is 1.00. This is documented above
and in the runner module docstring.
- The pack-driven divergence runner uses simple humanization (`_`→space)
on predicate tokens; case wording is awkward (e.g. "Bob is sibling
of Carol") but this is irrelevant — the measurement is about *whether
the pack's surface preferences fire*, not naturalness. Naturalness
is an articulation pipeline concern, not an identity-pack concern.
- Adding alignment bands or cases requires re-baselining the headline
numbers in this ADR. The test gates flags + structural inequality,
not exact rates, so small case-set growth is safe.
## How to verify
```bash
PYTHONPATH=. python3 scripts/publish_pack_measurements.py
PYTHONPATH=. python3 -m pytest tests/test_pack_measurements_phase2.py -q
```
## Where it lives
- `evals/identity_divergence/pack_runner.py`
- `evals/refusal_calibration/pack_runner.py`
- `scripts/publish_pack_measurements.py`
- `tests/test_pack_measurements_phase2.py`
- `evals/results/phase2_pack_measurements.json` (artifact)
- `evals/identity_divergence/results/packs_v1/measurements.json`
- `evals/refusal_calibration/results/packs_v1/measurements.json`
## Related
- [ADR-0027](ADR-0027-identity-packs.md) — identity packs.
- [ADR-0028](ADR-0028-identity-surface-wiring.md) — surface preferences.
- [ADR-0042](ADR-0042-audit-tour-demo.md) — the qualitative tour these
measurements now back with numbers.