Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ load).
| `schema_match` | Always True (keys/enums guaranteed by construction). |
| `confidence_model` | `"slots"` or `"labels"`. |
| `prompt_sha256` / `prompt_version` | SHA-256 over the full prompt token ids; the version is read from `engine.PROMPT_VERSION` (v8) — never a literal elsewhere. |
| `probability_status` | How to read the probabilities. |
| `probability_status` | W5b-13: a SUMMARY over the per-field semantics records — one clause per distinct (score_source, temperature, calibrator_id, prior_mode) group with its field count. Not authoritative for any single field; the per-field truth is `field_telemetry[..]['semantics']`. An empty group set raises (every engine path sets records). |
| `prior_correction` / `constraints_applied` | Whether the prior pass ran / case-level constraints were applied. |
| `reconciled_fields` | Fields whose value changed under constrained MAP. |
| `parsed_json` | `{field: {"value": …, "prob": …}}`. |
Expand Down Expand Up @@ -235,6 +235,7 @@ Batched-only keys (`run_parallel_generation_batched`, every result): `group_wall
| `rows` | Rows the field consumed (0 for cardinality-1 fields). |
| `tie` / `rescored` | Scalar: whether the top-2 gap is inside `INSTABILITY_BAND` (subsumes exact-equality ties), and whether the batch=1 rescore replaced the batched result. Multi: `rescored` when any option's Y/N pair was rescored. |
| `evidence_source` | Scalar only (W5-B, PR #45): which scoring path produced the final evidence — `batch` (batched pass), `batch1` (canonical rescore replaced it), `dependency` (second pass), `oracle` (forced re-score). Rides on scalar entries and on second-pass re-decides. |
| `semantics` | W5b-13: the per-field semantics record (coerced to frozen `api.FieldSemantics` at the public boundary): `score_source` (`batched`/`rescored_batch1`/`dependency`/`oracle`), `temperature` actually applied (None for count rows and calibrated multi selections), `calibrator_id` (bundle identity when a fitted calibrator set the selection), `prior_mode` (`off`/`neutral_v1`), `constraint_changed` (a reconciler overrode the raw winner — an actual selection change, not merely a binding constraint), `dependency_rescored`. REQUIRED on every entry — `_build_field_results` raises without it. |
| `legal_mass` | Probability the model assigned to the union of allowed continuations at the winner's branch point(s), against the full vocabulary = sum(exp(z_allowed)) / sum(exp(z_vocab)). Per-branch leakage signal — the constrained distribution can confidently pick A over B even when almost all unconstrained mass is on a reasoning token/newline/label text. Product over the winner's branch path (scalar); per-option Y/N branches (multi); the count row has its own (`legal_mass` + `min_option_legal_mass` on the `<field>#count` entry). 1.0 for cardinality-1 fields (nothing branched). Always computed. Raw, pre-prior-correction logits. |
| `legal_mass_logs` | Per-choice (scalar) / per-option (multi) natural-log legal-mass product along the branch path, keyed by the real choice/option string. Raw, T=1. Calibration feature for the abstention model. The legal-mass callback is LOG-space end to end (W5-D finding 37: `score_trie`'s `legal_mass_at_node` returns natural-log floats; the trie stays MLX-free). |
| `min_option_legal_mass` / `mean_log_legal_mass` | Multi only (W5-D finding 38): cardinality-free field-level stats replacing the old underflowing, cardinality-confounded product as the headline numbers — the worst option's legal mass in probability space, and the mean per-option log mass (additive, stable). The per-option logs stay on `legal_mass_logs`. |
Expand All @@ -255,6 +256,7 @@ Batched-only keys (`run_parallel_generation_batched`, every result): `group_wall
| `model` | `"slots"` or `"labels"`. |
| `alternatives` | Top 3 (choice, probability) pairs; multi: per-option (option, P(yes)) sorted desc. |
| `reason` | None, `"none_of_above"` (caller opted in via `allow_none_of_above=True`, model picked the explicit opt-out → None), or `"abstain"` (`abstain_below_margin` set and the field's margin — `probability_margin` scalar / `threshold_distance` multi — fell below the cut; value withheld from the validated instance, raw kept for provenance). The single source of truth — no separate abstain flag. |
| `semantics` | Frozen `api.FieldSemantics` (W5b-13): how THIS field's reported probabilities were produced — score_source, the temperature actually applied, the calibrator bundle id, prior_mode, constraint_changed, dependency_rescored. Required, kw-only; coerced from the telemetry record, never None. |

### Case-level constraints — `constraints.py`, applied by `_constrained_map`

Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,23 @@
(per-row peak-memory slope per width bin under `mx.reset_peak_memory`,
B=1/2/4/8 at widths 4/8/16/32) and `adapters` (max abs diff + timing of
full-width head vs decision-position head). No engine wiring.
- Per-field probability semantics (W5b-13): every engine scoring stage —
scalar finalization, multi selection, count rows, case-constraint MAP,
dependency waves — sets a frozen semantics record
(`api.FieldSemantics`: score_source / temperature / calibrator_id /
prior_mode / constraint_changed / dependency_rescored) on its
field_telemetry entry, and `FieldResult.semantics` (required, kw-only)
coerces it at the public API boundary — a missing record raises, so
'required' holds at every decide() return. The result-level
`probability_status` is now a SUMMARY over the distinct
(score_source, temperature, calibrator_id, prior_mode) groups — one
clause per group with its field count; the old global-only statement is
gone (a result mixing temperature-scaled scalars, calibrated multi
options and dependency re-scores cannot be described by one sentence).
A calibrated multi records temperature None (the log-odds cut ignores
the caller T); count rows always do (fixed T=1 buckets);
constraint_changed reflects an actual selection change, not merely a
binding constraint.
- Results contract v2 in the results tooling (PR #48):
`check_results.py` requires `timing.json` on parallel-track combos with
the full timing-split median (incl. `peak_incremental_bytes` +
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ Read the result. `decide(...)` returns a `Decision`: `.value` (a validated
P(yes)), `score`, `model` (`"slots"`/`"labels"`), `calibrated`, `legal_mass`
(probability mass in allowed continuations at the branch points — a leakage
signal when low despite a confident decision).
- `semantics` (a frozen `FieldSemantics`): how THIS field's reported
probabilities were produced — which scoring path (`score_source`:
`batched` / `rescored_batch1` / `dependency` / `oracle`), the temperature
actually applied (`None` for count rows and calibrated multi selections,
whose log-odds cut ignores the caller temperature), the calibrator bundle
id when a fitted calibrator set the selection, the prior mode
(`off`/`neutral_v1`), and whether a constraint or a dependency wave
overrode the raw winner. The result-level `probability_status` summarizes
the distinct semantic groups (one clause per
`(score_source, temperature, calibrator_id, prior_mode)` group with its
field count) and is not authoritative for any single field.
- Margins, one per field: `log_score_margin` / `probability_margin` (scalar,
top1-top2 gap in log/probability units), `threshold_distance` (multi, how
close the closest yes/no call sat to the cut). Multi fields carry
Expand Down
60 changes: 59 additions & 1 deletion jevmlx/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class Fraud(BaseModel):
"NONE_OF_ABOVE_DESCRIPTION",
"Decision",
"FieldResult",
"FieldSemantics",
"decide",
"decide_many",
"schema_from_model",
Expand Down Expand Up @@ -81,6 +82,44 @@ def _choice_values(name: str, values: list) -> list[str]:


@dataclasses.dataclass(frozen=True)
class FieldSemantics:
"""How ONE field's reported probabilities were produced (W5b-13, §C9).

The per-field truth a global ``probability_status`` string cannot
carry: a result mixes temperature-scaled scalars, calibrated multi
options that ignore the caller temperature, count-row bucket scores,
prior-corrected fields, and dependency/oracle re-scores. Each field
records its own semantics; the result-level ``probability_status``
becomes a summary of these records.

Attributes:
score_source: Which scoring path produced the final evidence:
"batched" (batched pass), "rescored_batch1" (canonical batch=1
rescore replaced it), "dependency" (second pass conditioned on
the parent), "oracle" (forced re-score under oracle_overrides).
temperature: The temperature actually applied to the reported
distribution. None for count rows (fixed T=1 bucket scores)
and for calibrated multi selections (the calibrated log-odds
``a*(yes-no)+b`` cut ignores the caller temperature).
calibrator_id: Identity of the CalibrationBundle whose fitted
calibrator set the selection (multi only); None = uncalibrated.
prior_mode: "off" or "neutral_v1" — whether (and how) the scores
were prior-corrected against the neutral-context pass.
constraint_changed: A reconciler (count / set constraints / case
MAP) overrode the raw winner.
dependency_rescored: Re-scored conditioned on the parent value in
a dependency wave.
"""

score_source: str
temperature: float | None
calibrator_id: str | None
prior_mode: str
constraint_changed: bool
dependency_rescored: bool


@dataclasses.dataclass(frozen=True, kw_only=True)
class FieldResult:
"""Provenance for one decided field.

Expand Down Expand Up @@ -119,6 +158,10 @@ class FieldResult:
gate (calibrated abstention is a later milestone). The
only values are None, "none_of_above" and "abstain"; a
withheld decision is exactly ``reason == "abstain"``.
semantics: HOW this field's reported probabilities were produced —
see :class:`FieldSemantics`. Required: the engine's stages set
the record and _build_field_results coerces it; a telemetry
entry without one raises (never a silent None).
"""

value: object
Expand All @@ -130,7 +173,8 @@ class FieldResult:
calibrated: bool
model: str
alternatives: tuple[tuple[str, float], ...]
reason: str | None = None
reason: str | None
semantics: FieldSemantics


@dataclasses.dataclass
Expand Down Expand Up @@ -357,6 +401,19 @@ def _build_field_results(
# Provenance (the bundle identity) belongs to the telemetry, not to
# confidence_model: model stays the clean decision-model name.
calibrated_flag = telemetry.get("calibrated") is not None
# W5b-13 step 2: the engine's stages set the semantics record; the
# public API coerces it into the frozen FieldSemantics. A telemetry
# entry without one is a contract violation — fail loudly, never
# ship a None semantics. (This is the boundary CHECK, not a
# tolerant read: the index below raises when the record is absent
# or not a dict.)
sem_dict = telemetry["semantics"]
if not isinstance(sem_dict, dict):
raise ValueError(
f"field telemetry for {name!r} carries no semantics record "
"(results-contract violation; engine stages must set "
"field_telemetry[fname]['semantics'])"
)
fields[name] = FieldResult(
value=telemetry["value"],
score=score,
Expand All @@ -368,6 +425,7 @@ def _build_field_results(
model=confidence_model,
alternatives=alternatives,
reason=reason,
semantics=FieldSemantics(**sem_dict),
)
return fields

Expand Down
Loading
Loading