mixle.reason.language_bridge module

The language<->belief bridge (roadmap M5, part (c)): NL/record -> M0 evidence via a declared schema, and posterior -> calibrated text through A1’s CalibratedGenerator.

Two independent directions, each a thin composition of already-tested machinery – no new extraction or generation model is built here (see notes/designs/M5.md part (c) for the full contract):

  • parse_evidence() – an extractor callable (the same teacher(x) -> dict shape mixle.task.structured_out.solve_structured() decomposes) produces a raw {field: value} dict from NL/record input; this module validates it against a caller-declared schema BEFORE it reaches infer() / run_inference_program(), so a schema violation is a clear ValueError here rather than a confusing downstream log_density crash. A schema field may be declared with the plain shorthand string "categorical" | "numeric" – the same shape schema already returns, treated as a REQUIRED field with no further restriction – or a full SchemaField for an OPTIONAL field or a categorical field restricted to a declared closed set of values. Validation happens in two passes: the schema itself is checked (every declared field’s kind, and categories if any) BEFORE the extractor ever runs, so a malformed schema cannot hide behind a call that happens not to exercise the bad field; then the extractor’s actual output is checked field-by-field against it – every required field must be present, every numeric value must be finite, and every categorical value must already be a legitimate str (a member of categories when declared) rather than an arbitrary object silently stringified into a fabricated label.

  • PosteriorDescriber / claim_score() – draft k candidate Claims at different ABSOLUTE precision widths (multiples of a required tol, the same “caller declares the precision that counts” contract mixle.task.regress.solve_regression() already uses for numeric fields – NOT widths relative to the posterior’s own spread, which would be scale-invariant and could never detect “too diffuse to answer”), score each against the posterior it describes, and serve the best one under A1’s held-out selective-risk gate. claim_score() is exported standalone (not only reachable through PosteriorDescriber) so B2 (claim-checking, built elsewhere) can score an ALREADY-EMITTED claim against any posterior directly, with no dependency on candidate generation/calibration at all.

class SchemaField(kind, required=True, categories=None)[source]

Bases: object

One declared evidence field: its value kind, whether the extractor must supply it, and (for categorical fields) the closed set of values that count as legitimate.

kind="numeric" accepts only finite int/float (bool excluded – never a genuine numeric measurement; NaN/Infinity excluded – “validated” evidence must be usable arithmetic, not merely type-correct). kind="categorical" accepts only values that are ALREADY a str – never an arbitrary object (including None) silently stringified into a fabricated label – and, when categories is declared, only a value already a MEMBER of that closed set (an undeclared label is rejected, not silently admitted as a new de facto category).

required=True (the default) means parse_evidence() raises if the extractor omits this field; required=False preserves the “partial evidence is fine” contract for fields the caller genuinely expects may go unmentioned.

Parameters:
parse_evidence(text, schema, extractor)[source]

NL scenario/constraint (or any record extractor accepts) -> validated M0/L2 evidence.

extractor(text) -> {field: raw_value} does the actual parsing (a keyword/regex rule, a calibrated solve_structured() student, an LLM call – this module is agnostic to how); this function’s only job is enforcing the declared schema BEFORE the result is trusted as evidence: the schema itself is validated first (so a malformed schema raises even on a call whose extractor happens not to touch the bad field), then every required field must be present, numeric fields must be finite numbers, and categorical fields must already be a str (and a declared category, when one is set) – never an arbitrary object silently stringified into a fabricated label. The returned dict is ready to pass straight to CrossModalJoint.infer(...) or as run_inference_program’s evidence=.

Parameters:
Return type:

dict[str, Any]

class Claim(field, lo, hi, probe=())[source]

Bases: object

A declared interval assertion about one posterior field: field lies in [lo, hi].

probe caches the sample batch a PosteriorDescriber drew to score this claim at generation time, so CalibratedGenerator()’s single-argument score(candidate) contract can call claim_score() with no extra prompt/posterior plumbing. A hand-authored Claim (e.g. from B2) simply omits probe and passes posterior= to claim_score() explicitly instead.

Parameters:
claim_score(claim, posterior=None, *, n_samples=200, seed=0)[source]

How well claim is supported by the posterior it describes: coverage of [claim.lo, claim.hi] under fresh posterior draws, PER UNIT WIDTH (coverage / width) – a density-like score, not a linear coverage-minus-penalty one. The ratio form matters, not just tie-breaking: a coverage-minus-linear-penalty score is shift-invariant under softmax whenever coverage SATURATES to the same constant across every candidate width, which happens in BOTH the confident regime (a sharp posterior’s mass fits inside every candidate width, coverage saturates near 1) and the clueless regime (a posterior far more diffuse than the widest candidate has near-locally-uniform density, so coverage saturates near density(center) * width for every candidate) – softmax over a constant offset cannot tell those two regimes apart. coverage / width does not saturate the same way: in the confident regime it grows as 1 / width (the NARROWEST candidate wins decisively), while in the clueless regime coverage / width -> density(center), the SAME value for every candidate width (a uniform, non-committal softmax) – exactly the “no candidate is more informative than any other” signal PosteriorDescriber.describe() abstains on.

Reusable standalone by B2’s claim-checking: pass posterior= to score an independently-authored claim against any posterior; a PosteriorDescriber-generated claim can instead be re-scored with no posterior argument, reusing the sample batch cached at generation time.

Parameters:
  • claim (Claim)

  • posterior (Any)

  • n_samples (int)

  • seed (int | None)

Return type:

float

class PosteriorDescriber(field_name, *, tol, k=3, alpha=0.1, width_multiples=(1.0, 3.0, 10.0), n_probe=300, seed=0)[source]

Bases: object

Posterior -> calibrated text for one field, via A1’s CalibratedGenerator.

tol is the caller’s required precision (the same “the caller states what precision counts as an answer” contract solve_regression() uses) – candidate claim widths are ABSOLUTE multiples of tol, not relative to the posterior’s own spread, so a genuinely diffuse posterior (spread >> tol) cannot fake confidence by simply widening every candidate in lockstep: none of them will cover well enough to clear the calibrated threshold, and describe() abstains (acceptance criterion (d)).

Parameters:
calibrate(calibration_set, *, seed=None)[source]

Fit the selective-risk threshold from (posterior, true_value) held-out pairs.

MXR-080-0291: is_correct is EXACTLY “does the emitted interval cover the truth” – claim.contains(true_value), i.e. claim.lo <= true_value <= claim.hi – checked independently per candidate, non-strict on both ends. Nothing else. This module previously assigned correctness to a single artificial distance BAND per candidate (the true value’s distance from the shared center had to land in an EXCLUSIVE, disjoint slice (prev_half, this_half]), which is a different and unsound event: a truth sitting exactly on the shared center (distance 0) failed the narrowest band’s strict 0 < dist lower bound and therefore failed EVERY band, so the objectively best-supported truth was scored as covered by nothing; and a wider candidate that genuinely contains the truth (dist <= its own half-width) was marked wrong whenever a narrower nested candidate ALSO contained it, purely because the narrower band “claimed” that distance range first – docking a candidate for a coverage fact about a DIFFERENT candidate. Both are boundary/exclusivity artifacts of the banding scheme itself, not properties of whether the claim actually covers the truth.

The direct correctness check has no such artifact: it evaluates exactly the top candidate the selective generator would serve. Calibration proposes a top-score threshold on one split and certifies the accepted interval-error rate on an independent split; it does not treat the prompt-specific interval candidates as a fixed conformal label space.

Scope of the certificate (it is CalibratedGenerator.calibrate()’s): assuming the calibration pairs and future posteriors are i.i.d. draws from the same distribution, it holds with probability at least the underlying gate’s confidence over the calibration draw; distribution shift voids it silently, so re-calibrate on drifted traffic.

Parameters:
Return type:

PosteriorDescriber

describe(posterior, *, seed=None)[source]

The best calibrated claim about posterior, or ABSTAIN (None) when no candidate width clears the selective-risk threshold – i.e. the posterior is too diffuse relative to tol for any of this describer’s claims to be trustworthy.

Parameters:
  • posterior (Any)

  • seed (int | None)

Return type:

Claim | None