Compare commits
4 commits
add-ask-ac
...
workbench/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac947845c7 | ||
|
|
a4a24de836 | ||
|
|
685aaae40e | ||
|
|
4d7c605256 |
835 changed files with 4490 additions and 102993 deletions
12
.github/workflows/smoke.yml
vendored
12
.github/workflows/smoke.yml
vendored
|
|
@ -1,11 +1,7 @@
|
|||
name: smoke
|
||||
|
||||
# Fast PR gate — runs the smoke suite on every PR push.
|
||||
# Covers: chat runtime, pipeline, architectural invariants, audio sensorium
|
||||
# (tests/test_audio_*.py — compiler/eval-gates/mount/CRDT-merge/teachers, ~3s),
|
||||
# and identity falsifiability (tests/test_pack_measurements_phase2.py, ADR-0043
|
||||
# — ratified packs diverge directionally; pack-invariant refusal floor; no
|
||||
# fabrication). The falsifiability lane adds ~4 min but blocks-on-regression.
|
||||
# Fast PR gate — runs the smoke suite (~2-3 min) on every PR push.
|
||||
# Covers: chat runtime, pipeline, architectural invariants.
|
||||
#
|
||||
# Full pytest runs post-merge to main (see full-pytest.yml).
|
||||
# Regressions caught here block the PR; anything outside the smoke
|
||||
|
|
@ -53,6 +49,4 @@ jobs:
|
|||
tests/test_achat.py \
|
||||
tests/test_runtime_config.py \
|
||||
tests/test_cognitive_turn_pipeline.py \
|
||||
tests/test_architectural_invariants.py \
|
||||
tests/test_audio_*.py \
|
||||
tests/test_pack_measurements_phase2.py
|
||||
tests/test_architectural_invariants.py
|
||||
|
|
|
|||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -25,10 +25,6 @@ frontier_wave1.json
|
|||
# Claude Code local session artifacts (per-developer, never tracked)
|
||||
.claude/
|
||||
|
||||
# Local macro->micro system map — a per-developer NAVIGATION INDEX (like an IDE
|
||||
# symbol index), NOT a project artifact. Regenerated on demand; never tracked.
|
||||
.system-map/
|
||||
|
||||
# Runnable audit-passed showcases (ADR-0112 + ADR-0113) are generated on
|
||||
# demand from the signed claim + on-disk lane result files. The inputs
|
||||
# are committed; the renders are not.
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ status predicate evaluates to `reasoning-capable` with no open gaps.
|
|||
| Domain | Status | ADR | Packs | Open gaps |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `philosophy_theology` | reasoning-capable | ADR-0085 | 2 | 0 |
|
||||
| `mathematics_logic` | audit-passed | ADR-0097 | 1 | 0 |
|
||||
| `mathematics_logic` | expert | ADR-0097 | 1 | 0 |
|
||||
| `physics` | audit-passed | ADR-0100 | 1 | 0 |
|
||||
| `systems_software` | audit-passed | ADR-0101 | 1 | 0 |
|
||||
| `hebrew_greek_textual_reasoning` | reasoning-capable | ADR-0102 | 4 | 0 |
|
||||
|
|
@ -42,7 +42,6 @@ is a CI failure (`.github/workflows/lane-shas.yml`).
|
|||
| ADR-0099 | `public_demo` | Public showcase runs deterministically under 30s; all claims supported | `evals/public_demo/results/v1_dev.json` | `e323adb35ea17987991395424c603ff93bca08c11bc2713bd9f6338e86bb269f` |
|
||||
| ADR-0104 | `curriculum_loop_closure` | Curriculum-sourced proposals route through single reviewed teaching path | `evals/curriculum_loop_closure/results/v1_dev.json` | `b46d56b2d209172cc3ffaf3776dc8dcfe55093f13587c5cb67372be6dfa23e8d` |
|
||||
| ADR-0131 | `math_teaching_corpus_v1` | Math teaching corpus replays deterministically; all chains pass exit criterion (correct_rate=1.0, wrong=0) | `evals/math_teaching_corpus/v1/report.json` | `eaf160d145da29f9050ede8d58bf111b0f651dd40aeae9201857d0b97e014dd4` |
|
||||
| ADR-0206 | `deductive_logic_v1` | Propositional entailment scored against an independent truth-table oracle; dev+holdout+external 716/716 correct, wrong=0, refused=0 | `evals/deductive_logic/report.json` | `97a230949016e38d5e3f37a69e4245b320575ee70e5af92ff7607f7b05f74b5f` |
|
||||
|
||||
## Verification
|
||||
|
||||
|
|
|
|||
107
CLAUDE.md
107
CLAUDE.md
|
|
@ -58,16 +58,6 @@ Allowed sites:
|
|||
- `ingest/gate.py` for raw input injection.
|
||||
- `language_packs/compiler.py` and vocabulary construction.
|
||||
- `algebra/versor.py` for algebra-owned sandwich closure.
|
||||
- `sensorium/*/canonical.py` and pack-governed modality compiler construction
|
||||
boundaries for pinned signal canonicalization and quantization.
|
||||
- `session/context.py` for session-scoped **semantic anchoring** of the field
|
||||
toward the session concept-attractor (the anchor pull, hemisphere
|
||||
consistency). Allowed ONLY because every such op (1) preserves
|
||||
`versor_condition` BY CONSTRUCTION — composed from `rotor_power` /
|
||||
`word_transition_rotor` / `versor_apply` on the Spin manifold, never a
|
||||
post-hoc `unitize`/grade-projection — AND (2) carries semantic meaning in
|
||||
the cognitive model. An op that needs a post-hoc closure repair (the
|
||||
rejected `_slerp_toward`) fails clause (1) and stays forbidden.
|
||||
|
||||
Forbidden sites:
|
||||
|
||||
|
|
@ -80,15 +70,6 @@ Do not add drift repair, grade projection, watchdogs, timers, hot-path
|
|||
normalizers, or monitoring functions whose only purpose is to repair another
|
||||
function.
|
||||
|
||||
**The bright line — semantic anchoring vs. drift repair.** An op is *semantic
|
||||
anchoring* (allowed at the sites above) iff it preserves `versor_condition` by
|
||||
construction AND expresses a relation in the cognitive model. It is *drift
|
||||
repair* (forbidden) iff its purpose is to restore a numerical invariant a prior
|
||||
function should have preserved. Closure of field transitions is owned solely
|
||||
by `algebra/versor.py` (`_close_applied_versor`); no other site may "fix" it.
|
||||
Naming must not disguise the distinction: an op that anchors semantically must
|
||||
not be named or documented as a "drift fix".
|
||||
|
||||
CGA null vectors are not unit versors. Preserve null vectors as null vectors.
|
||||
|
||||
## Core Primitives
|
||||
|
|
@ -123,23 +104,6 @@ runtime path. Vault recall is exact and deterministic.
|
|||
- `calibration/*` — bounded replay-based calibration.
|
||||
- `docs/runtime_contracts.md` — response, telemetry, memory, identity, and testing contracts.
|
||||
|
||||
### GSM8K math comprehension substrate (sealed; serving `7/43/0`, wrong=0 — moves only via ratified PRs)
|
||||
|
||||
- `core/reliability_gate/` — calibrated-learning ledger + gate (ADR-0175): `ClassTally` counts, `conservative_floor` (one-sided Wilson, N_MIN=10), θ ceilings.
|
||||
- `generate/derivation/` — the comprehension composer: `extract.py` (lexeme quantity extraction, EX-1/4/5 + function-word unit filter), `clauses.py` (GB-1 segmentation), `compose.py` (GB-2a list-sum + GB-3a clause-scoped referent guard), `accumulate.py` (GB-3b.1 single-referent gain/loss chaining), `goal_residual.py` (ADR-0207 R4 goal-residual production), `multistep.py`/`search.py` (bounded search), `verify.py` (the wrong=0 self-verification gate: grounding ∧ cue ∧ unit ∧ completeness ∧ uniqueness).
|
||||
- `generate/cue_precision/` — `(cue, op, unit_shape)` reliability ledger + trainer (ADR-0177 CP-1/CP-2a); inert (consulted by no serving/gate path yet).
|
||||
- `evals/gsm8k_math/` — `train_sample/` (real GSM8K dev sample, currently 7 correct / 43 refused / 0 wrong), `practice/` (sealed attempt-and-eliminate lane + ADR-0163-F additive set), `confusers/` (ADR-0163-F2 discrimination probe — scored by `wrong→0` + pair-consistency, NOT flip-count).
|
||||
- `scripts/verify_lane_shas.py`, `scripts/generate_claims.py --check` — the serving-frozen gate (pinned eval-lane SHAs + `CLAIMS.md`).
|
||||
|
||||
### Sensorium / modality compiler substrate (parallel, afferent gates; no broad capability claim)
|
||||
|
||||
- `sensorium/compiler/` — shared compiler law for content-addressed afferent compilation units, canonical deltas, local arenas, and trace-safe merge hashes.
|
||||
- `sensorium/audio/` + `sensorium/adapters/audio.py` — `audio_core_v1`, deterministic audio compiler substrate, gate closed by default.
|
||||
- `sensorium/vision/` + `sensorium/adapters/vision.py` — `vision_core_v1`, tile-first deterministic visual compiler substrate over synthetic eval fixtures, gate closed by default.
|
||||
- `sensorium/environment/` — ADR-0208 observation-frame contract for bundles of already-compiled afferent units; not late fusion and not a mutable world model.
|
||||
- `sensorium/sensorimotor/` + `sensorium/adapters/sensorimotor.py` — ADR-0209 afferent proprioception/contact/action-result evidence substrate; no decode path.
|
||||
- `sensorium/registry.py::decode*` + `AuthorityToken` / `EfferentGate` — ADR-0198 fail-closed efferent governance path. This is not a ratified motor decoder or actuator interface; no real action emission is claimed.
|
||||
|
||||
## Efficiency and Performance Doctrine
|
||||
|
||||
Performance is part of correctness for this project because slow feedback hides
|
||||
|
|
@ -329,77 +293,6 @@ Current near-term sequence:
|
|||
Avoid broad docs-first churn, dashboard work, or large infrastructure unless it
|
||||
unlocks one of these steps.
|
||||
|
||||
The afferent sensorium/modalities arc (ADR-0013 -> 0181/0197/0208/0209; ADR-0198
|
||||
reserves the efferent/motor half) is a **sanctioned parallel track** — not part
|
||||
of the near-term sequence above and not licensed to displace it. It is disjoint
|
||||
from the GSM8K serving path (no `generate.derivation` / `core.reliability_gate`
|
||||
import), so it cannot regress the serving metric; its efferent half stays gated
|
||||
behind ADR-0198's fail-closed boundary and a dedicated motor governance ADR
|
||||
(ratified afferent ADRs carry `Accepted (ratified ...)`; ADR-0198 stays a
|
||||
partially-implemented spike).
|
||||
|
||||
## Lookback Review Discipline
|
||||
|
||||
Multi-PR architectural work accumulates latent defects when each PR
|
||||
is reviewed only against its own acceptance criteria. A hazard
|
||||
introduced in PR N can sit dormant until PR N+2 exercises it — by
|
||||
which point the substrate is harder to fix and three PRs are
|
||||
implicated rather than one.
|
||||
|
||||
**Mandatory lookback review** is triggered at three points:
|
||||
|
||||
1. **Before starting the next phase of a multi-phase ADR.** Before
|
||||
any code on Phase N+1, audit Phase N's shipped substrate. Check
|
||||
for: ADR-doc vs implementation drift, untested predicate paths,
|
||||
wrong=0 hazard surfaces, cross-phase trace/event/rank consistency,
|
||||
things the ADR says that didn't actually ship.
|
||||
|
||||
2. **Before merging a stacked PR sequence into main.** When 2+ PRs
|
||||
stack (PR #420 stacked on #416, PR #423 stacked on #420), the
|
||||
review-each-PR-individually pattern misses cross-PR consistency
|
||||
issues. Audit the whole stack as one unit before any merge.
|
||||
|
||||
3. **After any 3+ PR sequence on the same module or architectural
|
||||
surface.** When work concentrates on one area, regression risk
|
||||
compounds. Audit before claiming the surface is "stable" or
|
||||
"ready for the next layer."
|
||||
|
||||
**What a lookback review covers** (template — adjust per scope):
|
||||
|
||||
- **Documentation drift.** Does what shipped match what the ADR / brief
|
||||
said would ship? Signature differences, scope reductions, missing
|
||||
pieces — flag them.
|
||||
- **Test coverage gaps.** Run the test suite under coverage. For every
|
||||
predicate/branch in a closed-set contract (like
|
||||
`VALID_PREDICATE_NAMES`), confirm at least one test asserts the
|
||||
specific elimination/admission path. Vacuous tests (assertions
|
||||
that pass under broken impl) are coverage gaps.
|
||||
- **Parity gaps.** When a new implementation claims byte-equivalence
|
||||
with an existing one, exercise BOTH on the same inputs and confirm
|
||||
identical outputs — including failure modes, not just success.
|
||||
- **wrong=0 hazard surface.** Every new code path: under what input
|
||||
conditions could it admit a candidate the prior path would have
|
||||
refused? Trace upstream to confirm no input class can trigger it.
|
||||
If a class CAN trigger it, build the defensive refusal NOW, before
|
||||
the next phase makes it load-bearing.
|
||||
- **Cross-PR consistency.** Trace event shapes, rank handling,
|
||||
determinism contracts, dataclass invariants — do they compose
|
||||
cleanly across PRs?
|
||||
- **Honest LOC accounting.** Did this phase net add or net remove
|
||||
lines? ADR claims of "removes ~N lines" only count post-collapse;
|
||||
intermediate phases that ADD substrate before removal happens
|
||||
should be called out.
|
||||
|
||||
**Output.** The review produces a structured report with findings
|
||||
categorized as: solid, gaps (no risk), drift (need amendment), and
|
||||
hazards (live wrong=0 risks). Hazards require a fix-before-next-phase
|
||||
decision.
|
||||
|
||||
**Cost.** A lookback review on a 3-PR substrate typically takes
|
||||
20-40 minutes of focused tool calls. Skipping it costs more: every
|
||||
PR built on an undetected hazard becomes implicated when the hazard
|
||||
fires, and the fix has to land across multiple PRs instead of one.
|
||||
|
||||
## Architectural Scan Exclusions
|
||||
|
||||
The invariant tests in `tests/test_architectural_invariants.py` perform
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
# HANDOFF gpt55 2026-06-03
|
||||
|
||||
Branch: `codex/ntimes-completeness-guard`
|
||||
|
||||
Final commit SHA: reported in the thread final after commit creation.
|
||||
|
||||
## File Status
|
||||
|
||||
| file | path | status | purpose | ready-to-merge |
|
||||
|---|---|---|---|---|
|
||||
| recognizer anchor injector | `generate/recognizer_anchor_inject.py` | FINAL | Refusal-only guard preventing no-reference `<N> times ...` comparative multipliers from being injected as `CandidateInitial(value=N, unit="times")`. | Y |
|
||||
| completeness guard tests | `tests/test_candidate_graph_completeness_guard.py` | FINAL | Pins the §9 hard negative matrix, with solve/refusal controls. | Y |
|
||||
| question-layer gap survey | `docs/analysis/question-layer-gap-survey.md` | FINAL | Canonical audited 44-refusal partition and backlog interpretation. | Y |
|
||||
| solver operation coverage | `docs/analysis/solver-operation-coverage.md` | FINAL | Canonical read-only audit of existing solver op coverage and representation gaps. | Y |
|
||||
| composition capability scope | `docs/analysis/composition-capability-scope.md` | FINAL | Canonical execution-lane v2 scope for ADR-0174 Phase 5b emission/representation and §9 guard precondition. | Y |
|
||||
| comprehension primitive inventory | `docs/analysis/comprehension-primitive-inventory.md` | FINAL | Canonical consolidated inventory and cross-subject leverage map from the execution lane. | Y |
|
||||
| handoff | `HANDOFF-gpt55-2026-06-03.md` | FINAL | Merge/readiness manifest for this thread. | Y |
|
||||
|
||||
## Validation
|
||||
|
||||
Completed on branch `codex/ntimes-completeness-guard`:
|
||||
|
||||
- `uv run python -m pytest tests/test_candidate_graph_completeness_guard.py -q`
|
||||
- `21 passed`
|
||||
- `uv run python -m pytest tests/test_adr_0131_G2_comparatives.py tests/test_adr_0131_G2a_comparative_verb_widening.py -q`
|
||||
- `30 passed`
|
||||
- `uv run python -m pytest tests/test_aggregate_total_question_forms.py tests/test_discrete_count_open_noun_class.py tests/test_adr_0163_d2_discrete_count_injection.py -q`
|
||||
- `59 passed`
|
||||
- Train-sample probe through `generate.math_candidate_graph.parse_and_solve`
|
||||
- `6 correct / 44 refused / 0 wrong`
|
||||
- `refused_nonzero_count = 0`
|
||||
- admitted IDs: `0003`, `0014`, `0018`, `0021`, `0024`, `0042`
|
||||
|
||||
## New Since Last Message
|
||||
|
||||
- Broadened the §9 guard from phrase-specific `times as many|more` matching to the structural captured shape: cardinal immediately followed by `times` inside the discrete-count injector.
|
||||
- Added no-reference hard negatives for `3 times the number of apples` and `3 times the apples`.
|
||||
- Added explicit safe-refusal controls for no-reference `twice`, no-reference `double`, and case 605.
|
||||
- Added canonical execution-lane `composition-capability-scope.md`.
|
||||
- Added canonical execution-lane `comprehension-primitive-inventory.md`.
|
||||
- Added this handoff manifest.
|
||||
|
||||
## Not Done / WIP
|
||||
|
||||
- No 5b emission/representation slice was implemented.
|
||||
- No <=20-case validation sub-corpus was authored.
|
||||
- No solver operation kinds or binding-graph node types were added.
|
||||
- No serving/eval/claims-ledger files were changed.
|
||||
- Superseded `0001` / `0002` / `0003` patch files from the Opus handoff directory were not committed.
|
||||
31
README.md
31
README.md
|
|
@ -27,20 +27,6 @@ When facing a design decision, the world offers two visible options: use what al
|
|||
|
||||
---
|
||||
|
||||
## Native Substrate Direction — Python, Rust, Zig
|
||||
|
||||
CORE is not moving toward a wholesale Zig rewrite. The architecture is moving toward a stricter native-substrate boundary:
|
||||
|
||||
- **Python** remains the semantic source of truth: cognition runtime, teaching/review workflows, pack ratification, eval harnesses, and Workbench/operator tooling.
|
||||
- **Rust** remains the incumbent native algebra backend: Cl(4,1) products, versor operations, CGA inner product, exact recall, and diffusion surfaces already proven by parity gates.
|
||||
- **Zig** is a candidate material for the next native substrate layer: Delta-CRDT arenas/deltas/merge kernels, deterministic modality compilers such as `audio_core_v1`, stable C ABI surfaces, edge-native ingestion, and selected exact recall challenge kernels only after parity and benchmark proof.
|
||||
|
||||
The rule is component law, not language preference. Zig may enter where explicit allocation, deterministic buffer ownership, C ABI clarity, and edge-native deployment materially strengthen CORE. Zig must not replace review-gated semantics, introduce approximate recall, hide repair in native code, or turn teacher/shadow models into substrate.
|
||||
|
||||
Decision package: [`docs/zig/README.md`](docs/zig/README.md). Adoption gates: [`docs/zig/adoption-gates.md`](docs/zig/adoption-gates.md).
|
||||
|
||||
---
|
||||
|
||||
## The Truth-Seeking Schema
|
||||
|
||||
Co-equal with the algebraic substrate. CORE's epistemic schema is a foundational architectural commitment: every claim that enters the runtime field carries a typed position in a revision graph (`SPECULATIVE`, `COHERENT`, `CONTESTED`, `FALSIFIED`); coherence — not source authority — is the only admission signal; no claim is ever locked, even when COHERENT; identity cannot be rewritten by content; and exactly one mutation path admits knowledge, enforced by a CI-level architectural-invariant test.
|
||||
|
|
@ -78,7 +64,7 @@ English establishes the operational base. Hebrew and Koine Greek bring the hidde
|
|||
```bash
|
||||
pip install -e ".[dev]"
|
||||
pytest tests/test_versor_closure.py # the core invariant — must pass first
|
||||
pytest tests/ # full suite (~8,337 tests; some pre-existing reds — see docs/test-debt-quarantine.md)
|
||||
pytest tests/ # full suite (~4 minutes, 1099 tests)
|
||||
```
|
||||
|
||||
### Watch the flywheel turn — one command
|
||||
|
|
@ -308,16 +294,11 @@ ADR-0114a's 10 anti-overfitting proof obligations are all discharged for the
|
|||
it does not confabulate. The zero-confabulation property holds against the external
|
||||
benchmark.
|
||||
|
||||
**ADR-0120 (the first `expert` promotion contract) has since been built and
|
||||
exercised.** On 2026-05-23 `mathematics_logic` was signed and briefly promoted to
|
||||
`expert` — then **auto-reverted to `audit-passed`** when its evidence bundle
|
||||
drifted: a *non-gating* GSM8K coverage metric moved, which changed the
|
||||
evidence-derived digest and invalidated the signature. That revert is the
|
||||
contract's fail-closed property working as designed — CORE revoked its own expert
|
||||
claim rather than carry a stale one. **No domain is at `expert` today**, and when
|
||||
`expert` is held at all it rests on CORE-authored lanes, not external GSM8K. Full
|
||||
record: [ADR-0200](docs/decisions/ADR-0200-expert-claim-reconciliation.md) and
|
||||
[`docs/claims_ledger.md`](docs/claims_ledger.md).
|
||||
**ADR-0120 (first `expert` promotion contract) is the next gate.** It will set the
|
||||
numeric expert threshold and ε, require all 10 ADR-0114a obligations as hard gates,
|
||||
and sign the first `expert_claims` entry — or defer honestly if the correct_rate
|
||||
gate is not yet met. **No domain is at `expert` today.** That status string remains
|
||||
reserved namespace.
|
||||
|
||||
To run the GSM8K math eval lane:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,5 @@
|
|||
from .cl41 import geometric_product, reverse, grade_project, scalar_part, norm_squared, basis_vector
|
||||
from .versor import versor_apply, normalize_to_versor, versor_condition
|
||||
from .cga import (
|
||||
EMBED_EXACT_MAX,
|
||||
cga_inner,
|
||||
outer_product,
|
||||
is_null,
|
||||
null_project,
|
||||
embed_point,
|
||||
read_scalar_e1,
|
||||
blade_grade,
|
||||
blade_norm,
|
||||
graded_wedge,
|
||||
is_incident,
|
||||
dual,
|
||||
meet,
|
||||
)
|
||||
from .cga import cga_inner, outer_product, is_null, null_project, embed_point
|
||||
from .holonomy import holonomy_encode, holonomy_similarity
|
||||
from .rotor import word_transition_rotor
|
||||
|
|
|
|||
158
algebra/cga.py
158
algebra/cga.py
|
|
@ -19,34 +19,13 @@ No cosine similarity. No L2 norm. No approximate indexing.
|
|||
"""
|
||||
|
||||
import numpy as np
|
||||
from .cl41 import (
|
||||
geometric_product,
|
||||
grade_project,
|
||||
reverse,
|
||||
scalar_part,
|
||||
N_COMPONENTS,
|
||||
)
|
||||
|
||||
# The unit pseudoscalar I5 = e1 e2 e3 e4 e5 (the grade-5 blade, component 31).
|
||||
# In Cl(4,1) with signature (+,+,+,+,-), I5^2 = -1, so I5^{-1} = -I5. Used by
|
||||
# ``dual`` / ``meet``. Module-level singleton; never mutated.
|
||||
_PSEUDOSCALAR_INDEX = 31
|
||||
_I5 = np.zeros(N_COMPONENTS, dtype=np.float64)
|
||||
_I5[_PSEUDOSCALAR_INDEX] = 1.0
|
||||
from .cl41 import geometric_product, scalar_part, basis_vector, N_COMPONENTS
|
||||
|
||||
# Basis-vector component indices for e4/e5 inside the grade-1 block.
|
||||
# component 1=e1, 2=e2, 3=e3, 4=e4, 5=e5.
|
||||
_E4_IDX = 4
|
||||
_E5_IDX = 5
|
||||
|
||||
# Pinned magnitude ceiling for f64-exact embedding + read-back (Phase 0A).
|
||||
# Below this bound, ``embed_point(..., dtype=np.float64)`` round-trips integer
|
||||
# coordinates exactly through ``read_scalar_e1`` and the conformal distance metric
|
||||
# stays exact (proven in tests/test_cga_f64_exactness.py). The field-reasoner reader
|
||||
# REFUSES any quantity whose magnitude exceeds this bound; the refusal lives in the
|
||||
# reader — this module only states the bound. Generous vs GSM8K (quantities ~< 1e5).
|
||||
EMBED_EXACT_MAX: int = 1_000_000
|
||||
|
||||
|
||||
def cga_inner(X: np.ndarray, Y: np.ndarray) -> float:
|
||||
"""
|
||||
|
|
@ -59,19 +38,10 @@ def cga_inner(X: np.ndarray, Y: np.ndarray) -> float:
|
|||
|
||||
|
||||
def outer_product(X: np.ndarray, Y: np.ndarray) -> np.ndarray:
|
||||
"""The antisymmetric (commutator) product ``0.5 * (XY - YX)``.
|
||||
|
||||
HONEST CONTRACT: this equals the grade-raising wedge ``X ^ Y`` **only when both
|
||||
operands are grade 1** (vectors). For higher-grade operands it is the *commutator*
|
||||
(Lie bracket), which is NOT the wedge — in particular it does NOT build a k-blade
|
||||
by repeated application (a bivector commuted with a vector collapses the grade-3
|
||||
part to grade 1). Existing callers use the result as an opaque, deterministic
|
||||
relationship feature (folded into a scalar via :func:`cga_inner`), where the
|
||||
commutator is well-defined regardless; none read it by grade.
|
||||
|
||||
For the true grade-raising exterior product (lines/planes/incidence) use
|
||||
:func:`graded_wedge`. (Renamed contract only — behaviour is unchanged, so every
|
||||
current caller is byte-identical.)
|
||||
"""
|
||||
Outer (wedge) product: X ^ Y.
|
||||
For a prompt versor X_p and response versor X_r,
|
||||
X_p ^ X_r is a grade-2 object encoding their geometric relationship.
|
||||
"""
|
||||
XY = geometric_product(X, Y)
|
||||
YX = geometric_product(Y, X)
|
||||
|
|
@ -92,25 +62,18 @@ def null_project(X: np.ndarray) -> np.ndarray:
|
|||
return embed_point(euclidean)
|
||||
|
||||
|
||||
def embed_point(x: np.ndarray, *, dtype: "np.typing.DTypeLike" = np.float32) -> np.ndarray:
|
||||
def embed_point(x: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Embed a Euclidean point x in R^3 into the CGA null cone.
|
||||
|
||||
X = x + n_o + 0.5|x|^2 n_inf,
|
||||
where n_o = 0.5(e5-e4), n_inf = e4+e5.
|
||||
|
||||
``dtype`` defaults to ``float32`` so every existing caller is byte-unchanged.
|
||||
The field-reasoner reader passes ``dtype=np.float64`` to get an exact embedding:
|
||||
``geometric_product`` already preserves float64 (``np.result_type``), so the
|
||||
only thing that forced f32 was this construction. f32 silently collapses the
|
||||
``n_o`` weight past ~1e4 (the ``0.5|x|^2`` terms lose the ``±1``); f64 keeps it
|
||||
exact up to :data:`EMBED_EXACT_MAX` (see tests/test_cga_f64_exactness.py).
|
||||
"""
|
||||
x = np.asarray(x, dtype=dtype)
|
||||
x = np.asarray(x, dtype=np.float32)
|
||||
assert len(x) == 3, "embed_point expects a 3D vector"
|
||||
|
||||
x_sq = float(np.dot(x, x))
|
||||
result = np.zeros(N_COMPONENTS, dtype=dtype)
|
||||
result = np.zeros(N_COMPONENTS, dtype=np.float32)
|
||||
result[1:4] = x
|
||||
|
||||
# n_o + 0.5|x|^2 n_inf
|
||||
|
|
@ -119,108 +82,3 @@ def embed_point(x: np.ndarray, *, dtype: "np.typing.DTypeLike" = np.float32) ->
|
|||
result[_E4_IDX] = 0.5 * (x_sq - 1.0)
|
||||
result[_E5_IDX] = 0.5 * (x_sq + 1.0)
|
||||
return result
|
||||
|
||||
|
||||
def read_scalar_e1(X: np.ndarray) -> float:
|
||||
"""Projective dehomogenization on the e1 axis — the exact, weight-invariant
|
||||
read-back of a scalar coordinate from a (possibly dilated) conformal point.
|
||||
|
||||
A point at coordinate ``v`` on the e1 number line embeds as
|
||||
``X = v*e1 + n_o + 0.5 v^2 n_inf``; a uniform conformal dilation by ``k``
|
||||
scales the whole null vector. The coordinate is recovered as
|
||||
``e1_coefficient / n_o_weight`` where the n_o weight is ``X[e5] - X[e4]``
|
||||
(== 1 for an un-dilated point), so any dilation weight divides out. This is
|
||||
the correct read-back for weight-changing operators; a raw distance-from-origin
|
||||
is wrong for them.
|
||||
|
||||
Raises ``ValueError`` on a degenerate (zero) n_o weight — a point at infinity
|
||||
or an f32 weight-collapse — rather than returning a silently wrong value.
|
||||
"""
|
||||
no_weight = float(X[_E5_IDX] - X[_E4_IDX])
|
||||
if no_weight == 0.0:
|
||||
raise ValueError(
|
||||
"read_scalar_e1: degenerate n_o weight (point at infinity or f32 collapse)"
|
||||
)
|
||||
return float(X[1]) / no_weight
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Incidence algebra — the corrected grade-raising wedge, dual, and meet.
|
||||
# These let the inner product operate on RELATIONS among entities (lines, planes,
|
||||
# incidence) rather than only pairwise point distance. Built only from the existing
|
||||
# Cl(4,1) primitives (geometric_product, grade_project) + the pseudoscalar; they add
|
||||
# no normalization, no approximation, and leave the versor_condition path untouched
|
||||
# (flats are null-cone outer products, not unit versors).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_MAX_GRADE = 5 # Cl(4,1): grades 0..5
|
||||
|
||||
|
||||
def blade_grade(X: np.ndarray) -> int:
|
||||
"""The single grade of a homogeneous blade. Raises if X is zero or grade-mixed.
|
||||
|
||||
Grade is detected by EXACT nonzero (no tolerance): integer-coordinate embeddings
|
||||
produce exact integer blades in float64, so a grade block is exactly 0 or not.
|
||||
"""
|
||||
grades = [k for k in range(_MAX_GRADE + 1) if np.any(grade_project(X, k))]
|
||||
if len(grades) != 1:
|
||||
raise ValueError(f"not a homogeneous blade: nonzero grades {grades}")
|
||||
return grades[0]
|
||||
|
||||
|
||||
def graded_wedge(X: np.ndarray, Y: np.ndarray) -> np.ndarray:
|
||||
"""The true grade-raising exterior product ``X ^ Y`` for homogeneous blades.
|
||||
|
||||
``X ^ Y = <X Y>_{grade(X)+grade(Y)}`` — the top-grade part of the geometric
|
||||
product. Unlike :func:`outer_product` (the commutator) this composes correctly:
|
||||
``graded_wedge(graded_wedge(P, Q), n_inf)`` builds the grade-3 line P^Q^n_inf,
|
||||
and so on. If the grades sum past the pseudoscalar (>5) the wedge is identically
|
||||
zero. For two grade-1 vectors it agrees with :func:`outer_product` exactly.
|
||||
"""
|
||||
gx, gy = blade_grade(X), blade_grade(Y)
|
||||
if gx + gy > _MAX_GRADE:
|
||||
return np.zeros(N_COMPONENTS, dtype=geometric_product(X, Y).dtype)
|
||||
return grade_project(geometric_product(X, Y), gx + gy)
|
||||
|
||||
|
||||
def blade_norm(X: np.ndarray) -> float:
|
||||
"""Reversion norm ``sqrt(|<X reverse(X)>_0|)`` — zero iff X is the zero blade."""
|
||||
return float(np.sqrt(abs(scalar_part(geometric_product(X, reverse(X))))))
|
||||
|
||||
|
||||
def is_incident(point: np.ndarray, flat: np.ndarray) -> bool:
|
||||
"""Exact incidence test: is ``point`` on ``flat`` (a line/plane OPNS blade)?
|
||||
|
||||
True iff ``point ^ flat == 0`` EXACTLY (every component zero) — no float
|
||||
tolerance to admit (the wrong=0 discipline: a near-incident point is REFUSED,
|
||||
not admitted). Exact for integer-coordinate points within ``EMBED_EXACT_MAX``.
|
||||
"""
|
||||
return not bool(np.any(graded_wedge(point, flat)))
|
||||
|
||||
|
||||
def dual(X: np.ndarray) -> np.ndarray:
|
||||
"""Pseudoscalar dual ``X * I5^{-1}`` (``I5^{-1} = -I5`` since ``I5^2 = -1``).
|
||||
|
||||
Maps a grade-k blade to grade ``5-k``. Involutive up to sign:
|
||||
``dual(dual(X)) == -X``.
|
||||
"""
|
||||
return geometric_product(X, -_I5)
|
||||
|
||||
|
||||
def meet(A: np.ndarray, B: np.ndarray) -> np.ndarray:
|
||||
"""The meet (intersection) ``dual(dual(A) ^ dual(B))`` of two homogeneous blades.
|
||||
|
||||
Correct for operands in GENERAL POSITION whose join spans the space — e.g. two
|
||||
non-parallel planes meet in their intersection line. The grade of the result is
|
||||
``grade(A)+grade(B)-5``.
|
||||
|
||||
HONEST ENVELOPE: this full-pseudoscalar meet DEGENERATES for operands that share
|
||||
a proper subspace (e.g. two coplanar lines, two parallel planes): the inner wedge
|
||||
``dual(A) ^ dual(B)`` is then identically zero, so ``meet`` returns the **zero
|
||||
multivector** — a detectable signal of "no transversal meet", never a silently
|
||||
wrong value. The general intersection of such operands (e.g. the point where two
|
||||
coplanar lines cross) requires the *join-relative* meet, which is deliberately
|
||||
NOT implemented here; the caller MUST check ``blade_norm(result) == 0`` and treat
|
||||
zero as degenerate/refuse rather than as a geometric object.
|
||||
"""
|
||||
return dual(graded_wedge(dual(A), dual(B)))
|
||||
|
|
|
|||
|
|
@ -50,14 +50,6 @@ DEFAULT_RESOLVABLE_PACK_IDS: tuple[str, ...] = (
|
|||
"en_core_spatial_v1",
|
||||
"en_core_causation_v1",
|
||||
"en_core_polarity_v1",
|
||||
# Foundation-curriculum syntax substrate. Mounted AFTER the
|
||||
# high-frequency content packs so it is purely additive: every lemma
|
||||
# it owns is disjoint from earlier mounted packs. Two colliders were
|
||||
# renamed at authoring time so syntax never steals prior resolution
|
||||
# (`agent`→`agent_role`, owned by en_core_meta_v1; `comparison`→
|
||||
# `comparison_relation`, owned by en_core_cognition_v1). `negation`
|
||||
# is kept bare: no *mounted* pack owned it, so syntax now grounds it.
|
||||
"en_core_syntax_v1",
|
||||
"en_core_relations_v1",
|
||||
"en_core_relations_v2",
|
||||
# ADR-0073c — synthetic English anchor lemmas for cross-lang collapse.
|
||||
|
|
|
|||
424
chat/runtime.py
424
chat/runtime.py
|
|
@ -4,7 +4,6 @@ from dataclasses import dataclass, replace
|
|||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import warnings
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, List
|
||||
|
||||
|
|
@ -38,8 +37,6 @@ from core.epistemic_state import (
|
|||
epistemic_state_for_grounding_source,
|
||||
normative_detail_from_verdicts,
|
||||
)
|
||||
from core.proposal_review.summary import ProposalReviewIdleSummary, idle_summary
|
||||
from core.response_governance import govern_response, shape_surface
|
||||
from chat.telemetry import (
|
||||
TurnEventSink,
|
||||
format_correction_event_jsonl,
|
||||
|
|
@ -54,8 +51,7 @@ from teaching.discovery import (
|
|||
format_candidate_jsonl,
|
||||
)
|
||||
from teaching.discovery_sink import DiscoveryCandidateSink
|
||||
from engine_state import EngineStateStore, get_git_revision
|
||||
from core.engine_identity import engine_identity_for_config
|
||||
from engine_state import EngineStateStore
|
||||
from recognition.anti_unifier import derive_recognizer
|
||||
from recognition.outcome import FeatureBundle
|
||||
from recognition.registry import RecognizerRegistry
|
||||
|
|
@ -389,28 +385,6 @@ def _make_trajectory_from_result(result, turn: int):
|
|||
return operator.build(frames, trajectory_id=f"turn_{turn}")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TurnAccrual:
|
||||
"""The inline-realization outcome of one turn (Step B / E).
|
||||
|
||||
``kind`` is ``"realized"`` (a declarative fact was accrued into the held self),
|
||||
``"determined"`` (a question was answered over realized knowledge), ``"estimated"``
|
||||
(Step E — a converse query DETERMINE refused, for which a calibrated converse-guess
|
||||
exists), or ``"none"`` (nothing comprehensible to accrue/determine). The payload
|
||||
carries the typed result for introspection. B-1 records, does not surface; B-2 and E
|
||||
surface only when their flag is on.
|
||||
"""
|
||||
|
||||
kind: str
|
||||
realized: Any = None # generate.realize.Realized | NotRealized | None
|
||||
determination: Any = None # generate.determine.Determined | Undetermined | None
|
||||
# Step E — the converse-guess candidate and its SERVE license, when ``kind`` is
|
||||
# ``"estimated"``. ``estimate`` is a ``ConverseEstimate``; ``license`` a
|
||||
# ``LicenseDecision`` (None if the predicate-class is absent from the ratified ledger).
|
||||
estimate: Any = None
|
||||
license: Any = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ChatResponse:
|
||||
surface: str
|
||||
|
|
@ -473,13 +447,6 @@ class ChatResponse:
|
|||
# Comma-separated violated boundary/commitment IDs when normative
|
||||
# clearance is VIOLATED or SUPPRESSED; empty string otherwise.
|
||||
normative_detail: str = ""
|
||||
# ADR-0206 — Response Governance Bridge reach level for this turn.
|
||||
# Mirrors the TurnEvent field so callers (CLI, demos, tests) can read
|
||||
# the governed reach level from ChatResponse. Scaffold contract:
|
||||
# always "strict" (govern_response is STRICT-only until widening is
|
||||
# built). ``"strict"`` default preserves byte-identity for callers
|
||||
# that construct ChatResponse without this field.
|
||||
reach_level: str = "strict"
|
||||
# ADR-0072 (R5) — operator-visible register identity per turn.
|
||||
# Mirrors the TurnEvent fields so callers (CLI, demos, tests) can
|
||||
# read the register state from ChatResponse without re-parsing the
|
||||
|
|
@ -528,29 +495,6 @@ class ChatResponse:
|
|||
dispatch_trace: DispatchTrace | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class IdleTickResult:
|
||||
"""Outcome of one ``idle_tick``.
|
||||
|
||||
The proposal pass is PROPOSAL-ONLY learning, never ratification (HITL ratifies). The
|
||||
Step D consolidation pass is SESSION-memory learning: ``facts_consolidated`` derived
|
||||
facts were written back into the held self so the next ``determine`` reaches them
|
||||
directly — still never corpus mutation, never a proposal. The proposal-review sub-pass
|
||||
(``proposal_review``) is READ-ONLY: it surfaces pending comprehension-failure proposals for
|
||||
review and mutates nothing.
|
||||
"""
|
||||
|
||||
candidates_contemplated: int
|
||||
proposals_created: int
|
||||
pending_proposals: int
|
||||
#: Step D — derived facts consolidated into the held self this tick (0 unless
|
||||
#: ``config.consolidate_determinations`` and the closure had a new layer to add).
|
||||
facts_consolidated: int = 0
|
||||
#: IT — read-only proposal-review summary (None unless ``config.review_pending_proposals``).
|
||||
#: Surfaces pending comprehension-failure proposals for review; mutates nothing.
|
||||
proposal_review: ProposalReviewIdleSummary | None = None
|
||||
|
||||
|
||||
class ChatRuntime:
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -709,11 +653,6 @@ class ChatRuntime:
|
|||
# W-013 — last classified intent, updated each turn for /explain REPL use.
|
||||
self._last_intent: Any | None = None
|
||||
self._last_input_text: str = ""
|
||||
# Step B (inline realization) — the last turn's accrual outcome (what the turn
|
||||
# realized into the held self, or determined over it). Introspectable; the
|
||||
# surface contract is unchanged (slice B-1 records, does not surface).
|
||||
self._last_turn_accrual: TurnAccrual | None = None
|
||||
self._relational_pack_lemmas: frozenset[str] | None = None
|
||||
self._engine_state_store: EngineStateStore | None = (
|
||||
None if no_load_state else EngineStateStore(engine_state_path)
|
||||
)
|
||||
|
|
@ -727,27 +666,6 @@ class ChatRuntime:
|
|||
# checkpoint is loaded, flushed to the sink on attach_telemetry_sink.
|
||||
# None means no reboot was detected this session.
|
||||
self._pending_reboot_payload: str | None = None
|
||||
# L11 — the engine's content-derived identity (who am I), and the
|
||||
# identity stamped in the loaded checkpoint (the lineage parent for the
|
||||
# next checkpoint). ``_loaded_engine_identity`` stays "" at genesis.
|
||||
self._engine_identity: str = engine_identity_for_config(
|
||||
self.config, get_git_revision()
|
||||
)
|
||||
self._loaded_engine_identity: str = ""
|
||||
# CL — the persistent reviewed-learning proposal log. ``idle_tick()``
|
||||
# advances it during idle (proposal-only); it lives alongside the engine
|
||||
# state so the learning backlog survives reboot. None for no_load_state
|
||||
# (ephemeral runtimes don't accumulate a learning lineage).
|
||||
self._proposal_log: Any | None = None
|
||||
if self._engine_state_store is not None:
|
||||
from teaching.proposals import ProposalLog
|
||||
|
||||
self._proposal_log = ProposalLog(
|
||||
path=self._engine_state_store.path / "proposals.jsonl"
|
||||
)
|
||||
# L11 — set True on reboot when the stamped checkpoint identity differs
|
||||
# from the recomputed identity (the ratified substrate changed while down).
|
||||
self.identity_continuity_break: bool = False
|
||||
if self._engine_state_store is not None and self._engine_state_store.exists():
|
||||
self._load_engine_state()
|
||||
|
||||
|
|
@ -755,46 +673,11 @@ class ChatRuntime:
|
|||
store = self._engine_state_store
|
||||
if store is None:
|
||||
return
|
||||
# Schema-version compatibility gates the whole load: load_manifest()
|
||||
# refuses (raises) a checkpoint written by newer code BEFORE we read any
|
||||
# recognizers/candidates (L10 step-2 migration discipline).
|
||||
manifest = store.load_manifest() or {}
|
||||
recognizers = store.load_recognizers()
|
||||
self._recognizer_registry = RecognizerRegistry.from_recognizers(recognizers)
|
||||
self._pending_candidates = store.load_discovery_candidates()
|
||||
manifest = store.load_manifest() or {}
|
||||
self._turn_count = int(manifest.get("turn_count", 0))
|
||||
# L11 — the identity this checkpoint was written under becomes the lineage
|
||||
# parent of the next checkpoint we write. If it differs from the identity
|
||||
# we recomputed at boot, the ratified substrate changed during downtime:
|
||||
# we would resume the lived state under a DIFFERENT identity. Surface it
|
||||
# (warn + flag); refuse only under strict_identity_continuity.
|
||||
self._loaded_engine_identity = str(manifest.get("engine_identity", ""))
|
||||
if (
|
||||
self._loaded_engine_identity
|
||||
and self._loaded_engine_identity != self._engine_identity
|
||||
):
|
||||
self.identity_continuity_break = True
|
||||
message = (
|
||||
"engine identity continuity break: checkpoint was written under "
|
||||
f"{self._loaded_engine_identity[:12]}… but this build computes "
|
||||
f"{self._engine_identity[:12]}… — the ratified identity substrate "
|
||||
"changed while the engine was down. Resuming would carry the lived "
|
||||
"state into a different identity."
|
||||
)
|
||||
if self.config.strict_identity_continuity:
|
||||
from core.engine_identity import IdentityContinuityError
|
||||
|
||||
raise IdentityContinuityError(message)
|
||||
warnings.warn(message, RuntimeWarning, stacklevel=2)
|
||||
# Shape B+ (schema v2): restore the lived session state into the live
|
||||
# context so a reboot resumes the SAME life (field/vault/anchor/graph/
|
||||
# referents/dialogue). Opt-in (config.persist_session_state); None for a
|
||||
# v1 checkpoint -> fresh session (the historical Shape B behavior), so old
|
||||
# checkpoints stay loadable.
|
||||
if self.config.persist_session_state and self._context is not None:
|
||||
session_snapshot = store.load_session_state()
|
||||
if session_snapshot is not None:
|
||||
self._context.restore(session_snapshot)
|
||||
# W-024 / ADR-0158 — buffer reboot event for emission when sink attaches.
|
||||
from engine_state import _git_revision
|
||||
self._pending_reboot_payload = format_reboot_event_jsonl(
|
||||
|
|
@ -828,130 +711,7 @@ class ChatRuntime:
|
|||
for c in candidates_to_save
|
||||
]
|
||||
store.save_discovery_candidates(candidates_to_save)
|
||||
# Shape B+ (schema v2): persist the lived session state (field, vault,
|
||||
# anchor, graph, referents, dialogue) BEFORE the manifest, so the
|
||||
# manifest stays the last durable act — the commit marker for the turn.
|
||||
# Opt-in (config.persist_session_state): a deliberate resume mode, off by
|
||||
# default so one-shot runtimes don't pay the per-turn snapshot cost.
|
||||
if self._context is not None and self.config.persist_session_state:
|
||||
store.save_session_state(self._context.snapshot())
|
||||
# L11 — stamp the engine's identity and its lineage parent (the identity
|
||||
# of the prior checkpoint). Same substrate -> identity == parent (a stable
|
||||
# life); a ratified substrate change -> identity != parent (the bump).
|
||||
store.save_manifest(
|
||||
self._turn_count,
|
||||
engine_identity=self._engine_identity,
|
||||
parent_engine_identity=self._loaded_engine_identity,
|
||||
)
|
||||
self._loaded_engine_identity = self._engine_identity
|
||||
|
||||
def _count_pending_proposals(self) -> int:
|
||||
if self._proposal_log is None:
|
||||
return 0
|
||||
return sum(
|
||||
1
|
||||
for entry in self._proposal_log.current_state().values()
|
||||
if entry.get("state") == "pending"
|
||||
)
|
||||
|
||||
def idle_tick(self) -> "IdleTickResult":
|
||||
"""Advance learning during idle (NO user turn). Two disjoint passes:
|
||||
|
||||
1. PROPOSAL pass (the reviewed-learning flywheel): turn the pending discovery
|
||||
backlog into reviewable teaching proposals. Contemplate each pending
|
||||
candidate (enrichment) and run the replay-gated ``propose_from_candidate``,
|
||||
which leaves a PROPOSAL-ONLY ``pending`` entry in the persistent proposal
|
||||
log. An idle tick NEVER ratifies — ratification (appending to the corpus)
|
||||
stays HITL via ``teaching/review`` (CLAUDE.md teaching safety). The tick only
|
||||
*proposes*; the reviewed loop is not bypassed or duplicated.
|
||||
|
||||
2. CONSOLIDATION pass (Step D — CLOSE): the loop learns from *determined* facts.
|
||||
Run one semi-naive layer of the member/subset deductive closure over the held
|
||||
self (``generate.determine.consolidate_once``); each soundly-derived,
|
||||
proof_chain-verified fact is written back as a SPECULATIVE realized record so
|
||||
the next ``determine`` reaches it directly. SESSION memory (immediate,
|
||||
allowed) — NOT corpus mutation, NOT a proposal; the HITL path is untouched.
|
||||
Gated by ``config.consolidate_determinations``. The closure converges (a
|
||||
saturated tick consolidates nothing — ``at_fixed_point``).
|
||||
|
||||
The proposal log, the candidate backlog, and (with ``persist_session_state``)
|
||||
the consolidated facts all live in the engine-state dir, so this learning
|
||||
progress persists across reboot (CL-2).
|
||||
"""
|
||||
contemplated_count = 0
|
||||
created = 0
|
||||
facts_consolidated = 0
|
||||
did_work = False
|
||||
|
||||
# 1. Proposal pass — unchanged behavior, runs only with a log + backlog.
|
||||
if self._proposal_log is not None and self._pending_candidates:
|
||||
from teaching.contemplation import contemplate
|
||||
from teaching.proposals import (
|
||||
ProposalError,
|
||||
TeachingChainProposal,
|
||||
_current_revision,
|
||||
propose_from_candidate,
|
||||
)
|
||||
from teaching.source import ProposalSource
|
||||
|
||||
vault_probe = (
|
||||
_vault_probe_for_context(self._context) if self._context else None
|
||||
)
|
||||
contemplated = [
|
||||
contemplate(candidate, vault_probe=vault_probe)
|
||||
for candidate in self._pending_candidates
|
||||
]
|
||||
contemplated_count = len(contemplated)
|
||||
for candidate in contemplated:
|
||||
source = ProposalSource(
|
||||
kind="contemplation",
|
||||
source_id=candidate.candidate_id,
|
||||
emitted_at_revision=_current_revision(),
|
||||
)
|
||||
try:
|
||||
result = propose_from_candidate(
|
||||
candidate, log=self._proposal_log, source=source
|
||||
)
|
||||
except ProposalError:
|
||||
continue
|
||||
if isinstance(result, TeachingChainProposal):
|
||||
created += 1
|
||||
did_work = True
|
||||
|
||||
# 2. Consolidation pass (Step D) — runs independently of the backlog.
|
||||
if self.config.consolidate_determinations and self._context is not None:
|
||||
from generate.determine import consolidate_once
|
||||
|
||||
facts_consolidated = consolidate_once(self._context).consolidated
|
||||
did_work = True
|
||||
|
||||
# 3. Proposal-review sub-pass (IT) — READ-ONLY. Surfaces pending comprehension-failure
|
||||
# proposals (the contemplation pass's N5 artifacts) for review. It NEVER mutates an
|
||||
# artifact, NEVER sets ``did_work`` (no state change → no checkpoint), NEVER ratifies
|
||||
# or mounts. A reporter failure is CAPTURED, not propagated, so it cannot corrupt the
|
||||
# tick. Gated off by default.
|
||||
proposal_review = None
|
||||
if self.config.review_pending_proposals:
|
||||
try:
|
||||
proposal_review = idle_summary()
|
||||
except Exception as exc: # noqa: BLE001 — a reporter failure must not corrupt the tick
|
||||
proposal_review = ProposalReviewIdleSummary(
|
||||
safe=False, total=0, review_needed=0, malformed=0, by_family=(),
|
||||
errors=(f"proposal_review_failed:{type(exc).__name__}",),
|
||||
)
|
||||
|
||||
# Persist the advanced state once (backlog + lineage +, with
|
||||
# persist_session_state, the consolidated facts). Skipped on a no-op tick so an
|
||||
# idle engine with nothing to learn does not churn the checkpoint.
|
||||
if did_work:
|
||||
self.checkpoint_engine_state()
|
||||
return IdleTickResult(
|
||||
candidates_contemplated=contemplated_count,
|
||||
proposals_created=created,
|
||||
pending_proposals=self._count_pending_proposals(),
|
||||
facts_consolidated=facts_consolidated,
|
||||
proposal_review=proposal_review,
|
||||
)
|
||||
store.save_manifest(self._turn_count)
|
||||
|
||||
def record_recognition_example(
|
||||
self,
|
||||
|
|
@ -1004,164 +764,9 @@ class ChatRuntime:
|
|||
|
||||
def _checkpointed_response(self, response: ChatResponse) -> ChatResponse:
|
||||
self._turn_count += 1
|
||||
# Step B — inline realization, BEFORE the checkpoint so an accrued fact is in
|
||||
# the snapshot this turn (survives reboot with persist_session_state). Gated;
|
||||
# off by default. The surface contract is unchanged (the outcome is recorded,
|
||||
# not surfaced).
|
||||
if self.config.accrue_realized_knowledge:
|
||||
self._accrue_in_turn(self._last_input_text)
|
||||
response = self._maybe_surface_determination(response)
|
||||
self.checkpoint_engine_state()
|
||||
return response
|
||||
|
||||
def _maybe_surface_determination(self, response: ChatResponse) -> ChatResponse:
|
||||
"""Step B-2 / E — select the user-facing surface from the turn's accrual.
|
||||
|
||||
B-2: when the turn DETERMINED an answer over realized knowledge, select the
|
||||
rendered determination as ``surface`` (the realizer's ``articulation_surface``
|
||||
is retained as evidence). An ``Undetermined`` turn keeps the default surface.
|
||||
|
||||
E: when the turn is ``estimated`` (a refused converse query with a calibrated
|
||||
guess) AND ``estimation_enabled``, route the guess through the ADR-0206 bridge —
|
||||
``govern_response`` widens to APPROXIMATE iff the predicate-class holds a genuine
|
||||
SERVE license, and ``shape_surface`` DISCLOSES it as ``[approximate] …``. An
|
||||
unlicensed class stays STRICT (the surface is unchanged — the honest refusal).
|
||||
Off-flag turns never reach here. See ``docs/runtime_contracts.md``.
|
||||
"""
|
||||
accrual = self._last_turn_accrual
|
||||
if accrual is None:
|
||||
return response
|
||||
if accrual.kind == "determined":
|
||||
from generate.determine import Determined, render_determination
|
||||
|
||||
if not isinstance(accrual.determination, Determined):
|
||||
return response # Undetermined → keep the default surface
|
||||
return replace(response, surface=render_determination(accrual.determination))
|
||||
if accrual.kind == "estimated" and self.config.estimation_enabled:
|
||||
return self._surface_estimate(response, accrual)
|
||||
return response
|
||||
|
||||
def _surface_estimate(self, response: ChatResponse, accrual: "TurnAccrual") -> ChatResponse:
|
||||
"""Surface a licensed converse-guess as a DISCLOSED ``[approximate]`` estimate.
|
||||
|
||||
The license gates the widening (``govern_response`` returns STRICT for an
|
||||
unlicensed class → surface unchanged); ``shape_surface`` guarantees the
|
||||
disclosure prefix because a converse guess is ``UNVERIFIED_POSSIBLE``, never in
|
||||
APPROXIMATE's admissible (fully-grounded) set. So a wrong estimate is always a
|
||||
DISCLOSED wrong — wrong=0 (silent) is preserved.
|
||||
"""
|
||||
from core.epistemic_state import EpistemicState
|
||||
from core.response_governance import ReachLevel, govern_response, shape_surface
|
||||
from generate.determine import ConverseEstimate, render_estimate
|
||||
|
||||
estimate, license_decision = accrual.estimate, accrual.license
|
||||
if not isinstance(estimate, ConverseEstimate):
|
||||
return response
|
||||
policy = govern_response(
|
||||
epistemic_state=EpistemicState.UNVERIFIED_POSSIBLE,
|
||||
license_decision=license_decision,
|
||||
)
|
||||
if policy.level is ReachLevel.STRICT:
|
||||
return response # unlicensed → no widening, honest refusal stands
|
||||
disclosed = shape_surface(
|
||||
policy,
|
||||
committed_surface=response.surface,
|
||||
decode_state=EpistemicState.UNVERIFIED_POSSIBLE,
|
||||
disclosed_alternative=render_estimate(estimate),
|
||||
)
|
||||
return replace(response, surface=disclosed, reach_level=policy.level.value)
|
||||
|
||||
def last_turn_accrual(self) -> TurnAccrual | None:
|
||||
"""The most recent turn's inline-realization outcome (Step B), or None when
|
||||
accrual is off or did not complete. Introspection only — never surfaced."""
|
||||
return self._last_turn_accrual
|
||||
|
||||
def _accrue_in_turn(self, text: str) -> None:
|
||||
"""Inline realization (Step B): a comprehensible declarative turn accrues a
|
||||
realized fact into the held self (session vault, SPECULATIVE / as-told); a
|
||||
comprehensible question turn is determined over realized knowledge. Records the
|
||||
typed outcome on ``self._last_turn_accrual``.
|
||||
|
||||
Never raises into the turn — accrual is ADDITIVE, so any failure is a clean
|
||||
no-op (the turn's response is untouched). This is SESSION memory (immediate),
|
||||
NOT ratified learning: it proposes nothing and leaves the teaching/review HITL
|
||||
path untouched. Realization writes go through ``generate.realize`` (the INV-21
|
||||
allowed vault writer); DETERMINE and the readers are total (typed results, no
|
||||
raises), so the broad guard is a defensive backstop, not expected control flow.
|
||||
"""
|
||||
self._last_turn_accrual = None
|
||||
if self._context is None or not text or not text.strip():
|
||||
return
|
||||
try:
|
||||
from generate.determine import determine
|
||||
from generate.meaning_graph.reader import Comprehension, comprehend
|
||||
from generate.meaning_graph.relational import comprehend_relational
|
||||
from generate.realize import realize_comprehension
|
||||
|
||||
if self._relational_pack_lemmas is None:
|
||||
from generate.meaning_graph.relational import load_relational_pack_lemmas
|
||||
|
||||
self._relational_pack_lemmas = load_relational_pack_lemmas()
|
||||
|
||||
readings = (
|
||||
comprehend(text),
|
||||
comprehend_relational(text, self._relational_pack_lemmas),
|
||||
)
|
||||
comprehensions = [c for c in readings if isinstance(c, Comprehension)]
|
||||
|
||||
# A question turn (query-bearing) is DETERMINED over realized knowledge.
|
||||
for c in comprehensions:
|
||||
if c.queries:
|
||||
determination = determine(c, self._context)
|
||||
self._last_turn_accrual = self._accrue_estimate_if_refused(
|
||||
c, determination
|
||||
)
|
||||
return
|
||||
# A declarative turn (a single told fact) is REALIZED into the held self.
|
||||
for c in comprehensions:
|
||||
if not c.queries and c.meaning_graph.relations:
|
||||
self._last_turn_accrual = TurnAccrual(
|
||||
kind="realized", realized=realize_comprehension(c, self._context)
|
||||
)
|
||||
return
|
||||
self._last_turn_accrual = TurnAccrual(kind="none")
|
||||
except Exception: # additive: accrual must never crash a turn # noqa: BLE001
|
||||
self._last_turn_accrual = None
|
||||
|
||||
def _accrue_estimate_if_refused(self, comprehension: Any, determination: Any) -> "TurnAccrual":
|
||||
"""Step E: turn a REFUSED converse query into an ``estimated`` accrual.
|
||||
|
||||
When DETERMINE refused (``Undetermined``) a single non-negated binary query whose
|
||||
converse was told (``p(a,b)`` realized, ``p(b,a)`` asked), produce the calibrated
|
||||
converse-guess + its SERVE license. Off-flag (or any non-converse refusal) returns
|
||||
the plain ``determined`` accrual unchanged — nothing widens. The license is only
|
||||
*attached* here; the surface decision (and the disclosure) is the bridge's, in
|
||||
``_maybe_surface_determination``.
|
||||
"""
|
||||
from generate.determine import Undetermined
|
||||
|
||||
if not (self.config.estimation_enabled and isinstance(determination, Undetermined)):
|
||||
return TurnAccrual(kind="determined", determination=determination)
|
||||
queries = getattr(comprehension, "queries", ())
|
||||
if len(queries) != 1:
|
||||
return TurnAccrual(kind="determined", determination=determination)
|
||||
query = queries[0]
|
||||
if getattr(query, "negated", False) or len(getattr(query, "arguments", ())) != 2:
|
||||
return TurnAccrual(kind="determined", determination=determination)
|
||||
|
||||
from generate.determine import estimate_converse, serve_license
|
||||
|
||||
subject, target = query.arguments[0], query.arguments[1]
|
||||
estimate = estimate_converse(self._context, query.predicate, subject, target)
|
||||
if estimate is None: # no told converse to generalize from → plain refusal
|
||||
return TurnAccrual(kind="determined", determination=determination)
|
||||
return TurnAccrual(
|
||||
kind="estimated",
|
||||
determination=determination,
|
||||
estimate=estimate,
|
||||
license=serve_license(query.predicate),
|
||||
)
|
||||
|
||||
@property
|
||||
def session(self) -> SessionContext:
|
||||
return self._context
|
||||
|
|
@ -2730,24 +2335,9 @@ class ChatRuntime:
|
|||
refusal_emitted=refusal_emitted,
|
||||
hedge_injected=hedge_injected,
|
||||
)
|
||||
main_epistemic = epistemic_state_for_grounding_source(main_grounding_source)
|
||||
main_epistemic_state = main_epistemic.value
|
||||
# ADR-0206 — Response Governance Bridge seam (cognition path).
|
||||
# govern_response is STRICT-only (scaffold), so shape_surface is the
|
||||
# identity transform and response_surface is returned verbatim —
|
||||
# byte-identical to the pre-bridge path. This is live wiring, not
|
||||
# dead code: the response surface now flows through the policy
|
||||
# consumer, and the ONLY thing keeping it strict is the STRICT
|
||||
# return value (proven by the live-wiring test). The risk-reward
|
||||
# widening pathway and the math-serving seam are deferred to their
|
||||
# own PRs (ADR-0206 §5); wrong==0 is untouched here.
|
||||
main_reach_policy = govern_response(epistemic_state=main_epistemic)
|
||||
main_reach_level = main_reach_policy.level.value
|
||||
response_surface = shape_surface(
|
||||
main_reach_policy,
|
||||
committed_surface=response_surface,
|
||||
decode_state=main_epistemic,
|
||||
)
|
||||
main_epistemic_state = epistemic_state_for_grounding_source(
|
||||
main_grounding_source
|
||||
).value
|
||||
main_normative_clearance = clearance_from_verdicts(verdicts_bundle).value
|
||||
main_normative_detail = normative_detail_from_verdicts(verdicts_bundle)
|
||||
turn_event = TurnEvent(
|
||||
|
|
@ -2781,7 +2371,6 @@ class ChatRuntime:
|
|||
epistemic_state=main_epistemic_state,
|
||||
normative_clearance=main_normative_clearance,
|
||||
normative_detail=main_normative_detail,
|
||||
reach_level=main_reach_level,
|
||||
)
|
||||
self.turn_log.append(turn_event)
|
||||
self._emit_turn_event(turn_event)
|
||||
|
|
@ -2832,7 +2421,6 @@ class ChatRuntime:
|
|||
epistemic_state=main_epistemic_state,
|
||||
normative_clearance=main_normative_clearance,
|
||||
normative_detail=main_normative_detail,
|
||||
reach_level=main_reach_level,
|
||||
refusal_reason=refusal_surface if refusal_emitted else "",
|
||||
dispatch_trace=dispatch_trace,
|
||||
)
|
||||
|
|
|
|||
20
conftest.py
20
conftest.py
|
|
@ -25,26 +25,6 @@ from __future__ import annotations
|
|||
|
||||
import pytest
|
||||
|
||||
import engine_state
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolate_engine_state_default(tmp_path_factory, monkeypatch):
|
||||
"""Isolate the default engine-state checkpoint dir per test.
|
||||
|
||||
A bare ``ChatRuntime()`` (no ``engine_state_path``) falls back to
|
||||
``engine_state._DEFAULT_DIR`` — the shared repo ``engine_state/`` directory.
|
||||
Tests must not share that mutable dir: one test's checkpoint (recognizers,
|
||||
candidates, the stamped engine-identity, and — under resume mode — the lived
|
||||
session_state) leaks into another test's fresh-state assumptions (and, since
|
||||
L11, raises spurious identity-continuity-break warnings when a later test
|
||||
boots under a different identity over the same dir). Point the default at a
|
||||
fresh per-test temp dir. Tests passing an explicit ``engine_state_path`` are
|
||||
unaffected; within one test, repeated ``ChatRuntime()`` share this dir.
|
||||
"""
|
||||
isolated = tmp_path_factory.mktemp("engine_state_default")
|
||||
monkeypatch.setattr(engine_state, "_DEFAULT_DIR", isolated)
|
||||
|
||||
|
||||
QUARANTINE: frozenset[str] = frozenset()
|
||||
|
||||
|
|
|
|||
|
|
@ -9,9 +9,7 @@
|
|||
//! The CGA inner product in Cl(4,1) is structurally diagonal with
|
||||
//! ±1 metric values, so per-versor scoring collapses to
|
||||
//!
|
||||
//! ```text
|
||||
//! sum_i metric[i] * v[i] * q[i]
|
||||
//! ```
|
||||
//! sum_i metric[i] * v[i] * q[i]
|
||||
//!
|
||||
//! which is 32 multiplies + 32 adds, not the 1024-op full
|
||||
//! geometric_product the reference scalar path computes. Bit-
|
||||
|
|
@ -152,205 +150,3 @@ pub fn vault_reproject_parallel(versors: &[[f32; 32]]) -> Vec<[f32; 32]> {
|
|||
use crate::cga::null_project_raw;
|
||||
versors.par_iter().map(|v| null_project_raw(v)).collect()
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Delta-CRDT substrate (ADR-0180 §2.1 / §2.2)
|
||||
// ===========================================================================
|
||||
//
|
||||
// `LocalArena` is the thread-local, share-nothing write cache each modality
|
||||
// adapter accumulates into (ADR-0180 §2.1). It never touches global state, so
|
||||
// it needs no locks: concurrency safety comes from each thread owning its own
|
||||
// arena, not from synchronisation.
|
||||
//
|
||||
// `SemilatticeDelta` is the join-semilattice contract (ADR-0180 §2.2): the
|
||||
// merge of deltas is commutative, associative, and idempotent. The Merge
|
||||
// Kernel folds deltas into a single content-addressed, deduplicated, totally
|
||||
// ordered set — the order-invariant state the Vault consumes. Ordering is by
|
||||
// content (the versor's IEEE-754 bit pattern + provenance bytes), never
|
||||
// arrival order, per the §2.2 content-addressed-tiebreak amendment
|
||||
// (2026-05-29). That is precisely what makes
|
||||
// `hash(Sequential_Ingest) == hash(Concurrent_CRDT_Ingest)` (§4.3) reachable:
|
||||
// the merged state cannot depend on the order deltas arrived in.
|
||||
//
|
||||
// This is the pure-CPU Rust substrate (§1.5.5); no MLX/UMA handshake and no
|
||||
// Python binding land here — those are downstream (ADR-0180 §4.1 item 2, and
|
||||
// ADR-0181 PR-5 respectively).
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
/// One write accumulated into an arena: a Cl(4,1) versor plus opaque
|
||||
/// provenance bytes. Provenance is part of the content key, so two writes of
|
||||
/// the same versor under different provenance are distinct semilattice
|
||||
/// elements (both retained); two byte-identical writes collapse (the
|
||||
/// idempotence leg of the join semilattice, ADR-0180 §2.2).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArenaEntry {
|
||||
pub versor: [f32; 32],
|
||||
pub provenance: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ArenaEntry {
|
||||
pub fn new(versor: [f32; 32], provenance: Vec<u8>) -> Self {
|
||||
Self { versor, provenance }
|
||||
}
|
||||
}
|
||||
|
||||
/// Total, arrival-independent content order over arena entries.
|
||||
///
|
||||
/// f32 has no total `Ord` (NaN is unordered), so we compare the raw IEEE-754
|
||||
/// bit patterns — which is exactly what "content-addressed" means here:
|
||||
/// byte-identical versors sort together and deduplicate, and `-0.0`/`+0.0`
|
||||
/// (distinct bytes) are treated as distinct content, as a byte-addressed merge
|
||||
/// requires. Ties on the versor fall through to the provenance bytes, giving a
|
||||
/// total order (ADR-0180 §2.2 amendment, mirroring ADR-0181 §2.2's merge key).
|
||||
fn content_cmp(a: &ArenaEntry, b: &ArenaEntry) -> Ordering {
|
||||
for i in 0..32 {
|
||||
let o = a.versor[i].to_bits().cmp(&b.versor[i].to_bits());
|
||||
if o != Ordering::Equal {
|
||||
return o;
|
||||
}
|
||||
}
|
||||
a.provenance.cmp(&b.provenance)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn content_eq(a: &ArenaEntry, b: &ArenaEntry) -> bool {
|
||||
content_cmp(a, b) == Ordering::Equal
|
||||
}
|
||||
|
||||
/// A snapshot of newly-ingested entries (ADR-0180 §2.2). A `Delta` is always
|
||||
/// held in content-addressed order with byte-identical duplicates removed, so
|
||||
/// it is a canonical join-semilattice element regardless of the order its
|
||||
/// entries were inserted in.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Delta {
|
||||
entries: Vec<ArenaEntry>,
|
||||
}
|
||||
|
||||
impl Delta {
|
||||
/// Canonicalise an arbitrary entry list into a Delta: sort by content,
|
||||
/// drop byte-identical duplicates. `sort_by` is stable, but the dedup key
|
||||
/// is the *whole* content, so stability is immaterial to the result.
|
||||
pub fn from_entries(mut entries: Vec<ArenaEntry>) -> Self {
|
||||
entries.sort_by(content_cmp);
|
||||
entries.dedup_by(|a, b| content_eq(a, b));
|
||||
Self { entries }
|
||||
}
|
||||
|
||||
pub fn entries(&self) -> &[ArenaEntry] {
|
||||
&self.entries
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entries.is_empty()
|
||||
}
|
||||
|
||||
/// Canonical little-endian serialization of this Delta — the cross-language
|
||||
/// G1 contract (ADR-0180 / ADR-0196). Byte-for-byte identical to the
|
||||
/// Python reference `vault.crdt.canonical_bytes`; parity is pinned by
|
||||
/// `tests/test_crdt_hash_parity.rs`. Layout:
|
||||
///
|
||||
/// ```text
|
||||
/// u64 entry_count
|
||||
/// per entry (canonical order):
|
||||
/// 32 x f32 versor components (IEEE-754, little-endian, 4 bytes each)
|
||||
/// u64 provenance_length
|
||||
/// bytes provenance
|
||||
/// ```
|
||||
pub fn canonical_bytes(&self) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
out.extend_from_slice(&(self.entries.len() as u64).to_le_bytes());
|
||||
for entry in &self.entries {
|
||||
for component in &entry.versor {
|
||||
out.extend_from_slice(&component.to_le_bytes());
|
||||
}
|
||||
out.extend_from_slice(&(entry.provenance.len() as u64).to_le_bytes());
|
||||
out.extend_from_slice(&entry.provenance);
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// The join-semilattice contract for Delta-CRDT state (ADR-0180 §2.2).
|
||||
///
|
||||
/// Implementors must satisfy, with content-addressed equality (`content_cmp`):
|
||||
/// * commutativity `a.join(b) == b.join(a)`
|
||||
/// * associativity `a.join(b).join(c) == a.join(b.join(c))`
|
||||
/// * idempotence `a.join(a) == a`
|
||||
///
|
||||
/// These are exercised as failable tests in `tests/test_arena.rs`; if `join`
|
||||
/// ever orders by arrival instead of content, or stops deduplicating, those
|
||||
/// tests fail loudly (CLAUDE.md §Schema-Defined Proof Obligations).
|
||||
pub trait SemilatticeDelta: Sized {
|
||||
fn join(&self, other: &Self) -> Self;
|
||||
}
|
||||
|
||||
impl SemilatticeDelta for Delta {
|
||||
fn join(&self, other: &Self) -> Self {
|
||||
let mut merged =
|
||||
Vec::with_capacity(self.entries.len() + other.entries.len());
|
||||
merged.extend_from_slice(&self.entries);
|
||||
merged.extend_from_slice(&other.entries);
|
||||
Delta::from_entries(merged)
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread-local, share-nothing write cache for one modality adapter
|
||||
/// (ADR-0180 §2.1). Adapters push entries here lock-free; nothing is ever
|
||||
/// written to global state from an arena. `snapshot` emits the order-invariant
|
||||
/// `Delta` the Merge Kernel folds.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct LocalArena {
|
||||
entries: Vec<ArenaEntry>,
|
||||
}
|
||||
|
||||
impl LocalArena {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entries: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Lock-free local write. Push order is irrelevant: `snapshot`
|
||||
/// canonicalises into content-addressed order.
|
||||
pub fn push(&mut self, versor: [f32; 32], provenance: Vec<u8>) {
|
||||
self.entries.push(ArenaEntry::new(versor, provenance));
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entries.is_empty()
|
||||
}
|
||||
|
||||
/// Emit a canonical Delta of everything accumulated so far. Does not drain
|
||||
/// the arena — flush/GC policy is the Merge Kernel's concern, not the
|
||||
/// arena's.
|
||||
pub fn snapshot(&self) -> Delta {
|
||||
Delta::from_entries(self.entries.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// The Merge Kernel (ADR-0180 §2.2): fold a batch of deltas into a single
|
||||
/// content-addressed, deduplicated, totally ordered entry set. The result is
|
||||
/// invariant under any permutation of `deltas` (commutativity + associativity)
|
||||
/// and under duplicate deltas (idempotence) — the property §4.3's
|
||||
/// `hash(Sequential) == hash(Concurrent)` proof rides on.
|
||||
///
|
||||
/// Implemented as a single canonicalisation of the union rather than a
|
||||
/// `fold(join)` chain; `tests/test_arena.rs` pins that the two are
|
||||
/// content-equal, so the cheap path can never silently diverge from the
|
||||
/// semilattice fold it stands in for.
|
||||
pub fn merge_kernel(deltas: &[Delta]) -> Delta {
|
||||
let mut all = Vec::new();
|
||||
for d in deltas {
|
||||
all.extend_from_slice(&d.entries);
|
||||
}
|
||||
Delta::from_entries(all)
|
||||
}
|
||||
|
|
|
|||
13
core-rs/tests/fixtures/crdt_parity_expected.rs
vendored
13
core-rs/tests/fixtures/crdt_parity_expected.rs
vendored
|
|
@ -1,13 +0,0 @@
|
|||
// @generated by tests/fixtures/crdt/_generate.py — DO NOT EDIT.
|
||||
// Expected canonical_bytes hex per case (mirrors merge_fixtures.json):
|
||||
// the Python-canonical Delta-CRDT G1 contract (ADR-0180 / ADR-0196)
|
||||
// the Rust substrate must reproduce byte-for-byte.
|
||||
// (include!d mid-module, so plain // comments — not //! — are required.)
|
||||
|
||||
pub const EXPECTED_CANONICAL_BYTES_HEX: &[(&str, &str)] = &[
|
||||
("single_entry", "01000000000000000000803f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000061"),
|
||||
("dedup_within_delta", "01000000000000000000803f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000061"),
|
||||
("distinct_provenance_retained", "0200000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000006c656674000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000007269676874"),
|
||||
("three_delta_merge", "0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000071000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000006d000000000000404000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000007a0000803f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000061"),
|
||||
("signed_zero_distinct", "020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000700000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000070"),
|
||||
];
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
//! Delta-CRDT substrate proof obligations (ADR-0180 §2.1 / §2.2).
|
||||
//!
|
||||
//! Each test is written to FAIL LOUDLY under the specific violation it names
|
||||
//! (CLAUDE.md §Schema-Defined Proof Obligations): a schema/trait is only a
|
||||
//! proven property when a test would break if the property were silently
|
||||
//! removed. The map:
|
||||
//!
|
||||
//! * commutativity / associativity / idempotence — the three join-semilattice
|
||||
//! legs ADR-0180 §2.2 claims `Delta::join` satisfies.
|
||||
//! * permutation-invariant `merge_kernel` — the load-bearing property §4.3's
|
||||
//! `hash(Sequential) == hash(Concurrent)` rides on. Fails if `from_entries`
|
||||
//! stops sorting (i.e. orders by arrival).
|
||||
//! * distinct-provenance retention — guards the dedup from collapsing on the
|
||||
//! versor alone, which would silently drop epistemic state (a wrong=0-style
|
||||
//! data-loss hazard at the merge layer).
|
||||
|
||||
use core_rs::vault::{merge_kernel, ArenaEntry, Delta, LocalArena, SemilatticeDelta};
|
||||
|
||||
/// Deterministic distinct versor per seed.
|
||||
fn versor(seed: u8) -> [f32; 32] {
|
||||
let mut v = [0f32; 32];
|
||||
for (i, slot) in v.iter_mut().enumerate() {
|
||||
*slot = (seed as f32) * 0.5 + (i as f32) * 0.125;
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
fn entry(seed: u8, prov: &str) -> ArenaEntry {
|
||||
ArenaEntry::new(versor(seed), prov.as_bytes().to_vec())
|
||||
}
|
||||
|
||||
/// Canonical, comparable view of a Delta: (versor bits, provenance) per entry,
|
||||
/// in the Delta's own order. Two Deltas are content-equal iff these match,
|
||||
/// including order — so this also pins that ordering is content-addressed.
|
||||
fn keys(d: &Delta) -> Vec<([u32; 32], Vec<u8>)> {
|
||||
d.entries()
|
||||
.iter()
|
||||
.map(|e| {
|
||||
let mut bits = [0u32; 32];
|
||||
for (i, b) in bits.iter_mut().enumerate() {
|
||||
*b = e.versor[i].to_bits();
|
||||
}
|
||||
(bits, e.provenance.clone())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn delta(entries: Vec<ArenaEntry>) -> Delta {
|
||||
Delta::from_entries(entries)
|
||||
}
|
||||
|
||||
// --- the three join-semilattice legs (ADR-0180 §2.2) ----------------------
|
||||
|
||||
#[test]
|
||||
fn join_is_commutative() {
|
||||
let a = delta(vec![entry(1, "p1"), entry(3, "p3")]);
|
||||
let b = delta(vec![entry(2, "p2"), entry(3, "p3")]);
|
||||
// Fails if join carries arrival order: a-first vs b-first would differ.
|
||||
assert_eq!(keys(&a.join(&b)), keys(&b.join(&a)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_is_associative() {
|
||||
let a = delta(vec![entry(1, "p1")]);
|
||||
let b = delta(vec![entry(2, "p2")]);
|
||||
let c = delta(vec![entry(3, "p3")]);
|
||||
assert_eq!(keys(&a.join(&b).join(&c)), keys(&a.join(&b.join(&c))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_is_idempotent() {
|
||||
let a = delta(vec![entry(1, "p1"), entry(2, "p2")]);
|
||||
// a ∘ a == a — fails if dedup is removed (length would double).
|
||||
let joined = a.join(&a);
|
||||
assert_eq!(keys(&joined), keys(&a));
|
||||
assert_eq!(joined.len(), 2);
|
||||
}
|
||||
|
||||
// --- the load-bearing property for §4.3 -----------------------------------
|
||||
|
||||
#[test]
|
||||
fn merge_kernel_is_permutation_invariant() {
|
||||
let d0 = delta(vec![entry(5, "a"), entry(1, "b")]);
|
||||
let d1 = delta(vec![entry(3, "c")]);
|
||||
let d2 = delta(vec![entry(9, "d"), entry(2, "e"), entry(7, "f")]);
|
||||
|
||||
let forward = merge_kernel(&[d0.clone(), d1.clone(), d2.clone()]);
|
||||
let reversed = merge_kernel(&[d2.clone(), d1.clone(), d0.clone()]);
|
||||
let shuffled = merge_kernel(&[d1.clone(), d0.clone(), d2.clone()]);
|
||||
|
||||
// hash(Sequential) == hash(Concurrent): merged state is independent of the
|
||||
// order deltas arrived in. Fails the instant `from_entries` orders by
|
||||
// arrival instead of content.
|
||||
assert_eq!(keys(&forward), keys(&reversed));
|
||||
assert_eq!(keys(&forward), keys(&shuffled));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_kernel_dedups_duplicate_deltas() {
|
||||
let d = delta(vec![entry(4, "x"), entry(6, "y")]);
|
||||
// Re-ingesting the same delta is a no-op (idempotence at the kernel).
|
||||
let once = merge_kernel(&[d.clone()]);
|
||||
let twice = merge_kernel(&[d.clone(), d.clone()]);
|
||||
assert_eq!(keys(&once), keys(&twice));
|
||||
assert_eq!(twice.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_kernel_equals_semilattice_fold() {
|
||||
let deltas = [
|
||||
delta(vec![entry(8, "a"), entry(2, "b")]),
|
||||
delta(vec![entry(5, "c")]),
|
||||
delta(vec![entry(2, "b"), entry(9, "d")]), // overlaps the first delta
|
||||
];
|
||||
let folded = deltas
|
||||
.iter()
|
||||
.fold(Delta::default(), |acc, d| acc.join(d));
|
||||
// The cheap union-then-canonicalise path must equal the explicit
|
||||
// semilattice fold, or the kernel has silently diverged from the trait.
|
||||
assert_eq!(keys(&merge_kernel(&deltas)), keys(&folded));
|
||||
}
|
||||
|
||||
// --- data-loss / over-dedup guard -----------------------------------------
|
||||
|
||||
#[test]
|
||||
fn distinct_provenance_is_not_collapsed() {
|
||||
// Same versor, different provenance => two distinct semilattice elements.
|
||||
// Fails if dedup keys on the versor alone (which would drop state).
|
||||
let same_versor = vec![entry(7, "alpha"), entry(7, "beta")];
|
||||
let merged = delta(same_versor);
|
||||
assert_eq!(merged.len(), 2);
|
||||
|
||||
// Byte-identical content (same versor + same provenance) => collapses.
|
||||
let identical = vec![entry(7, "alpha"), entry(7, "alpha")];
|
||||
assert_eq!(delta(identical).len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_result_is_content_sorted() {
|
||||
let d = merge_kernel(&[delta(vec![entry(9, "z"), entry(1, "a"), entry(5, "m")])]);
|
||||
let ks = keys(&d);
|
||||
let mut sorted = ks.clone();
|
||||
sorted.sort();
|
||||
assert_eq!(ks, sorted, "merge output must be in content-addressed order");
|
||||
}
|
||||
|
||||
// --- LocalArena (ADR-0180 §2.1) -------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn arena_snapshot_independent_of_push_order() {
|
||||
let mut a = LocalArena::new();
|
||||
a.push(versor(3), b"p3".to_vec());
|
||||
a.push(versor(1), b"p1".to_vec());
|
||||
a.push(versor(2), b"p2".to_vec());
|
||||
|
||||
let mut b = LocalArena::new();
|
||||
b.push(versor(2), b"p2".to_vec());
|
||||
b.push(versor(3), b"p3".to_vec());
|
||||
b.push(versor(1), b"p1".to_vec());
|
||||
|
||||
// Two arenas fed the same writes in different orders snapshot to the same
|
||||
// canonical Delta (ADR-0180 §2.1: push order is irrelevant).
|
||||
assert_eq!(keys(&a.snapshot()), keys(&b.snapshot()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arena_snapshot_does_not_drain() {
|
||||
let mut a = LocalArena::new();
|
||||
a.push(versor(1), b"p1".to_vec());
|
||||
let _ = a.snapshot();
|
||||
// Flush/GC is the Merge Kernel's concern, not the arena's; snapshot must
|
||||
// be non-destructive so a delayed merge (the §3.2 window) cannot lose it.
|
||||
assert_eq!(a.len(), 1);
|
||||
assert_eq!(a.snapshot().len(), 1);
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
//! ADR-0180 / ADR-0196 gate G1 (slice ZC-0) — Rust↔Python byte-parity.
|
||||
//!
|
||||
//! Proves the Rust Delta-CRDT substrate (`core_rs::vault`) produces
|
||||
//! `canonical_bytes` identical to the Python reference `vault/crdt.py` over the
|
||||
//! shared golden corpus. Expected hex is generated *from* the Python reference
|
||||
//! (single source of truth) into `tests/fixtures/crdt_parity_expected.rs`, so
|
||||
//! the two languages cannot silently disagree. Fails loudly if the Rust
|
||||
//! serialization, content ordering, or dedup ever diverges from the locked
|
||||
//! Python contract (CLAUDE.md §Schema-Defined Proof Obligations).
|
||||
|
||||
use core_rs::vault::{merge_kernel, ArenaEntry, Delta};
|
||||
|
||||
include!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/fixtures/crdt_parity_expected.rs"
|
||||
));
|
||||
|
||||
fn v(idx: usize, val: f32) -> [f32; 32] {
|
||||
let mut a = [0.0f32; 32];
|
||||
a[idx] = val;
|
||||
a
|
||||
}
|
||||
|
||||
fn e(idx: usize, val: f32, prov: &str) -> ArenaEntry {
|
||||
ArenaEntry::new(v(idx, val), prov.as_bytes().to_vec())
|
||||
}
|
||||
|
||||
fn delta(entries: Vec<ArenaEntry>) -> Delta {
|
||||
Delta::from_entries(entries)
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
let mut s = String::with_capacity(bytes.len() * 2);
|
||||
for b in bytes {
|
||||
s.push_str(&format!("{:02x}", b));
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn expected(name: &str) -> &'static str {
|
||||
EXPECTED_CANONICAL_BYTES_HEX
|
||||
.iter()
|
||||
.find(|entry| entry.0 == name)
|
||||
.map(|entry| entry.1)
|
||||
.unwrap_or_else(|| panic!("no expected fixture for case {name}"))
|
||||
}
|
||||
|
||||
fn assert_parity(name: &str, deltas: Vec<Delta>) {
|
||||
let merged = merge_kernel(&deltas);
|
||||
assert_eq!(hex(&merged.canonical_bytes()), expected(name), "case {name}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_single_entry() {
|
||||
assert_parity("single_entry", vec![delta(vec![e(0, 1.0, "a")])]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_dedup_within_delta() {
|
||||
assert_parity(
|
||||
"dedup_within_delta",
|
||||
vec![delta(vec![e(0, 1.0, "a"), e(0, 1.0, "a")])],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_distinct_provenance_retained() {
|
||||
assert_parity(
|
||||
"distinct_provenance_retained",
|
||||
vec![delta(vec![e(5, 2.0, "left"), e(5, 2.0, "right")])],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_three_delta_merge() {
|
||||
assert_parity(
|
||||
"three_delta_merge",
|
||||
vec![
|
||||
delta(vec![e(1, 3.0, "z"), e(0, 1.0, "a")]),
|
||||
delta(vec![e(2, 2.0, "m"), e(0, 1.0, "a")]),
|
||||
delta(vec![e(10, 5.0, "q")]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_signed_zero_distinct() {
|
||||
assert_parity(
|
||||
"signed_zero_distinct",
|
||||
vec![delta(vec![e(0, 0.0, "p"), e(0, -0.0, "p")])],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_permutation_invariant_matches_python() {
|
||||
// Reordering input deltas must not change the merged bytes (and thus the
|
||||
// Python-pinned hash). Mirrors the Python permutation-invariance test.
|
||||
let d0 = delta(vec![e(1, 3.0, "z"), e(0, 1.0, "a")]);
|
||||
let d1 = delta(vec![e(2, 2.0, "m"), e(0, 1.0, "a")]);
|
||||
let d2 = delta(vec![e(10, 5.0, "q")]);
|
||||
let forward = hex(&merge_kernel(&[d0.clone(), d1.clone(), d2.clone()]).canonical_bytes());
|
||||
let reversed = hex(&merge_kernel(&[d2, d1, d0]).canonical_bytes());
|
||||
assert_eq!(forward, reversed);
|
||||
assert_eq!(forward, expected("three_delta_merge"));
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
"""Bit-exact (de)serialization of numpy arrays for deterministic persistence.
|
||||
|
||||
A numpy array encodes to ``{"dtype", "shape", "b64"}`` where ``b64`` is base64 of
|
||||
the array's raw bytes. This is **bit-exact**: every float round-trips with zero
|
||||
precision loss, so a restored versor keeps ``versor_condition < 1e-6`` and a
|
||||
replayed turn keeps its ``trace_hash``.
|
||||
|
||||
NEVER serialize field arrays as decimal/JSON floats. Decimal truncates the
|
||||
mantissa and silently breaks both closure and deterministic replay — the Cl(4,1)
|
||||
float-truncation pitfall. ``dtype`` carries byte order (``'<f4'``/``'<f8'``), so
|
||||
the encoding is portable, and ``float32`` is never conflated with ``float64``.
|
||||
|
||||
This module is a leaf: it imports only numpy + base64, so every layer (field,
|
||||
vault, session, engine_state) can use it without an import cycle.
|
||||
|
||||
Zig-codec follow-up (tagged — NOT authorized). This bit-exact codec is the natural
|
||||
locked **reference contract** (ADR-0196 decision rule 1) for a future Ring-1 Zig
|
||||
byte-exact serialization component: deterministic buffer ownership, stable layout, and
|
||||
edge-native build are exactly Zig's profile. It is gated behind the G0–G8 ladder and
|
||||
is **only** worth proposing AFTER (1) persistence becomes incremental/append-only
|
||||
(O(Δ)/turn — the algorithmic fix, in Python), and (2) the edge-budget gate
|
||||
(``evals/edge_budget/``) proves the bounded per-turn codec is still the device
|
||||
bottleneck. A Zig rewrite of today's O(n) snapshot would only speed up the wrong
|
||||
asymptotics. See ``evals/edge_budget/contract.md``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def encode_array(arr: np.ndarray) -> dict[str, Any]:
|
||||
"""Encode a numpy array to a bit-exact, JSON-safe dict."""
|
||||
contiguous = np.ascontiguousarray(arr)
|
||||
return {
|
||||
"dtype": contiguous.dtype.str, # byte-order-aware, e.g. '<f4', '<f8', '<i4'
|
||||
"shape": list(contiguous.shape),
|
||||
"b64": base64.b64encode(contiguous.tobytes()).decode("ascii"),
|
||||
}
|
||||
|
||||
|
||||
def decode_array(payload: dict[str, Any]) -> np.ndarray:
|
||||
"""Decode a payload produced by ``encode_array`` back to an exact array.
|
||||
|
||||
Returns a writable copy (``np.frombuffer`` is read-only) so the restored
|
||||
array can be composed and mutated like a freshly-built one.
|
||||
"""
|
||||
dtype = np.dtype(payload["dtype"])
|
||||
raw = base64.b64decode(payload["b64"])
|
||||
flat = np.frombuffer(raw, dtype=dtype)
|
||||
return flat.reshape(payload["shape"]).copy()
|
||||
|
||||
|
||||
def encode_optional_array(arr: np.ndarray | None) -> dict[str, Any] | None:
|
||||
"""Encode an array, or return ``None`` for ``None`` (e.g. optional holonomy)."""
|
||||
return None if arr is None else encode_array(arr)
|
||||
|
||||
|
||||
def decode_optional_array(payload: dict[str, Any] | None) -> np.ndarray | None:
|
||||
"""Decode an optional-array payload, or return ``None`` for ``None``."""
|
||||
return None if payload is None else decode_array(payload)
|
||||
115
core/cli.py
115
core/cli.py
|
|
@ -41,14 +41,6 @@ _TEST_SUITES: dict[str, tuple[str, ...]] = {
|
|||
"tests/test_runtime_config.py",
|
||||
"tests/test_cognitive_turn_pipeline.py",
|
||||
"tests/test_architectural_invariants.py",
|
||||
# ADR-0043 — identity falsifiability: ratified identity packs must
|
||||
# produce distinct, directionally-correct articulations, with a
|
||||
# pack-invariant grounding/refusal floor and zero fabrication. Lives
|
||||
# only under ``full`` historically, so a divergence regression cleared
|
||||
# the PR gate and surfaced only post-merge. Promoted into smoke so
|
||||
# the falsifiability claim blocks-on-regression rather than
|
||||
# detect-after-merge.
|
||||
"tests/test_pack_measurements_phase2.py",
|
||||
),
|
||||
"runtime": (
|
||||
"tests/test_chat_runtime.py",
|
||||
|
|
@ -114,29 +106,6 @@ _TEST_SUITES: dict[str, tuple[str, ...]] = {
|
|||
"tests/test_versor_condition_rust_parity.py",
|
||||
"tests/test_versor_apply_rust_parity.py",
|
||||
),
|
||||
"sensorium": (
|
||||
"tests/test_sensorium_compiler_delta.py",
|
||||
"tests/test_audio_compiler.py",
|
||||
"tests/test_audio_crdt_merge.py",
|
||||
"tests/test_audio_eval_gates.py",
|
||||
"tests/test_audio_pack_manifest.py",
|
||||
"tests/test_audio_sensorium_mount.py",
|
||||
"tests/test_vision_compiler.py",
|
||||
"tests/test_event_vision_compiler.py",
|
||||
"tests/test_vision_crdt_merge.py",
|
||||
"tests/test_vision_eval_gates.py",
|
||||
"tests/test_vision_sensorium_mount.py",
|
||||
"tests/test_sensorimotor_contract.py",
|
||||
"tests/test_sensorimotor_pack_manifest.py",
|
||||
"tests/test_observation_frame_contract.py",
|
||||
"tests/test_observation_frame_harness.py",
|
||||
"tests/test_environment_falsification.py",
|
||||
"tests/test_environment_falsification_eval_cli.py",
|
||||
"tests/test_witness_log_importer.py",
|
||||
"tests/test_tabletop_lab_protocol.py",
|
||||
"tests/test_sensorium_eval_cli.py",
|
||||
"tests/test_efferent_gate.py",
|
||||
),
|
||||
"pulse": (
|
||||
"tests/test_pulse_integration.py",
|
||||
"tests/test_graph_diffusion.py",
|
||||
|
|
@ -188,9 +157,6 @@ _TEST_SUITES: dict[str, tuple[str, ...]] = {
|
|||
"math": (
|
||||
"tests/test_adr_0126_train_sample_runner.py",
|
||||
),
|
||||
"deductive": (
|
||||
"tests/test_deductive_logic_entail.py",
|
||||
),
|
||||
"full": ("tests/",),
|
||||
}
|
||||
|
||||
|
|
@ -2072,10 +2038,14 @@ def cmd_teaching_coverage(args: argparse.Namespace) -> int:
|
|||
return 1
|
||||
report_path = lane_dir / "report.json"
|
||||
|
||||
use_reader = bool(args.use_reader)
|
||||
|
||||
if args.run or not report_path.exists():
|
||||
import subprocess
|
||||
runner_module = f"evals.{lane}.{split}.{version}.runner"
|
||||
runner_args = [sys.executable, "-m", runner_module]
|
||||
if use_reader:
|
||||
runner_args.append("--use-reader")
|
||||
try:
|
||||
subprocess.run(
|
||||
runner_args,
|
||||
|
|
@ -2099,13 +2069,14 @@ def cmd_teaching_coverage(args: argparse.Namespace) -> int:
|
|||
lane=lane,
|
||||
split=split,
|
||||
version=version,
|
||||
use_reader=use_reader,
|
||||
baseline_path=baseline_path,
|
||||
)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(report.as_dict(), indent=2, sort_keys=True))
|
||||
else:
|
||||
print(f"Lane: {report.lane}/{report.split}/{report.version}")
|
||||
print(f"Lane: {report.lane}/{report.split}/{report.version} (use_reader={report.use_reader})")
|
||||
if report.delta:
|
||||
print(
|
||||
f"Counts: correct={report.counts.correct} "
|
||||
|
|
@ -2375,10 +2346,6 @@ def cmd_doctor(args: argparse.Namespace) -> int:
|
|||
|
||||
def cmd_eval(args: argparse.Namespace) -> int:
|
||||
"""Run an eval lane by name, or list available lanes."""
|
||||
if getattr(args, "lane", None) == "sensorium":
|
||||
return cmd_eval_sensorium(args)
|
||||
if getattr(args, "lane", None) == "environment-falsification":
|
||||
return cmd_eval_environment_falsification(args)
|
||||
if getattr(args, "lane", None) == "math-contemplation":
|
||||
return cmd_eval_math_contemplation(args)
|
||||
|
||||
|
|
@ -2486,66 +2453,6 @@ def cmd_eval(args: argparse.Namespace) -> int:
|
|||
return 0
|
||||
|
||||
|
||||
def cmd_eval_sensorium(args: argparse.Namespace) -> int:
|
||||
"""Run deterministic sensorium modality evidence reports."""
|
||||
from evals.sensorium import build_sensorium_report
|
||||
|
||||
modality = getattr(args, "modality", "vision") or "vision"
|
||||
try:
|
||||
report = build_sensorium_report(modality)
|
||||
except ValueError as exc:
|
||||
_die(str(exc), code=2)
|
||||
|
||||
if getattr(args, "json", False):
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True))
|
||||
else:
|
||||
print(f"lane : {report['lane']}")
|
||||
print(f"modality : {report['modality']}")
|
||||
print(f"pack_id : {report['pack_id']}")
|
||||
print(f"gate_engaged : {report['gate_engaged']}")
|
||||
print(f"gate_closed : {report['gate_closed']}")
|
||||
print(f"cases : {report['total']}")
|
||||
print(f"passed : {report['passed']}")
|
||||
print(f"failed : {report['failed']}")
|
||||
|
||||
if getattr(args, "report", None):
|
||||
report_path = Path(args.report)
|
||||
report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
report_path.write_text(
|
||||
json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True)
|
||||
)
|
||||
print(f"\nreport written: {report_path}", file=sys.stderr)
|
||||
|
||||
return 0 if report["failed"] == 0 and report["gate_closed"] else 1
|
||||
|
||||
|
||||
def cmd_eval_environment_falsification(args: argparse.Namespace) -> int:
|
||||
"""Run deterministic environmental falsification replay reports."""
|
||||
from evals.environment_falsification import build_environment_falsification_report
|
||||
|
||||
report = build_environment_falsification_report()
|
||||
|
||||
if getattr(args, "json", False):
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True))
|
||||
else:
|
||||
print(f"lane : {report['lane']}")
|
||||
print(f"version : {report['version']}")
|
||||
print(f"cases : {report['total']}")
|
||||
print(f"passed : {report['passed']}")
|
||||
print(f"failed : {report['failed']}")
|
||||
print(f"report_sha256 : {report['report_sha256']}")
|
||||
|
||||
if getattr(args, "report", None):
|
||||
report_path = Path(args.report)
|
||||
report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
report_path.write_text(
|
||||
json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True)
|
||||
)
|
||||
print(f"\nreport written: {report_path}", file=sys.stderr)
|
||||
|
||||
return 0 if report["failed"] == 0 and report["expected_report_hash_ok"] else 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ADR-0172 W3 — math-contemplation CLI lane
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -4724,6 +4631,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
teaching_coverage.add_argument(
|
||||
"--version", default="v1", help="lane version (default: v1)",
|
||||
)
|
||||
teaching_coverage.add_argument(
|
||||
"--use-reader", action="store_true",
|
||||
help="pass --use-reader to the runner (matches train_sample default)",
|
||||
)
|
||||
teaching_coverage.add_argument(
|
||||
"--run", action="store_true",
|
||||
help="re-run the lane's runner even if report.json exists",
|
||||
|
|
@ -4979,12 +4890,6 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
eval_cmd.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
||||
eval_cmd.add_argument("--save", action="store_true", help="write result to lane results/ directory")
|
||||
eval_cmd.add_argument("--report", metavar="PATH", help="write JSON report to file")
|
||||
eval_cmd.add_argument(
|
||||
"--modality",
|
||||
choices=["audio", "vision", "event-vision", "sensorimotor"],
|
||||
default="vision",
|
||||
help="sensorium lane modality to evaluate (default: vision)",
|
||||
)
|
||||
eval_cmd.add_argument(
|
||||
"--audit",
|
||||
metavar="PATH",
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ from field.state import FieldState
|
|||
from core.cognition.result import CognitiveTurnResult
|
||||
from core.cognition.surface_resolution import resolve_surface
|
||||
from core.cognition.trace import compute_trace_hash, hash_admissibility_trace
|
||||
from core.reasoning.adapters import evidence_from_entailment_trace
|
||||
from generate.intent import classify_compound_intent
|
||||
from generate.intent_bridge import _is_useful_surface
|
||||
from generate.intent_ratifier import (
|
||||
|
|
@ -48,7 +47,6 @@ from generate.operators import (
|
|||
multi_relation_walk,
|
||||
transitive_walk,
|
||||
)
|
||||
from generate.proof_chain import EntailmentTrace, evaluate_entailment_with_trace
|
||||
from teaching.correction import CorrectionCandidate, extract_correction
|
||||
from teaching.epistemic import EpistemicStatus
|
||||
from teaching.review import ReviewedTeachingExample, review_correction
|
||||
|
|
@ -124,7 +122,7 @@ class CognitiveTurnPipeline:
|
|||
) -> None: # runtime: ChatRuntime (no import cycle)
|
||||
self.runtime = runtime
|
||||
self._last_node_id: str | None = None
|
||||
self.teaching_store = teaching_store if teaching_store is not None else TeachingStore()
|
||||
self.teaching_store = teaching_store or TeachingStore()
|
||||
if recognizer is not None:
|
||||
self._recognizer = recognizer
|
||||
elif hasattr(runtime, "first_admitted_recognizer"):
|
||||
|
|
@ -306,8 +304,6 @@ class CognitiveTurnPipeline:
|
|||
):
|
||||
compose_surface = CognitiveTurnPipeline._render_compose_surface(compose_result)
|
||||
|
||||
entailment_trace = self._maybe_entailment_trace(intent, triples)
|
||||
|
||||
resolved = resolve_surface(
|
||||
canonical_surface=canonical,
|
||||
pre_decoration_surface=pre_decoration,
|
||||
|
|
@ -397,14 +393,13 @@ class CognitiveTurnPipeline:
|
|||
epistemic_status = proposal.epistemic_status.value if proposal is not None else ""
|
||||
walk_serialised = CognitiveTurnPipeline._serialize_operator(walk_result)
|
||||
compose_serialised = CognitiveTurnPipeline._serialize_operator(compose_result)
|
||||
entailment_serialised = CognitiveTurnPipeline._serialize_entailment_trace(
|
||||
entailment_trace
|
||||
)
|
||||
# Deterministic concatenation: walk, compose, then entailment. Empty
|
||||
# strings are dropped so unaffected turns keep existing trace bytes.
|
||||
operator_invocation = "|".join(
|
||||
s for s in (walk_serialised, compose_serialised, entailment_serialised)
|
||||
if s
|
||||
# Deterministic concatenation: walk record, then compose record.
|
||||
# Empty strings are dropped so single-operator turns keep their
|
||||
# existing trace_hash byte-for-byte.
|
||||
operator_invocation = (
|
||||
f"{walk_serialised}|{compose_serialised}"
|
||||
if compose_serialised
|
||||
else walk_serialised
|
||||
)
|
||||
# ADR-0023 — admissibility trace + ratification provenance.
|
||||
# Comb pass 2026-05-21 — direct attribute access; the fields
|
||||
|
|
@ -695,39 +690,6 @@ class CognitiveTurnPipeline:
|
|||
relation=intent.relation,
|
||||
)
|
||||
|
||||
def _maybe_entailment_trace(
|
||||
self,
|
||||
intent,
|
||||
triples: tuple[tuple[str, str, str], ...],
|
||||
) -> EntailmentTrace | None:
|
||||
"""Compile exact verification triples into propositional entailment.
|
||||
|
||||
Telemetry-only v1: the result is folded into ``operator_invocation`` and
|
||||
never changes the user-facing surface. Runs only when classification
|
||||
exposes a precise positive ``subject relation object`` shape.
|
||||
"""
|
||||
if intent.tag is not IntentTag.VERIFICATION:
|
||||
return None
|
||||
if intent.negated or not intent.relation or not intent.object:
|
||||
return None
|
||||
head = self._proof_atom(intent.subject)
|
||||
tail = self._proof_atom(intent.object)
|
||||
if not head or not tail:
|
||||
return None
|
||||
|
||||
relation = intent.relation.strip().lower()
|
||||
premises: list[str] = []
|
||||
for h, r, t in triples:
|
||||
if r.strip().lower() != relation:
|
||||
continue
|
||||
h_atom = self._proof_atom(h)
|
||||
t_atom = self._proof_atom(t)
|
||||
if h_atom and t_atom:
|
||||
premises.append(f"{h_atom} -> {t_atom}")
|
||||
if not premises:
|
||||
return None
|
||||
return evaluate_entailment_with_trace(tuple(premises), f"{head} -> {tail}")
|
||||
|
||||
@staticmethod
|
||||
def _render_compose_surface(compose: FrameComposeResult) -> str:
|
||||
"""Render a frame-transfer composition suffix without selecting authority."""
|
||||
|
|
@ -759,19 +721,6 @@ class CognitiveTurnPipeline:
|
|||
return ""
|
||||
return json.dumps(op.as_dict(), sort_keys=True, ensure_ascii=False)
|
||||
|
||||
@staticmethod
|
||||
def _serialize_entailment_trace(trace: EntailmentTrace | None) -> str:
|
||||
if trace is None:
|
||||
return ""
|
||||
return f"entailment:{evidence_from_entailment_trace(trace).canonical_json()}"
|
||||
|
||||
@staticmethod
|
||||
def _proof_atom(text: str) -> str:
|
||||
parts = [p for p in _SUBJECT_SPLIT_RE.split(text.lower()) if p]
|
||||
if not parts:
|
||||
return ""
|
||||
return "atom_" + "_".join(parts)
|
||||
|
||||
@staticmethod
|
||||
def _render_walk_surface(walk: WalkResult) -> str:
|
||||
"""Render a chain-aware walk suffix without selecting authority."""
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
"""Shared typed record of a comprehension organ's attempt at a problem (N2).
|
||||
|
||||
The normalization layer the contemplation batch (N3 router, N4 registry, N6 pass manager) reasons
|
||||
over — uniform across the R1 and R2 setup compilers. Off-serving; imports no `evals`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from core.comprehension_attempt.classify import (
|
||||
classify_cmb,
|
||||
classify_r1,
|
||||
classify_r2,
|
||||
classify_r3,
|
||||
cmb_reason,
|
||||
)
|
||||
from core.comprehension_attempt.failure_family import (
|
||||
REGISTRY,
|
||||
FailureFamily,
|
||||
enrich_family,
|
||||
family_by_name,
|
||||
family_for_reason,
|
||||
)
|
||||
from core.comprehension_attempt.model import ComprehensionAttempt, Organ, Outcome
|
||||
from core.comprehension_attempt.proposal import (
|
||||
FailureProposal,
|
||||
build_proposal,
|
||||
emit_proposal,
|
||||
)
|
||||
from core.comprehension_attempt.router import (
|
||||
RouteResult,
|
||||
RouteStatus,
|
||||
cmb_is_authoritative,
|
||||
route_setup,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"REGISTRY",
|
||||
"ComprehensionAttempt",
|
||||
"FailureFamily",
|
||||
"FailureProposal",
|
||||
"build_proposal",
|
||||
"emit_proposal",
|
||||
"Organ",
|
||||
"Outcome",
|
||||
"RouteResult",
|
||||
"RouteStatus",
|
||||
"classify_cmb",
|
||||
"classify_r1",
|
||||
"classify_r2",
|
||||
"classify_r3",
|
||||
"cmb_is_authoritative",
|
||||
"cmb_reason",
|
||||
"enrich_family",
|
||||
"family_by_name",
|
||||
"family_for_reason",
|
||||
"route_setup",
|
||||
]
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
"""Normalize R1/R2 organ output into a typed ``ComprehensionAttempt`` (N2, setup-level).
|
||||
|
||||
`classify_r1` / `classify_r2` run their organ and report **produce-mode** setup outcomes:
|
||||
`setup_refused` (with the organ's typed reason) or `setup_correct` (an admissible setup was
|
||||
produced, with a deterministic signature). They do NOT solve, do NOT compare to gold, and import
|
||||
nothing from `evals` (signatures are computed inline) — keeping this a thin, dependency-light
|
||||
normalizer. Answer-level outcomes are reached downstream (N6) when the solver/verifier run.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from core.comprehension_attempt.model import ComprehensionAttempt
|
||||
from generate.combined_rate_comprehension.model import CombinedRateProblem
|
||||
from generate.combined_rate_comprehension.reader import read_combined_rate_problem
|
||||
from generate.constraint_comprehension.model import ConstraintProblem
|
||||
from generate.constraint_comprehension.reader import read_constraint_problem
|
||||
from generate.meaning_graph.reader import Refusal
|
||||
from generate.quantitative_comprehension import comprehend_quantitative, to_relational_metric
|
||||
from generate.rate_comprehension.model import RateProblem
|
||||
from generate.rate_comprehension.reader import read_rate_problem
|
||||
|
||||
#: CMB reader/solver reasons that are namespaced ``cmb_*`` before the (reason-string-keyed) failure
|
||||
#: registry sees them, so a CMB boundary never inherits R2/R3's family for the SAME bare string
|
||||
#: (R3 ``rate_unit_mismatch`` is a growth surface; R2/R3 ``non_integer_solution`` carry other
|
||||
#: owners). The two ``input_shape`` reasons stay bare so they map to the cross ``input_shape``
|
||||
#: family (router hygiene). A ``cmb_``-prefixed attempt reason is also the signal that CMB
|
||||
#: *substantively* recognized the text (used by the CMB-over-R3 precedence rule in the router).
|
||||
_CMB_BARE_REASONS = frozenset({"not_combined_rate_shaped", "empty"})
|
||||
|
||||
|
||||
def cmb_reason(reason: str) -> str:
|
||||
"""Namespace a CMB refusal reason for the failure registry (see :data:`_CMB_BARE_REASONS`)."""
|
||||
return reason if reason in _CMB_BARE_REASONS else f"cmb_{reason}"
|
||||
|
||||
|
||||
def _r1_signature(relations: list[dict[str, Any]]) -> str:
|
||||
"""Deterministic, order-independent string signature of the projected R1 relations."""
|
||||
items: list[tuple] = []
|
||||
for r in relations:
|
||||
kind = r["kind"]
|
||||
if kind == "fact":
|
||||
items.append((kind, r["entity"], int(r["value"])))
|
||||
elif kind in ("more_than", "fewer_than"):
|
||||
items.append((kind, r["entity"], r["ref"], int(r["delta"])))
|
||||
elif kind == "times_as_many":
|
||||
items.append((kind, r["entity"], r["ref"], int(r["factor"])))
|
||||
elif kind == "divide_by":
|
||||
items.append((kind, r["entity"], r["ref"], int(r["divisor"])))
|
||||
elif kind == "sum_of":
|
||||
items.append((kind, r["entity"], tuple(sorted(r["parts"]))))
|
||||
else: # pragma: no cover - defensive
|
||||
items.append(("unhandled", kind, r.get("entity", "")))
|
||||
return repr(tuple(sorted(items, key=repr)))
|
||||
|
||||
|
||||
def _r2_signature(problem: ConstraintProblem) -> str:
|
||||
"""Deterministic, order-independent string signature of an R2 ConstraintProblem setup."""
|
||||
unknowns = tuple(sorted((u.symbol, u.unit, u.domain) for u in problem.unknowns))
|
||||
constraints: list[tuple] = []
|
||||
for c in problem.constraints:
|
||||
merged: dict[str, int] = {}
|
||||
for symbol, coeff in c.lhs.terms:
|
||||
merged[symbol] = merged.get(symbol, 0) + coeff
|
||||
terms = tuple(sorted((s, v) for s, v in merged.items() if v != 0))
|
||||
constraints.append((terms, c.relation, c.rhs - c.lhs.constant))
|
||||
query = (problem.query.symbol, problem.query.unit)
|
||||
return repr((unknowns, tuple(sorted(constraints, key=repr)), query))
|
||||
|
||||
|
||||
def classify_r1(text: str, *, case_id: str | None = None) -> ComprehensionAttempt:
|
||||
"""Attempt the R1 relational-arithmetic setup compiler on *text*."""
|
||||
comp = comprehend_quantitative(text)
|
||||
if isinstance(comp, Refusal):
|
||||
return ComprehensionAttempt(
|
||||
"r1_quantitative", "setup_refused", case_id=case_id, refusal_reason=comp.reason
|
||||
)
|
||||
projected = to_relational_metric(comp)
|
||||
if projected is None:
|
||||
return ComprehensionAttempt(
|
||||
"r1_quantitative", "setup_refused", case_id=case_id, refusal_reason="unprojectable"
|
||||
)
|
||||
relations, _query = projected
|
||||
return ComprehensionAttempt(
|
||||
"r1_quantitative", "setup_correct", case_id=case_id, setup_signature=_r1_signature(relations)
|
||||
)
|
||||
|
||||
|
||||
def classify_r2(text: str, *, case_id: str | None = None) -> ComprehensionAttempt:
|
||||
"""Attempt the R2 two-category constraint setup compiler on *text*."""
|
||||
problem = read_constraint_problem(text)
|
||||
if isinstance(problem, Refusal):
|
||||
return ComprehensionAttempt(
|
||||
"r2_constraints", "setup_refused", case_id=case_id, refusal_reason=problem.reason
|
||||
)
|
||||
return ComprehensionAttempt(
|
||||
"r2_constraints", "setup_correct", case_id=case_id, setup_signature=_r2_signature(problem)
|
||||
)
|
||||
|
||||
|
||||
def _r3_signature(problem: RateProblem) -> str:
|
||||
"""Deterministic string signature of an R3 single-rate setup."""
|
||||
return repr(
|
||||
(
|
||||
(problem.rate_unit.numerator, problem.rate_unit.denominator),
|
||||
("rate", problem.rate),
|
||||
("time", problem.time),
|
||||
("quantity", problem.quantity),
|
||||
problem.query,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def classify_r3(text: str, *, case_id: str | None = None) -> ComprehensionAttempt:
|
||||
"""Attempt the R3 single-rate setup compiler on *text*."""
|
||||
problem = read_rate_problem(text)
|
||||
if isinstance(problem, Refusal):
|
||||
return ComprehensionAttempt(
|
||||
"r3_rate", "setup_refused", case_id=case_id, refusal_reason=problem.reason
|
||||
)
|
||||
return ComprehensionAttempt(
|
||||
"r3_rate", "setup_correct", case_id=case_id, setup_signature=_r3_signature(problem)
|
||||
)
|
||||
|
||||
|
||||
def _cmb_signature(problem: CombinedRateProblem) -> str:
|
||||
"""Deterministic string signature of an R4 combined-rate setup. The leading ``"cmb"`` tag
|
||||
guarantees it never coincides with an R1/R2/R3 signature (cross-organ distinctness). ``sum`` is
|
||||
commutative, so its two rates are sorted; ``difference`` keeps order (which rate is the drain)."""
|
||||
rates = (problem.rate_a, problem.rate_b)
|
||||
if problem.combine_mode == "sum":
|
||||
rates = tuple(sorted(rates))
|
||||
return repr(
|
||||
(
|
||||
"cmb",
|
||||
problem.combine_mode,
|
||||
(problem.rate_unit.numerator, problem.rate_unit.denominator),
|
||||
rates,
|
||||
("time", problem.time),
|
||||
("quantity", problem.quantity),
|
||||
problem.query,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def classify_cmb(text: str, *, case_id: str | None = None) -> ComprehensionAttempt:
|
||||
"""Attempt the R4 combined-rate setup compiler on *text* (refusal reasons namespaced ``cmb_*``)."""
|
||||
problem = read_combined_rate_problem(text)
|
||||
if isinstance(problem, Refusal):
|
||||
return ComprehensionAttempt(
|
||||
"r4_combined_rate", "setup_refused", case_id=case_id, refusal_reason=cmb_reason(problem.reason)
|
||||
)
|
||||
return ComprehensionAttempt(
|
||||
"r4_combined_rate", "setup_correct", case_id=case_id, setup_signature=_cmb_signature(problem)
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["classify_cmb", "classify_r1", "classify_r2", "classify_r3", "cmb_reason"]
|
||||
|
|
@ -1,250 +0,0 @@
|
|||
"""Failure-family registry (N4) — the heart of the contemplation batch.
|
||||
|
||||
Partitions every typed organ refusal reason (R1 reader/admissibility, R2 reader/solver/
|
||||
answer-choice) into a named **failure family** that declares:
|
||||
|
||||
- ``owner`` — which organ surfaces it (``r1`` / ``r2`` / ``cross``)
|
||||
- ``must_remain_refused`` — is this a correct wrong=0 boundary that must stay refused?
|
||||
- ``proposal_allowed`` — is this a genuine coverage gap a proposal may target?
|
||||
- ``safe_next_action`` — the human-readable next step
|
||||
- ``proposal_target`` — what artifact a proposal would suggest (e.g. ``r2_gold_fixture``)
|
||||
|
||||
Only three families are growth surfaces (``proposal_allowed = True``): the R2 ``missing_*``
|
||||
gaps. Everything else is a correct boundary — `correct refusal != missing capability`. The
|
||||
registry is a **partition**: every reachable reason maps to exactly one family (asserted by
|
||||
test), so ``family_for_reason`` is total and unambiguous. ``answer_key_contradiction`` carries
|
||||
no refusal reason — it is reached from the answer-choice ``contradiction`` *verdict* (N6).
|
||||
|
||||
Some R1 reasons are coarse (``unreadable_quantity_clause`` covers both the pronoun and distractor
|
||||
cases; ``admissibility_refused`` covers both ungrounded and unit-incompatible). v0 folds each to a
|
||||
single conservative family — the *action* (refuse, no proposal) is identical for the folded cases,
|
||||
so no wrong=0 signal is lost. The reserved families are forward-declared for R3 with no current
|
||||
reason mapping.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import Literal
|
||||
|
||||
from core.comprehension_attempt.model import ComprehensionAttempt
|
||||
|
||||
Owner = Literal["r1", "r2", "r3", "r4", "cross"]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FailureFamily:
|
||||
"""A named class of comprehension failure with its growth/refusal policy."""
|
||||
|
||||
name: str
|
||||
owner: Owner
|
||||
must_remain_refused: bool
|
||||
proposal_allowed: bool
|
||||
safe_next_action: str
|
||||
proposal_target: str | None = None
|
||||
refusal_reasons: tuple[str, ...] = ()
|
||||
|
||||
|
||||
#: The registry. Every reachable organ refusal reason appears in exactly one family.
|
||||
REGISTRY: tuple[FailureFamily, ...] = (
|
||||
# --- correct wrong=0 boundaries (no proposal) ---------------------------------------- #
|
||||
FailureFamily(
|
||||
"input_shape", "cross", True, False,
|
||||
"refuse — the text is not a readable problem shape",
|
||||
refusal_reasons=(
|
||||
"empty", "no_quantity_template", "non_digit_quantity", "non_identifier_name",
|
||||
"unreadable_quantity_query", "invalid_binding_graph", "query_target_not_a_category",
|
||||
"unprojectable", "category_pair_not_found", "query_target_unrecognized", "no_query",
|
||||
"not_rate_shaped", "not_combined_rate_shaped",
|
||||
),
|
||||
),
|
||||
FailureFamily(
|
||||
"unsupported_clause_shape", "r1", True, False,
|
||||
"refuse — compound/pronoun clause the template cannot isolate (subsumes "
|
||||
"ambiguous_referent + unsupported_distractor_clause until a finer signal exists)",
|
||||
refusal_reasons=("unreadable_quantity_clause",),
|
||||
),
|
||||
FailureFamily(
|
||||
"ungrounded_base", "r1", True, False,
|
||||
"refuse — the asked quantity has no grounded anchor (underdetermined)",
|
||||
refusal_reasons=("no_single_quantity_query",),
|
||||
),
|
||||
FailureFamily(
|
||||
"admissibility_incompatible", "cross", True, False,
|
||||
"refuse — operands are ungrounded or unit-incompatible (cannot combine across dimensions)",
|
||||
refusal_reasons=("admissibility_refused", "coefficient_unit_mismatch", "coefficient_conflict"),
|
||||
),
|
||||
FailureFamily(
|
||||
"over_determined", "r1", True, False,
|
||||
"refuse — structurally incoherent (multiple bases / partition mismatch)",
|
||||
refusal_reasons=(
|
||||
"multiple_inverse_bases", "multiple_partitions",
|
||||
"partition_query_mismatch", "partition_container_mismatch",
|
||||
),
|
||||
),
|
||||
FailureFamily(
|
||||
"unsupported_system_size", "r2", True, False,
|
||||
"refuse — more than two categories; needs an n-variable solver (R3)",
|
||||
refusal_reasons=("too_many_categories",),
|
||||
),
|
||||
FailureFamily(
|
||||
"indistinguishable_system", "r2", True, False,
|
||||
"refuse — the system is singular/underdetermined; no unique solution",
|
||||
refusal_reasons=("indistinguishable_weights", "query_target_unsolved", "verification_failed"),
|
||||
),
|
||||
FailureFamily(
|
||||
"non_integer_solution", "r2", True, False,
|
||||
"refuse — no integer solution exists; never round",
|
||||
refusal_reasons=("non_integer_solution",),
|
||||
),
|
||||
FailureFamily(
|
||||
"negative_solution", "r2", True, False,
|
||||
"refuse — a solved count is negative; out of domain",
|
||||
refusal_reasons=("negative_solution",),
|
||||
),
|
||||
FailureFamily(
|
||||
"answer_choice_unresolved", "r2", True, False,
|
||||
"refuse — the proven value cannot be tied to exactly one option",
|
||||
refusal_reasons=(
|
||||
"no_matching_option", "ambiguous_options", "no_options",
|
||||
"unknown_provided_label", "unparseable_option",
|
||||
),
|
||||
),
|
||||
# --- growth surfaces (proposal allowed) ---------------------------------------------- #
|
||||
FailureFamily(
|
||||
"missing_total_count", "r2", False, True,
|
||||
"propose a total-count-constraint gold fixture for review",
|
||||
proposal_target="r2_gold_fixture", refusal_reasons=("missing_total_count",),
|
||||
),
|
||||
FailureFamily(
|
||||
"missing_weighted_total", "r2", False, True,
|
||||
"propose a weighted-total-constraint gold fixture for review",
|
||||
proposal_target="r2_gold_fixture", refusal_reasons=("missing_weighted_total",),
|
||||
),
|
||||
# --- verdict (not a refusal) --------------------------------------------------------- #
|
||||
FailureFamily(
|
||||
"answer_key_contradiction", "r2", False, False,
|
||||
"report the contradiction — the proven value disagrees with the supplied key",
|
||||
refusal_reasons=(),
|
||||
),
|
||||
# --- reserved / forward-declared for R3 (no current emitter) ------------------------- #
|
||||
FailureFamily(
|
||||
"missing_category_pair", "r2", False, True,
|
||||
"RESERVED — propose a category-pair fixture once the reader distinguishes a partial "
|
||||
"(one-category) R2 problem from non-R2 text; the raw category_pair_not_found reason is "
|
||||
"too broad to propose against safely (it fires on any non-R2 text), so it maps to "
|
||||
"input_shape until that split exists",
|
||||
proposal_target="r2_gold_fixture",
|
||||
),
|
||||
FailureFamily(
|
||||
"missing_attribute_coefficient", "r2", False, True,
|
||||
"RESERVED — propose an attribute-coefficient fixture (no emitter yet)",
|
||||
proposal_target="r2_gold_fixture",
|
||||
),
|
||||
# --- R3 single-rate organ (reachable; growth vs boundary by the anti-over-propose rule) -- #
|
||||
FailureFamily(
|
||||
"unsupported_rate_duration", "r3", False, True,
|
||||
"propose a rate fixture for a recognized-but-unsupported rate feature (unit conversion / "
|
||||
"multi-rate). GROWTH surface: rate_unit_mismatch + combined_rates are emitted ONLY after a "
|
||||
"rate clause is recognized, so they are always rate-like — never arbitrary text.",
|
||||
proposal_target="r3_gold_fixture",
|
||||
refusal_reasons=("rate_unit_mismatch", "combined_rates"),
|
||||
),
|
||||
FailureFamily(
|
||||
"rate_underdetermined", "r3", True, False,
|
||||
"refuse — a single-rate problem missing a needed value (underdetermined), like ungrounded_base",
|
||||
refusal_reasons=("missing_rate", "missing_time", "missing_quantity"),
|
||||
),
|
||||
FailureFamily(
|
||||
"unsupported_temporal_state", "r3", True, False,
|
||||
"refuse — elapsed clock-time is R3.x. NOT a growth surface: the clock-marker detector can "
|
||||
"fire on non-rate text, so temporal_state is not reliably rate-like.",
|
||||
refusal_reasons=("temporal_state",),
|
||||
),
|
||||
# --- R4 combined-rate organ (reasons namespaced cmb_*; CMB owns them over R3's same-string ---- #
|
||||
# reasons via the CMB-over-R3 precedence rule in the router) ------------------------------ #
|
||||
FailureFamily(
|
||||
"cmb_unit_mismatch", "r4", True, False,
|
||||
"refuse — the two rates' units are incompatible. must_remain_refused UNTIL a dimension "
|
||||
"registry exists: CMB v1 cannot distinguish a convertible pair (gallons/min vs gallons/hour) "
|
||||
"from a dimensionally-incompatible one (rooms/hour vs liters/minute), so a 'try conversion' "
|
||||
"proposal would be wrong=0-unsafe. NOT 'forever impossible' — a convertible split is future work.",
|
||||
refusal_reasons=("cmb_rate_unit_mismatch",),
|
||||
),
|
||||
FailureFamily(
|
||||
"cmb_combine_ambiguous", "r4", True, False,
|
||||
"refuse — two same-unit rates but no licensed sum/difference cue. An ambiguity, not a "
|
||||
"coverage gap: no fixture can teach which mode the text intends.",
|
||||
refusal_reasons=("cmb_combine_mode_ambiguous",),
|
||||
),
|
||||
FailureFamily(
|
||||
"cmb_underdetermined", "r4", True, False,
|
||||
"refuse — combined-shaped but a contributing rate is unstated (under-specified input); no "
|
||||
"reader enhancement can infer the missing rate.",
|
||||
refusal_reasons=("cmb_missing_second_rate",),
|
||||
),
|
||||
FailureFamily(
|
||||
"cmb_non_positive_net", "r4", True, False,
|
||||
"refuse — non-positive net rate; a quantity/time query cannot make progress (solver boundary).",
|
||||
refusal_reasons=("cmb_non_positive_net_rate",),
|
||||
),
|
||||
FailureFamily(
|
||||
"cmb_non_integer", "r4", True, False,
|
||||
"refuse — no exact integer answer; never round (solver boundary, exact-integer v1).",
|
||||
refusal_reasons=("cmb_non_integer_solution",),
|
||||
),
|
||||
# --- R4 growth surfaces (proposal allowed) — emitted ONLY after positive combined recognition -- #
|
||||
FailureFamily(
|
||||
"cmb_unsupported_rate_count", "r4", False, True,
|
||||
"propose a combined-rate fixture for ≥3 contributing rates (future capability)",
|
||||
proposal_target="cmb_gold_fixture", refusal_reasons=("cmb_three_or_more_rates",),
|
||||
),
|
||||
FailureFamily(
|
||||
"cmb_unsupported_reciprocal", "r4", False, True,
|
||||
"propose a reciprocal work-rate fixture (1/(1/a+1/b); reciprocal rates + rational arithmetic)",
|
||||
proposal_target="cmb_gold_fixture", refusal_reasons=("cmb_reciprocal_work_rate_deferred",),
|
||||
),
|
||||
FailureFamily(
|
||||
"cmb_unsupported_clock_interval", "r4", False, True,
|
||||
"propose a clock-interval fixture (elapsed-clock-time duration in a combined-rate problem)",
|
||||
proposal_target="cmb_gold_fixture", refusal_reasons=("cmb_clock_interval_deferred",),
|
||||
),
|
||||
)
|
||||
|
||||
_BY_NAME: dict[str, FailureFamily] = {f.name: f for f in REGISTRY}
|
||||
_BY_REASON: dict[str, FailureFamily] = {
|
||||
reason: family for family in REGISTRY for reason in family.refusal_reasons
|
||||
}
|
||||
|
||||
#: The verdict-derived family (no refusal reason maps to it).
|
||||
ANSWER_KEY_CONTRADICTION = _BY_NAME["answer_key_contradiction"]
|
||||
|
||||
|
||||
def family_for_reason(reason: str | None) -> FailureFamily | None:
|
||||
"""The single failure family a typed organ refusal reason belongs to (or ``None``)."""
|
||||
if reason is None:
|
||||
return None
|
||||
return _BY_REASON.get(reason)
|
||||
|
||||
|
||||
def family_by_name(name: str) -> FailureFamily | None:
|
||||
return _BY_NAME.get(name)
|
||||
|
||||
|
||||
def enrich_family(attempt: ComprehensionAttempt) -> ComprehensionAttempt:
|
||||
"""Return *attempt* with its ``family`` resolved from its refusal reason (or unchanged)."""
|
||||
family = family_for_reason(attempt.refusal_reason)
|
||||
if family is None:
|
||||
return attempt
|
||||
return replace(attempt, family=family.name)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ANSWER_KEY_CONTRADICTION",
|
||||
"FailureFamily",
|
||||
"Owner",
|
||||
"REGISTRY",
|
||||
"enrich_family",
|
||||
"family_by_name",
|
||||
"family_for_reason",
|
||||
]
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
"""Typed, immutable record of one comprehension organ's attempt at a problem (N2).
|
||||
|
||||
A small normalization layer over the R1 (`generate.quantitative_comprehension`) and R2
|
||||
(`generate.constraint_comprehension`) setup compilers: it turns each organ's heterogeneous
|
||||
output (a typed setup, or a typed `Refusal`) into one uniform, frozen `ComprehensionAttempt`.
|
||||
Nothing here changes reader behavior — it only *describes* an outcome so the router (N3),
|
||||
failure-family registry (N4), and contemplation pass manager (N6) can reason over both organs
|
||||
uniformly.
|
||||
|
||||
Outcome semantics. `classify` (N2) produces **produce-mode** outcomes — what the organ did on
|
||||
its own gates, with no gold in hand: `setup_refused` (the organ refused) or `setup_correct`
|
||||
(an admissible setup was produced). The gold-relative outcomes (`setup_wrong`, `answer_wrong`)
|
||||
are representable here but are emitted only in **eval mode** by the lanes that hold gold — never
|
||||
fabricated by `classify`. `answer_*` / `contradiction` are reached when the solver / answer-choice
|
||||
verifier run downstream (N6), not at setup classification time.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
|
||||
from generate.binding_graph.model import SourceSpanLink
|
||||
|
||||
Organ = Literal["r1_quantitative", "r2_constraints", "r3_rate", "r4_combined_rate"]
|
||||
|
||||
Outcome = Literal[
|
||||
"setup_correct", # an admissible setup was produced (produce-mode) / matches gold (eval-mode)
|
||||
"setup_refused", # the organ refused to assemble a setup
|
||||
"setup_wrong", # eval-mode only: produced setup diverges from gold (a wrong=0 breach)
|
||||
"answer_correct", # a value was produced and self-verified / matches gold
|
||||
"answer_refused", # setup produced but the solver/verifier refused
|
||||
"answer_wrong", # eval-mode only: produced value diverges from gold
|
||||
"contradiction", # a verified value contradicts a supplied answer key
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ComprehensionAttempt:
|
||||
"""One organ's attempt at one problem. Immutable; carries the outcome, the refusal reason
|
||||
(if any), a deterministic setup signature (for cross-organ comparison), the answer (if a
|
||||
value was produced), and source-span evidence. ``family`` is left ``None`` by ``classify``
|
||||
and resolved later by the N4 failure-family registry."""
|
||||
|
||||
organ: Organ
|
||||
outcome: Outcome
|
||||
case_id: str | None = None
|
||||
refusal_reason: str | None = None
|
||||
family: str | None = None
|
||||
setup_signature: str | None = None
|
||||
answer: int | None = None
|
||||
evidence: tuple[SourceSpanLink, ...] = ()
|
||||
|
||||
@property
|
||||
def is_setup_correct(self) -> bool:
|
||||
return self.outcome == "setup_correct"
|
||||
|
||||
@property
|
||||
def is_refusal(self) -> bool:
|
||||
return self.outcome in ("setup_refused", "answer_refused")
|
||||
|
||||
|
||||
__all__ = ["ComprehensionAttempt", "Organ", "Outcome"]
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
"""Proposal-only failure-artifact emitter (N5) — deliberately toothless.
|
||||
|
||||
When the contemplation pass (N6) meets a **growth-surface** failure family (``proposal_allowed
|
||||
= True``), it may emit a single content-addressed JSON artifact under
|
||||
``teaching/proposals/comprehension_failures/<hash>.json``. That artifact can *propose* a next
|
||||
fixture/rule for human review. It can do nothing else:
|
||||
|
||||
```text
|
||||
status is always "proposal_only"
|
||||
mounted is always false
|
||||
requires_review is always true
|
||||
serving never reads these files
|
||||
no reader/test is modified
|
||||
```
|
||||
|
||||
This routes into the existing proposal-only teaching flywheel (ADR-0055/0056/0057) — it is NOT a
|
||||
parallel correction path (CLAUDE.md teaching-safety). The alignment is
|
||||
``failure -> classification -> proposal -> review -> ratification``, never ``failure -> self-patch``.
|
||||
|
||||
Content-addressing: the filename is ``sha256(failure_family : sha256(problem_text))`` — so the
|
||||
same failure on the same text always writes the same path (idempotent), and the raw problem text
|
||||
is **hashed, never stored**. Deterministic; no clock, no randomness.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from core.comprehension_attempt.failure_family import FailureFamily
|
||||
from core.comprehension_attempt.model import ComprehensionAttempt
|
||||
|
||||
_PROPOSAL_STATUS = "proposal_only"
|
||||
|
||||
|
||||
def default_proposal_root() -> Path:
|
||||
"""``<repo>/teaching/proposals/comprehension_failures`` — the write-only proposal sink."""
|
||||
return Path(__file__).resolve().parents[2] / "teaching" / "proposals" / "comprehension_failures"
|
||||
|
||||
|
||||
def _sha256(text: str) -> str:
|
||||
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _attempt_to_dict(attempt: ComprehensionAttempt) -> dict[str, Any]:
|
||||
return {
|
||||
"organ": attempt.organ,
|
||||
"outcome": attempt.outcome,
|
||||
"refusal_reason": attempt.refusal_reason,
|
||||
"family": attempt.family,
|
||||
"setup_signature": attempt.setup_signature,
|
||||
"answer": attempt.answer,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FailureProposal:
|
||||
"""A proposal-only artifact. The invariant fields (``status``/``requires_review``/``mounted``)
|
||||
are enforced in ``__post_init__`` so even a hand-constructed proposal cannot be made
|
||||
serving-mountable."""
|
||||
|
||||
failure_family: str
|
||||
problem_text_sha256: str
|
||||
observed_attempts: tuple[dict[str, Any], ...]
|
||||
status: str = _PROPOSAL_STATUS
|
||||
suggested_next_fixture: None = None # v0: always None — a human authors the fixture on review
|
||||
requires_review: bool = True
|
||||
mounted: bool = False
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.status != _PROPOSAL_STATUS:
|
||||
raise ValueError(f"proposal status must be {_PROPOSAL_STATUS!r}; got {self.status!r}")
|
||||
if self.mounted:
|
||||
raise ValueError("a proposal can never be mounted")
|
||||
if not self.requires_review:
|
||||
raise ValueError("a proposal always requires review")
|
||||
|
||||
def content_hash(self) -> str:
|
||||
"""Deterministic content address: same failure on same text -> same hash."""
|
||||
return hashlib.sha256(
|
||||
f"{self.failure_family}:{self.problem_text_sha256}".encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
def to_json_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"status": self.status,
|
||||
"failure_family": self.failure_family,
|
||||
"problem_text_sha256": self.problem_text_sha256,
|
||||
"observed_attempts": list(self.observed_attempts),
|
||||
"suggested_next_fixture": self.suggested_next_fixture,
|
||||
"requires_review": self.requires_review,
|
||||
"mounted": self.mounted,
|
||||
}
|
||||
|
||||
|
||||
def build_proposal(
|
||||
text: str, family: FailureFamily, attempts: tuple[ComprehensionAttempt, ...]
|
||||
) -> FailureProposal | None:
|
||||
"""Build a proposal for a growth-surface family, or ``None`` for a correct wrong=0 boundary.
|
||||
|
||||
A family with ``proposal_allowed = False`` (every must-remain-refused boundary) yields NO
|
||||
proposal — the loop never proposes against a faithful refusal.
|
||||
"""
|
||||
if not family.proposal_allowed:
|
||||
return None
|
||||
return FailureProposal(
|
||||
failure_family=family.name,
|
||||
problem_text_sha256=_sha256(text),
|
||||
observed_attempts=tuple(_attempt_to_dict(a) for a in attempts),
|
||||
)
|
||||
|
||||
|
||||
def proposal_path(proposal: FailureProposal, root: Path | None = None) -> Path:
|
||||
base = root if root is not None else default_proposal_root()
|
||||
return base / f"{proposal.content_hash()}.json"
|
||||
|
||||
|
||||
def emit_proposal(
|
||||
text: str,
|
||||
family: FailureFamily,
|
||||
attempts: tuple[ComprehensionAttempt, ...],
|
||||
*,
|
||||
root: Path | None = None,
|
||||
) -> Path | None:
|
||||
"""Write a proposal-only artifact for a growth-surface family; return its path, or ``None``.
|
||||
|
||||
Idempotent: the same failure on the same text writes the same content-addressed path with
|
||||
byte-identical content (``sort_keys``). Creates the sink directory on demand.
|
||||
"""
|
||||
proposal = build_proposal(text, family, attempts)
|
||||
if proposal is None:
|
||||
return None
|
||||
path = proposal_path(proposal, root)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(proposal.to_json_dict(), indent=2, sort_keys=True), encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
__all__ = [
|
||||
"FailureProposal",
|
||||
"build_proposal",
|
||||
"default_proposal_root",
|
||||
"emit_proposal",
|
||||
"proposal_path",
|
||||
]
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
"""Deterministic multi-organ setup router (N3).
|
||||
|
||||
Boring on purpose: attempt the R1 and R2 setup compilers, collect their typed attempts, and
|
||||
select a setup ONLY when exactly one organ produced an admissible one. No dynamic "best"
|
||||
scoring, no priority heuristics.
|
||||
|
||||
```text
|
||||
exactly one setup_correct -> routed (use it)
|
||||
zero setup_correct -> all_refused (classify downstream)
|
||||
>= 2 setup_correct, signatures agree-> routed (organs concur)
|
||||
>= 2 setup_correct, signatures differ-> ambiguous (refuse — never pick)
|
||||
```
|
||||
|
||||
Cross-organ signatures are produced by different functions and never coincide, so in practice
|
||||
two admitting organs always resolve to ``ambiguous``. The router never solves and never emits
|
||||
``setup_wrong`` — that is an eval-only outcome; against gold the routed setup must match (the
|
||||
wrong=0 invariant, asserted by the router tests).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
|
||||
from core.comprehension_attempt.classify import classify_cmb, classify_r1, classify_r2, classify_r3
|
||||
from core.comprehension_attempt.model import ComprehensionAttempt
|
||||
|
||||
RouteStatus = Literal["routed", "all_refused", "ambiguous"]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RouteResult:
|
||||
"""The outcome of routing one problem across the organs: every attempt, the selected setup
|
||||
(or ``None``), and the routing status."""
|
||||
|
||||
attempts: tuple[ComprehensionAttempt, ...]
|
||||
selected: ComprehensionAttempt | None
|
||||
status: RouteStatus
|
||||
|
||||
|
||||
def cmb_is_authoritative(attempts: tuple[ComprehensionAttempt, ...]) -> bool:
|
||||
"""True when the R4 combined-rate organ **positively** recognized combined-rate shape — a setup,
|
||||
or a substantive ``cmb_*`` refusal (NOT the bare ``not_combined_rate_shaped`` / ``empty``
|
||||
step-aside). When it does, R3's broader single-rate read of the SAME text is inadmissible: a more
|
||||
specific positive recognition beats a broader partial one ("Anna and Ben paint together; Anna
|
||||
paints 3 rooms/hour" must not be answered as a single-rate 12). This is the narrow CMB↔R3
|
||||
instance of a future general domain-specificity adjudication; it does NOT mean "CMB always wins"
|
||||
(on ``input_shape`` CMB cedes to R3, e.g. a plain single-rate car problem)."""
|
||||
cmb = next((a for a in attempts if a.organ == "r4_combined_rate"), None)
|
||||
if cmb is None:
|
||||
return False
|
||||
return cmb.is_setup_correct or (cmb.refusal_reason or "").startswith("cmb_")
|
||||
|
||||
|
||||
def route_setup(text: str, *, case_id: str | None = None) -> RouteResult:
|
||||
"""Route *text* to the single organ that admits an honest setup, or refuse."""
|
||||
attempts = (
|
||||
classify_r1(text, case_id=case_id),
|
||||
classify_r2(text, case_id=case_id),
|
||||
classify_r3(text, case_id=case_id),
|
||||
classify_cmb(text, case_id=case_id),
|
||||
)
|
||||
# CMB-over-R3 domain precedence: a substantive CMB recognition vetoes R3's single-rate over-read.
|
||||
vetoed = cmb_is_authoritative(attempts)
|
||||
correct = tuple(
|
||||
a for a in attempts if a.is_setup_correct and not (vetoed and a.organ == "r3_rate")
|
||||
)
|
||||
if len(correct) == 1:
|
||||
return RouteResult(attempts, correct[0], "routed")
|
||||
if not correct:
|
||||
return RouteResult(attempts, None, "all_refused")
|
||||
if len({a.setup_signature for a in correct}) == 1:
|
||||
return RouteResult(attempts, correct[0], "routed")
|
||||
return RouteResult(attempts, None, "ambiguous")
|
||||
|
||||
|
||||
__all__ = ["RouteResult", "RouteStatus", "cmb_is_authoritative", "route_setup"]
|
||||
|
|
@ -276,73 +276,16 @@ class RuntimeConfig:
|
|||
# ADR-0151 — generate TeachingChainProposals from enriched candidates on load.
|
||||
auto_proposal_enabled: bool = False
|
||||
|
||||
# Shape B+ (L10 resume-as-same-life) — persist and restore the FULL lived
|
||||
# session state (field, vault, anchor, graph, referents, dialogue) across
|
||||
# reboot, not just recognizers/candidates (Shape B). OFF by default: it is a
|
||||
# deliberate always-on-runtime mode, and per-turn snapshotting has an O(turns)
|
||||
# cost, so demos/evals/one-shot runtimes must not pay for resume they don't
|
||||
# use. Enabled by the L10 continuity lane and the production L10 process.
|
||||
persist_session_state: bool = False
|
||||
|
||||
# L11 — on reboot, if the stamped checkpoint identity != the recomputed
|
||||
# engine identity (the ratified substrate changed during downtime), REFUSE to
|
||||
# start (raise IdentityContinuityError) rather than the default warn-and-flag.
|
||||
# OFF by default: reboot is recovery, not control flow (ADR-0157), and the
|
||||
# operator must not be bricked by a benign ratified pack swap. Deployments
|
||||
# wanting a hard identity-continuity guarantee opt in.
|
||||
strict_identity_continuity: bool = False
|
||||
|
||||
# Step B (inline realization) — when on, each turn ACCRUES knowledge into the
|
||||
# held self: a comprehensible declarative turn is realized into the session vault
|
||||
# (SPECULATIVE, as-told), and a comprehensible question turn is determined over
|
||||
# realized knowledge. This is the "one continuous life" telos made real — a
|
||||
# conversation accumulates knowledge it can recall and reason over. OFF by default
|
||||
# (one-shot/eval runtimes don't accrue); the production L10 process enables it
|
||||
# alongside persist_session_state so accrued facts survive reboot. Realization is
|
||||
# SESSION memory (immediate), NOT ratified learning — it proposes nothing; the
|
||||
# teaching/review HITL path is untouched.
|
||||
accrue_realized_knowledge: bool = False
|
||||
|
||||
# Step D (CLOSE) — when on, idle_tick consolidates soundly-derived determinations
|
||||
# back into the held self: one semi-naive layer of the member/subset deductive
|
||||
# closure (member∘subset, subset∘subset — NEVER member∘member) per tick, each
|
||||
# derived edge proof_chain-verified, written as a SPECULATIVE realized record with
|
||||
# derived-provenance. This is how the loop "learns from determined facts": derive
|
||||
# once, remember, reach one hop further next tick — the directly answerable set
|
||||
# climbs monotonically to the deductive-closure fixed point. OFF by default; the
|
||||
# production L10 process enables it alongside accrue_realized_knowledge +
|
||||
# persist_session_state. SESSION memory (immediate), NOT reviewed/corpus learning —
|
||||
# no proposal coupling, the teaching/review HITL path is untouched. Bounded by the
|
||||
# same _SUBSUMPTION_SUBSET_FACT_BUDGET; converges (a saturated tick is a no-op).
|
||||
consolidate_determinations: bool = False
|
||||
|
||||
# IT (proposal-review surface) — when on, idle_tick runs a READ-ONLY sub-pass that scans
|
||||
# the comprehension-failure proposal sink and surfaces a summary in
|
||||
# IdleTickResult.proposal_review. It NEVER mutates an artifact, sets did_work, checkpoints,
|
||||
# ratifies, mounts, or modifies readers; a reporter failure is captured, not propagated.
|
||||
# OFF by default — idle ticks don't pay for the scan unless a deployment wants the surface.
|
||||
review_pending_proposals: bool = False
|
||||
|
||||
# Step E (ESTIMATION) — when on, a converse query the engine would otherwise REFUSE
|
||||
# (told p(a,b), asked p(b,a)) may be answered with a DISCLOSED [approximate] estimate
|
||||
# IF the predicate-class has earned the SERVE license on the ratified, committed
|
||||
# reliability ledger (ADR-0175 license_for, θ_SERVE=0.99) — routed through the
|
||||
# ADR-0206 govern_response/shape_surface bridge. OFF by default; only meaningful with
|
||||
# accrue_realized_knowledge (the estimate is computed in the accrual path). wrong=0 is
|
||||
# preserved by construction: an estimate is ALWAYS disclosed ([approximate]), never
|
||||
# asserted as fact, and is offered only for a class whose committed track record
|
||||
# clears the Wilson floor. Absent a cleared license -> STRICT refuse (the safe
|
||||
# default); the engine never raises its own ceiling.
|
||||
estimation_enabled: bool = False
|
||||
|
||||
# ASK serving gate enable flag. When True, ASK serving is allowed.
|
||||
# Default False (dark).
|
||||
ask_serving_enabled: bool = False
|
||||
|
||||
# VERIFIED serving gate enable flag. When True, VERIFIED serving is allowed.
|
||||
# Default False (dark).
|
||||
verified_serving_enabled: bool = False
|
||||
|
||||
# ADR-0164 Phase 1 — incremental comprehension reader for question sentences.
|
||||
# When True, the candidate-graph path uses the comprehension reader
|
||||
# (generate/comprehension/lifecycle.py) to parse question sentences BEFORE
|
||||
# consulting the regex question patterns (Pattern A/B/C in
|
||||
# generate/math_candidate_parser.py). On reader refusal, falls through to
|
||||
# the existing regex parser — the reader is purely additive at Phase 1.
|
||||
# Default False: flag-OFF behaviour is byte-identical to today.
|
||||
# Phase 3 (per ADR-0164 §Phasing) removes the regex question parser entirely;
|
||||
# that work is deferred — this PR is the Phase 1 stopgap.
|
||||
comprehension_reader_questions: bool = False
|
||||
|
||||
|
||||
DEFAULT_IDENTITY_PACK: str = "default_general_v1"
|
||||
|
|
|
|||
|
|
@ -2,10 +2,8 @@
|
|||
|
||||
from .contradiction_detection import mine_contradiction_detection_report
|
||||
from .frontier_compare import mine_frontier_compare_report
|
||||
from .learning_arena import mine_learning_arena_report
|
||||
|
||||
__all__ = [
|
||||
"mine_contradiction_detection_report",
|
||||
"mine_frontier_compare_report",
|
||||
"mine_learning_arena_report",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,111 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from core.contemplation.schema import (
|
||||
ContemplationEvidenceRef,
|
||||
ContemplationFinding,
|
||||
FindingKind,
|
||||
)
|
||||
|
||||
|
||||
def mine_learning_arena_report(
|
||||
report_path: str | Path,
|
||||
*,
|
||||
substrate_hash: str,
|
||||
min_coverage: float = 0.25,
|
||||
) -> tuple[ContemplationFinding, ...]:
|
||||
"""Convert learning-arena report weaknesses into speculative findings.
|
||||
|
||||
Read-only: parses ``report_path`` and returns immutable findings. It never
|
||||
writes packs, teaching examples, proposals, or runtime state.
|
||||
"""
|
||||
path = Path(report_path)
|
||||
report = json.loads(path.read_text(encoding="utf-8"))
|
||||
source_id = str(path)
|
||||
findings: list[ContemplationFinding] = []
|
||||
|
||||
for class_name, row in sorted(_per_class(report).items()):
|
||||
committed = int(row.get("committed", 0) or 0)
|
||||
coverage = float(row.get("coverage", 0.0) or 0.0)
|
||||
t2_verified = int(row.get("t2_verified", 0) or 0)
|
||||
if coverage < min_coverage:
|
||||
evidence = ContemplationEvidenceRef(
|
||||
source_type="learning_arena_report",
|
||||
source_id=source_id,
|
||||
pointer=f"class={class_name}",
|
||||
summary=f"coverage={coverage};committed={committed};min={min_coverage}",
|
||||
)
|
||||
findings.append(
|
||||
ContemplationFinding(
|
||||
kind=FindingKind.COVERAGE_GAP,
|
||||
subject=class_name,
|
||||
predicate="weak_coverage",
|
||||
object=None,
|
||||
evidence_refs=(evidence,),
|
||||
proposed_action=(
|
||||
"Inspect refusal diagnoses for this capability class and add "
|
||||
"practice or reviewed operators only where the missing piece is named."
|
||||
),
|
||||
substrate_hash=substrate_hash,
|
||||
)
|
||||
)
|
||||
if committed > 0 and t2_verified == 0:
|
||||
evidence = ContemplationEvidenceRef(
|
||||
source_type="learning_arena_report",
|
||||
source_id=source_id,
|
||||
pointer=f"class={class_name}",
|
||||
summary=f"committed={committed};t2_verified=0",
|
||||
)
|
||||
findings.append(
|
||||
ContemplationFinding(
|
||||
kind=FindingKind.UNPROVED_RELATION,
|
||||
subject=class_name,
|
||||
predicate="missing_tier2_evidence",
|
||||
object=None,
|
||||
evidence_refs=(evidence,),
|
||||
proposed_action=(
|
||||
"Add convergent self-verification evidence before promoting this "
|
||||
"class beyond gold-scored practice."
|
||||
),
|
||||
substrate_hash=substrate_hash,
|
||||
)
|
||||
)
|
||||
|
||||
for record in report.get("elimination_records", ()) or ():
|
||||
if not isinstance(record, dict):
|
||||
continue
|
||||
case_id = str(record.get("case_id", "unknown_case"))
|
||||
class_name = str(record.get("class_name", "unknown_class"))
|
||||
evidence = ContemplationEvidenceRef(
|
||||
source_type="learning_arena_report",
|
||||
source_id=source_id,
|
||||
pointer=f"case={case_id}",
|
||||
summary=str(record.get("reason", "wrong attempt")),
|
||||
)
|
||||
findings.append(
|
||||
ContemplationFinding(
|
||||
kind=FindingKind.BENCHMARK_CASE,
|
||||
subject=f"{class_name}/{case_id}",
|
||||
predicate="wrong_attempt",
|
||||
object=str(record.get("gold", "")),
|
||||
evidence_refs=(evidence,),
|
||||
proposed_action=(
|
||||
"Use this gold-caught wrong attempt as elimination evidence; do not "
|
||||
"promote the class until the faulty derivation shape is pruned."
|
||||
),
|
||||
substrate_hash=substrate_hash,
|
||||
)
|
||||
)
|
||||
|
||||
return tuple(findings)
|
||||
|
||||
|
||||
def _per_class(report: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||
rows = report.get("per_class", {})
|
||||
return {str(k): v for k, v in rows.items() if isinstance(v, dict)}
|
||||
|
||||
|
||||
__all__ = ["mine_learning_arena_report"]
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
"""EngineIdentity — content-derived identity of the ratified substrate (L11).
|
||||
|
||||
``EngineIdentity`` is the sha256 of the canonical serialization of the engine's
|
||||
ratified PERSONALITY substrate — the active identity / safety / ethics / register
|
||||
/ anchor-lens pack files — plus the code revision. It is **content-derived, NOT
|
||||
entropy-based**: two engines with the same ratified substrate compute the SAME
|
||||
identity, because they ARE the same engine functionally (substrate is shareable).
|
||||
|
||||
This is the "who am I" hash. It is bumped only by a ratified change to the
|
||||
identity substrate (a new identity pack, a safety-axis change), NOT by lived
|
||||
learning (recall, teaching) — that is the engine's *experience*, carried across
|
||||
reboot by the Shape B+ lived-state lineage, not its identity. The git-like
|
||||
lineage chain (parent links on ratification) and the reboot identity verification
|
||||
build on this primitive.
|
||||
|
||||
Honest scope (per the EngineIdentity candidate note): this is a *convention*
|
||||
naming the existing per-pack content hashes, not a new pack format. The ratified
|
||||
teaching corpus / recognizer-registry head can be folded into the tuple later
|
||||
(additive — a new key changes the identity, which is the correct semantic for a
|
||||
ratified-substrate change).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from core.config import (
|
||||
DEFAULT_ANCHOR_LENS,
|
||||
DEFAULT_ETHICS_PACK,
|
||||
DEFAULT_IDENTITY_PACK,
|
||||
DEFAULT_REGISTER_PACK,
|
||||
RuntimeConfig,
|
||||
)
|
||||
|
||||
# The never-swappable safety pack default (packs/safety/loader.py).
|
||||
DEFAULT_SAFETY_PACK: str = "core_safety_axes_v1"
|
||||
|
||||
_PACKS_ROOT = Path(__file__).resolve().parents[1] / "packs"
|
||||
|
||||
#: role -> packs/ subdirectory holding ``<pack_id>.json``.
|
||||
_ROLE_DIRS: dict[str, str] = {
|
||||
"identity": "identity",
|
||||
"safety": "safety",
|
||||
"ethics": "ethics",
|
||||
"register": "register",
|
||||
"anchor_lens": "anchor_lens",
|
||||
}
|
||||
|
||||
#: The ratified roles that constitute the engine's identity, in canonical order.
|
||||
RATIFIED_ROLES: tuple[str, ...] = (
|
||||
"identity",
|
||||
"safety",
|
||||
"ethics",
|
||||
"register",
|
||||
"anchor_lens",
|
||||
)
|
||||
|
||||
|
||||
class EngineIdentityError(RuntimeError):
|
||||
"""A ratified pack named by the identity tuple could not be resolved."""
|
||||
|
||||
|
||||
class IdentityContinuityError(RuntimeError):
|
||||
"""A reboot found the stamped checkpoint identity != the recomputed identity.
|
||||
|
||||
The ratified substrate changed while the engine was down, so it would resume
|
||||
the lived state under a DIFFERENT identity than the checkpoint was written
|
||||
under. Raised only under strict identity continuity; the default surfaces a
|
||||
warning and a queryable break flag (reboot is recovery, not control flow).
|
||||
"""
|
||||
|
||||
|
||||
def _pack_content_hash(role: str, pack_id: str) -> str:
|
||||
path = _PACKS_ROOT / _ROLE_DIRS[role] / f"{pack_id}.json"
|
||||
if not path.exists():
|
||||
raise EngineIdentityError(
|
||||
f"ratified {role} pack not found: {_ROLE_DIRS[role]}/{pack_id}.json"
|
||||
)
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def ratified_substrate(pack_ids: dict[str, str], git_revision: str) -> dict[str, Any]:
|
||||
"""The canonical, auditable identity tuple.
|
||||
|
||||
``{role: {"pack_id", "sha256"}}`` for every ratified role plus
|
||||
``"code_revision"``. ``pack_ids`` must name a pack for every role in
|
||||
``RATIFIED_ROLES``.
|
||||
"""
|
||||
substrate: dict[str, Any] = {}
|
||||
for role in RATIFIED_ROLES:
|
||||
pack_id = pack_ids[role]
|
||||
substrate[role] = {
|
||||
"pack_id": pack_id,
|
||||
"sha256": _pack_content_hash(role, pack_id),
|
||||
}
|
||||
substrate["code_revision"] = git_revision
|
||||
return substrate
|
||||
|
||||
|
||||
def compute_engine_identity(pack_ids: dict[str, str], git_revision: str) -> str:
|
||||
"""The sha256 EngineIdentity over the ratified substrate tuple."""
|
||||
canonical = json.dumps(
|
||||
ratified_substrate(pack_ids, git_revision),
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=False,
|
||||
)
|
||||
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _resolve_pack_ids(config: RuntimeConfig) -> dict[str, str]:
|
||||
"""Resolve each ratified role to its active pack id (config override or DEFAULT)."""
|
||||
return {
|
||||
"identity": config.identity_pack or DEFAULT_IDENTITY_PACK,
|
||||
"safety": DEFAULT_SAFETY_PACK, # never-swappable
|
||||
"ethics": config.ethics_pack or DEFAULT_ETHICS_PACK,
|
||||
"register": config.register_pack_id or DEFAULT_REGISTER_PACK,
|
||||
"anchor_lens": config.anchor_lens_id or DEFAULT_ANCHOR_LENS,
|
||||
}
|
||||
|
||||
|
||||
def engine_identity_for_config(config: RuntimeConfig, git_revision: str) -> str:
|
||||
"""EngineIdentity for a runtime config (resolving every role to its active pack)."""
|
||||
return compute_engine_identity(_resolve_pack_ids(config), git_revision)
|
||||
|
||||
|
||||
def ratified_substrate_for_config(
|
||||
config: RuntimeConfig, git_revision: str
|
||||
) -> dict[str, Any]:
|
||||
"""The auditable identity tuple for a runtime config."""
|
||||
return ratified_substrate(_resolve_pack_ids(config), git_revision)
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
"""The Epistemic Disclosure spine (P0-1+).
|
||||
|
||||
CORE's served-surface governance: comprehension produces evidence, the limitation
|
||||
pass classifies *what kind of gap* is blocking resolution, and (in later slices) a
|
||||
disposition + disclosure-claim decide what reaches the user and under what epistemic
|
||||
claim. This package is the *owner* of that machine — ``generate/derivation/verify.py``
|
||||
and other serving sites may eventually *consume* it, but must never *define* it.
|
||||
|
||||
Shipped so far (all off-serving — nothing here imports ``generate.derivation`` /
|
||||
``core.reliability_gate``):
|
||||
|
||||
* :mod:`~core.epistemic_disclosure.limitation` (P0-1) — the typed limitation
|
||||
vocabulary, a CONSOLIDATING VIEW over the shipped failure-family registry +
|
||||
contemplation terminals (no fourth taxonomy).
|
||||
* :mod:`~core.epistemic_disclosure.disclosure_claim` (P0-2) — the ``DisclosureClaim``
|
||||
axis (the epistemic claim a response makes), kept SEPARATE from ``ReachLevel``.
|
||||
* :mod:`~core.epistemic_disclosure.disposition` (P0-3) — ``ServedDisposition`` and
|
||||
``choose_served_disposition``: the pure mapping
|
||||
``EpistemicState × LimitationAssessment × DisclosureClaim → ServedDisposition``.
|
||||
Mapping scaffold only — no rendering, no bus, no ``verify.py``; nothing consumes
|
||||
it yet.
|
||||
* :mod:`~core.epistemic_disclosure.ask_serving` — a narrow Q1-D served-ASK artifact
|
||||
adapter. It validates already-rendered question artifacts and returns a typed
|
||||
decision; it does not render prose and does not acquire runtime contemplation.
|
||||
* :mod:`~core.epistemic_disclosure.verified_contract` (P1-A) — the VERIFIED contract:
|
||||
the obligation, the proof shape, the validator, and the single sanctioned route to
|
||||
``EpistemicState.VERIFIED`` / ``DisclosureClaim.VERIFIED``. Contract only — no
|
||||
producer; a faithful solve of a WRONG read must not verify.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from core.epistemic_disclosure.ask_serving import (
|
||||
ServedAskDecision,
|
||||
evaluate_served_ask,
|
||||
)
|
||||
from core.epistemic_disclosure.disclosure_claim import (
|
||||
DEFAULT_DISCLOSURE_CLAIM,
|
||||
DisclosureClaim,
|
||||
)
|
||||
from core.epistemic_disclosure.disposition import (
|
||||
ServedDisposition,
|
||||
choose_served_disposition,
|
||||
)
|
||||
from core.epistemic_disclosure.limitation import (
|
||||
Q1B_ASK_CARVE_OUT,
|
||||
LimitationAssessment,
|
||||
LimitationKind,
|
||||
MissingSlot,
|
||||
ResolutionAction,
|
||||
assess_from_attempt,
|
||||
assess_from_family,
|
||||
terminal_for_action,
|
||||
)
|
||||
from core.epistemic_disclosure.verified_contract import (
|
||||
VERIFICATION_OBLIGATION,
|
||||
VerificationObligation,
|
||||
VerificationProof,
|
||||
VerificationResult,
|
||||
VerificationVerdict,
|
||||
disclosure_for_verification,
|
||||
evaluate_verification,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_DISCLOSURE_CLAIM",
|
||||
"Q1B_ASK_CARVE_OUT",
|
||||
"VERIFICATION_OBLIGATION",
|
||||
"DisclosureClaim",
|
||||
"LimitationAssessment",
|
||||
"LimitationKind",
|
||||
"MissingSlot",
|
||||
"ResolutionAction",
|
||||
"ServedAskDecision",
|
||||
"ServedDisposition",
|
||||
"VerificationObligation",
|
||||
"VerificationProof",
|
||||
"VerificationResult",
|
||||
"VerificationVerdict",
|
||||
"assess_from_attempt",
|
||||
"assess_from_family",
|
||||
"choose_served_disposition",
|
||||
"disclosure_for_verification",
|
||||
"evaluate_served_ask",
|
||||
"evaluate_verification",
|
||||
"terminal_for_action",
|
||||
]
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
"""Stage 2 ASK served-surface artifact adapter.
|
||||
|
||||
This module is intentionally narrow: it validates a pre-rendered Q1-D
|
||||
``DeliveredQuestion`` artifact and decides whether that artifact is eligible to
|
||||
be exposed as a served ASK/QUESTION_NEEDED surface. It does not acquire
|
||||
contemplation results from runtime and does not render question prose.
|
||||
|
||||
Validation enforces the Q1-D artifact contract:
|
||||
|
||||
- top-level JSON object only;
|
||||
- ``status == "question_only"``;
|
||||
- ``requires_review is True``;
|
||||
- ``served is False``;
|
||||
- ``answer_binding`` is absent or ``None``;
|
||||
- ``question`` is an object;
|
||||
- ``question.text`` is a non-empty string;
|
||||
- ``question.slot_name`` is a non-empty string;
|
||||
- ``question_path`` exists on disk and differs from ``proposal_path``.
|
||||
|
||||
Any validation failure fails closed to the caller's fallback surface and
|
||||
standing disposition. The served text is consumed from the artifact exactly; no
|
||||
runtime prose construction or mutation happens here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from core.epistemic_disclosure.disposition import ServedDisposition, choose_served_disposition
|
||||
from core.epistemic_disclosure.limitation import LimitationAssessment
|
||||
from core.epistemic_questions.serving_gate import ask_serving_enabled
|
||||
from core.epistemic_state import EpistemicState
|
||||
|
||||
_MISSING = object()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ServedAskDecision:
|
||||
"""The adapter's served-ASK decision."""
|
||||
|
||||
served: bool
|
||||
terminal: str
|
||||
surface: str
|
||||
disposition: ServedDisposition
|
||||
|
||||
|
||||
def _terminal_value(contemplation_result: Any) -> str:
|
||||
terminal = getattr(contemplation_result, "terminal", None)
|
||||
if terminal is None:
|
||||
return "NO_PROGRESS"
|
||||
return str(getattr(terminal, "value", terminal))
|
||||
|
||||
|
||||
def _fallback_disposition(terminal: str) -> ServedDisposition:
|
||||
if terminal == "PROPOSAL_EMITTED":
|
||||
return ServedDisposition.PROPOSE
|
||||
if terminal == "SOLVED_VERIFIED":
|
||||
return ServedDisposition.COMMIT
|
||||
return ServedDisposition.REFUSE
|
||||
|
||||
|
||||
def _fallback_decision(contemplation_result: Any, fallback_surface: str) -> ServedAskDecision:
|
||||
terminal = _terminal_value(contemplation_result)
|
||||
return ServedAskDecision(
|
||||
served=False,
|
||||
terminal=terminal,
|
||||
surface=fallback_surface,
|
||||
disposition=_fallback_disposition(terminal),
|
||||
)
|
||||
|
||||
|
||||
def _validate_question_artifact(data: Any, *, question_path: Path, proposal_path: Any) -> str | None:
|
||||
"""Return the valid question text, or ``None`` for any contract violation."""
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return None
|
||||
if data.get("status") != "question_only":
|
||||
return None
|
||||
if data.get("requires_review") is not True:
|
||||
return None
|
||||
served = data.get("served", _MISSING)
|
||||
if served is _MISSING or served is not False:
|
||||
return None
|
||||
answer_binding = data.get("answer_binding", _MISSING)
|
||||
if answer_binding is not _MISSING and answer_binding is not None:
|
||||
return None
|
||||
|
||||
question = data.get("question")
|
||||
if not isinstance(question, dict):
|
||||
return None
|
||||
|
||||
text = question.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
return None
|
||||
|
||||
slot_name = question.get("slot_name")
|
||||
if not isinstance(slot_name, str) or not slot_name.strip():
|
||||
return None
|
||||
|
||||
if proposal_path is not None and str(question_path) == str(proposal_path):
|
||||
return None
|
||||
|
||||
return text.strip()
|
||||
|
||||
|
||||
def evaluate_served_ask(
|
||||
config: Any,
|
||||
contemplation_result: Any,
|
||||
fallback_surface: str,
|
||||
) -> ServedAskDecision:
|
||||
"""Evaluate whether a Q1-D question artifact may be surfaced as ASK.
|
||||
|
||||
This is a bus/disposition adapter, not a renderer and not the runtime
|
||||
acquisition path. The caller supplies a contemplation result that already
|
||||
points to a delivered question artifact. When the gate is disabled or any
|
||||
artifact invariant fails, the adapter returns the fallback surface and the
|
||||
standing fallback disposition.
|
||||
"""
|
||||
|
||||
if not ask_serving_enabled(config):
|
||||
return _fallback_decision(contemplation_result, fallback_surface)
|
||||
|
||||
if _terminal_value(contemplation_result) != "QUESTION_NEEDED":
|
||||
return _fallback_decision(contemplation_result, fallback_surface)
|
||||
|
||||
question_path_value = getattr(contemplation_result, "question_path", None)
|
||||
proposal_path_value = getattr(contemplation_result, "proposal_path", None)
|
||||
if not question_path_value or question_path_value == proposal_path_value:
|
||||
return _fallback_decision(contemplation_result, fallback_surface)
|
||||
|
||||
question_path = Path(question_path_value)
|
||||
if not question_path.is_file():
|
||||
return _fallback_decision(contemplation_result, fallback_surface)
|
||||
|
||||
try:
|
||||
payload = json.loads(question_path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return _fallback_decision(contemplation_result, fallback_surface)
|
||||
|
||||
question_text = _validate_question_artifact(
|
||||
payload,
|
||||
question_path=question_path,
|
||||
proposal_path=proposal_path_value,
|
||||
)
|
||||
if question_text is None:
|
||||
return _fallback_decision(contemplation_result, fallback_surface)
|
||||
|
||||
limitation = LimitationAssessment(
|
||||
limitation_kind="missing_information",
|
||||
resolution_action="ask_question",
|
||||
epistemic_state=EpistemicState.UNDETERMINED,
|
||||
owner_organ=payload.get("owner_organ"),
|
||||
blocking_reason=str(payload.get("blocking_reason", "")),
|
||||
)
|
||||
disposition = choose_served_disposition(
|
||||
epistemic_state=EpistemicState.UNDETERMINED,
|
||||
limitation=limitation,
|
||||
)
|
||||
|
||||
return ServedAskDecision(
|
||||
served=True,
|
||||
terminal="QUESTION_NEEDED",
|
||||
surface=question_text,
|
||||
disposition=disposition,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["ServedAskDecision", "evaluate_served_ask"]
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
"""P0-2 — the DisclosureClaim axis (the epistemic claim a served response makes).
|
||||
|
||||
A served response carries two ORTHOGONAL governance properties:
|
||||
|
||||
* ``ReachLevel`` (``core/response_governance/policy.py``) — how far PAST
|
||||
fully-grounded fact the response reaches (STRICT < APPROXIMATE < EXTRAPOLATE
|
||||
< CREATIVE).
|
||||
* ``DisclosureClaim`` (here) — the EPISTEMIC CLAIM the response makes about its
|
||||
own truth status.
|
||||
|
||||
These are deliberately separate axes. ``verified`` is **not** a reach level — it is
|
||||
a claim about *proof state*, not about how far the response speculates. Conflating
|
||||
them (e.g. a hypothetical ``ReachLevel.VERIFIED``) would let a proven answer inherit
|
||||
an "approximate" surface, or an approximation inherit a "verified" badge. Keeping the
|
||||
axes distinct is the architectural commitment behind the Stage-2 lockfile
|
||||
(``docs/analysis/stage2-epistemic-disclosure-bus-verified-v1-scoping-2026-06-08.md`` §0).
|
||||
|
||||
**Discipline — no claim without a producer** (the spine enforces on itself what it
|
||||
enforces on answers). Every member has a real or imminent emitter:
|
||||
|
||||
* ``NONE`` — every response today (the baseline; STRICT + NONE).
|
||||
* ``APPROXIMATE`` — active: the cognition Step-E disclosed estimate
|
||||
(``estimation_enabled`` / ADR-0206 §5).
|
||||
* ``PROPOSAL_ONLY`` — active: ``teaching/proposals`` emits review-only proposals.
|
||||
* ``VERIFIED`` — the imminent frontier: its producer is Phase 1 (P1-A..),
|
||||
declared because it is the v1 target the bus is built around.
|
||||
|
||||
Two claims are intentionally ABSENT, because nothing can emit them — and the spine
|
||||
will not declare a label it cannot earn:
|
||||
|
||||
* ``PROVEN`` — a claim stronger than VERIFIED; no plan to build a producer.
|
||||
* ``ESTIMATED`` — a *future* split of ``APPROXIMATE`` into a distinct
|
||||
numeric-estimate claim, added ONLY once a real estimator producer
|
||||
exists. Until then the cognition estimate is ``APPROXIMATE``.
|
||||
|
||||
P0-2 ships ONLY the axis + its default. No bus behaviour, no mapping to a disposition
|
||||
(that is P0-3 / ServedDisposition). Off-serving: this module imports nothing.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum, unique
|
||||
|
||||
|
||||
@unique
|
||||
class DisclosureClaim(str, Enum):
|
||||
"""The epistemic claim a served surface makes about its own truth status.
|
||||
|
||||
Orthogonal to ``ReachLevel``. ``str``-valued for stable serialization into
|
||||
telemetry / disposition records (the same convention as ``EpistemicState``).
|
||||
"""
|
||||
|
||||
NONE = "none" # no epistemic claim beyond the plain surface (the default)
|
||||
VERIFIED = "verified" # independently confirmed under a canonical-comparison contract
|
||||
APPROXIMATE = "approximate" # a disclosed best-estimate from incomplete evidence
|
||||
PROPOSAL_ONLY = "proposal_only" # offered as a proposal for review, not asserted
|
||||
|
||||
|
||||
#: The default claim: a surface asserts nothing about its truth status unless a
|
||||
#: producer upgrades it. ``STRICT`` reach + ``NONE`` claim is today's every-response
|
||||
#: baseline.
|
||||
DEFAULT_DISCLOSURE_CLAIM: DisclosureClaim = DisclosureClaim.NONE
|
||||
|
||||
|
||||
__all__ = ["DEFAULT_DISCLOSURE_CLAIM", "DisclosureClaim"]
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
"""P0-3 — ServedDisposition: the served-surface decision (mapping scaffold only).
|
||||
|
||||
The third axis of the Epistemic Disclosure bus. Given (a) the epistemic state, (b)
|
||||
the limitation assessment (if resolution is blocked), and (c) the disclosure claim,
|
||||
:func:`choose_served_disposition` decides WHAT KIND OF MOVE the served surface makes:
|
||||
commit / disclose / ask / propose / report / explain / refuse / step-aside.
|
||||
|
||||
This is a PURE MAPPING. No rendering, no bus behaviour, no ``verify.py``, no
|
||||
``govern_response`` mutation — nothing consumes the result yet. P0-3 only fixes the
|
||||
decision table so a later slice / Phase 1 can wire it.
|
||||
|
||||
The load-bearing rule (the Phase-1 guard): a ``DisclosureClaim.VERIFIED`` discloses
|
||||
ONLY when the epistemic state is actually ``EpistemicState.VERIFIED``. An unbacked
|
||||
verified claim degrades to a plain ``COMMIT`` — it is NEVER served as verified before
|
||||
the producer exists. This protects the whole VERIFIED lane from accidental
|
||||
"verified-looking" serving.
|
||||
|
||||
Off-serving: imports no ``generate.derivation`` / ``core.reliability_gate``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum, unique
|
||||
|
||||
from core.epistemic_disclosure.disclosure_claim import DisclosureClaim
|
||||
from core.epistemic_disclosure.limitation import LimitationAssessment
|
||||
from core.epistemic_state import EpistemicState
|
||||
|
||||
|
||||
@unique
|
||||
class ServedDisposition(str, Enum):
|
||||
"""What kind of move the served surface makes.
|
||||
|
||||
``str``-valued for stable serialization (the ``EpistemicState`` /
|
||||
``DisclosureClaim`` convention).
|
||||
"""
|
||||
|
||||
COMMIT = "commit" # serve a fully-grounded answer as-is, no epistemic caveat
|
||||
DISCLOSE = "disclose" # serve under a disclosure claim ([verified] / [approximate])
|
||||
ASK = "ask" # ask the user for the missing/ambiguous datum (Q1 tenant)
|
||||
PROPOSE = "propose" # offer a review-only capability proposal, do not assert
|
||||
REPORT = "report" # report a contradiction with a supplied answer key
|
||||
EXPLAIN = "explain" # explain that the ask is outside the current envelope (scope)
|
||||
REFUSE = "refuse" # hard-stop refusal (impossible / unreadable / known boundary)
|
||||
STEP_ASIDE = "step_aside" # not this organ's domain — cede
|
||||
|
||||
|
||||
def choose_served_disposition(
|
||||
*,
|
||||
epistemic_state: EpistemicState,
|
||||
limitation: LimitationAssessment | None,
|
||||
disclosure_claim: DisclosureClaim = DisclosureClaim.NONE,
|
||||
) -> ServedDisposition:
|
||||
"""Decide the served disposition. Pure, deterministic, total.
|
||||
|
||||
A blocking limitation dominates — you do not serve an answer, you ask / propose /
|
||||
report / explain / refuse / step aside per its resolution action. (A limitation
|
||||
whose action is ``answer`` is non-blocking and falls through to the serve
|
||||
decision.) With no blocking limitation, the disclosure claim + epistemic state pick
|
||||
the serve mode, under the Phase-1 ``VERIFIED`` guard.
|
||||
|
||||
NOTE (scaffold trust boundary): a blocking epistemic state (CONTRADICTED,
|
||||
AMBIGUOUS, UNDETERMINED) reaches this function AS a limitation (e.g. a contradiction
|
||||
arrives as ``report_contradiction``), not as ``limitation=None``. The serve branch
|
||||
trusts ``limitation=None`` to mean "servable".
|
||||
"""
|
||||
if limitation is not None:
|
||||
match limitation.resolution_action:
|
||||
case "ask_question":
|
||||
return ServedDisposition.ASK
|
||||
case "emit_proposal":
|
||||
return ServedDisposition.PROPOSE
|
||||
case "report_contradiction":
|
||||
return ServedDisposition.REPORT
|
||||
case "step_aside":
|
||||
return ServedDisposition.STEP_ASIDE
|
||||
case "refuse_known_boundary":
|
||||
# scope_boundary is the governed "outside the current envelope"
|
||||
# disposition — it may render as a refusal later, but must NOT collapse
|
||||
# into a hard boundary here.
|
||||
if limitation.limitation_kind == "scope_boundary":
|
||||
return ServedDisposition.EXPLAIN
|
||||
return ServedDisposition.REFUSE
|
||||
case "answer":
|
||||
pass # non-blocking — fall through to the serve decision
|
||||
|
||||
return _serve_disposition(epistemic_state, disclosure_claim)
|
||||
|
||||
|
||||
def _serve_disposition(
|
||||
epistemic_state: EpistemicState, disclosure_claim: DisclosureClaim
|
||||
) -> ServedDisposition:
|
||||
"""The no-blocking-limitation branch: claim + state pick the serve mode."""
|
||||
match disclosure_claim:
|
||||
case DisclosureClaim.VERIFIED:
|
||||
# The Phase-1 guard: disclose-as-verified ONLY with the backing state.
|
||||
if epistemic_state is EpistemicState.VERIFIED:
|
||||
return ServedDisposition.DISCLOSE
|
||||
return ServedDisposition.COMMIT
|
||||
case DisclosureClaim.APPROXIMATE:
|
||||
return ServedDisposition.DISCLOSE
|
||||
case DisclosureClaim.PROPOSAL_ONLY:
|
||||
return ServedDisposition.PROPOSE
|
||||
case DisclosureClaim.NONE:
|
||||
return ServedDisposition.COMMIT
|
||||
case _: # pragma: no cover - exhaustive over the 4-member enum; loud if extended
|
||||
raise AssertionError(f"unhandled disclosure_claim: {disclosure_claim!r}")
|
||||
|
||||
|
||||
__all__ = ["ServedDisposition", "choose_served_disposition"]
|
||||
|
|
@ -1,378 +0,0 @@
|
|||
"""P0-1 — the pre-question limitation pass, as a CONSOLIDATING VIEW (session §1.5).
|
||||
|
||||
The intake gate of the Epistemic Disclosure spine: before contemplation chooses a
|
||||
served disposition, it classifies *what KIND of limitation* is blocking resolution.
|
||||
Asking a question is only one possible resolution, and asking is wrong unless the
|
||||
limitation is specifically the kind a question resolves (missing/ambiguous external
|
||||
information). Mis-classify and intake breaks two ways — refuse-when-should-ask (lose
|
||||
the unlocking datum) or ask-when-should-propose (waste the channel).
|
||||
|
||||
This module is the first slice: the typed vocabulary (:data:`LimitationKind` /
|
||||
:data:`ResolutionAction` / :class:`LimitationAssessment`) and the *mapping* that
|
||||
DERIVES each from the already-shipped failure-family registry
|
||||
(:data:`core.comprehension_attempt.failure_family.REGISTRY`) and contemplation
|
||||
:class:`~generate.contemplation.findings.Terminal` set.
|
||||
|
||||
**Hard invariant (no fourth taxonomy).** Every assessment is mechanically derived
|
||||
from an existing ``FailureFamily``; the *only* genuinely new resolution action is
|
||||
``ask_question`` (the Q1/ASK tenant). Through Q1-C it was the one action with no
|
||||
shipped terminal; Q1-D adds :attr:`~generate.contemplation.findings.Terminal.QUESTION_NEEDED`
|
||||
(sibling of ``PROPOSAL_EMITTED``), so :func:`terminal_for_action` is now total — every
|
||||
action maps onto a shipped terminal, which is what makes this a consolidating view.
|
||||
|
||||
**Q1-B transitional carve-out (this slice).** Two shipped families —
|
||||
``missing_total_count`` and ``missing_weighted_total`` — are classified here as
|
||||
``missing_information`` / ``ask_question``: the user *could state the total* and
|
||||
unblock solving, so they are missing data, not capability gaps. The shipped
|
||||
:data:`REGISTRY` still flags them ``proposal_allowed = True`` so that existing
|
||||
consumers (:mod:`core.comprehension_attempt.proposal` /
|
||||
:mod:`generate.contemplation.pass_manager`) keep emitting proposal-only artifacts
|
||||
to the pile — until Q1-C/Q1-D wire ASK delivery to a served surface there is
|
||||
nowhere for an ``ask_question`` to go, and silently dropping the proposal signal
|
||||
would be a capability regression with no compensating intake. Once ASK is serving,
|
||||
the REGISTRY flag flips to ``False`` and the carve-out
|
||||
(:data:`Q1B_ASK_CARVE_OUT`) retires. The carve-out is named, enumerated, and
|
||||
covered by an explicit test (``tests/test_limitation_assessment.py``) so its
|
||||
removal is a conscious act, not a silent re-key.
|
||||
|
||||
**Off-serving.** Imports no ``generate.derivation`` / ``core.reliability_gate``; it
|
||||
cannot move the sealed GSM8K metric. Nothing consumes ``resolution_action`` to change
|
||||
serving yet — this slice only *classifies* and *describes residue*.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Literal
|
||||
|
||||
from core.comprehension_attempt.failure_family import (
|
||||
FailureFamily,
|
||||
family_for_reason,
|
||||
)
|
||||
from core.comprehension_attempt.model import ComprehensionAttempt
|
||||
from core.epistemic_state import EpistemicState
|
||||
from generate.contemplation.findings import Terminal
|
||||
|
||||
#: Why resolution is blocked. The split from :data:`ResolutionAction` keeps *state*
|
||||
#: ("what is true / missing") distinct from *action* ("what to do about it").
|
||||
LimitationKind = Literal[
|
||||
"missing_information", # a needed datum is absent — could be supplied → ask
|
||||
"ambiguous_structure", # data present, relationship unclear → ask (when user-resolvable)
|
||||
"scope_boundary", # exceeds the current capability/evidence envelope → refuse/explain
|
||||
"capability_gap", # info present, CORE lacks the transform → propose (or refuse if signal too coarse)
|
||||
"hard_boundary", # mathematically/logically impossible or undefined → refuse
|
||||
"contradiction", # evidence conflicts with a claimed answer → report
|
||||
"renderability_gap", # asking is right but terms aren't grounded enough to name safely → ask-generic
|
||||
"input_shape", # not this organ's domain → step aside
|
||||
]
|
||||
|
||||
#: What to do about a limitation. ``ask_question`` is the genuinely new action this
|
||||
#: spine introduces; the other five each correspond to a shipped contemplation
|
||||
#: terminal (:func:`terminal_for_action`).
|
||||
ResolutionAction = Literal[
|
||||
"answer",
|
||||
"ask_question",
|
||||
"emit_proposal",
|
||||
"refuse_known_boundary",
|
||||
"report_contradiction",
|
||||
"step_aside",
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MissingSlot:
|
||||
"""One typed slot the ASK limitation identifies as missing.
|
||||
|
||||
A *structural* description (NOT user-renderable prose): ``slot_name`` is the
|
||||
family-defined slot identifier (e.g. ``"total_count"``); ``expected_unit_or_type``
|
||||
is the family-typed expectation a future bound answer must satisfy (e.g.
|
||||
``"count_int"``); ``binding_target`` is the structural role the slot fills in
|
||||
the organ's setup (e.g. ``"collective_unit_total"``), which Q2 answer-binding
|
||||
re-enters the gate against (scoping §4). Renderable, user-facing strings come
|
||||
later from grounded text spans (:attr:`LimitationAssessment.grounded_terms`),
|
||||
NEVER from these snake_case identifiers — that split is what keeps the renderer
|
||||
wrong=0-safe (scoping §2).
|
||||
"""
|
||||
|
||||
slot_name: str
|
||||
expected_unit_or_type: str
|
||||
binding_target: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LimitationAssessment:
|
||||
"""One typed classification of the limitation blocking a comprehension attempt.
|
||||
|
||||
``epistemic_state`` (what is true) and ``resolution_action`` (what to do) are
|
||||
deliberately distinct: e.g. ``UNDETERMINED`` + ``ask_question`` for a missing
|
||||
datum. ``blocking_reason`` is the failure-family key (the partition key), so the
|
||||
assessment is back-traceable to the shipped registry.
|
||||
|
||||
``missing_slots`` and ``grounded_terms`` are the ASK *typed residue* — populated
|
||||
only for ``ask_question`` resolutions by :func:`assess_from_attempt`. Both
|
||||
default to empty so existing P0-1 callers using :func:`assess_from_family`
|
||||
continue to work unchanged. The wrong=0 invariant (scoping §2): a slot here
|
||||
carries family-typed structural identifiers only; renderable prose must come
|
||||
from ``grounded_terms`` (verbatim text spans), never fabricated.
|
||||
"""
|
||||
|
||||
limitation_kind: LimitationKind
|
||||
resolution_action: ResolutionAction
|
||||
epistemic_state: EpistemicState
|
||||
owner_organ: str | None
|
||||
blocking_reason: str
|
||||
missing_slots: tuple[MissingSlot, ...] = field(default_factory=tuple)
|
||||
grounded_terms: tuple[str, ...] = field(default_factory=tuple)
|
||||
|
||||
|
||||
# --- The consolidating mapping (derived from the shipped registry, not invented) ---
|
||||
|
||||
#: ``EpistemicState`` each kind asserts. ``scope_boundary`` lights up the RESERVED
|
||||
#: ``SCOPE_BOUNDARY`` state with a real producer; ``contradiction`` / ``ambiguous``
|
||||
#: reuse the ACTIVE states. The refuse/propose kinds share ``UNDETERMINED`` — the
|
||||
#: *kind* carries the distinction, the *state* only says "no answer determined".
|
||||
_KIND_TO_STATE: dict[LimitationKind, EpistemicState] = {
|
||||
"missing_information": EpistemicState.UNDETERMINED,
|
||||
"ambiguous_structure": EpistemicState.AMBIGUOUS,
|
||||
"scope_boundary": EpistemicState.SCOPE_BOUNDARY,
|
||||
"capability_gap": EpistemicState.UNDETERMINED,
|
||||
"hard_boundary": EpistemicState.UNDETERMINED,
|
||||
"contradiction": EpistemicState.CONTRADICTED,
|
||||
"renderability_gap": EpistemicState.UNDETERMINED,
|
||||
"input_shape": EpistemicState.UNDETERMINED,
|
||||
}
|
||||
|
||||
#: The shipped contemplation ``Terminal`` each action corresponds to. Through Q1-C
|
||||
#: ``ask_question`` mapped to ``None`` (the one action the spine added with no terminal
|
||||
#: yet); Q1-D ships ``QUESTION_NEEDED`` (sibling of ``PROPOSAL_EMITTED``), so the map is
|
||||
#: now TOTAL — all six actions correspond to shipped terminals. This totality is the
|
||||
#: proof the limitation pass is a consolidating view, not a new universe.
|
||||
#:
|
||||
#: NOTE: this is the action's *home* terminal "in principle". The terminal a Q1-D
|
||||
#: *delivery* actually emits depends on renderability — an unrenderable ``ask_question``
|
||||
#: falls back to the family's standing disposition (the D2 guard in
|
||||
#: :mod:`core.epistemic_questions.delivery`), it does NOT emit a contentless
|
||||
#: ``QUESTION_NEEDED``.
|
||||
_ACTION_TO_TERMINAL: dict[ResolutionAction, Terminal] = {
|
||||
"answer": Terminal.SOLVED_VERIFIED,
|
||||
"emit_proposal": Terminal.PROPOSAL_EMITTED,
|
||||
"refuse_known_boundary": Terminal.REFUSED_KNOWN_BOUNDARY,
|
||||
"report_contradiction": Terminal.CONTRADICTION_DETECTED,
|
||||
"step_aside": Terminal.NO_PROGRESS,
|
||||
"ask_question": Terminal.QUESTION_NEEDED,
|
||||
}
|
||||
|
||||
#: **Transitional carve-out (Q1-B).** Families this slice classifies as
|
||||
#: ``ask_question`` in the limitation layer while their shipped
|
||||
#: :data:`REGISTRY` entries keep ``proposal_allowed = True`` so the contemplation
|
||||
#: pass and proposal pile keep working unchanged. This is an honest migration seam:
|
||||
#: the disclosure layer speaks the truthful classification now; the operational
|
||||
#: layer keeps the current signal so nothing regresses before Q1-C/Q1-D wires ASK
|
||||
#: delivery. Retirement: once ASK is serving, flip ``proposal_allowed = False`` on
|
||||
#: these two families in :mod:`core.comprehension_attempt.failure_family`, drop
|
||||
#: this set, and amend the ``proposal_allowed`` invariant in tests.
|
||||
Q1B_ASK_CARVE_OUT: frozenset[str] = frozenset(
|
||||
{"missing_total_count", "missing_weighted_total"}
|
||||
)
|
||||
|
||||
#: family name → (LimitationKind, ResolutionAction). Keys must equal the REGISTRY's
|
||||
#: family names exactly (asserted total by test). Classification rationale per family:
|
||||
#: - growth surfaces (``proposal_allowed``) → ``capability_gap`` / ``emit_proposal``
|
||||
#: EXCEPT :data:`Q1B_ASK_CARVE_OUT` — see that constant's docstring
|
||||
#: - underdetermined / missing-datum refusals → ``missing_information`` / ``ask_question``
|
||||
#: - same-unit-but-no-cue ambiguity the user could resolve → ``ambiguous_structure`` / ask
|
||||
#: - math/logic impossibility, incoherence, coarse-signal parse gaps → ``hard_boundary`` / refuse
|
||||
#: - beyond-current-solver-envelope → ``scope_boundary`` / refuse
|
||||
#: - the answer-key verdict → ``contradiction`` / ``report_contradiction``
|
||||
#: - foreign text → ``input_shape`` / ``step_aside``
|
||||
_FAMILY_TO_LIMITATION: dict[str, tuple[LimitationKind, ResolutionAction]] = {
|
||||
# cross-organ
|
||||
"input_shape": ("input_shape", "step_aside"),
|
||||
"admissibility_incompatible": ("hard_boundary", "refuse_known_boundary"),
|
||||
# R1
|
||||
"unsupported_clause_shape": ("capability_gap", "refuse_known_boundary"),
|
||||
"ungrounded_base": ("missing_information", "ask_question"),
|
||||
"over_determined": ("hard_boundary", "refuse_known_boundary"),
|
||||
# R2 boundaries
|
||||
"unsupported_system_size": ("scope_boundary", "refuse_known_boundary"),
|
||||
"indistinguishable_system": ("hard_boundary", "refuse_known_boundary"),
|
||||
"non_integer_solution": ("hard_boundary", "refuse_known_boundary"),
|
||||
"negative_solution": ("hard_boundary", "refuse_known_boundary"),
|
||||
"answer_choice_unresolved": ("ambiguous_structure", "refuse_known_boundary"),
|
||||
# R2 growth surfaces — Q1-B reclassification (see Q1B_ASK_CARVE_OUT):
|
||||
# disclosure says ask; REGISTRY still emits proposals to the pile.
|
||||
"missing_total_count": ("missing_information", "ask_question"),
|
||||
"missing_weighted_total": ("missing_information", "ask_question"),
|
||||
"missing_category_pair": ("capability_gap", "emit_proposal"),
|
||||
"missing_attribute_coefficient": ("capability_gap", "emit_proposal"),
|
||||
# R2 verdict
|
||||
"answer_key_contradiction": ("contradiction", "report_contradiction"),
|
||||
# R3
|
||||
"unsupported_rate_duration": ("capability_gap", "emit_proposal"),
|
||||
"rate_underdetermined": ("missing_information", "ask_question"),
|
||||
"unsupported_temporal_state": ("scope_boundary", "refuse_known_boundary"),
|
||||
# R4 combined-rate boundaries
|
||||
"cmb_unit_mismatch": ("hard_boundary", "refuse_known_boundary"),
|
||||
"cmb_combine_ambiguous": ("ambiguous_structure", "ask_question"),
|
||||
"cmb_underdetermined": ("missing_information", "ask_question"),
|
||||
"cmb_non_positive_net": ("hard_boundary", "refuse_known_boundary"),
|
||||
"cmb_non_integer": ("hard_boundary", "refuse_known_boundary"),
|
||||
# R4 growth surfaces
|
||||
"cmb_unsupported_rate_count": ("capability_gap", "emit_proposal"),
|
||||
"cmb_unsupported_reciprocal": ("capability_gap", "emit_proposal"),
|
||||
"cmb_unsupported_clock_interval": ("capability_gap", "emit_proposal"),
|
||||
}
|
||||
|
||||
#: family name → typed slots an ASK assessment for that family identifies as missing.
|
||||
#: Only families with a *known* slot structure appear here; an ask-mapped family without
|
||||
#: an entry yields an empty residue (the "minimal" stance — never fabricate a slot the
|
||||
#: family's contract has not pinned down yet). Slot semantics, per family:
|
||||
#: - ``missing_total_count`` : the collective-unit total count (R2 constraint C7);
|
||||
#: ``binding_target`` matches the equation slot the augmented input would fill
|
||||
#: (Q2 re-entry, scoping §4).
|
||||
#: - ``missing_weighted_total``: the measured-unit weighted total (R2 constraint C8).
|
||||
#: Other ask-mapped families (``ungrounded_base``, ``rate_underdetermined``,
|
||||
#: ``cmb_underdetermined``, ``cmb_combine_ambiguous``) get slots in later Q1 sub-PRs
|
||||
#: once their per-family slot signatures are pinned with tests.
|
||||
_FAMILY_TO_MISSING_SLOTS: dict[str, tuple[MissingSlot, ...]] = {
|
||||
"missing_total_count": (
|
||||
MissingSlot(
|
||||
slot_name="total_count",
|
||||
expected_unit_or_type="count_int",
|
||||
binding_target="collective_unit_total",
|
||||
),
|
||||
),
|
||||
"missing_weighted_total": (
|
||||
MissingSlot(
|
||||
slot_name="weighted_total",
|
||||
expected_unit_or_type="measured_unit_int",
|
||||
binding_target="weighted_total_value",
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
# A conservative refusal for any family/reason that is not in the mapping. The total
|
||||
# mapping (asserted by test against REGISTRY) means this is dead in practice; if a new
|
||||
# family ever lands unmapped, it refuses — it NEVER silently becomes an answerable
|
||||
# question (the wrong=0-safe default).
|
||||
_CONSERVATIVE_DEFAULT: tuple[LimitationKind, ResolutionAction] = (
|
||||
"hard_boundary",
|
||||
"refuse_known_boundary",
|
||||
)
|
||||
|
||||
|
||||
def assess_from_family(family: FailureFamily) -> LimitationAssessment:
|
||||
"""The limitation a failure family expresses, as a typed assessment.
|
||||
|
||||
Total over :data:`REGISTRY` (asserted by test). An unmapped family falls to the
|
||||
conservative refuse default — never ``ask_question``. Residue defaults to empty;
|
||||
populating typed slots / grounded terms requires a specific attempt (use
|
||||
:func:`assess_from_attempt`).
|
||||
"""
|
||||
kind, action = _FAMILY_TO_LIMITATION.get(family.name, _CONSERVATIVE_DEFAULT)
|
||||
return LimitationAssessment(
|
||||
limitation_kind=kind,
|
||||
resolution_action=action,
|
||||
epistemic_state=_KIND_TO_STATE[kind],
|
||||
owner_organ=family.owner,
|
||||
blocking_reason=family.name,
|
||||
)
|
||||
|
||||
|
||||
def _residue_from_attempt(
|
||||
attempt: ComprehensionAttempt,
|
||||
family: FailureFamily,
|
||||
action: ResolutionAction,
|
||||
) -> tuple[tuple[MissingSlot, ...], tuple[str, ...]]:
|
||||
"""Typed ASK residue for an ask-mapped attempt — empty for any other action.
|
||||
|
||||
Wrong=0 invariant (scoping §2): ``grounded_terms`` carries only verbatim text from
|
||||
:attr:`ComprehensionAttempt.evidence` SourceSpanLinks. If the attempt carries no
|
||||
evidence (today: every refused attempt — :mod:`core.comprehension_attempt.classify`
|
||||
leaves ``evidence = ()``), ``grounded_terms`` is empty, NEVER fabricated from the
|
||||
family or the refusal reason. ``missing_slots`` is family-derived (snake_case
|
||||
structural identifiers, not renderable prose) so it is always safe to emit; absent
|
||||
from :data:`_FAMILY_TO_MISSING_SLOTS` ⇒ no slots (minimal stance — never invent a
|
||||
slot the family contract has not pinned down).
|
||||
"""
|
||||
if action != "ask_question":
|
||||
return ((), ())
|
||||
slots = _FAMILY_TO_MISSING_SLOTS.get(family.name, ())
|
||||
grounded = tuple(link.text for link in attempt.evidence)
|
||||
return (slots, grounded)
|
||||
|
||||
|
||||
def assess_from_attempt(attempt: ComprehensionAttempt) -> LimitationAssessment | None:
|
||||
"""Classify the limitation a comprehension attempt expresses, or ``None``.
|
||||
|
||||
- ``contradiction`` outcome → report it (no refusal reason carries this; it is the
|
||||
answer-choice verdict).
|
||||
- a refusal → classify via its failure family; an *unclassified* refusal reason
|
||||
falls to the conservative refuse default (never ``ask_question``).
|
||||
- any non-limitation outcome (solved/admissible setup, or eval-only ``*_wrong``)
|
||||
→ ``None``: there is no resolvable limitation to act on.
|
||||
|
||||
For ask-mapped refusals, the returned assessment carries typed residue
|
||||
(:attr:`~LimitationAssessment.missing_slots` / ``grounded_terms``) per
|
||||
:func:`_residue_from_attempt`'s wrong=0 invariant.
|
||||
"""
|
||||
if attempt.outcome == "contradiction":
|
||||
return LimitationAssessment(
|
||||
limitation_kind="contradiction",
|
||||
resolution_action="report_contradiction",
|
||||
epistemic_state=EpistemicState.CONTRADICTED,
|
||||
owner_organ=attempt.organ,
|
||||
blocking_reason="answer_key_contradiction",
|
||||
)
|
||||
if not attempt.is_refusal:
|
||||
return None
|
||||
family = family_for_reason(attempt.refusal_reason)
|
||||
if family is None:
|
||||
kind, action = _CONSERVATIVE_DEFAULT
|
||||
return LimitationAssessment(
|
||||
limitation_kind=kind,
|
||||
resolution_action=action,
|
||||
epistemic_state=_KIND_TO_STATE[kind],
|
||||
owner_organ=attempt.organ,
|
||||
blocking_reason=attempt.refusal_reason or "unclassified",
|
||||
)
|
||||
base = assess_from_family(family)
|
||||
missing_slots, grounded_terms = _residue_from_attempt(
|
||||
attempt, family, base.resolution_action
|
||||
)
|
||||
if not missing_slots and not grounded_terms:
|
||||
return base
|
||||
return LimitationAssessment(
|
||||
limitation_kind=base.limitation_kind,
|
||||
resolution_action=base.resolution_action,
|
||||
epistemic_state=base.epistemic_state,
|
||||
owner_organ=attempt.organ,
|
||||
blocking_reason=base.blocking_reason,
|
||||
missing_slots=missing_slots,
|
||||
grounded_terms=grounded_terms,
|
||||
)
|
||||
|
||||
|
||||
def terminal_for_action(action: ResolutionAction) -> Terminal:
|
||||
"""The shipped contemplation ``Terminal`` an action corresponds to — total.
|
||||
|
||||
Every action maps to a shipped terminal; ``ask_question`` resolves to
|
||||
``QUESTION_NEEDED`` (added by Q1-D, sibling of ``PROPOSAL_EMITTED``). That totality
|
||||
is what makes this a consolidating view rather than a new taxonomy. This is the
|
||||
action's *home* terminal; the terminal a Q1-D delivery actually emits may differ
|
||||
when the question is unrenderable (the D2 fallback — see
|
||||
:mod:`core.epistemic_questions.delivery`).
|
||||
"""
|
||||
return _ACTION_TO_TERMINAL[action]
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Q1B_ASK_CARVE_OUT",
|
||||
"LimitationAssessment",
|
||||
"LimitationKind",
|
||||
"MissingSlot",
|
||||
"ResolutionAction",
|
||||
"assess_from_attempt",
|
||||
"assess_from_family",
|
||||
"terminal_for_action",
|
||||
]
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
"""VERIFIED serving gate helper — default-dark, no served-surface wiring.
|
||||
|
||||
This module centralizes the future kill-switch predicate for VERIFIED serving.
|
||||
It is default-dark / fail-closed: if the config field is missing or malformed,
|
||||
it must evaluate to False.
|
||||
|
||||
This helper only centralizes the future kill-switch predicate so that future serving code
|
||||
has one audited predicate. It does not wire any served-surface or implement served
|
||||
VERIFIED behavior. Missing field means False.
|
||||
|
||||
Note that eval-gold-backed producers (such as the verification producer in
|
||||
evals/constraint_oracle/verified_producer.py) are not serving-eligible.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from core.config import DEFAULT_CONFIG, RuntimeConfig
|
||||
|
||||
|
||||
def verified_serving_enabled(config: RuntimeConfig | Any | None = None) -> bool:
|
||||
"""Return whether served VERIFIED delivery is explicitly enabled.
|
||||
|
||||
Missing attribute means False. This is the load-bearing dark-gate invariant:
|
||||
the served VERIFIED path cannot light merely because the helper exists or because
|
||||
an older RuntimeConfig instance lacks the future field.
|
||||
"""
|
||||
cfg = DEFAULT_CONFIG if config is None else config
|
||||
return bool(getattr(cfg, "verified_serving_enabled", False))
|
||||
|
||||
|
||||
__all__ = ["verified_serving_enabled"]
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
"""P1-A — the VERIFIED contract (the meaning of "verified", before any producer).
|
||||
|
||||
This module defines WHAT IT MEANS for a result to earn ``DisclosureClaim.VERIFIED`` /
|
||||
``EpistemicState.VERIFIED`` — the soundness≠correctness gate the whole VERIFIED lane
|
||||
rests on. It ships ONLY the contract: the obligation, the proof SHAPE a producer must
|
||||
fill, the validator that enforces the obligation, and the single sanctioned route from
|
||||
a validated proof to the verified state/claim.
|
||||
|
||||
It does NOT produce proofs. There is no reader, no solver, no back-substitution
|
||||
computation, no serving, no ``verify.py`` call, no ``verified_serving_enabled``. P1-B+
|
||||
fill :class:`VerificationProof` with real digests; P1-A only fixes the rules a proof
|
||||
must satisfy — and, above all, the rule that makes the lane safe:
|
||||
|
||||
A faithful solve of a WRONG read must NOT verify.
|
||||
|
||||
**The mechanism.** Verification requires TWO INDEPENDENT reads (distinct reader
|
||||
lineages) that CONVERGE on the same canonical structure:
|
||||
|
||||
* a *wrong* primary read is caught because the independent read **disagrees** —
|
||||
back-substitution alone cannot catch a read error (it only proves the solver was
|
||||
faithful to whatever structure it was handed);
|
||||
* a single reader run twice ("same answer twice") is rejected as **not independent**.
|
||||
|
||||
Neither gold-agreement, nor absence-of-refusal, nor a second solver over ONE read can
|
||||
earn VERIFIED; only this contract can. (See [[VERIFIED-canonical-comparison-scoping-2026-06-06]]:
|
||||
"independence must be in the READING, not the solving".)
|
||||
|
||||
**Discipline.** ``EpistemicState.VERIFIED`` must be reached ONLY via
|
||||
:func:`disclosure_for_verification` over a VERIFIED :class:`VerificationResult` — never
|
||||
constructed directly by producer/serving code. A future architectural invariant can
|
||||
scan for direct emission; P1-A establishes the route.
|
||||
|
||||
Off-serving: imports no ``generate.derivation`` / ``core.reliability_gate``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, unique
|
||||
|
||||
from core.epistemic_disclosure.disclosure_claim import DisclosureClaim
|
||||
from core.epistemic_disclosure.limitation import LimitationAssessment
|
||||
from core.epistemic_state import EpistemicState
|
||||
|
||||
|
||||
@unique
|
||||
class VerificationVerdict(str, Enum):
|
||||
"""Whether a result earns the VERIFIED claim. There is no middle state — a result
|
||||
either survives every obligation or it does not verify (refuse-preferring)."""
|
||||
|
||||
VERIFIED = "verified"
|
||||
NOT_VERIFIED = "not_verified"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class VerificationObligation:
|
||||
"""The declarative contract: which checks a proof must survive to be VERIFIED.
|
||||
|
||||
A schema naming proof obligations (CLAUDE.md "Schema-Defined Proof Obligations").
|
||||
The canonical :data:`VERIFICATION_OBLIGATION` sets every flag ``True``; the flags
|
||||
exist so a test can prove each obligation is LOAD-BEARING — relax exactly one and
|
||||
the corresponding poison case slips through (see ``test_verified_contract.py``).
|
||||
"""
|
||||
|
||||
requires_independent_read: bool # two DISTINCT reader lineages — not the same read twice
|
||||
rejects_wrong_read_even_if_solved: bool # the two reads must CONVERGE — catches a wrong read
|
||||
requires_bound_slots: bool # the answer binds to the STATED slots (query/unknowns), not phantom ones
|
||||
requires_back_substitution: bool # the answer back-substitutes into the canonical structure
|
||||
requires_boundary_clear: bool # no organ boundary fired in the chain
|
||||
|
||||
|
||||
#: The canonical, fully-strict obligation. VERIFIED requires ALL of it.
|
||||
VERIFICATION_OBLIGATION: VerificationObligation = VerificationObligation(
|
||||
requires_independent_read=True,
|
||||
rejects_wrong_read_even_if_solved=True,
|
||||
requires_bound_slots=True,
|
||||
requires_back_substitution=True,
|
||||
requires_boundary_clear=True,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class VerificationProof:
|
||||
"""The replayable proof shape a producer (P1-B+) must fill. P1-A defines the shape
|
||||
and the rules; it computes none of these digests.
|
||||
|
||||
The two reads are kept separate ON PURPOSE: ``primary_reader_lineage`` /
|
||||
``independent_reader_lineage`` must DIFFER (independence), and ``primary_read_digest``
|
||||
/ ``independent_read_digest`` must MATCH (convergence on one canonical structure).
|
||||
Independence + convergence together are what reject a faithful solve of a wrong read.
|
||||
|
||||
The "from the stated quantities" obligation is split into THREE separable digests
|
||||
(P1-C hardening — for replay, audit, and failure localization):
|
||||
``derivation_digest`` (a solve happened from the structure), ``bound_slots_digest``
|
||||
(the answer binds to a STATED slot — the asked unknown — not a phantom), and
|
||||
``back_substitution_digest`` (the answer satisfies the constraints). A complete
|
||||
derivation does NOT imply the answer bound to a declared slot, so the two are
|
||||
distinct obligations (``test_derivation_digest_alone_is_insufficient_without_bound_slots``).
|
||||
"""
|
||||
|
||||
source_problem_digest: str # provenance: hash of the problem text
|
||||
primary_reader_lineage: str # identity of the primary reader
|
||||
independent_reader_lineage: str # identity of the independent cross-check reader
|
||||
primary_read_digest: str # canonical structure the primary read produced
|
||||
independent_read_digest: str # canonical structure the independent read produced
|
||||
derivation_digest: str # the derivation (solve) from the STATED quantities
|
||||
bound_slots_digest: str # the answer binds to the STATED slots (asked unknown), not a phantom
|
||||
back_substitution_digest: str # back-substitution into the canonical structure
|
||||
boundary_clear: bool # no organ boundary fired
|
||||
contradiction_clear: bool # no contradiction family fired
|
||||
|
||||
|
||||
# Reason codes for a failed obligation — each names exactly one violated rule.
|
||||
REASON_READS_NOT_INDEPENDENT = "reads_not_independent" # same reader lineage twice
|
||||
REASON_READS_DISAGREE = "reads_disagree" # the wrong-read catcher
|
||||
REASON_NO_BOUND_SLOTS = "no_bound_slots" # answer did not bind to a stated slot
|
||||
REASON_NO_BACK_SUBSTITUTION = "no_back_substitution"
|
||||
REASON_BOUNDARY_FIRED = "boundary_fired"
|
||||
REASON_CONTRADICTION_PRESENT = "contradiction_present"
|
||||
REASON_INCOMPLETE_PROOF = "incomplete_proof_digest"
|
||||
REASON_UNRESOLVED_LIMITATION = "unresolved_limitation"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class VerificationResult:
|
||||
"""The verdict plus the specific obligations that failed (empty iff VERIFIED)."""
|
||||
|
||||
verdict: VerificationVerdict
|
||||
failed_checks: tuple[str, ...]
|
||||
|
||||
|
||||
def evaluate_verification(
|
||||
proof: VerificationProof,
|
||||
*,
|
||||
limitation: LimitationAssessment | None,
|
||||
obligation: VerificationObligation = VERIFICATION_OBLIGATION,
|
||||
) -> VerificationResult:
|
||||
"""Apply the VERIFIED contract to a proof. Refuse-preferring: VERIFIED only if
|
||||
EVERY obligation survives; otherwise NOT_VERIFIED with the failed checks.
|
||||
|
||||
Pure logic over the proof fields — it does NOT compute or trust any digest, it only
|
||||
enforces the rules a producer's proof must satisfy. ``limitation`` is the
|
||||
contemplation outcome: a verified answer cannot coexist with an unresolved
|
||||
limitation (a contradiction, an ambiguity, a missing datum).
|
||||
"""
|
||||
failed: list[str] = []
|
||||
|
||||
if obligation.requires_independent_read and (
|
||||
proof.primary_reader_lineage == proof.independent_reader_lineage
|
||||
):
|
||||
failed.append(REASON_READS_NOT_INDEPENDENT)
|
||||
|
||||
if obligation.rejects_wrong_read_even_if_solved and (
|
||||
proof.primary_read_digest != proof.independent_read_digest
|
||||
):
|
||||
failed.append(REASON_READS_DISAGREE)
|
||||
|
||||
if obligation.requires_bound_slots and not proof.bound_slots_digest:
|
||||
failed.append(REASON_NO_BOUND_SLOTS)
|
||||
|
||||
if obligation.requires_back_substitution and not proof.back_substitution_digest:
|
||||
failed.append(REASON_NO_BACK_SUBSTITUTION)
|
||||
|
||||
if obligation.requires_boundary_clear and not proof.boundary_clear:
|
||||
failed.append(REASON_BOUNDARY_FIRED)
|
||||
|
||||
# Non-negotiable checks (NOT gated by the obligation flags):
|
||||
if not proof.contradiction_clear:
|
||||
failed.append(REASON_CONTRADICTION_PRESENT)
|
||||
|
||||
if not (
|
||||
proof.source_problem_digest
|
||||
and proof.primary_read_digest
|
||||
and proof.derivation_digest
|
||||
):
|
||||
failed.append(REASON_INCOMPLETE_PROOF)
|
||||
|
||||
if limitation is not None:
|
||||
failed.append(REASON_UNRESOLVED_LIMITATION)
|
||||
|
||||
verdict = (
|
||||
VerificationVerdict.VERIFIED if not failed else VerificationVerdict.NOT_VERIFIED
|
||||
)
|
||||
return VerificationResult(verdict=verdict, failed_checks=tuple(failed))
|
||||
|
||||
|
||||
def disclosure_for_verification(
|
||||
result: VerificationResult,
|
||||
) -> tuple[EpistemicState, DisclosureClaim]:
|
||||
"""The ONLY sanctioned route to a verified state/claim.
|
||||
|
||||
VERIFIED verdict → (``EpistemicState.VERIFIED``, ``DisclosureClaim.VERIFIED``);
|
||||
anything else → (``UNDETERMINED``, ``NONE``). Producer/serving code must reach
|
||||
``EpistemicState.VERIFIED`` THROUGH this gate, never by constructing it directly —
|
||||
that is what keeps "gold agreement" / "same answer twice" / "no refusal" from ever
|
||||
masquerading as verified.
|
||||
"""
|
||||
if result.verdict is VerificationVerdict.VERIFIED:
|
||||
return EpistemicState.VERIFIED, DisclosureClaim.VERIFIED
|
||||
return EpistemicState.UNDETERMINED, DisclosureClaim.NONE
|
||||
|
||||
|
||||
__all__ = [
|
||||
"VERIFICATION_OBLIGATION",
|
||||
"VerificationObligation",
|
||||
"VerificationProof",
|
||||
"VerificationResult",
|
||||
"VerificationVerdict",
|
||||
"disclosure_for_verification",
|
||||
"evaluate_verification",
|
||||
]
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
"""The epistemic question organ — render (Q1-C) and off-serving delivery (Q1-D).
|
||||
|
||||
Q1-C (:mod:`core.epistemic_questions.render`) turns an ASK
|
||||
:class:`~core.epistemic_disclosure.limitation.LimitationAssessment` into an
|
||||
:class:`~core.epistemic_questions.render.EpistemicQuestion` under the wrong=0
|
||||
grounded-rendering invariant (scoping §2) — render only, no fabrication.
|
||||
|
||||
Q1-D (:mod:`core.epistemic_questions.delivery`) routes that rendered question onto
|
||||
the contemplation bus as the ``QUESTION_NEEDED`` tenant and writes a proposal-only
|
||||
artifact to the ``teaching/questions`` sink — consuming the renderer verbatim, never
|
||||
rendering. Off-serving: no served surface, no ``ask_serving_enabled`` yet.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from core.epistemic_questions.delivery import (
|
||||
AnswerBinding,
|
||||
DeliveredQuestion,
|
||||
DeliveryOutcome,
|
||||
default_question_root,
|
||||
deliver_ask,
|
||||
emit_question,
|
||||
question_path,
|
||||
)
|
||||
from core.epistemic_questions.render import (
|
||||
EpistemicQuestion,
|
||||
render_question,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AnswerBinding",
|
||||
"DeliveredQuestion",
|
||||
"DeliveryOutcome",
|
||||
"EpistemicQuestion",
|
||||
"default_question_root",
|
||||
"deliver_ask",
|
||||
"emit_question",
|
||||
"question_path",
|
||||
"render_question",
|
||||
]
|
||||
|
|
@ -1,272 +0,0 @@
|
|||
"""Q1-D — off-serving ASK delivery: route a rendered question onto the bus.
|
||||
|
||||
The fourth and final Q1 rung. Given an ``ask_question``
|
||||
:class:`~core.epistemic_disclosure.limitation.LimitationAssessment`, decide the
|
||||
contemplation :class:`~generate.contemplation.findings.Terminal` and, when a question
|
||||
can honestly be asked, produce a :class:`DeliveredQuestion` artifact for the
|
||||
review-gated, **question-only** ``teaching/questions`` sink. A question is an *intake
|
||||
request*, NOT a capability proposal — ``question_only`` is its own lane, distinct from
|
||||
the ``proposal_only`` lane of :mod:`core.comprehension_attempt.proposal`. This is the
|
||||
ASK *analogue* of that emitter (and just as toothless: it never serves, never mounts,
|
||||
always requires review), but the artifacts must never be conflated.
|
||||
|
||||
**The rung separation (the steer).** Q1-D *consumes* the Q1-C
|
||||
:class:`~core.epistemic_questions.render.EpistemicQuestion` — it does NOT render.
|
||||
:func:`render_question` is called exactly once (in :func:`deliver_ask`) and its
|
||||
result is wrapped verbatim; Q1-D constructs no user-facing prose of its own, so the
|
||||
Q1-C grounded-rendering wrong=0 guard is never bypassed by a second surface.
|
||||
|
||||
Q1-B: typed residue (what is missing, as typed slots)
|
||||
Q1-C: renderability (can it be asked without fabricating? → EpistemicQuestion)
|
||||
Q1-D: delivery (route the rendered question onto the bus) ← here
|
||||
|
||||
**D2 — the delivery-side wrong=0 guard.** A contentless ``QUESTION_NEEDED`` is worse
|
||||
than useless: it is a *false intake surface* (the user is invited to answer a question
|
||||
that names nothing). So when :func:`render_question` returns ``unrenderable``
|
||||
(``renderability_gap`` / ``multi_slot_not_supported`` / ``no_missing_slot`` / the
|
||||
fabrication backstop), :func:`deliver_ask` does NOT emit ``QUESTION_NEEDED``. It falls
|
||||
back to the family's *standing disposition*:
|
||||
|
||||
family still proposes (``proposal_allowed``) → ``PROPOSAL_EMITTED``
|
||||
otherwise → ``NO_PROGRESS``
|
||||
|
||||
This guard is enforced twice: in :func:`deliver_ask`'s branch, and structurally — a
|
||||
:class:`DeliveredQuestion` *cannot wrap an unrenderable question* (its
|
||||
``__post_init__`` refuses), so ``QUESTION_NEEDED`` is unreachable without a real,
|
||||
rendered question.
|
||||
|
||||
**D3 — the carve-out stays.** Q1-D delivery is OFF-SERVING. It does not flip the
|
||||
``Q1B_ASK_CARVE_OUT`` (``missing_total_count`` / ``missing_weighted_total`` keep
|
||||
``proposal_allowed = True`` in the registry, so the proposal pile keeps working). Both
|
||||
signals coexist during the carve-out — the off-serving question artifact AND the
|
||||
existing proposal — so no operational signal is lost. The flip to ``ask`` waits on a
|
||||
future ``ask_serving_enabled`` gate, not on this rung.
|
||||
|
||||
**D5 — single-slot only.** Q1-C refuses multi-slot rendering, so Q1-D delivers at most
|
||||
one ``DeliveredQuestion`` per assessment; a multi-slot assessment is ``unrenderable``
|
||||
(``multi_slot_not_supported``) and takes the D2 fallback. No fan-out here.
|
||||
|
||||
**Off-serving.** Imports nothing from ``generate.derivation`` / ``core.reliability_gate``;
|
||||
it cannot move the sealed GSM8K metric, and there is NO served surface — no
|
||||
``ask_serving_enabled``, no ``chat/runtime.py`` wiring. The artifacts land in the
|
||||
review-gated teaching sink and nowhere else.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from core.comprehension_attempt.failure_family import family_by_name
|
||||
from core.epistemic_disclosure.limitation import LimitationAssessment
|
||||
from core.epistemic_questions.render import EpistemicQuestion, render_question
|
||||
from generate.contemplation.findings import Terminal
|
||||
|
||||
#: The sink status — the ASK analogue of the proposal emitter's ``"proposal_only"``.
|
||||
#: A delivered question is review-gated intake, never a served answer.
|
||||
_QUESTION_STATUS = "question_only"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AnswerBinding:
|
||||
"""RESERVED for Q2 — the typed seat a future answer round-trip binds into.
|
||||
|
||||
Q1-D produces NO ``AnswerBinding`` (every :class:`DeliveredQuestion` carries
|
||||
``answer_binding = None``; see that class's ``__post_init__``). The seat exists so
|
||||
the Q2 binder — which must re-enter the limitation gate with augmented input, never
|
||||
mutate the model mid-flight (scoping §4) — is wireable without reshaping the
|
||||
artifact. ``target_slot`` / ``binding_target`` mirror the
|
||||
:class:`~core.epistemic_disclosure.limitation.MissingSlot` the answer fills.
|
||||
"""
|
||||
|
||||
target_organ: str
|
||||
target_slot: str
|
||||
binding_target: str
|
||||
parser: str
|
||||
unit: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DeliveredQuestion:
|
||||
"""A review-gated, question-only ASK artifact wrapping a rendered Q1-C question.
|
||||
|
||||
``question_only`` is its OWN lane (an intake request), not the ``proposal_only``
|
||||
lane — the analogy to :class:`~core.comprehension_attempt.proposal.FailureProposal`
|
||||
is structural (toothless, review-gated, never served), never semantic.
|
||||
|
||||
The invariant fields are enforced in ``__post_init__`` so even a hand-constructed
|
||||
instance cannot become a contentless question, a served question, or an
|
||||
answer-bound (Q2) question — illegal states are unrepresentable:
|
||||
|
||||
- it can never wrap an ``unrenderable`` question (the D2 guard, structurally) —
|
||||
so a ``QUESTION_NEEDED`` terminal always carries real, rendered text;
|
||||
- it can never be ``served`` (off-serving; ``ask_serving_enabled`` does not exist);
|
||||
- it always ``requires_review``;
|
||||
- its ``answer_binding`` is always ``None`` in Q1-D (the Q2 seat is reserved, unbound).
|
||||
"""
|
||||
|
||||
question: EpistemicQuestion
|
||||
owner_organ: str | None
|
||||
blocking_reason: str
|
||||
answer_binding: AnswerBinding | None = None
|
||||
status: str = _QUESTION_STATUS
|
||||
requires_review: bool = True
|
||||
served: bool = False
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.question.unrenderable or self.question.text is None:
|
||||
raise ValueError(
|
||||
"a DeliveredQuestion cannot wrap an unrenderable question "
|
||||
f"(reason={self.question.reason!r}); the D2 fallback handles it"
|
||||
)
|
||||
if self.status != _QUESTION_STATUS:
|
||||
raise ValueError(
|
||||
f"question status must be {_QUESTION_STATUS!r}; got {self.status!r}"
|
||||
)
|
||||
if self.served:
|
||||
raise ValueError("a Q1-D delivered question is never served")
|
||||
if not self.requires_review:
|
||||
raise ValueError("a delivered question always requires review")
|
||||
if self.answer_binding is not None:
|
||||
raise ValueError("answer_binding is reserved for Q2; Q1-D emits None")
|
||||
|
||||
def content_hash(self) -> str:
|
||||
"""Deterministic content address: same question on the same blocking family and
|
||||
slot always yields the same hash (idempotent sink writes). No clock, no
|
||||
randomness. The rendered ``text`` is included so a template change re-addresses.
|
||||
"""
|
||||
slot_name = self.question.slot.slot_name if self.question.slot else ""
|
||||
payload = f"{self.blocking_reason}:{slot_name}:{self.question.text}"
|
||||
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||||
|
||||
def to_json_dict(self) -> dict[str, Any]:
|
||||
slot = self.question.slot
|
||||
return {
|
||||
"status": self.status,
|
||||
"blocking_reason": self.blocking_reason,
|
||||
"owner_organ": self.owner_organ,
|
||||
"question": {
|
||||
"text": self.question.text,
|
||||
"reason": self.question.reason,
|
||||
"slot_name": slot.slot_name if slot else None,
|
||||
"expected_unit_or_type": slot.expected_unit_or_type if slot else None,
|
||||
"binding_target": slot.binding_target if slot else None,
|
||||
},
|
||||
"answer_binding": None, # reserved (Q2)
|
||||
"requires_review": self.requires_review,
|
||||
"served": self.served,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DeliveryOutcome:
|
||||
"""The result of routing one ``ask_question`` assessment.
|
||||
|
||||
``question`` is the artifact iff ``terminal is QUESTION_NEEDED`` (a renderable ask);
|
||||
otherwise it is ``None`` and ``fallback_reason`` carries the unrenderable reason that
|
||||
triggered the D2 standing-disposition fallback.
|
||||
"""
|
||||
|
||||
terminal: Terminal
|
||||
question: DeliveredQuestion | None
|
||||
fallback_reason: str | None
|
||||
|
||||
|
||||
def _standing_disposition(blocking_reason: str) -> Terminal:
|
||||
"""The D2 fallback terminal for an unrenderable ask: the family's standing move.
|
||||
|
||||
A family that still proposes (``proposal_allowed``) falls back to
|
||||
``PROPOSAL_EMITTED`` — its existing operational signal, preserved (D3). Anything
|
||||
else (an unknown reason, or a family that does not propose) falls back to
|
||||
``NO_PROGRESS``. Never a contentless ``QUESTION_NEEDED``.
|
||||
"""
|
||||
family = family_by_name(blocking_reason)
|
||||
if family is not None and family.proposal_allowed:
|
||||
return Terminal.PROPOSAL_EMITTED
|
||||
return Terminal.NO_PROGRESS
|
||||
|
||||
|
||||
def deliver_ask(assessment: LimitationAssessment) -> DeliveryOutcome:
|
||||
"""Route an ``ask_question`` assessment to a terminal — render via Q1-C, never here.
|
||||
|
||||
Renderable → ``QUESTION_NEEDED`` + a :class:`DeliveredQuestion` wrapping the Q1-C
|
||||
result verbatim. Unrenderable → the D2 standing-disposition fallback (no artifact).
|
||||
|
||||
Raises ``ValueError`` if called on a non-ASK assessment: the bus routes only
|
||||
``ask_question`` resolutions here, so any other action is a wiring error, not a
|
||||
runtime input — fail loudly rather than silently mis-deliver.
|
||||
"""
|
||||
if assessment.resolution_action != "ask_question":
|
||||
raise ValueError(
|
||||
"deliver_ask is only valid for ask_question assessments; got "
|
||||
f"{assessment.resolution_action!r}"
|
||||
)
|
||||
|
||||
question = render_question(assessment)
|
||||
if question.unrenderable:
|
||||
return DeliveryOutcome(
|
||||
terminal=_standing_disposition(assessment.blocking_reason),
|
||||
question=None,
|
||||
fallback_reason=question.reason,
|
||||
)
|
||||
|
||||
delivered = DeliveredQuestion(
|
||||
question=question,
|
||||
owner_organ=assessment.owner_organ,
|
||||
blocking_reason=assessment.blocking_reason,
|
||||
)
|
||||
return DeliveryOutcome(
|
||||
terminal=Terminal.QUESTION_NEEDED,
|
||||
question=delivered,
|
||||
fallback_reason=None,
|
||||
)
|
||||
|
||||
|
||||
def default_question_root() -> Path:
|
||||
"""``<repo>/teaching/questions`` — the write-only, review-gated ASK sink.
|
||||
|
||||
A sibling of ``teaching/proposals`` (D4): questions are intake requests, not
|
||||
capability proposals, so they do not overload the proposal pile.
|
||||
"""
|
||||
return Path(__file__).resolve().parents[2] / "teaching" / "questions"
|
||||
|
||||
|
||||
def question_path(delivered: DeliveredQuestion, root: Path | None = None) -> Path:
|
||||
base = root if root is not None else default_question_root()
|
||||
return base / f"{delivered.content_hash()}.json"
|
||||
|
||||
|
||||
def emit_question(
|
||||
assessment: LimitationAssessment, *, root: Path | None = None
|
||||
) -> Path | None:
|
||||
"""Deliver an ask assessment and, iff it renders, write the artifact to the sink.
|
||||
|
||||
Returns the artifact path for a ``QUESTION_NEEDED`` delivery, or ``None`` when the
|
||||
ask was unrenderable and fell back (D2) — no contentless artifact is ever written.
|
||||
Idempotent: the same question writes the same content-addressed path with
|
||||
byte-identical content (``sort_keys``). Creates the sink directory on demand.
|
||||
"""
|
||||
outcome = deliver_ask(assessment)
|
||||
if outcome.question is None:
|
||||
return None
|
||||
path = question_path(outcome.question, root)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(
|
||||
json.dumps(outcome.question.to_json_dict(), indent=2, sort_keys=True),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return path
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AnswerBinding",
|
||||
"DeliveredQuestion",
|
||||
"DeliveryOutcome",
|
||||
"default_question_root",
|
||||
"deliver_ask",
|
||||
"emit_question",
|
||||
"question_path",
|
||||
]
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
"""The grounded-only question renderer (Q1-C) — wrong=0-safe by construction.
|
||||
|
||||
This is the renderer rung of the Epistemic Disclosure ASK spine: given a
|
||||
:class:`~core.epistemic_disclosure.limitation.LimitationAssessment` whose
|
||||
``resolution_action == "ask_question"`` and which carries at least one typed
|
||||
:class:`~core.epistemic_disclosure.limitation.MissingSlot`, produce an
|
||||
:class:`EpistemicQuestion` — a rendered user-facing question, or an explicit
|
||||
``question_unrenderable`` verdict. Nothing here delivers, serves, or chooses a
|
||||
disposition (that is Q1-D); this is *only* surface realization of the residue.
|
||||
|
||||
**The wrong=0 invariant (scoping §2 / session §1.5.7) — the whole point.**
|
||||
|
||||
A question may name an entity, slot, unit, or relation only if it appears
|
||||
*verbatim* in the assessment's ``grounded_terms``. When the grounded terms
|
||||
lack what a question needs, degrade to a generic question or emit
|
||||
``question_unrenderable`` — never a named guess.
|
||||
|
||||
Two substrate facts force the conservative policy below:
|
||||
|
||||
1. ``grounded_terms`` is empty for every assessment produced today — the readers
|
||||
do not yet emit verbatim evidence on refusal (scoping §3, the substrate gap
|
||||
Q1 must close first). So there is no grounded problem-entity to name.
|
||||
2. A *missing* slot's referent is, by definition, absent from the comprehension
|
||||
trace — the missing thing can never appear in ``grounded_terms`` even once
|
||||
readers do emit evidence. ``grounded_terms`` can only supply *context*
|
||||
entities, and binding a slot to its context entity needs an alignment step
|
||||
that Q1-C does not have (a later rung).
|
||||
|
||||
**Chosen rendering policy — generic-structural, names zero problem entities.**
|
||||
|
||||
Because Q1-C can neither (today) read grounded context nor (ever, for the slot
|
||||
itself) name the missing referent from grounded text, the only wrong=0-safe
|
||||
artifact it can render is a *generic* question whose sole variable content is a
|
||||
controlled English phrase for the slot's structural *type*
|
||||
(``expected_unit_or_type``), drawn from the closed, audited
|
||||
:data:`_CLOSED_TYPE_PHRASES` map below. Concretely the renderer:
|
||||
|
||||
- NEVER surfaces ``slot_name`` or ``binding_target`` — these are snake_case
|
||||
structural identifiers, and user-facing prose must never come from them
|
||||
(limitation.py ``MissingSlot`` docstring; session §1.5.7). ``slot_name`` is
|
||||
also the field most likely to *read* like a fabricated entity (``ben_rate`` →
|
||||
the forbidden "Ben"), so it is never touched.
|
||||
- NEVER prettifies a snake_case identifier into a natural-language entity — no
|
||||
capitalization, no possessive, no splitting on underscores.
|
||||
- Translates ``expected_unit_or_type`` through the closed map only; an unmapped
|
||||
type degrades to ``question_unrenderable`` (a ``renderability_gap``) rather
|
||||
than dumping raw snake_case or guessing.
|
||||
- Names no problem-specific entity at all. The closed type phrases ("a
|
||||
whole-number count") are generic structural descriptors that assert nothing
|
||||
about *this* problem — distinct from problem-specific names, which would need
|
||||
grounding. This is the line scoping §2 draws ("generic, all terms grounded"
|
||||
vs. the fabricated "Ben").
|
||||
|
||||
A post-render fabrication guard (:func:`_names_only_grounded`) re-checks that
|
||||
every word in the rendered text is closed-vocabulary scaffold or appears in
|
||||
``grounded_terms`` — defense in depth so a fabricated token can never escape even
|
||||
if the template were later edited carelessly.
|
||||
|
||||
**Off-serving.** Imports nothing from ``generate.derivation`` or
|
||||
``core.reliability_gate``; it cannot move the sealed GSM8K metric.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
from core.epistemic_disclosure.limitation import (
|
||||
LimitationAssessment,
|
||||
MissingSlot,
|
||||
)
|
||||
|
||||
#: Closed, audited map from a family-pinned ``expected_unit_or_type`` to a
|
||||
#: controlled English phrase. Keys are exactly the slot types that ship today
|
||||
#: (``core.epistemic_disclosure.limitation._FAMILY_TO_MISSING_SLOTS``). A slot
|
||||
#: whose type is absent here is NOT rendered — the renderer refuses with a
|
||||
#: ``renderability_gap`` rather than surfacing raw snake_case. New slot types
|
||||
#: earn a phrase here (with a test) when their family lands, never by guessing.
|
||||
_CLOSED_TYPE_PHRASES: dict[str, str] = {
|
||||
"count_int": "a whole-number count",
|
||||
"measured_unit_int": "a whole-number quantity",
|
||||
}
|
||||
|
||||
#: The fixed question scaffold. The only hole is the closed type phrase; every
|
||||
#: other word is constant, problem-independent English. It names no entity.
|
||||
_TEMPLATE = (
|
||||
"To answer this, one more value is still needed — {phrase} — that the "
|
||||
"problem does not state. What is it?"
|
||||
)
|
||||
|
||||
#: Machine reasons for an :class:`EpistemicQuestion`. Closed set.
|
||||
_REASON_RENDERED = "rendered"
|
||||
_REASON_NOT_ASK = "not_ask"
|
||||
_REASON_NO_SLOT = "no_missing_slot"
|
||||
_REASON_MULTI_SLOT = "multi_slot_not_supported"
|
||||
_REASON_RENDERABILITY_GAP = "renderability_gap"
|
||||
_REASON_FABRICATION_GUARD = "fabrication_guard"
|
||||
|
||||
|
||||
def _tokens(text: str) -> set[str]:
|
||||
"""Lowercased maximal alphabetic runs — the unit the fabrication guard checks."""
|
||||
return set(re.findall(r"[a-z]+", text.lower()))
|
||||
|
||||
|
||||
#: Every word the renderer is allowed to emit *without* grounding: the scaffold
|
||||
#: words plus the words of every closed type phrase. Built once at import.
|
||||
_ALLOWED_SCAFFOLD_WORDS: frozenset[str] = frozenset(
|
||||
_tokens(_TEMPLATE.replace("{phrase}", " "))
|
||||
| {w for phrase in _CLOSED_TYPE_PHRASES.values() for w in _tokens(phrase)}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class EpistemicQuestion:
|
||||
"""The rendered ASK artifact, or an explicit unrenderable verdict.
|
||||
|
||||
``slot`` is the bound :class:`MissingSlot` the question is about — present
|
||||
whenever a slot was selected, ``None`` when the assessment carried no slot to
|
||||
bind (non-ASK, or zero slots). ``text`` is the rendered question, or ``None``
|
||||
when ``unrenderable``. ``reason`` is a closed-set machine string (one of the
|
||||
``_REASON_*`` constants) explaining the verdict; for a renderable question it
|
||||
is :data:`_REASON_RENDERED`.
|
||||
"""
|
||||
|
||||
slot: MissingSlot | None
|
||||
text: str | None
|
||||
unrenderable: bool
|
||||
reason: str
|
||||
|
||||
|
||||
def _unrenderable(reason: str, slot: MissingSlot | None = None) -> EpistemicQuestion:
|
||||
"""A ``question_unrenderable`` verdict with no text."""
|
||||
return EpistemicQuestion(slot=slot, text=None, unrenderable=True, reason=reason)
|
||||
|
||||
|
||||
def _names_only_grounded(text: str, grounded_terms: tuple[str, ...]) -> bool:
|
||||
"""True iff every word in ``text`` is closed-vocab scaffold or grounded.
|
||||
|
||||
The wrong=0 guard, enforced post-render as defense in depth: a fabricated
|
||||
entity (a word neither in the closed scaffold/phrase vocabulary nor verbatim
|
||||
in ``grounded_terms``) makes this return ``False``, and the renderer refuses.
|
||||
With today's empty ``grounded_terms`` and a fully closed-vocab template this
|
||||
holds by construction; the guard exists so that can never silently change.
|
||||
"""
|
||||
allowed = _ALLOWED_SCAFFOLD_WORDS | _tokens(" ".join(grounded_terms))
|
||||
return _tokens(text) <= allowed
|
||||
|
||||
|
||||
def render_question(assessment: LimitationAssessment) -> EpistemicQuestion:
|
||||
"""Render a single-slot generic ASK question, or refuse to render.
|
||||
|
||||
Strictly single-slot: Q1-C renders only when the assessment carries
|
||||
*exactly one* missing slot. The fixed template asserts "one more value is
|
||||
still needed" — a globally-quantified claim that exactly one value is
|
||||
missing — so rendering the first of several slots and ignoring the rest
|
||||
would make that sentence subtly false (it would imply the single rendered
|
||||
value closes the gap when others remain). Rather than weaken the template to
|
||||
an honest-but-vaguer plural, a multi-slot assessment refuses with
|
||||
``multi_slot_not_supported`` (slot ``None``); one-question-per-slot fan-out
|
||||
is a later rung. The renderer also refuses (``question_unrenderable``) when
|
||||
the assessment is not an ASK, carries no slot, or the slot's structural type
|
||||
is outside the closed phrase map. It NEVER fabricates a natural-language
|
||||
entity name — see the module docstring for the policy and the wrong=0
|
||||
rationale.
|
||||
"""
|
||||
if assessment.resolution_action != "ask_question":
|
||||
return _unrenderable(_REASON_NOT_ASK)
|
||||
if not assessment.missing_slots:
|
||||
return _unrenderable(_REASON_NO_SLOT)
|
||||
if len(assessment.missing_slots) > 1:
|
||||
# Strictly single-slot: the template claims exactly one value is
|
||||
# missing, which is false when several slots remain. Refuse rather than
|
||||
# render the first and silently drop the rest.
|
||||
return _unrenderable(_REASON_MULTI_SLOT, slot=None)
|
||||
|
||||
slot = assessment.missing_slots[0]
|
||||
phrase = _CLOSED_TYPE_PHRASES.get(slot.expected_unit_or_type)
|
||||
if phrase is None:
|
||||
# Unknown structural type: refuse rather than surface raw snake_case.
|
||||
return _unrenderable(_REASON_RENDERABILITY_GAP, slot=slot)
|
||||
|
||||
text = _TEMPLATE.format(phrase=phrase)
|
||||
if not _names_only_grounded(text, assessment.grounded_terms):
|
||||
# Unreachable by construction; the guard is the wrong=0 backstop.
|
||||
return _unrenderable(_REASON_FABRICATION_GUARD, slot=slot)
|
||||
|
||||
return EpistemicQuestion(
|
||||
slot=slot, text=text, unrenderable=False, reason=_REASON_RENDERED
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"EpistemicQuestion",
|
||||
"render_question",
|
||||
]
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
"""ASK serving gate helper — default-dark, no served-surface wiring.
|
||||
|
||||
This module is the first code slice after the ASK serving-integration scoping brief.
|
||||
It intentionally does **not** call ``deliver_ask``/``emit_question``, does not import
|
||||
``chat.runtime``, and does not expose any user-facing surface. It only centralizes the
|
||||
kill-switch read so future serving code has one audited predicate.
|
||||
|
||||
The planned config field is ``RuntimeConfig.ask_serving_enabled``. During this dark-gate
|
||||
slice the predicate is conservative: absent field == ``False``. That lets the helper land
|
||||
without widening behavior and preserves the current default for every existing
|
||||
``RuntimeConfig`` instance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from core.config import DEFAULT_CONFIG, RuntimeConfig
|
||||
|
||||
|
||||
def ask_serving_enabled(config: RuntimeConfig | Any | None = None) -> bool:
|
||||
"""Return whether served ASK delivery is explicitly enabled.
|
||||
|
||||
Missing attribute means ``False``. That is the load-bearing dark-gate invariant:
|
||||
the served ASK path cannot light merely because the helper exists or because an
|
||||
older ``RuntimeConfig`` instance lacks the future field.
|
||||
"""
|
||||
cfg = DEFAULT_CONFIG if config is None else config
|
||||
return bool(getattr(cfg, "ask_serving_enabled", False))
|
||||
|
||||
|
||||
__all__ = ["ask_serving_enabled"]
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
"""ADR-0199 — cross-domain learning arena.
|
||||
|
||||
The shared engine + interfaces every base subject plugs into. Domains live
|
||||
outside this package (e.g. ``evals/gsm8k_math/practice``); this package never
|
||||
imports a concrete domain.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from core.learning_arena.engine import run_practice
|
||||
from core.learning_arena.protocols import (
|
||||
Attempt,
|
||||
BaseAttempt,
|
||||
DomainProblem,
|
||||
DomainSolver,
|
||||
GoldTether,
|
||||
Problem,
|
||||
Tier2Verifier,
|
||||
)
|
||||
from core.learning_arena.report import (
|
||||
REFUSAL_DIAGNOSES,
|
||||
EliminationRecord,
|
||||
PracticeReport,
|
||||
bucket_counts,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"run_practice",
|
||||
"Attempt",
|
||||
"BaseAttempt",
|
||||
"DomainProblem",
|
||||
"DomainSolver",
|
||||
"GoldTether",
|
||||
"Problem",
|
||||
"Tier2Verifier",
|
||||
"REFUSAL_DIAGNOSES",
|
||||
"EliminationRecord",
|
||||
"PracticeReport",
|
||||
"bucket_counts",
|
||||
]
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
"""ADR-0199 §2.2 — the domain-agnostic practice engine.
|
||||
|
||||
This is the extraction of the GSM8K ``run_practice`` fold into a subject-neutral
|
||||
core. It is the **only** new per-domain code path a subject needs to reach: a
|
||||
subject supplies a :class:`DomainSolver` + :class:`GoldTether` and gets a
|
||||
:class:`PracticeReport` whose ``.ledger`` is the ``dict[str, ClassTally]`` the
|
||||
reliability gate (``propose_from_ledger``) consumes unchanged.
|
||||
|
||||
Invariants (the load-bearing ADR-0199 mandates, enforced structurally here):
|
||||
|
||||
- **L-1 (one floor).** Reliability is computed only via :class:`ClassTally`
|
||||
(which calls the single pinned ``conservative_floor``). This module defines
|
||||
no pessimism constant of its own.
|
||||
- **L-3 (seal).** ``run_practice`` returns a report and mutates nothing. It
|
||||
never touches a serving path or the active teaching corpus. Promotion is the
|
||||
caller's separate ``propose_from_ledger`` step into the reviewed corridor.
|
||||
- **L-4 (determinism).** Pure fold over the input order; identical
|
||||
(problems, solver, tether, diagnose, tier2_verifier) -> identical report.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Sequence
|
||||
|
||||
from core.learning_arena.protocols import (
|
||||
Attempt,
|
||||
DomainProblem,
|
||||
DomainSolver,
|
||||
GoldTether,
|
||||
Tier2Verifier,
|
||||
)
|
||||
from core.learning_arena.report import EliminationRecord, PracticeReport
|
||||
from core.reliability_gate import ClassTally
|
||||
|
||||
|
||||
def _default_diagnose(_reason: str) -> str:
|
||||
"""Conservative default: assume a missing piece (ADR-0175 §8).
|
||||
|
||||
A domain supplies its own router (e.g. a refusal-reason vocabulary) via the
|
||||
``diagnose`` parameter; absent one, refusals are attributed to a knowledge
|
||||
gap rather than silently dropped.
|
||||
"""
|
||||
return "knowledge_gap"
|
||||
|
||||
|
||||
def run_practice(
|
||||
problems: Sequence[DomainProblem],
|
||||
solver: DomainSolver,
|
||||
tether: GoldTether,
|
||||
*,
|
||||
diagnose: Callable[[str], str] = _default_diagnose,
|
||||
tier2_verifier: Tier2Verifier | None = None,
|
||||
) -> PracticeReport:
|
||||
"""Sealed practice: attempt -> gold-tether score -> per-class ledger.
|
||||
|
||||
For each problem, in input order: the solver attempts it; the verdict is
|
||||
``refused`` when the attempt is uncommitted, else ``correct``/``wrong`` per
|
||||
the tether's independent gold check. Counts and per-class :class:`ClassTally`
|
||||
accumulate; each wrong yields an :class:`EliminationRecord`; each refusal is
|
||||
routed by ``diagnose``.
|
||||
"""
|
||||
counts = {"correct": 0, "wrong": 0, "refused": 0}
|
||||
ledger: dict[str, ClassTally] = {}
|
||||
diagnoses: dict[str, str] = {}
|
||||
elims: list[EliminationRecord] = []
|
||||
|
||||
for problem in problems:
|
||||
cls = problem.class_name
|
||||
attempt: Attempt = solver.attempt(problem)
|
||||
gold_correct = False
|
||||
|
||||
if not attempt.committed:
|
||||
verdict = "refused"
|
||||
else:
|
||||
gold_correct = tether.is_correct(attempt, problem)
|
||||
verdict = "correct" if gold_correct else "wrong"
|
||||
|
||||
counts[verdict] = counts.get(verdict, 0) + 1
|
||||
tally = ledger.get(cls) or ClassTally(cls)
|
||||
t2_verified = 0
|
||||
t2_agrees_gold = 0
|
||||
if attempt.committed and tier2_verifier is not None:
|
||||
t2_verdict = tier2_verifier.verify(attempt, problem)
|
||||
if t2_verdict.verified:
|
||||
t2_verified = 1
|
||||
t2_agrees_gold = 1 if gold_correct else 0
|
||||
|
||||
if verdict == "correct":
|
||||
tally = tally.record(
|
||||
correct=1,
|
||||
t2_verified=t2_verified,
|
||||
t2_agrees_gold=t2_agrees_gold,
|
||||
)
|
||||
elif verdict == "wrong":
|
||||
tally = tally.record(
|
||||
wrong=1,
|
||||
t2_verified=t2_verified,
|
||||
t2_agrees_gold=t2_agrees_gold,
|
||||
)
|
||||
elims.append(
|
||||
EliminationRecord(
|
||||
case_id=attempt.case_id,
|
||||
class_name=cls,
|
||||
attempted=attempt.answer,
|
||||
gold=tether.gold_answer(problem),
|
||||
reason=attempt.reason or "",
|
||||
)
|
||||
)
|
||||
else: # refused
|
||||
tally = tally.record(refused=1)
|
||||
diagnoses[attempt.case_id] = diagnose(attempt.reason or "")
|
||||
|
||||
ledger[cls] = tally
|
||||
|
||||
return PracticeReport(
|
||||
counts=counts,
|
||||
ledger=ledger,
|
||||
refusal_diagnoses=diagnoses,
|
||||
elimination_records=tuple(elims),
|
||||
)
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
"""ADR-0199 §2.2 — the cross-domain learning-arena interfaces.
|
||||
|
||||
A subject becomes a learning arena by supplying four domain-specific pieces
|
||||
(``DomainSolver``, a gold anchor set, capability classes, a Tier-2 verifier)
|
||||
and reusing the shared engine (:mod:`core.learning_arena.engine`) and the
|
||||
shared reliability gate (:mod:`core.reliability_gate`) unchanged.
|
||||
|
||||
These protocols are structural (PEP 544). A domain provides concrete classes;
|
||||
the engine never imports a concrete domain. The first instance is GSM8K math
|
||||
(``evals/gsm8k_math/practice/v1/runner.py``), re-expressed against this
|
||||
contract with no behavior change.
|
||||
|
||||
Note on the ADR's illustrative signatures: the ADR sketched
|
||||
``is_correct(attempt, problem_id)``. We pass the whole ``DomainProblem`` (which
|
||||
carries its ``problem_id``) so a tether can reach class/payload without a
|
||||
separate lookup table — strictly more general, same contract.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Protocol, runtime_checkable
|
||||
|
||||
from core.reasoning.evidence import Tier2Verdict
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class DomainProblem(Protocol):
|
||||
"""One problem in a practice arena.
|
||||
|
||||
``class_name`` is the capability axis this problem exercises (the ledger
|
||||
key); it is resolved up front by a domain adapter that may consult gold.
|
||||
``payload`` is opaque to the engine — only the domain's solver/tether read
|
||||
it.
|
||||
"""
|
||||
|
||||
problem_id: str
|
||||
class_name: str
|
||||
payload: Any
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class Attempt(Protocol):
|
||||
"""The result of a single attempt.
|
||||
|
||||
``committed is False`` means the engine refused (always safe; excluded
|
||||
from reliability's denominator per ADR-0175 §4). ``derivations`` are the
|
||||
≥2 structurally-distinct paths a Tier-2 verifier inspects; ``trace_sha256``
|
||||
is replayable provenance carrying no raw content beyond hashes.
|
||||
"""
|
||||
|
||||
committed: bool
|
||||
answer: Any
|
||||
reason: str
|
||||
case_id: str
|
||||
derivations: tuple[Any, ...]
|
||||
trace_sha256: str
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class DomainSolver(Protocol):
|
||||
"""Attempts a grounded derivation over the subject's operations.
|
||||
|
||||
This is where intelligence lives (ADR-0175 Pivot-2). The engine calls
|
||||
:meth:`attempt` once per problem and never inspects how the answer was
|
||||
reached beyond the :class:`Attempt` fields.
|
||||
"""
|
||||
|
||||
domain_id: str
|
||||
|
||||
def attempt(self, problem: DomainProblem) -> Attempt: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class GoldTether(Protocol):
|
||||
"""The Tier-1 truth anchor for a subject.
|
||||
|
||||
ADR-0199 mandate 2: the truth ``is_correct`` consults must come from a
|
||||
source **independent of the solver's derivation** (proof obligation L-2).
|
||||
For dataset domains the gold is the dataset's own answer; for software it
|
||||
is execution; etc.
|
||||
"""
|
||||
|
||||
domain_id: str
|
||||
|
||||
def is_correct(self, attempt: Attempt, problem: DomainProblem) -> bool: ...
|
||||
|
||||
def gold_answer(self, problem: DomainProblem) -> Any: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class Tier2Verifier(Protocol):
|
||||
"""Optional convergent self-verifier for a domain attempt."""
|
||||
|
||||
domain_id: str
|
||||
|
||||
def verify(self, attempt: Attempt, problem: DomainProblem) -> Tier2Verdict: ...
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Problem:
|
||||
"""Concrete :class:`DomainProblem` a domain adapter can build directly."""
|
||||
|
||||
problem_id: str
|
||||
class_name: str
|
||||
payload: Any = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class BaseAttempt:
|
||||
"""Concrete :class:`Attempt` for domains that need no extra fields."""
|
||||
|
||||
committed: bool
|
||||
answer: Any = None
|
||||
reason: str = ""
|
||||
case_id: str = ""
|
||||
derivations: tuple[Any, ...] = field(default_factory=tuple)
|
||||
trace_sha256: str = ""
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
"""ADR-0199 / ADR-0175 — the domain-agnostic practice report.
|
||||
|
||||
Extracted from ``evals/gsm8k_math/practice/v1/runner.py`` so every subject's
|
||||
arena emits the same report shape. The report now also exposes the existing
|
||||
Tier-2 ledger counts when a domain supplies convergent self-verification
|
||||
evidence; domains without a Tier-2 verifier report zeros.
|
||||
|
||||
The three refusal-diagnosis axes are the universal ADR-0175 §8 router
|
||||
(skill / knowledge / ambiguity), not a domain quantity — so they live here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Mapping
|
||||
|
||||
from core.reliability_gate import ClassTally
|
||||
|
||||
# ADR-0175 §8 — the universal "name the missing piece" axes.
|
||||
REFUSAL_DIAGNOSES: tuple[str, ...] = ("skill_gap", "knowledge_gap", "genuine_ambiguity")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class EliminationRecord:
|
||||
"""A wrong practice attempt that gold caught — the pruning signal (§9)."""
|
||||
|
||||
case_id: str
|
||||
class_name: str
|
||||
attempted: float | None
|
||||
gold: float
|
||||
reason: str
|
||||
|
||||
|
||||
def bucket_counts(diagnoses: Mapping[str, str]) -> dict[str, int]:
|
||||
out = {d: 0 for d in REFUSAL_DIAGNOSES}
|
||||
for d in diagnoses.values():
|
||||
out[d] = out.get(d, 0) + 1
|
||||
return out
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PracticeReport:
|
||||
counts: Mapping[str, int]
|
||||
ledger: Mapping[str, ClassTally]
|
||||
refusal_diagnoses: Mapping[str, str]
|
||||
elimination_records: tuple[EliminationRecord, ...]
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"adr": "0175",
|
||||
"regime": "practice",
|
||||
"counts": dict(self.counts),
|
||||
"per_class": {
|
||||
cls: {
|
||||
"correct": t.correct,
|
||||
"wrong": t.wrong,
|
||||
"refused": t.refused,
|
||||
"committed": t.committed,
|
||||
"reliability": t.reliability,
|
||||
"coverage": t.coverage,
|
||||
"t2_verified": t.t2_verified,
|
||||
"t2_agrees_gold": t.t2_agrees_gold,
|
||||
"t2_precision": t.t2_precision,
|
||||
}
|
||||
for cls, t in sorted(self.ledger.items())
|
||||
},
|
||||
"refusal_diagnoses": dict(sorted(self.refusal_diagnoses.items())),
|
||||
"diagnosis_counts": bucket_counts(self.refusal_diagnoses),
|
||||
"elimination_records": [
|
||||
{
|
||||
"case_id": r.case_id,
|
||||
"class_name": r.class_name,
|
||||
"attempted": r.attempted,
|
||||
"gold": r.gold,
|
||||
"reason": r.reason,
|
||||
}
|
||||
for r in self.elimination_records
|
||||
],
|
||||
}
|
||||
|
|
@ -298,15 +298,6 @@ class TurnEvent:
|
|||
epistemic_state: str = "undetermined"
|
||||
normative_clearance: str = "unassessable"
|
||||
normative_detail: str = ""
|
||||
# ADR-0206 — Response Governance Bridge reach level for this turn.
|
||||
# The reach policy that governed the response surface, as a
|
||||
# lower_snake_case string mirroring core.response_governance.ReachLevel
|
||||
# without importing that module here (preserving identity.py's
|
||||
# low-coupling shared-value-type role). Scaffold contract: always
|
||||
# "strict" — govern_response emits STRICT-only until the risk-reward
|
||||
# widening loop is built (ADR-0206 §3). Default "strict" so callers
|
||||
# that omit the field stay byte-identical and conservatively governed.
|
||||
reach_level: str = "strict"
|
||||
# ADR-0153 (W-020a) — canonical SHA-256 trace hash for this turn,
|
||||
# back-stamped by ``CognitiveTurnPipeline.process`` after
|
||||
# ``compute_trace_hash`` runs. Empty string on construction;
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
"""Read-only proposal review reporter (RPT) — surfaces comprehension-failure proposals for review.
|
||||
|
||||
Observes ``teaching/proposals/comprehension_failures/*.json`` (emitted by the contemplation pass,
|
||||
N5), validates them, and reports pending review obligations. It is **read-only**: it does not
|
||||
advance the teaching loop, ratify, mount, modify readers, or affect serving. It is **not** an
|
||||
``idle_tick`` (``ChatRuntime.idle_tick`` remains the only one) and **not** L10 — it is the review
|
||||
surface that keeps proposal artifacts from becoming inert files. A future PR may call this reporter
|
||||
from ``idle_tick`` as a read-only sub-pass.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from core.proposal_review.model import MalformedArtifact, PendingProposal
|
||||
from core.proposal_review.report import (
|
||||
ProposalReviewReport,
|
||||
build_report,
|
||||
report_json,
|
||||
report_text,
|
||||
)
|
||||
from core.proposal_review.safety import SafetyVerdict, dry_check
|
||||
from core.proposal_review.scan import DEFAULT_SINK, default_sink, scan
|
||||
from core.proposal_review.summary import ProposalReviewIdleSummary, idle_summary
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_SINK",
|
||||
"MalformedArtifact",
|
||||
"PendingProposal",
|
||||
"ProposalReviewIdleSummary",
|
||||
"ProposalReviewReport",
|
||||
"SafetyVerdict",
|
||||
"build_report",
|
||||
"default_sink",
|
||||
"dry_check",
|
||||
"idle_summary",
|
||||
"report_json",
|
||||
"report_text",
|
||||
"scan",
|
||||
]
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
"""CLI for the read-only proposal review reporter (RPT-c).
|
||||
|
||||
python -m core.proposal_review # text report + safety dry-check
|
||||
python -m core.proposal_review comprehension-failures
|
||||
python -m core.proposal_review --json # machine-readable
|
||||
python -m core.proposal_review --root <path> # override the sink
|
||||
|
||||
Read-only: scans, reports, and dry-checks the comprehension-failure proposal sink. **Mutates
|
||||
nothing.** Exit 0 iff the safety dry-check passes (every artifact inert + serving unconsumed).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from core.proposal_review.report import build_report, report_text
|
||||
from core.proposal_review.safety import dry_check
|
||||
from core.proposal_review.scan import scan
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="python -m core.proposal_review",
|
||||
description="Read-only comprehension-failure proposal review (observes; never mounts/ratifies).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"target", nargs="?", default="comprehension-failures", choices=["comprehension-failures"]
|
||||
)
|
||||
parser.add_argument("--json", action="store_true", help="emit JSON instead of text")
|
||||
parser.add_argument("--root", default=None, help="override the sink path")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
root = Path(args.root) if args.root else None
|
||||
proposals, malformed = scan(root)
|
||||
report = build_report(proposals, malformed)
|
||||
verdict = dry_check(proposals, malformed, root=root)
|
||||
|
||||
if args.json:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"report": report.to_json_dict(),
|
||||
"safety": {"ok": verdict.ok, "violations": list(verdict.violations)},
|
||||
},
|
||||
indent=2,
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
else:
|
||||
print(report_text(report))
|
||||
print(f" safety: {'OK' if verdict.ok else f'VIOLATIONS ({len(verdict.violations)})'}")
|
||||
for v in verdict.violations:
|
||||
print(f" ! {v}")
|
||||
return 0 if verdict.ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
"""Typed records for the read-only proposal review reporter (RPT-a).
|
||||
|
||||
A ``PendingProposal`` is the parsed view of one ``teaching/proposals/comprehension_failures/
|
||||
<hash>.json`` artifact (emitted by the contemplation pass, N5). A ``MalformedArtifact`` is a file
|
||||
that could not be parsed into one. Pure value data — the reporter never mutates the sink.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PendingProposal:
|
||||
"""A parsed proposal artifact awaiting human review. ``content_hash`` is the filename stem
|
||||
(the content address). Safety fields (``status`` / ``mounted`` / ``requires_review``) are
|
||||
carried verbatim so the dry-check (RPT-c) can verify them independently of the emitter."""
|
||||
|
||||
path: str
|
||||
content_hash: str
|
||||
failure_family: str
|
||||
status: str
|
||||
mounted: bool
|
||||
requires_review: bool
|
||||
problem_text_sha256: str
|
||||
observed_attempts: tuple[dict[str, Any], ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MalformedArtifact:
|
||||
"""A file under the sink that is not a parseable proposal (bad JSON / missing or mistyped
|
||||
fields). Surfaced so a human notices corruption rather than the reporter silently skipping it."""
|
||||
|
||||
path: str
|
||||
reason: str
|
||||
|
||||
|
||||
__all__ = ["MalformedArtifact", "PendingProposal"]
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
"""Deterministic review report over the scanned proposals (RPT-b).
|
||||
|
||||
A pure projection of the scan results into a summary: total pending, counts by failure_family,
|
||||
counts by status, the malformed count, and the review-needed list. Deterministic given the sink
|
||||
contents (no clock): counts are sorted, the review-needed list is sorted by content hash.
|
||||
|
||||
Time-based "oldest/newest" is intentionally **omitted**: the proposal artifacts are
|
||||
content-addressed and carry no timestamp (the emitter is clock-free for idempotence), so an honest
|
||||
temporal ordering is not available from the data — only from non-deterministic filesystem mtime,
|
||||
which would make this report non-deterministic. A human can sort the sink by mtime if needed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from core.proposal_review.model import MalformedArtifact, PendingProposal
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ProposalReviewReport:
|
||||
"""A deterministic snapshot of the review obligations in the proposal sink."""
|
||||
|
||||
total: int
|
||||
by_family: dict[str, int]
|
||||
by_status: dict[str, int]
|
||||
malformed: int
|
||||
review_needed: tuple[str, ...]
|
||||
|
||||
def to_json_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"total": self.total,
|
||||
"by_family": self.by_family,
|
||||
"by_status": self.by_status,
|
||||
"malformed": self.malformed,
|
||||
"review_needed": list(self.review_needed),
|
||||
}
|
||||
|
||||
|
||||
def build_report(
|
||||
proposals: list[PendingProposal], malformed: list[MalformedArtifact]
|
||||
) -> ProposalReviewReport:
|
||||
by_family: dict[str, int] = {}
|
||||
by_status: dict[str, int] = {}
|
||||
review_needed: list[str] = []
|
||||
for p in proposals:
|
||||
by_family[p.failure_family] = by_family.get(p.failure_family, 0) + 1
|
||||
by_status[p.status] = by_status.get(p.status, 0) + 1
|
||||
if p.requires_review:
|
||||
review_needed.append(p.content_hash)
|
||||
return ProposalReviewReport(
|
||||
total=len(proposals),
|
||||
by_family=dict(sorted(by_family.items())),
|
||||
by_status=dict(sorted(by_status.items())),
|
||||
malformed=len(malformed),
|
||||
review_needed=tuple(sorted(review_needed)),
|
||||
)
|
||||
|
||||
|
||||
def report_json(report: ProposalReviewReport) -> str:
|
||||
"""Deterministic JSON (sorted keys)."""
|
||||
return json.dumps(report.to_json_dict(), indent=2, sort_keys=True)
|
||||
|
||||
|
||||
def report_text(report: ProposalReviewReport) -> str:
|
||||
"""Human-readable summary."""
|
||||
lines = [
|
||||
f"comprehension-failure proposals: {report.total} pending · {report.malformed} malformed",
|
||||
" by family:",
|
||||
*(f" {fam}: {n}" for fam, n in report.by_family.items()),
|
||||
" by status:",
|
||||
*(f" {status}: {n}" for status, n in report.by_status.items()),
|
||||
f" review-needed: {len(report.review_needed)}",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
__all__ = ["ProposalReviewReport", "build_report", "report_json", "report_text"]
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
"""Safety dry-check over the proposal sink (RPT-c) — the load-bearing part of the reporter.
|
||||
|
||||
The reporter's value is not just visibility; it is **independent safety verification**. The
|
||||
dry-check confirms — without trusting the emitter — that every artifact in the sink is inert:
|
||||
|
||||
```text
|
||||
status == "proposal_only"
|
||||
mounted == false
|
||||
requires_review == true
|
||||
content-address consistent: filename == sha256(failure_family : problem_text_sha256)
|
||||
path under the sink
|
||||
no malformed artifact (an unverifiable file in a safety-critical sink is a violation)
|
||||
serving never imports/reads the sink
|
||||
```
|
||||
|
||||
Any failure is a violation; the CLI exits non-zero. **Pure read** — verifies, never repairs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from core.proposal_review.model import MalformedArtifact, PendingProposal
|
||||
from core.proposal_review.scan import DEFAULT_SINK
|
||||
|
||||
#: Serving-path roots that must never read the proposal sink (CLAUDE.md forbidden/serving sites).
|
||||
_SERVING_TARGETS = (
|
||||
("generate", "stream.py"),
|
||||
("field", "propagate.py"),
|
||||
("vault", "store.py"),
|
||||
("generate", "derivation"),
|
||||
("core", "reliability_gate"),
|
||||
)
|
||||
_SINK_MARKER = "comprehension_failures"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SafetyVerdict:
|
||||
"""The outcome of the dry-check: ``ok`` iff there are no violations."""
|
||||
|
||||
ok: bool
|
||||
violations: tuple[str, ...]
|
||||
|
||||
|
||||
def _repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _serving_references_sink(repo_root: Path) -> list[str]:
|
||||
violations: list[str] = []
|
||||
for parts in _SERVING_TARGETS:
|
||||
target = repo_root.joinpath(*parts)
|
||||
if not target.exists():
|
||||
continue
|
||||
files = [target] if target.is_file() else sorted(target.rglob("*.py"))
|
||||
for f in files:
|
||||
try:
|
||||
if _SINK_MARKER in f.read_text(encoding="utf-8"):
|
||||
violations.append(f"serving reads the sink: {f.relative_to(repo_root)}")
|
||||
except (UnicodeDecodeError, OSError): # pragma: no cover - defensive
|
||||
continue
|
||||
return violations
|
||||
|
||||
|
||||
def dry_check(
|
||||
proposals: list[PendingProposal],
|
||||
malformed: list[MalformedArtifact],
|
||||
*,
|
||||
root: Path | None = None,
|
||||
repo_root: Path | None = None,
|
||||
) -> SafetyVerdict:
|
||||
"""Verify every artifact is inert and the sink is serving-unconsumed. Returns a SafetyVerdict."""
|
||||
base = (root if root is not None else DEFAULT_SINK).resolve()
|
||||
violations: list[str] = []
|
||||
|
||||
for p in proposals:
|
||||
tag = p.content_hash
|
||||
if p.status != "proposal_only":
|
||||
violations.append(f"{tag}: status={p.status!r} (must be 'proposal_only')")
|
||||
if p.mounted:
|
||||
violations.append(f"{tag}: mounted=True (must be False)")
|
||||
if not p.requires_review:
|
||||
violations.append(f"{tag}: requires_review=False (must be True)")
|
||||
expected = hashlib.sha256(
|
||||
f"{p.failure_family}:{p.problem_text_sha256}".encode("utf-8")
|
||||
).hexdigest()
|
||||
if p.content_hash != expected:
|
||||
violations.append(f"{tag}: content-address mismatch (expected {expected})")
|
||||
if not str(Path(p.path).resolve()).startswith(str(base)):
|
||||
violations.append(f"{tag}: path outside the sink")
|
||||
|
||||
for m in malformed:
|
||||
violations.append(f"malformed (unverifiable): {m.path} — {m.reason}")
|
||||
|
||||
violations.extend(_serving_references_sink(repo_root if repo_root is not None else _repo_root()))
|
||||
|
||||
return SafetyVerdict(not violations, tuple(violations))
|
||||
|
||||
|
||||
__all__ = ["SafetyVerdict", "dry_check"]
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
"""Read-only scanner over the comprehension-failure proposal sink (RPT-a).
|
||||
|
||||
Reads ``teaching/proposals/comprehension_failures/*.json`` into typed ``PendingProposal`` records;
|
||||
any file that does not parse into one is reported as a ``MalformedArtifact`` (never silently
|
||||
dropped). **Pure read** — opens files, never writes/moves/deletes. Deterministic: results are
|
||||
sorted by filename. The sink location is computed here independently of the emitter, so the
|
||||
reporter verifies the artifact contract without importing the writer.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from core.proposal_review.model import MalformedArtifact, PendingProposal
|
||||
|
||||
#: The proposal sink the contemplation pass (N5) writes to — known independently of the emitter.
|
||||
DEFAULT_SINK = (
|
||||
Path(__file__).resolve().parents[2] / "teaching" / "proposals" / "comprehension_failures"
|
||||
)
|
||||
|
||||
#: Required fields and their JSON types for a well-formed proposal artifact.
|
||||
_REQUIRED: tuple[tuple[str, type | tuple[type, ...]], ...] = (
|
||||
("status", str),
|
||||
("failure_family", str),
|
||||
("problem_text_sha256", str),
|
||||
("mounted", bool),
|
||||
("requires_review", bool),
|
||||
("observed_attempts", list),
|
||||
)
|
||||
|
||||
|
||||
def default_sink() -> Path:
|
||||
return DEFAULT_SINK
|
||||
|
||||
|
||||
def _parse(path: Path) -> PendingProposal | MalformedArtifact:
|
||||
try:
|
||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
||||
return MalformedArtifact(str(path), f"invalid_json: {exc}")
|
||||
if not isinstance(raw, dict):
|
||||
return MalformedArtifact(str(path), "not_a_json_object")
|
||||
for key, typ in _REQUIRED:
|
||||
if key not in raw:
|
||||
return MalformedArtifact(str(path), f"missing_field: {key}")
|
||||
# bool is a subclass of int; check bools explicitly so 0/1 don't pass as bool.
|
||||
if typ is bool and not isinstance(raw[key], bool):
|
||||
return MalformedArtifact(str(path), f"bad_type: {key}")
|
||||
if typ is not bool and not isinstance(raw[key], typ):
|
||||
return MalformedArtifact(str(path), f"bad_type: {key}")
|
||||
attempts: tuple[dict[str, Any], ...] = tuple(
|
||||
a for a in raw["observed_attempts"] if isinstance(a, dict)
|
||||
)
|
||||
return PendingProposal(
|
||||
path=str(path),
|
||||
content_hash=path.stem,
|
||||
failure_family=raw["failure_family"],
|
||||
status=raw["status"],
|
||||
mounted=raw["mounted"],
|
||||
requires_review=raw["requires_review"],
|
||||
problem_text_sha256=raw["problem_text_sha256"],
|
||||
observed_attempts=attempts,
|
||||
)
|
||||
|
||||
|
||||
def scan(root: Path | None = None) -> tuple[list[PendingProposal], list[MalformedArtifact]]:
|
||||
"""Scan the sink (default: the comprehension-failure sink). Returns ``(proposals, malformed)``,
|
||||
each sorted by path. A missing sink yields two empty lists (nothing to review yet)."""
|
||||
base = root if root is not None else DEFAULT_SINK
|
||||
if not base.exists():
|
||||
return [], []
|
||||
proposals: list[PendingProposal] = []
|
||||
malformed: list[MalformedArtifact] = []
|
||||
for path in sorted(base.glob("*.json")):
|
||||
parsed = _parse(path)
|
||||
if isinstance(parsed, PendingProposal):
|
||||
proposals.append(parsed)
|
||||
else:
|
||||
malformed.append(parsed)
|
||||
return proposals, malformed
|
||||
|
||||
|
||||
__all__ = ["DEFAULT_SINK", "default_sink", "scan"]
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue