Compare commits
115 commits
rnd/struct
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b67c703bc4 | |||
| e0eb57d139 | |||
|
|
a0a8c4d029 | ||
|
|
fa0f85573c | ||
| bc8276d693 | |||
| dc11d44d29 | |||
| f6cde8a8a3 | |||
|
|
e58e377e06 | ||
|
|
13a4b05d62 | ||
|
|
a35f6f7bd7 | ||
| 6ada6f7a03 | |||
|
|
55a3f79f18 | ||
| 0a26787f72 | |||
|
|
efd3bee9ce | ||
| 116cd18996 | |||
|
|
b10cbde6b8 | ||
|
|
bbca86b72f | ||
|
|
bec76d4889 | ||
|
|
bf17f42834 | ||
|
|
3b2c7608a9 | ||
|
|
50fa287d17 | ||
|
|
b318501c55 | ||
|
|
7bc7131fd9 | ||
|
|
71bf04fb44 | ||
|
|
676b6555b4 | ||
|
|
6a06cb2929 | ||
|
|
589d0ab4b0 | ||
|
|
a1f3947228 | ||
|
|
dbb1f730a0 | ||
|
|
b7b5577d97 | ||
|
|
a4d68682df | ||
|
|
b502065e61 | ||
|
|
38ee0854fc | ||
|
|
fa72b7323c | ||
|
|
e6b59cc585 | ||
|
|
5fd9a861db | ||
|
|
5dd6180308 | ||
|
|
79eee97d43 | ||
|
|
013f36cc13 | ||
|
|
0ae54ebb7b | ||
|
|
a8488e9c38 | ||
|
|
2a82c8a3de | ||
|
|
cbaf2aa22e | ||
|
|
353a52b833 | ||
|
|
0a17c49693 | ||
|
|
44e78aa438 | ||
|
|
e84c0e8428 | ||
| 379e1611ac | |||
|
|
d181ae30f9 | ||
|
|
f95ac26ec0 | ||
| 5224b5e006 | |||
|
|
ab6da7765e | ||
| dd2245a722 | |||
|
|
b938ad617f | ||
| 9405cf1943 | |||
|
|
a83b35de07 | ||
|
|
1a6ccaf9f1 | ||
|
|
ed7c809fd0 | ||
|
|
fdfb71f59b | ||
|
|
c98f1e07b2 | ||
|
|
6a31559921 | ||
|
|
82df3c08ae | ||
|
|
bb96adb771 | ||
| 6a54d27a78 | |||
|
|
de6e392953 | ||
| da3447e95c | |||
|
|
bc838bd1de | ||
| 19847f9007 | |||
|
|
4bf8f3be99 | ||
| 9a428d8466 | |||
|
|
5a343b49e9 | ||
| bf80bc7f55 | |||
| 80100c1831 | |||
|
|
e38291f8f6 | ||
|
|
1293f77c14 | ||
| e7140bf230 | |||
|
|
91650874b4 | ||
| f94dbd4045 | |||
|
|
91bb0a1b8c | ||
| 25504c9d0b | |||
|
|
e0d1b4754a | ||
| 18c578d960 | |||
|
|
8c4221d496 | ||
|
|
9f85832baa | ||
|
|
c71c00cca6 | ||
| 3de431dcdc | |||
| 7755fd4708 | |||
| 2a32887809 | |||
| 68cf8258fa | |||
| 1f1a94a39b | |||
|
|
121e9ebb3c | ||
|
|
f9f94d8df8 | ||
|
|
aaa8503f0b | ||
|
|
b1dc57ff22 | ||
|
|
8494a239e4 | ||
|
|
6ffea249ec | ||
|
|
120e9554ab | ||
|
|
e7d116c924 | ||
| 4b9dbe7236 | |||
|
|
ac4c3e04ad | ||
|
|
1a6efe0d13 | ||
| 6b8a013280 | |||
|
|
375da93e61 | ||
| 5fae5a67f2 | |||
|
|
08881ab665 | ||
|
|
84f7a6f7cc | ||
|
|
87b01868fa | ||
|
|
1d4d51bc9b | ||
|
|
df029a8485 | ||
| d8d62b8ea3 | |||
|
|
2544fda6f6 | ||
|
|
e5643454d9 | ||
| f6fd5030bb | |||
|
|
85781f0f4c | ||
|
|
943614c522 |
270 changed files with 37861 additions and 883 deletions
10
.github/workflows/smoke.yml
vendored
10
.github/workflows/smoke.yml
vendored
|
|
@ -3,9 +3,12 @@ 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
|
||||
# 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.
|
||||
# fabrication; adds ~4 min but blocks-on-regression), and the register axis
|
||||
# (tests/test_register_substantive_consumption.py, ADR-0069/0071/0077 — terse/
|
||||
# convivial registers must actually differ from neutral on served surfaces;
|
||||
# promoted after a 2-day silent regression, generalization-arc Tier S).
|
||||
#
|
||||
# Post-merge on main: full-pytest.yml runs the FAST lane
|
||||
# (-m "not quarantine and not slow"). Soak / proof / register-matrix coverage
|
||||
|
|
@ -56,4 +59,5 @@ jobs:
|
|||
tests/test_cognitive_turn_pipeline.py \
|
||||
tests/test_architectural_invariants.py \
|
||||
tests/test_audio_*.py \
|
||||
tests/test_pack_measurements_phase2.py
|
||||
tests/test_pack_measurements_phase2.py \
|
||||
tests/test_register_substantive_consumption.py
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
3.12.13
|
||||
3.12
|
||||
|
|
|
|||
19
AGENTS.md
19
AGENTS.md
|
|
@ -119,6 +119,22 @@ This boundary is a set of failing-when-violated invariants, not a convention:
|
|||
- **INV-29** — only `vault/store.py` may transition an `epistemic_status`.
|
||||
- **INV-30** — the open-world `determine()` gear constructs only `Determined(answer=True)` or refuses; it can never assert `answer=False`. Closed-world entailed-negation must use a distinct closed-world type and entry point.
|
||||
|
||||
### Convergence fail-closed invariants (2026-07)
|
||||
Registered from the Master Convergence stack (PR #95–#97; ADR-0244 §3, ADR-0253)
|
||||
under the 2026-07-22 weekly-audit T1 ruling. Full contracts:
|
||||
`docs/specs/runtime_contracts.md`.
|
||||
- **INV-32** — identity scoring is wave-only: `IdentityCheck.check` requires an
|
||||
explicit Cl(4,1) `wave_field`; absence raises typed `MissingWaveStateError`,
|
||||
malformed fields raise `ValueError`; no scalar-L2 fallback exists. Live
|
||||
refusal stays flag-gated (`identity_wave_gate`, default off, not authorized).
|
||||
- **INV-33** — dual-pack serve boundary: serve entrypoints never import draft
|
||||
language trees (`packs/he`, `packs/grc`, …) as Python packages; runtime packs
|
||||
load only from `packs/data/<pack_id>/` via `packs.compiler.load_pack`.
|
||||
- **INV-34** — cognition-pipeline failures are typed, never silent
|
||||
(`core/cognition/fail_closed.py`): every refusal carries an explicit failing
|
||||
condition + reason; a `None` `ContractAssessment` is itself a typed
|
||||
violation; unresolvable referents refuse rather than fill.
|
||||
|
||||
### Kernel substrate rule
|
||||
New derivation work should consume `KernelFacts` / `ProblemFrame` where the substrate can represent the meaning.
|
||||
Do not introduce new local prose parsers inside derivation organs unless explicitly marked as legacy exception with migration rationale.
|
||||
|
|
@ -247,6 +263,9 @@ To optimize server resources and bypass external CI billing dependencies, all ag
|
|||
uv run core test --suite smoke -q
|
||||
```
|
||||
Ensure all smoke tests pass (parity with the smoke gate is pinned by `test_cli_smoke_suite_covers_ci_smoke_gate`). Pushing broken code is a critical protocol violation.
|
||||
- **Automated pre-push hook (2026-07-22):** `sh scripts/hooks/install.sh` installs a `core.hooksPath` pre-push gate that runs the `smoke` suite **plus** the `warmed_session` consistency lane pin (`tests/test_warmed_session_lane.py`). The lane pin catches the T13 telemetry-consistency regression class that `smoke` does **not** cover (the #96 fail-closed resolve + #97 morph override escaped the smoke files and surfaced only in the warmed_session lane). The full ~12k fast-lane stays **async CI** by design; the hook is deliberately targeted so it blocks the regression class without gridlocking the push cycle. Emergency bypass (discouraged): `git push --no-verify`.
|
||||
- **One-command local CI (2026-07-25):** `sh scripts/ci/local-ci.sh [--tier smoke|gate|full]` runs the tiers from a single entry point. `gate` is the three pre-push steps; `full` runs the whole `tests/` tree in parallel. Suite membership is read from `core/cli_test.py::TEST_SUITES` through the CLI — never restated — so the runner cannot drift from the hook.
|
||||
- **Interpreter contract.** `pyproject.toml` pins `requires-python == "3.12.13"` exactly, so on a host without that patch version `uv sync --locked` fails and *every* gate becomes unrunnable. The runner is fail-closed about this: it refuses and tells you to `uv python install 3.12.13`. `--allow-interpreter-fallback` opts into any 3.12.x and stamps the run **NON-CANONICAL** on every line — a degraded run is legitimate, a degraded run reporting itself as the real thing is not. Only a canonical run is merge evidence.
|
||||
- **Pre-Merge Gate:** Before proposing a merge or requesting a review on a PR, you **MUST** run the larger validation suite relevant to your changes (e.g. `uv run core test --suite cognition` or `uv run core test --suite algebra`).
|
||||
- **No Docker CI for merge:** Do not run, wait on, or re-provision Docker-container CI to green a merge. Fix and prove in-worktree; merge on local evidence.
|
||||
- **PR Documentation:** When creating a PR on Forgejo (via the Forgejo MCP tools), document the local test execution in the PR description, matching this format:
|
||||
|
|
|
|||
10
CLAIMS.md
10
CLAIMS.md
|
|
@ -36,13 +36,15 @@ is a CI failure (`.github/workflows/lane-shas.yml`).
|
|||
| --- | --- | --- | --- | --- |
|
||||
| ADR-0092 | `reviewer_registry` | Reviewer registry schema validates + bootstrap entry self-seals | `evals/reviewer_registry/results/v1_dev.json` | `681a2aab5aa4ffd58cd837ce5673c8b2a9545b570117aec3c02726a12f6876e6` |
|
||||
| ADR-0093 | `domain_contract_validation` | All ratified packs satisfy the 9 ADR-0091 contract predicates | `evals/domain_contract_validation/results/v1_dev.json` | `98ace04e3f02bbc5a8ad655bb6593c3f1ee64cb67014f1122fe6c3c85f48d22f` |
|
||||
| ADR-0095 | `miner_loop_closure` | Miner-sourced proposals route through single reviewed teaching path | `evals/miner_loop_closure/results/v1_dev.json` | `9f071733abe7dcacf759f928548ce738fb639af3fd6e4c621a651b306d7e77ce` |
|
||||
| ADR-0095 | `miner_loop_closure` | Miner-sourced proposals route through single reviewed teaching path | `evals/miner_loop_closure/results/v1_dev.json` | `537094fe21d7e6cfbaf42bfc32b82d669fa9bb05a132d2bc93c72b3ceb7762a6` |
|
||||
| ADR-0096 | `fabrication_control_summary` | Phantom endpoints / cross-pack non-bridges / sibling collapses refuse | `evals/fabrication_control/results/v1_summary.json` | `01e1b6b711141f2b4a14551d7df3ea482d8d6dd7b364a25c509f4f8d08cda8a8` |
|
||||
| ADR-0098 | `demo_composition` | Demos compose from shipped modules; no parallel mechanism | `evals/demo_composition/results/v1_dev.json` | `e2ba2314d8768459fb6a8db082a4bbcf4107b5161d869804a4b2a33c3724081a` |
|
||||
| ADR-0099 | `public_demo` | Public showcase runs deterministically under 30s; all claims supported | `evals/public_demo/results/v1_dev.json` | `7d8ba0dbae9287cfe0bf15d231fa78a75abc627121c14900439293e01e1cc1d3` |
|
||||
| ADR-0104 | `curriculum_loop_closure` | Curriculum-sourced proposals route through single reviewed teaching path | `evals/curriculum_loop_closure/results/v1_dev.json` | `b46d56b2d209172cc3ffaf3776dc8dcfe55093f13587c5cb67372be6dfa23e8d` |
|
||||
| ADR-0098 | `demo_composition` | Demos compose from shipped modules; no parallel mechanism | `evals/demo_composition/results/v1_dev.json` | `f0611a2ce41721dd40767fc6a83a08470d3c7fd7fc8f1ae8ba003abf8a25ec97` |
|
||||
| ADR-0099 | `public_demo` | Public showcase runs deterministically under 30s; all claims supported | `evals/public_demo/results/v1_dev.json` | `da7fad654e77aac4573a6fcf6e9eaaf84540be8e135d2e033d9cfd15119df3fc` |
|
||||
| ADR-0104 | `curriculum_loop_closure` | Curriculum-sourced proposals route through single reviewed teaching path | `evals/curriculum_loop_closure/results/v1_dev.json` | `cb94ca0042d78ec2624129ff6493d52e767b69feea32d2997b85d88f1c0883af` |
|
||||
| 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` |
|
||||
| ADR-0256 | `deduction_serve_v1` | Flag-gated deduction serving decides real English/member/fused/verb/existential arguments end-to-end under earned SERVE licenses; wrong=0 across all splits | `evals/deduction_serve/report.json` | `0b461a5a49c8f8260ca87d0c9c9f9a17232bd1fdedd982e34649eedf9cca30b5` |
|
||||
| ADR-0262 | `curriculum_serve_v1` | Flag-gated curriculum serving answers exam questions from a subject's RATIFIED chain corpus only; untaught facts return UNKNOWN (anti-recall probes enforced), wrong=0 | `evals/curriculum_serve/report.json` | `d9e7ba500f040b865870413a940ee9a49910ac22e1a89c9feec1a60bdd2513f1` |
|
||||
|
||||
## Verification
|
||||
|
||||
|
|
|
|||
74
chat/curriculum_serve_license.py
Normal file
74
chat/curriculum_serve_license.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
"""Serving-side SERVE license for curriculum-grounded answers (ADR-0262).
|
||||
|
||||
Reads the **ratified, committed** curriculum-serve ledger
|
||||
(``chat/data/curriculum_serve_ledger.json``) and exposes, per *(subject ×
|
||||
relation family)* band, whether the FULL serving pipeline (curriculum
|
||||
compiler → argument reader → ROBDD engine) has earned ``Action.SERVE`` under
|
||||
the safe default ceilings (θ_SERVE=0.99). The engine READS this artifact; it
|
||||
never writes it — the sealed-practice output of
|
||||
``evals.curriculum_serve.practice.runner.seal_ledger`` is the only writer, and
|
||||
its ``content_sha256`` is verified on load so a hand-edited ledger is rejected
|
||||
rather than silently trusted.
|
||||
|
||||
The third instance of the seal → ratify → SHA-verify → serve-gate pattern, and
|
||||
the one that made the shared bridge worth extracting: this module is now an
|
||||
ADAPTER over ``core.ratified_ledger`` (ADR-0263). It names the artifact, keeps
|
||||
the memoization, and declares the one thing that genuinely differs — this
|
||||
ledger is legitimately ABSENT, because no curriculum band has earned anything
|
||||
yet (ADR-0262 §5.1: the binding constraint is ratified curriculum volume). An
|
||||
absent ledger reads as an empty table, so every answer is served DISCLOSED.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
from core.ratified_ledger import (
|
||||
RatifiedLedgerError as RatifiedCurriculumLedgerError,
|
||||
ledger_spec,
|
||||
load_capability_ledger,
|
||||
serve_license,
|
||||
)
|
||||
from core.reliability_gate import Ceilings, ClassTally, LicenseDecision
|
||||
|
||||
_LEDGER_CAPABILITY = "curriculum_serve"
|
||||
_LEDGER_PATH = ledger_spec(_LEDGER_CAPABILITY).path
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def load_ratified_ledger() -> dict[str, ClassTally]:
|
||||
"""Load + verify the ratified curriculum-serve ledger → per-band tallies.
|
||||
|
||||
An ABSENT ledger is not an error *for this capability*: no curriculum band
|
||||
has earned anything yet, and the honest reading of "no file" is "no
|
||||
committed evidence", which the gate turns into a disclosed answer rather
|
||||
than a withheld one.
|
||||
|
||||
That policy is declared once in ``CAPABILITY_LEDGERS``, not asserted here —
|
||||
this module names the capability and inherits its registered absence
|
||||
contract, so it cannot grant itself a softer failure mode than the bridge
|
||||
recorded (ADR-0263 rule 5).
|
||||
"""
|
||||
return load_capability_ledger(_LEDGER_CAPABILITY)
|
||||
|
||||
|
||||
def curriculum_serve_license(
|
||||
band: str,
|
||||
*,
|
||||
ledger: dict[str, ClassTally] | None = None,
|
||||
ceilings: Ceilings | None = None,
|
||||
) -> LicenseDecision | None:
|
||||
"""The ``Action.SERVE`` license for a curriculum band, or ``None``.
|
||||
|
||||
``None`` means the band has no committed evidence → never licensed; the
|
||||
caller serves a disclosed (hedged) surface, the safe default.
|
||||
"""
|
||||
ledger = ledger if ledger is not None else load_ratified_ledger()
|
||||
return serve_license(band, ledger, ceilings=ceilings)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"RatifiedCurriculumLedgerError",
|
||||
"curriculum_serve_license",
|
||||
"load_ratified_ledger",
|
||||
]
|
||||
406
chat/curriculum_surface.py
Normal file
406
chat/curriculum_surface.py
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
"""chat/curriculum_surface.py — CURRICULUM composer (generalization arc Phase 2).
|
||||
|
||||
The second question shape of the curriculum-entailment gold contract
|
||||
(docs/plans/generalization-arc-2026-07-24.md §4.2b): an exam question that
|
||||
supplies only the QUERY — "Does force cause acceleration?" — answered from the
|
||||
ratified curriculum of the subject it is about, and from nothing else.
|
||||
|
||||
The decision is made by the SAME argument bands the deduction composer uses
|
||||
(ADR-0256…0261). This module compiles ratified chains into premise sentences,
|
||||
appends the question as a "Therefore …" conclusion, and hands the assembled
|
||||
argument to `deduction_grounded_surface`'s decider. There is no subject-
|
||||
specific reasoning code anywhere in this path — physics differs from
|
||||
philosophy only in which curriculum rows load, which is what makes "port the
|
||||
lifecycle to a new subject" a data operation rather than an engineering one.
|
||||
|
||||
Why an UNTAUGHT fact is UNKNOWN and never "no": the curriculum is read
|
||||
OPEN-world. "The curriculum does not teach it" is not "it is false", and a
|
||||
system that decoded the former into the latter would be inventing negative
|
||||
knowledge it was never given. The only verdicts reachable from a purely
|
||||
positive curriculum are therefore ENTAILED and UNKNOWN — a real limit of the
|
||||
present corpora, recorded in ADR-0262 §5 rather than papered over.
|
||||
|
||||
Fail-closed (INV-34) BEHIND A NARROW GATE: once `is_curriculum_question` fires,
|
||||
every path below returns a committed, honest surface — typed refusal or decided
|
||||
verdict — never a silent fall-through. The gate itself is deliberately narrow
|
||||
(routable, not merely `Does …?`-shaped): shape alone is not a signal of intent
|
||||
for a polar question, so claiming every one of them would hijack ordinary turns.
|
||||
See :func:`is_curriculum_question`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
|
||||
from chat.curriculum_serve_license import curriculum_serve_license
|
||||
from core.reliability_gate import LicenseDecision
|
||||
from generate.proof_chain.entail import (
|
||||
INCONSISTENT_PREMISES,
|
||||
Entailment,
|
||||
evaluate_entailment_with_trace,
|
||||
)
|
||||
from generate.proof_chain.exist import ExistArgument, read_exist_argument
|
||||
from generate.proof_chain.verb import VerbArgument, read_verb_argument, verb_forms_link
|
||||
from teaching.curriculum_premises import (
|
||||
CONNECTIVE_FAMILY,
|
||||
compile_premises,
|
||||
load_curriculum,
|
||||
)
|
||||
|
||||
#: Subjects this composer serves, in a stable resolution order. A question is
|
||||
#: routed to the subject whose ratified vocabulary contains BOTH of its terms;
|
||||
#: more than one match is an `ambiguous_reading` refusal, never a guess.
|
||||
SERVED_DOMAINS: tuple[str, ...] = (
|
||||
"physics",
|
||||
"mathematics_logic",
|
||||
"systems_software",
|
||||
"philosophy_theology",
|
||||
)
|
||||
|
||||
#: The closed exam-question grammar: ``Does <subject> <verb> <object>?``.
|
||||
#: Deliberately one shape. A question the grammar cannot read refuses typed —
|
||||
#: the reader is never asked to guess which token is the relation.
|
||||
_QUESTION_RE = re.compile(r"^\s*does\s+(.+?)\s*\?\s*$", re.IGNORECASE)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CurriculumQuery:
|
||||
"""A read exam question: the edge the curriculum is being asked about."""
|
||||
|
||||
subject: str
|
||||
verb: str
|
||||
obj: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CurriculumRefusal:
|
||||
"""A typed refusal — the closed reason vocabulary of ADR-0262 §4.6."""
|
||||
|
||||
reason: str
|
||||
detail: str = ""
|
||||
|
||||
|
||||
def looks_like_curriculum_question(text: str) -> bool:
|
||||
"""True iff *text* has the ``Does …?`` polar-question SHAPE.
|
||||
|
||||
A cheap syntactic pre-filter, NOT the commit gate — see
|
||||
:func:`is_curriculum_question`, which is what decides whether this
|
||||
composer claims a turn.
|
||||
"""
|
||||
return bool(_QUESTION_RE.match(text or ""))
|
||||
|
||||
|
||||
def is_curriculum_question(text: str) -> bool:
|
||||
"""True iff *text* is a curriculum question this composer should CLAIM.
|
||||
|
||||
The deduction composer can commit on shape alone because its gate — a
|
||||
sentence-initial "therefore" — is a genuine signal of intent: text shaped
|
||||
that way IS an argument. ``Does …?`` carries no such signal; it is one of
|
||||
the most common ways to open any English question. Committing on it would
|
||||
take "Does the build pass?" away from the rest of dispatch and answer it
|
||||
with a remark about curriculum gaps, which is worse than useless.
|
||||
|
||||
So the gate is *routability*, not shape: claim the turn only when the
|
||||
question parses to three tokens AND its terms are vocabulary some served
|
||||
subject actually teaches. Everything past that point stays fail-closed
|
||||
(INV-34) — including ``ambiguous_reading``, where both terms ARE taught
|
||||
and the only problem is that two subjects claim them, and
|
||||
``out_of_curriculum``, where the terms are taught and only the relation is
|
||||
unknown. Those are real curriculum questions with honest answers. A
|
||||
question whose vocabulary CORE has never been taught is simply not this
|
||||
composer's turn.
|
||||
"""
|
||||
query = read_curriculum_question(text)
|
||||
if not isinstance(query, CurriculumQuery):
|
||||
return False
|
||||
domain = resolve_domain(query)
|
||||
if isinstance(domain, CurriculumRefusal):
|
||||
return domain.reason != "untaught_vocabulary"
|
||||
return True
|
||||
|
||||
|
||||
def read_curriculum_question(text: str) -> CurriculumQuery | CurriculumRefusal:
|
||||
"""Read *text* as ``Does <subject> <verb> <object>?``, or refuse (typed)."""
|
||||
match = _QUESTION_RE.match(text or "")
|
||||
if match is None:
|
||||
return CurriculumRefusal("question_shape_out_of_band", text or "")
|
||||
tokens = match.group(1).replace(",", " ").lower().split()
|
||||
if len(tokens) != 3:
|
||||
# Two-token ("does force act") and four-token ("does a force cause
|
||||
# acceleration") shapes are genuinely different readings; the closed
|
||||
# grammar refuses rather than dropping or inventing a slot.
|
||||
return CurriculumRefusal("question_shape_out_of_band", " ".join(tokens))
|
||||
return CurriculumQuery(tokens[0], tokens[1], tokens[2])
|
||||
|
||||
|
||||
def resolve_domain(query: CurriculumQuery) -> str | CurriculumRefusal:
|
||||
"""The subject whose ratified vocabulary contains BOTH of the query's
|
||||
terms — or a typed refusal (§4.6).
|
||||
|
||||
``untaught_vocabulary`` is the anti-recall boundary made mechanical: a
|
||||
term no mounted pack teaches cannot be reasoned about here at all, however
|
||||
familiar it is. ``ambiguous_reading`` fires when two subjects would both
|
||||
claim the question; the composer declines rather than picking one.
|
||||
"""
|
||||
matches = [
|
||||
domain
|
||||
for domain in SERVED_DOMAINS
|
||||
if {query.subject, query.obj} <= load_curriculum(domain).vocabulary
|
||||
]
|
||||
if not matches:
|
||||
return CurriculumRefusal(
|
||||
"untaught_vocabulary", f"{query.subject} / {query.obj}"
|
||||
)
|
||||
if len(matches) > 1:
|
||||
return CurriculumRefusal("ambiguous_reading", ", ".join(matches))
|
||||
return matches[0]
|
||||
|
||||
|
||||
def resolve_family(query: CurriculumQuery) -> str | CurriculumRefusal:
|
||||
"""The relation family of the query's verb, under the SAME closed
|
||||
agreement relation the verb-predicate band uses (ADR-0260) — so a question
|
||||
may spell the relation in its base form ("cause") while the curriculum
|
||||
states it in the third person ("causes")."""
|
||||
for connective, family in CONNECTIVE_FAMILY.items():
|
||||
if verb_forms_link(query.verb, connective):
|
||||
return family
|
||||
return CurriculumRefusal("out_of_curriculum", query.verb)
|
||||
|
||||
|
||||
def _resolve_connective(query: CurriculumQuery, family: str) -> str:
|
||||
"""The curriculum's own spelling of *query*'s relation, within *family* —
|
||||
the SAME closed agreement relation the verb-predicate band uses
|
||||
(ADR-0260), so a question may spell the relation in its base form while
|
||||
the curriculum states it in the third person. Guarded by
|
||||
:func:`resolve_family`: any *family* reaching this function already has a
|
||||
connective that links to ``query.verb``."""
|
||||
for connective in CONNECTIVE_FAMILY:
|
||||
if CONNECTIVE_FAMILY[connective] == family and verb_forms_link(
|
||||
query.verb, connective
|
||||
):
|
||||
return connective
|
||||
return query.verb # pragma: no cover - guarded above
|
||||
|
||||
|
||||
def _query_sentence(query: CurriculumQuery, family: str) -> str:
|
||||
"""The conclusion sentence, spelled with the CURRICULUM's own connective
|
||||
form so the reader's agreement linking has nothing to do — the question's
|
||||
base form is normalized here, once, visibly."""
|
||||
return f"{query.subject} {_resolve_connective(query, family)} {query.obj}"
|
||||
|
||||
|
||||
def band_for(domain: str, family: str) -> str:
|
||||
"""The capability band an answer is licensed under: *(subject × relation
|
||||
family)* — ADR-0262 §4."""
|
||||
return f"curriculum_{domain}_{family}"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CurriculumDecision:
|
||||
"""What the composer decided, before rendering — the lane scores this."""
|
||||
|
||||
verdict: str # entailed | refuted | unknown | declined
|
||||
band: str # "" when the question never reached a band
|
||||
reason: str # typed refusal reason, or "" when decided
|
||||
domain: str = ""
|
||||
family: str = ""
|
||||
premise_count: int = 0 # FAMILY size — user-visible (ADR-0264 R7)
|
||||
scope_size: int = 0 # compiled, query-scoped premise count (R5/R7)
|
||||
|
||||
|
||||
def decide_curriculum_question(text: str) -> CurriculumDecision:
|
||||
"""Read, route, compile, and decide — the production decision path.
|
||||
|
||||
Kept separate from the rendering below so the lane can pin the DECISION
|
||||
without pinning prose (the same split ``evals/deduction_serve/runner.py``
|
||||
uses).
|
||||
"""
|
||||
query = read_curriculum_question(text)
|
||||
if isinstance(query, CurriculumRefusal):
|
||||
return CurriculumDecision("declined", "", query.reason)
|
||||
domain = resolve_domain(query)
|
||||
if isinstance(domain, CurriculumRefusal):
|
||||
return CurriculumDecision("declined", "", domain.reason)
|
||||
family = resolve_family(query)
|
||||
if isinstance(family, CurriculumRefusal):
|
||||
return CurriculumDecision("declined", "", family.reason, domain=domain)
|
||||
|
||||
curriculum = load_curriculum(domain)
|
||||
family_chains = curriculum.family(family)
|
||||
if not family_chains:
|
||||
return CurriculumDecision(
|
||||
"declined", "", "empty_curriculum", domain=domain, family=family
|
||||
)
|
||||
family_size = len(family_chains)
|
||||
band = band_for(domain, family)
|
||||
connective = _resolve_connective(query, family)
|
||||
premises, _chain_ids = compile_premises(
|
||||
curriculum, family, query=(query.subject, connective, query.obj)
|
||||
)
|
||||
if not premises:
|
||||
# An empty SCOPE — no ratified chain mentions either query term — is
|
||||
# the open-world UNKNOWN reading (ADR-0264 R6). Distinct from an
|
||||
# empty FAMILY (handled above), which stays `empty_curriculum`.
|
||||
return CurriculumDecision(
|
||||
"unknown", band, "", domain=domain, family=family, premise_count=family_size,
|
||||
)
|
||||
conclusion = _query_sentence(query, family)
|
||||
argument = ". ".join(premises) + f". Therefore {conclusion}."
|
||||
|
||||
# The SAME argument bands decide it. The verb band reads these sentences
|
||||
# (each is `<term> <connective> <term>`); the existential band is tried
|
||||
# after it exactly as in serving, so the cascade order here matches the
|
||||
# deduction composer's tail and no subject-specific decider exists.
|
||||
arg = read_verb_argument(argument)
|
||||
if not isinstance(arg, VerbArgument):
|
||||
arg = read_exist_argument(argument)
|
||||
if not isinstance(arg, ExistArgument):
|
||||
return CurriculumDecision(
|
||||
"declined", "", "compiled_premises_unreadable",
|
||||
domain=domain, family=family, premise_count=family_size,
|
||||
)
|
||||
trace = evaluate_entailment_with_trace(arg.premise_formulas, arg.query_formula)
|
||||
verdict = {
|
||||
Entailment.ENTAILED: "entailed",
|
||||
Entailment.REFUTED: "refuted",
|
||||
Entailment.UNKNOWN: "unknown",
|
||||
Entailment.REFUSED: "declined",
|
||||
}[trace.outcome]
|
||||
reason = trace.reason if trace.outcome is Entailment.REFUSED else ""
|
||||
return CurriculumDecision(
|
||||
verdict,
|
||||
band,
|
||||
reason,
|
||||
domain=domain,
|
||||
family=family,
|
||||
premise_count=family_size,
|
||||
scope_size=len(premises),
|
||||
)
|
||||
|
||||
|
||||
_LicenseLookup = Callable[[str], LicenseDecision | None]
|
||||
|
||||
_REFUSAL_SURFACE = {
|
||||
"question_shape_out_of_band": (
|
||||
"That reads as a question about a subject I study, but not in a form I "
|
||||
"can decide yet (I read \"Does <term> <relation> <term>?\")."
|
||||
),
|
||||
"out_of_curriculum": (
|
||||
"I haven't been taught the relation \"{detail}\", so I can't decide "
|
||||
"that question from my curriculum."
|
||||
),
|
||||
"ambiguous_reading": (
|
||||
"Those terms are taught in more than one subject I study ({detail}), "
|
||||
"so I can't tell which curriculum you're asking about."
|
||||
),
|
||||
"empty_curriculum": (
|
||||
"I have no ratified material for that kind of relation yet, so there "
|
||||
"is nothing for me to decide it from."
|
||||
),
|
||||
"compiled_premises_unreadable": (
|
||||
"I have the curriculum for that, but I can't read it back precisely "
|
||||
"enough to decide the question yet."
|
||||
),
|
||||
INCONSISTENT_PREMISES: (
|
||||
"The curriculum I have for that is inconsistent — I won't assert "
|
||||
"anything from it until it is corrected."
|
||||
),
|
||||
}
|
||||
_DEFAULT_REFUSAL_SURFACE = (
|
||||
"I can't decide that from my curriculum as stated."
|
||||
)
|
||||
|
||||
#: Disclosed hedge for a band that has not earned SERVE — same posture as
|
||||
#: the deduction composer (ADR-0256): the answer is sound, the track record
|
||||
#: is not yet demonstrated, so it is served DISCLOSED rather than withheld.
|
||||
_UNVERIFIED_SHAPE_DISCLOSURE = (
|
||||
"(answered from my curriculum, but I haven't yet earned a verified track "
|
||||
"record on questions of this shape) "
|
||||
)
|
||||
|
||||
|
||||
def curriculum_grounded_surface(
|
||||
text: str, *, license_lookup: _LicenseLookup = curriculum_serve_license,
|
||||
) -> str | None:
|
||||
"""Return a deterministic CURRICULUM-tier surface, or ``None``.
|
||||
|
||||
``None`` when *text* is not a curriculum question this composer should
|
||||
claim (:func:`is_curriculum_question`) — the caller then falls through to
|
||||
its pre-existing dispatch, byte-identical to before this composer existed.
|
||||
"""
|
||||
if not is_curriculum_question(text):
|
||||
return None
|
||||
decision = decide_curriculum_question(text)
|
||||
query = read_curriculum_question(text)
|
||||
# Restate the question in the CURRICULUM's own spelling of the relation —
|
||||
# the normalization the reader performed is shown, not hidden.
|
||||
shown = (
|
||||
_query_sentence(query, decision.family)
|
||||
if isinstance(query, CurriculumQuery) and decision.family
|
||||
else text.strip()
|
||||
)
|
||||
if decision.verdict == "declined":
|
||||
template = _REFUSAL_SURFACE.get(decision.reason, _DEFAULT_REFUSAL_SURFACE)
|
||||
return template.format(detail=_detail_of(text, decision))
|
||||
if decision.verdict == "entailed":
|
||||
surface = (
|
||||
f"Yes — my {decision.domain} curriculum teaches that {shown}, "
|
||||
f"directly, among the {decision.premise_count} {decision.family} "
|
||||
f"relations it states."
|
||||
)
|
||||
elif decision.verdict == "refuted":
|
||||
surface = (
|
||||
f"No — my {decision.domain} curriculum rules out that {shown}."
|
||||
)
|
||||
else:
|
||||
surface = (
|
||||
f"My {decision.domain} curriculum doesn't settle whether {shown}. "
|
||||
f"It states {decision.premise_count} {decision.family} relations, "
|
||||
f"and none of them decides this one — which is not the same as "
|
||||
f"its being false."
|
||||
)
|
||||
return _license_gate(surface, decision.band, license_lookup)
|
||||
|
||||
|
||||
def _detail_of(text: str, decision: CurriculumDecision) -> str:
|
||||
"""The user-visible fragment a typed refusal names (empty when the reason
|
||||
carries no detail slot)."""
|
||||
query = read_curriculum_question(text)
|
||||
if not isinstance(query, CurriculumQuery):
|
||||
return ""
|
||||
if decision.reason == "out_of_curriculum":
|
||||
return query.verb
|
||||
if decision.reason == "ambiguous_reading":
|
||||
return ", ".join(
|
||||
d
|
||||
for d in SERVED_DOMAINS
|
||||
if {query.subject, query.obj} <= load_curriculum(d).vocabulary
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
def _license_gate(surface: str, band: str, license_lookup: _LicenseLookup) -> str:
|
||||
"""Serve *surface* authoritatively iff *band* holds a SERVE license; else
|
||||
serve the same sound answer DISCLOSED (hedged)."""
|
||||
decision = license_lookup(band)
|
||||
if decision is not None and decision.licensed:
|
||||
return surface
|
||||
return _UNVERIFIED_SHAPE_DISCLOSURE + surface
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CurriculumDecision",
|
||||
"CurriculumQuery",
|
||||
"CurriculumRefusal",
|
||||
"SERVED_DOMAINS",
|
||||
"band_for",
|
||||
"curriculum_grounded_surface",
|
||||
"decide_curriculum_question",
|
||||
"is_curriculum_question",
|
||||
"looks_like_curriculum_question",
|
||||
"read_curriculum_question",
|
||||
"resolve_domain",
|
||||
"resolve_family",
|
||||
]
|
||||
183
chat/data/deduction_serve_ledger.json
Normal file
183
chat/data/deduction_serve_ledger.json
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
{
|
||||
"classes": {
|
||||
"atomic": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"categorical": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"conditional_chain": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"conditional_single": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"disjunctive": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_atomic": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_conditional_chain": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_conditional_single": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_condmem_chain": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_condmem_conditional": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_condmem_disjunctive": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_condmem_fused": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_disjunctive": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_exist_chain": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_exist_negative": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_exist_universal": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_exist_witness": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_member_atomic": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_member_chain": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_member_negative": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_member_single": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_verb_chain": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_verb_fact": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_verb_negative": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_verb_universal": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
}
|
||||
},
|
||||
"content_sha256": "a056f899a47bbd4536e3141f47a9131c9058ab55f2e9567fdcb254145e2dc252",
|
||||
"note": "Sealed-practice committed ledger for deduction serving (ADR-0256). Engine reads, never writes. Ceilings stay at safe defaults (theta_SERVE=0.99). A band earns SERVE by demonstrated pipeline reliability (reader+projector+engine) at volume >= 657 committed.",
|
||||
"provenance": "evals.deduction_serve.practice.runner.seal_ledger",
|
||||
"schema": "deduction_serve_ledger_v1"
|
||||
}
|
||||
67
chat/deduction_serve_license.py
Normal file
67
chat/deduction_serve_license.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
"""Serving-side SERVE license for deduction (deduction-serve arc, Phase 3, ADR-0256).
|
||||
|
||||
Reads the **ratified, committed** deduction-serve ledger
|
||||
(``chat/data/deduction_serve_ledger.json``) and exposes, per propositional
|
||||
shape-band, whether the FULL serving pipeline (reader → projector → ROBDD
|
||||
engine) has earned ``Action.SERVE`` under the safe default ceilings
|
||||
(θ_SERVE=0.99). The engine READS this artifact; it never writes it. The
|
||||
artifact is the sealed-practice output of
|
||||
``evals.deduction_serve.practice.runner.seal_ledger`` — its ``content_sha256``
|
||||
is verified on load, so a hand-edited (un-ratified) ledger is rejected rather
|
||||
than silently trusted.
|
||||
|
||||
The load/verify/gate mechanics live in ``core.ratified_ledger`` (ADR-0263) —
|
||||
this module is the deduction-serve ADAPTER over that bridge: it names the
|
||||
artifact, keeps the memoization, and preserves its own public API. Ceilings
|
||||
stay at the safe defaults (invariant #4 — the engine cannot raise its own bar).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
from core.ratified_ledger import (
|
||||
RatifiedLedgerError,
|
||||
ledger_spec,
|
||||
load_capability_ledger,
|
||||
serve_license,
|
||||
)
|
||||
from core.reliability_gate import Ceilings, ClassTally, LicenseDecision
|
||||
|
||||
_LEDGER_CAPABILITY = "deduction_serve"
|
||||
_LEDGER_PATH = ledger_spec(_LEDGER_CAPABILITY).path
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def load_ratified_ledger() -> dict[str, ClassTally]:
|
||||
"""Load + verify the ratified deduction-serve ledger → per-band ``ClassTally``.
|
||||
|
||||
Raises :class:`RatifiedLedgerError` if the file is absent/malformed or its
|
||||
recomputed ``content_sha256`` does not match the committed one (tamper-evidence:
|
||||
only the sealed-practice output is trusted, never a hand-edited ledger).
|
||||
|
||||
This capability is registered ``missing_ok=False`` in ``CAPABILITY_LEDGERS``
|
||||
— it ships with its ledger, so absence is a broken deployment, and that
|
||||
verdict is the manifest's rather than this call's (ADR-0263 rule 5).
|
||||
"""
|
||||
return load_capability_ledger(_LEDGER_CAPABILITY)
|
||||
|
||||
|
||||
def deduction_serve_license(
|
||||
shape_band: str,
|
||||
*,
|
||||
ledger: dict[str, ClassTally] | None = None,
|
||||
ceilings: Ceilings | None = None,
|
||||
) -> LicenseDecision | None:
|
||||
"""The ``Action.SERVE`` license for a propositional ``shape_band``, or ``None``.
|
||||
|
||||
``None`` means the band is absent from the ratified ledger (no committed
|
||||
evidence → never licensed; the caller serves a disclosed/hedged surface, the
|
||||
safe default). Otherwise the deterministic ``license_for`` verdict under the
|
||||
safe default ceilings.
|
||||
"""
|
||||
ledger = ledger if ledger is not None else load_ratified_ledger()
|
||||
return serve_license(shape_band, ledger, ceilings=ceilings)
|
||||
|
||||
|
||||
__all__ = ["RatifiedLedgerError", "deduction_serve_license", "load_ratified_ledger"]
|
||||
298
chat/deduction_surface.py
Normal file
298
chat/deduction_surface.py
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
"""chat/deduction_surface.py — DEDUCTION composer (deduction-serve arc).
|
||||
|
||||
Wires the verified propositional-entailment engine (``generate.proof_chain``,
|
||||
716/716 wrong=0 on ``evals/deductive_logic``) into serving. When a prompt
|
||||
reads as a propositional argument — "P1. P2. ... Therefore C." — comprehend
|
||||
the text into a ``MeaningGraph``, project into ``(premises, query)`` formula
|
||||
strings, and decide with the sound+complete ROBDD entailment engine.
|
||||
Deterministic templates only (``generate.proof_chain.render``) — no LLM, no
|
||||
synthesis, matching every other composer in this package.
|
||||
|
||||
Bands (docs/research/deduction-serve-arc-phase0-baseline-2026-07-23.md):
|
||||
- Band v1 — propositional arguments with single-token atoms.
|
||||
- Band v1b (Phase 4) — categorical/syllogism arguments ("All M are P. All S are
|
||||
M. Therefore all S are P."), projected by ``to_syllogism`` and decided by
|
||||
``generate.proof_chain.categorical`` (a propositional-lowering decider that
|
||||
rides the same verified ROBDD engine). ``evals.syllogism.oracle`` must never
|
||||
be imported here: it is the sealed independence oracle the comprehension lane
|
||||
scores against, not a serving decider — importing it would collapse INV-25
|
||||
(independent gold).
|
||||
- Band v2-EN (ADR-0257) — natural-English propositional arguments over OPAQUE
|
||||
clause-atoms ("If it rains then the ground is wet. It rains. Therefore the
|
||||
ground is wet."), read by ``generate.proof_chain.english`` and decided by the
|
||||
same engine. Tried strictly AFTER v1/v1b (a fallback tier), so every argument
|
||||
those bands serve is served byte-identically; this band only widens coverage.
|
||||
- Band v3-MEM (ADR-0258) — singular-membership arguments with universal
|
||||
premises ("Socrates is a man. All men are mortal. Therefore Socrates is
|
||||
mortal."), read by ``generate.proof_chain.member`` via per-individual
|
||||
propositional lowering and decided by the same engine. Tried strictly AFTER
|
||||
v2-EN (which guards these shapes out) — again a pure widening tier.
|
||||
- Band v4-CM (ADR-0259) — conditional-membership fusion arguments ("If
|
||||
Socrates is a man then Socrates is mortal. Socrates is a man. Therefore
|
||||
Socrates is mortal."), composing v2-EN's connective grammar with v3-MEM's
|
||||
singular-membership sentence reading over one shared per-individual atom
|
||||
space, read by ``generate.proof_chain.cond_member``. Tried strictly AFTER
|
||||
v3-MEM (which guards these shapes out via ``mixed_structure_out_of_band``)
|
||||
— again a pure widening tier.
|
||||
- Band v5-VP (ADR-0260) — verb-predicate arguments ("All philosophers teach.
|
||||
Socrates is a philosopher. Therefore Socrates teaches."), read by
|
||||
``generate.proof_chain.verb`` via per-individual lowering with a second
|
||||
(individual, verb-group, object) atom family alongside v3-MEM's membership
|
||||
atoms, decided by the same engine. Tried strictly AFTER v4-CM (the earlier
|
||||
bands refuse quantifier-led verb universals, is-a + verb mixes, and
|
||||
``does not`` negation) — again a pure widening tier.
|
||||
- Band v6-EX (ADR-0261) — existential arguments ("All wolves are mammals. Some
|
||||
wolves are tame. Therefore some mammals are tame."), read by
|
||||
``generate.proof_chain.exist``: v5-VP's lowering over a domain widened with
|
||||
one Skolem witness per existential premise and one ARBITRARY element per
|
||||
existential conclusion (what keeps the domain open, so an existential is
|
||||
refuted only by a genuine universal). Tried LAST, strictly after v5-VP — the
|
||||
earlier fallback bands refuse every ``some``-led sentence, and the
|
||||
categorical band v1b, which reads some-syllogisms in its own synthetic
|
||||
regime, is tried well before this one and keeps them. A pure widening tier.
|
||||
|
||||
Fail-closed (INV-34): once ``looks_like_deductive_argument`` fires, every path
|
||||
below returns a committed, honest surface — reader refusal, out-of-band shape,
|
||||
and every decision outcome — never a silent fall-through to a different composer.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from typing import Callable
|
||||
|
||||
from chat.deduction_serve_license import deduction_serve_license
|
||||
from core.reliability_gate import LicenseDecision
|
||||
from generate.meaning_graph.projectors import to_deductive_logic, to_syllogism
|
||||
from generate.meaning_graph.reader import Comprehension, comprehend
|
||||
from generate.proof_chain.categorical import CategoricalError, decide_syllogism
|
||||
from generate.proof_chain.cond_member import CondMemberArgument, read_cond_member_argument
|
||||
from generate.proof_chain.english import EnglishArgument, read_english_argument
|
||||
from generate.proof_chain.entail import evaluate_entailment_with_trace
|
||||
from generate.proof_chain.exist import ExistArgument, read_exist_argument
|
||||
from generate.proof_chain.member import MemberArgument, read_member_argument
|
||||
from generate.proof_chain.render import (
|
||||
render_entailment,
|
||||
render_entailment_english,
|
||||
render_entailment_exist,
|
||||
render_entailment_member,
|
||||
render_entailment_verb,
|
||||
render_syllogism,
|
||||
)
|
||||
from generate.proof_chain.verb import VerbArgument, read_verb_argument
|
||||
from generate.proof_chain.shape import CATEGORICAL, classify_deduction_shape
|
||||
|
||||
#: An argument's conclusion clause starts a sentence with "therefore" — the
|
||||
#: same shape ``generate.meaning_graph.reader`` recognizes per-clause
|
||||
#: (``toks[0] == "therefore"``). Matching at this commit-gate with the same
|
||||
#: sentence-initial discipline avoids drift between "looks like an argument"
|
||||
#: and "is an argument" — the reader remains the sole decider of the latter.
|
||||
_ARGUMENT_CONCLUSION_RE = re.compile(r"(?:^|[.!?]\s+)therefore\b", re.IGNORECASE)
|
||||
|
||||
|
||||
def looks_like_deductive_argument(text: str) -> bool:
|
||||
"""True iff *text* has a sentence-initial "therefore" conclusion clause.
|
||||
|
||||
A cheap, deterministic COMMIT gate — not a decision. A match only
|
||||
signals "attempt deduction serving"; the reader and projector below
|
||||
remain the sole authority on whether the argument is actually
|
||||
well-formed and in-band.
|
||||
"""
|
||||
return bool(_ARGUMENT_CONCLUSION_RE.search(text))
|
||||
|
||||
|
||||
_READER_REFUSAL_SURFACE = (
|
||||
"That reads as an argument, but I can't parse it precisely enough to "
|
||||
"decide it yet ({reason})."
|
||||
)
|
||||
_OUT_OF_BAND_SURFACE = (
|
||||
"That reads as an argument, but its form is outside what I can decide yet "
|
||||
"(I handle plain propositional and categorical 'all/no/some' arguments)."
|
||||
)
|
||||
|
||||
#: Disclosed hedge prepended when a decided argument's shape-band has NOT earned
|
||||
#: the SERVE license (ADR-0256). The ROBDD engine is sound — the answer is not
|
||||
#: guessed — but an unearned band means the FULL pipeline (crucially the reader)
|
||||
#: has no demonstrated track record on this shape, so committing it as verified
|
||||
#: would overstate the pipeline's earned trust. The answer is served, disclosed.
|
||||
_UNVERIFIED_SHAPE_DISCLOSURE = (
|
||||
"(reasoned, but I haven't yet earned a verified track record on arguments "
|
||||
"of this shape) "
|
||||
)
|
||||
|
||||
_LicenseLookup = Callable[[str], LicenseDecision | None]
|
||||
|
||||
|
||||
def deduction_grounded_surface(
|
||||
text: str, *, license_lookup: _LicenseLookup = deduction_serve_license,
|
||||
) -> str | None:
|
||||
"""Return a deterministic DEDUCTION-tier surface, or ``None``.
|
||||
|
||||
Returns ``None`` only when *text* is not argument-shaped at all
|
||||
(``looks_like_deductive_argument`` is False) — the caller then falls
|
||||
through to the pre-existing dispatch, byte-identical to before this
|
||||
composer existed. Once argument-shaped, every branch below commits to
|
||||
an honest surface; see the module docstring's fail-closed contract.
|
||||
|
||||
Earned-license gate (ADR-0256): a decided argument's rendered surface is
|
||||
served AUTHORITATIVELY only when its propositional shape-band holds a
|
||||
genuine ``Action.SERVE`` license on the committed, SHA-sealed reliability
|
||||
ledger (``chat/deduction_serve_license``). A band that has not earned it —
|
||||
including the forward-looking case of a future shape absent from the ledger,
|
||||
or any deployment whose ledger is stripped — still gets the sound answer,
|
||||
but DISCLOSED (hedged), never presented as a verified capability. This is
|
||||
what makes deduction serving an *earned* capability, not merely a flagged
|
||||
one. ``license_lookup`` is injectable for testing; it defaults to the real
|
||||
ratified-ledger reader.
|
||||
"""
|
||||
if not looks_like_deductive_argument(text):
|
||||
return None
|
||||
comp = comprehend(text)
|
||||
if not isinstance(comp, Comprehension):
|
||||
# Band v2-EN fallback (ADR-0257): the shared reader could not read the
|
||||
# argument (e.g. multi-word English clauses), but the English-clause
|
||||
# argument reader may — a strict WIDENING: every argument the shared
|
||||
# reader serves is still served by the branches below, byte-identical.
|
||||
english = _english_band_surface(text, license_lookup)
|
||||
if english is not None:
|
||||
return english
|
||||
member = _member_band_surface(text, license_lookup)
|
||||
if member is not None:
|
||||
return member
|
||||
cond_member = _cond_member_band_surface(text, license_lookup)
|
||||
if cond_member is not None:
|
||||
return cond_member
|
||||
verb = _verb_band_surface(text, license_lookup)
|
||||
if verb is not None:
|
||||
return verb
|
||||
exist = _exist_band_surface(text, license_lookup)
|
||||
if exist is not None:
|
||||
return exist
|
||||
return _READER_REFUSAL_SURFACE.format(reason=comp.reason)
|
||||
# Band v1 — propositional argument.
|
||||
projected = to_deductive_logic(comp)
|
||||
if projected is not None:
|
||||
premises, query = projected
|
||||
trace = evaluate_entailment_with_trace(premises, query)
|
||||
surface = render_entailment(trace, premises, query)
|
||||
return _license_gate(surface, classify_deduction_shape(premises, query), license_lookup)
|
||||
# Band v1b — categorical / syllogism argument. Decided by the propositional-
|
||||
# lowering categorical decider (rides the same verified ROBDD engine); a
|
||||
# malformed categorical shape declines rather than guessing.
|
||||
syllogism = to_syllogism(comp)
|
||||
if syllogism is not None:
|
||||
structure, s_query = syllogism
|
||||
try:
|
||||
trace = decide_syllogism(structure, s_query)
|
||||
except CategoricalError:
|
||||
return _OUT_OF_BAND_SURFACE
|
||||
surface = render_syllogism(trace, structure, s_query)
|
||||
return _license_gate(surface, CATEGORICAL, license_lookup)
|
||||
# Band v2-EN fallback — comprehended, but projectable by neither v1 shape.
|
||||
english = _english_band_surface(text, license_lookup)
|
||||
if english is not None:
|
||||
return english
|
||||
# Band v3-MEM fallback — the membership/universal shapes v2-EN guards out.
|
||||
member = _member_band_surface(text, license_lookup)
|
||||
if member is not None:
|
||||
return member
|
||||
# Band v4-CM fallback — the conditional-membership shapes v3-MEM guards out.
|
||||
cond_member = _cond_member_band_surface(text, license_lookup)
|
||||
if cond_member is not None:
|
||||
return cond_member
|
||||
# Band v5-VP fallback — the verb-predicate shapes every earlier band guards out.
|
||||
verb = _verb_band_surface(text, license_lookup)
|
||||
if verb is not None:
|
||||
return verb
|
||||
# Band v6-EX fallback — the existential shapes every earlier band guards out.
|
||||
exist = _exist_band_surface(text, license_lookup)
|
||||
if exist is not None:
|
||||
return exist
|
||||
return _OUT_OF_BAND_SURFACE
|
||||
|
||||
|
||||
def _english_band_surface(text: str, license_lookup: _LicenseLookup) -> str | None:
|
||||
"""Band v2-EN (ADR-0257): read *text* as an English-clause argument over
|
||||
opaque atoms and decide it with the same verified ROBDD engine; ``None``
|
||||
when the English reader refuses (the caller keeps its pre-existing honest
|
||||
surface, so this band only ever widens what is decided, never narrows)."""
|
||||
arg = read_english_argument(text)
|
||||
if not isinstance(arg, EnglishArgument):
|
||||
return None
|
||||
trace = evaluate_entailment_with_trace(arg.premise_formulas, arg.query_formula)
|
||||
surface = render_entailment_english(trace, arg.premise_texts, arg.query_text)
|
||||
return _license_gate(surface, arg.band, license_lookup)
|
||||
|
||||
|
||||
def _member_band_surface(text: str, license_lookup: _LicenseLookup) -> str | None:
|
||||
"""Band v3-MEM (ADR-0258): read *text* as a singular-membership argument,
|
||||
lower per-individual, and decide with the same verified ROBDD engine;
|
||||
``None`` when the member reader refuses (the caller keeps its pre-existing
|
||||
honest surface — this band only ever widens what is decided)."""
|
||||
arg = read_member_argument(text)
|
||||
if not isinstance(arg, MemberArgument):
|
||||
return None
|
||||
trace = evaluate_entailment_with_trace(arg.premise_formulas, arg.query_formula)
|
||||
surface = render_entailment_member(trace, arg.premise_texts, arg.query_text)
|
||||
return _license_gate(surface, arg.band, license_lookup)
|
||||
|
||||
|
||||
def _cond_member_band_surface(text: str, license_lookup: _LicenseLookup) -> str | None:
|
||||
"""Band v4-CM (ADR-0259): read *text* as a conditional-membership argument
|
||||
(v2-EN connectives over v3-MEM singular-membership leaves), and decide
|
||||
with the same verified ROBDD engine; ``None`` when the reader refuses (the
|
||||
caller keeps its pre-existing honest surface — this band only ever widens
|
||||
what is decided). Reuses ``render_entailment_member`` unchanged: its
|
||||
UNKNOWN scoping is exactly as accurate here (every clause is read all the
|
||||
way to individual+class; connectives add no new hidden structure)."""
|
||||
arg = read_cond_member_argument(text)
|
||||
if not isinstance(arg, CondMemberArgument):
|
||||
return None
|
||||
trace = evaluate_entailment_with_trace(arg.premise_formulas, arg.query_formula)
|
||||
surface = render_entailment_member(trace, arg.premise_texts, arg.query_text)
|
||||
return _license_gate(surface, arg.band, license_lookup)
|
||||
|
||||
|
||||
def _verb_band_surface(text: str, license_lookup: _LicenseLookup) -> str | None:
|
||||
"""Band v5-VP (ADR-0260): read *text* as a verb-predicate argument
|
||||
(membership atoms + verb atoms over one per-individual space), and decide
|
||||
with the same verified ROBDD engine; ``None`` when the reader refuses (the
|
||||
caller keeps its pre-existing honest surface — this band only ever widens
|
||||
what is decided). Rendered by ``render_entailment_verb``, whose UNKNOWN
|
||||
scoping extends the member phrasing to the verb reading."""
|
||||
arg = read_verb_argument(text)
|
||||
if not isinstance(arg, VerbArgument):
|
||||
return None
|
||||
trace = evaluate_entailment_with_trace(arg.premise_formulas, arg.query_formula)
|
||||
surface = render_entailment_verb(trace, arg.premise_texts, arg.query_text)
|
||||
return _license_gate(surface, arg.band, license_lookup)
|
||||
|
||||
|
||||
def _exist_band_surface(text: str, license_lookup: _LicenseLookup) -> str | None:
|
||||
"""Band v6-EX (ADR-0261): read *text* as an existential argument (witness
|
||||
and arbitrary-element domain widening over v5-VP's two atom families), and
|
||||
decide with the same verified ROBDD engine; ``None`` when the reader
|
||||
refuses (the caller keeps its pre-existing honest surface — this band only
|
||||
ever widens what is decided). Rendered by ``render_entailment_exist``,
|
||||
whose UNKNOWN scoping discloses the no-existential-import reading."""
|
||||
arg = read_exist_argument(text)
|
||||
if not isinstance(arg, ExistArgument):
|
||||
return None
|
||||
trace = evaluate_entailment_with_trace(arg.premise_formulas, arg.query_formula)
|
||||
surface = render_entailment_exist(trace, arg.premise_texts, arg.query_text)
|
||||
return _license_gate(surface, arg.band, license_lookup)
|
||||
|
||||
|
||||
def _license_gate(surface: str, band: str, license_lookup: _LicenseLookup) -> str:
|
||||
"""Serve *surface* authoritatively iff *band* holds a SERVE license; else
|
||||
serve the same sound answer DISCLOSED (hedged). The one place the earned-
|
||||
license posture is applied, shared by the propositional and categorical
|
||||
bands (ADR-0256)."""
|
||||
decision = license_lookup(band)
|
||||
if decision is not None and decision.licensed:
|
||||
return surface # earned SERVE — authoritative
|
||||
return _UNVERIFIED_SHAPE_DISCLOSURE + surface # sound, but disclosed
|
||||
|
||||
|
||||
__all__ = ["deduction_grounded_surface", "looks_like_deductive_argument"]
|
||||
154
chat/runtime.py
154
chat/runtime.py
|
|
@ -94,6 +94,7 @@ from chat.register_variation import decorate_surface
|
|||
from chat.atom_equivalence import atoms_for_graph_nodes, compare_atom_sets
|
||||
from generate.realizer_guard import (
|
||||
DISCLOSURE_SURFACE as _GUARD_DISCLOSURE_SURFACE,
|
||||
QUOTED_TEMPLATE_EXEMPT_VERDICT as _GUARD_QUOTED_EXEMPT,
|
||||
check_surface as _check_realizer_surface,
|
||||
)
|
||||
from packs.anchor_lens.loader import AnchorLens, load_anchor_lens
|
||||
|
|
@ -465,6 +466,22 @@ class ChatResponse:
|
|||
# "teaching" — answer drawn from a reviewed teaching-chain corpus
|
||||
# (cold-start CAUSE/VERIFICATION — ADR-0052).
|
||||
# "none" — universal "insufficient grounding" disclosure on stub.
|
||||
# "deduction" — answer decided by the verified propositional
|
||||
# entailment engine (deduction-serve arc, Phase 1;
|
||||
# chat/deduction_surface.py — ADR-0256).
|
||||
# "curriculum" — answer decided from the ratified domain-chain
|
||||
# curriculum of the question's subject
|
||||
# (chat/curriculum_surface.py — ADR-0262).
|
||||
# Both are registered in core.epistemic_state.GroundingSource (the
|
||||
# Workbench-coupled closed Literal) and map to DECODED. That registration
|
||||
# is load-bearing rather than cosmetic: workbench/api.py's live chat route
|
||||
# builds a bare ChatRuntime(), so with deduction_serving_enabled ratified
|
||||
# ON these composers decide Workbench turns too. While the labels were
|
||||
# unregistered, the API's coercion floored them to "none" and recorded a
|
||||
# proved answer as ungrounded (pinned by
|
||||
# tests/test_workbench_deduction_provenance.py). Adding a grounding source
|
||||
# here means registering it there in the same change — the workbench-ui
|
||||
# enum-coverage test fails the build otherwise.
|
||||
# The string is preserved verbatim in TurnEvent for downstream audit.
|
||||
grounding_source: str = "none"
|
||||
# ADR-0071 (R4) — pre-decoration surface. ``surface`` is the
|
||||
|
|
@ -1195,6 +1212,53 @@ class ChatRuntime:
|
|||
cand, source_turn_trace=trace_hash
|
||||
)
|
||||
|
||||
def finalize_turn_surface(
|
||||
self,
|
||||
surface: str,
|
||||
articulation_surface: str | None = None,
|
||||
) -> None:
|
||||
"""Back-stamp the pipeline's final *served* surface onto the turn.
|
||||
|
||||
Weekly-audit T13 (2026-07-22): ``ChatRuntime.chat`` seals a
|
||||
``TurnEvent`` with the runtime-owned surface, but
|
||||
``CognitiveTurnPipeline`` applies the Shadow-Coherence /
|
||||
logos-morph surface authority AFTER ``chat`` returns. The
|
||||
telemetry record therefore captured a surface the user never
|
||||
saw — ``TurnEvent.surface != pipeline.run().surface`` — breaking
|
||||
the ``warmed_session_consistency`` telemetry-consistency invariant
|
||||
and audit/replay trust (Absolute Provenance).
|
||||
|
||||
The pipeline owns the final surface decision, so — exactly like
|
||||
``finalize_turn_trace_hash`` back-stamps the pipeline-owned
|
||||
``trace_hash`` — it back-stamps the resolved surface here after
|
||||
``resolve_surface`` + logos-morph. Mirrors that method's
|
||||
contract: operates on the most recent ``TurnEvent`` (main or
|
||||
refusal-stub path), no-ops when unchanged or when no turn exists.
|
||||
|
||||
NOTE: the opt-in external telemetry sink (``attach_telemetry_sink``)
|
||||
still receives the pre-override event, the same provisional-then-
|
||||
back-stamped limitation ``finalize_turn_trace_hash`` carries today
|
||||
(default sink is ``None`` → no-op). Deferring the single sink
|
||||
emission to the pipeline serve boundary — which would repair both
|
||||
surface and ``trace_hash`` in the durable stream — is tracked as a
|
||||
follow-up (audit ledger R7), not bolted onto this red-fix.
|
||||
"""
|
||||
if not self.turn_log:
|
||||
return
|
||||
from dataclasses import replace
|
||||
|
||||
last_event = self.turn_log[-1]
|
||||
updates: dict[str, str] = {}
|
||||
if surface != last_event.surface:
|
||||
updates["surface"] = surface
|
||||
if (
|
||||
articulation_surface is not None
|
||||
and articulation_surface != last_event.articulation_surface
|
||||
):
|
||||
updates["articulation_surface"] = articulation_surface
|
||||
if updates:
|
||||
self.turn_log[-1] = replace(last_event, **updates)
|
||||
|
||||
def first_admitted_recognizer(self):
|
||||
if not self.config.recognition_grounded_graph:
|
||||
return None
|
||||
|
|
@ -1673,16 +1737,50 @@ class ChatRuntime:
|
|||
to a walk fragment. CAUSE / VERIFICATION still return None
|
||||
when no teaching chain exists, preserving the discovery signal.
|
||||
"""
|
||||
if not allow_warm and gate_source != "empty_vault":
|
||||
if attempts is not None:
|
||||
for src in ("pack", "teaching", "partial", "oov"):
|
||||
attempts.append(DispatchAttempt(source=src, outcome="skipped", reason="warm_path_disabled"))
|
||||
return None
|
||||
if self.config.output_language != "en":
|
||||
if attempts is not None:
|
||||
for src in ("pack", "teaching", "partial", "oov"):
|
||||
attempts.append(DispatchAttempt(source=src, outcome="skipped", reason="non_english_output"))
|
||||
return None
|
||||
# Deduction-serve arc, Phase 1 — checked BEFORE the empty-vault gate
|
||||
# below (unlike pack/teaching/partial/oov, deduction serving is not a
|
||||
# cold-start-only fallback: a user may ask a logic question at any
|
||||
# point in a conversation, warm or cold vault). A pure function of
|
||||
# the input text — no vault/field dependency — so it is safe to
|
||||
# decide unconditionally of ``gate_source``/``allow_warm``.
|
||||
if self.config.deduction_serving_enabled:
|
||||
from chat.deduction_surface import deduction_grounded_surface
|
||||
from generate.intent import DialogueIntent, IntentTag as _IntentTag
|
||||
|
||||
deduction_surface = deduction_grounded_surface(text)
|
||||
if deduction_surface is not None:
|
||||
self._last_intent = DialogueIntent(tag=_IntentTag.DEDUCTION, subject=text) # W-013
|
||||
if attempts is not None:
|
||||
attempts.append(DispatchAttempt(source="deduction", outcome="admitted", reason="deduction_composer_committed"))
|
||||
return (deduction_surface, "deduction", ())
|
||||
if attempts is not None:
|
||||
attempts.append(DispatchAttempt(source="deduction", outcome="skipped", reason="not_argument_shaped"))
|
||||
# Generalization arc, Phase 2 (ADR-0262) — curriculum-grounded exam
|
||||
# questions, checked beside deduction serving for the same reason: a
|
||||
# pure function of the input text and the ratified curriculum, with no
|
||||
# vault/field dependency, so warm/cold state cannot change the answer.
|
||||
if self.config.curriculum_serving_enabled:
|
||||
from chat.curriculum_surface import curriculum_grounded_surface
|
||||
from generate.intent import DialogueIntent, IntentTag as _IntentTag
|
||||
|
||||
curriculum_surface = curriculum_grounded_surface(text)
|
||||
if curriculum_surface is not None:
|
||||
self._last_intent = DialogueIntent(tag=_IntentTag.DEDUCTION, subject=text)
|
||||
if attempts is not None:
|
||||
attempts.append(DispatchAttempt(source="curriculum", outcome="admitted", reason="curriculum_composer_committed"))
|
||||
return (curriculum_surface, "curriculum", ())
|
||||
if attempts is not None:
|
||||
attempts.append(DispatchAttempt(source="curriculum", outcome="skipped", reason="not_curriculum_question"))
|
||||
if not allow_warm and gate_source != "empty_vault":
|
||||
if attempts is not None:
|
||||
for src in ("pack", "teaching", "partial", "oov"):
|
||||
attempts.append(DispatchAttempt(source=src, outcome="skipped", reason="warm_path_disabled"))
|
||||
return None
|
||||
from generate.intent import IntentTag
|
||||
from generate.intent_bridge import classify_intent_from_input
|
||||
intent = classify_intent_from_input(text)
|
||||
|
|
@ -2273,9 +2371,18 @@ class ChatRuntime:
|
|||
# for telemetry — the stub path normally leaves walk_surface as
|
||||
# _UNKNOWN_DOMAIN_SURFACE, so this swap strictly increases
|
||||
# observability under rejection.
|
||||
guard_verdict_stub = _check_realizer_surface(
|
||||
response_surface,
|
||||
pos_lookup=self._pos_by_surface.get,
|
||||
# Deduction surfaces (ADR-0257) are OUT of C1's regime: a fixed,
|
||||
# test-audited template quoting the user's clauses verbatim, not a
|
||||
# slot-composed articulation — pack POS describes the composed sense,
|
||||
# not the user's quoted sense (e.g. pack ``open``=VERB rejecting an
|
||||
# honest "the door is not open"). Exempt, like empty surfaces.
|
||||
guard_verdict_stub = (
|
||||
_GUARD_QUOTED_EXEMPT
|
||||
if grounding_source == "deduction"
|
||||
else _check_realizer_surface(
|
||||
response_surface,
|
||||
pos_lookup=self._pos_by_surface.get,
|
||||
)
|
||||
)
|
||||
realizer_guard_status_stub = guard_verdict_stub.status
|
||||
realizer_guard_rule_stub = guard_verdict_stub.rule_id
|
||||
|
|
@ -2684,26 +2791,19 @@ class ChatRuntime:
|
|||
# --- end articulation fidelity ---
|
||||
|
||||
reasoning_trajectory = _make_trajectory_from_result(result, self._context.turn)
|
||||
# ADR-0244 §2.2 — operator-preservation identity gate (flag-gated). When
|
||||
# on, the check runs the metric-exact wave-field gate on the live versor
|
||||
# final_state.F; when off, wave_field=None selects the legacy scalar-L2
|
||||
# path (byte-identical). The boundary_ids intersection needs the
|
||||
# safety/ethics verdicts, which are computed below — it is supplemented
|
||||
# after those run.
|
||||
# ADR-0246 §3.7 — fuller admit surface, flag-gated + default-off. The
|
||||
# policy is placeholder/uncalibrated (calibrated=False); it only acts
|
||||
# when identity_wave_gate is also on (a wave_field exists).
|
||||
# ADR-0244 §2.2 — metric-exact operator-preservation identity score always
|
||||
# runs on the live versor final_state.F (scalar-L2 path excised). Live
|
||||
# *refusal* remains flag-gated via identity_wave_gate below.
|
||||
# ADR-0246 §3.7 — fuller admit surface, flag-gated + default-off.
|
||||
_admission_policy = (
|
||||
AdmissionPolicy.placeholder_default()
|
||||
if self.config.identity_action_surface
|
||||
if self.config.identity_action_surface and self.config.identity_wave_gate
|
||||
else None
|
||||
)
|
||||
identity_score = self._identity_check.check(
|
||||
reasoning_trajectory,
|
||||
self.identity_manifold,
|
||||
wave_field=(
|
||||
result.final_state.F if self.config.identity_wave_gate else None
|
||||
),
|
||||
wave_field=result.final_state.F,
|
||||
admission_policy=_admission_policy,
|
||||
turn_id=self._context.turn,
|
||||
pack_id=self.identity_pack_id,
|
||||
|
|
@ -2908,9 +3008,15 @@ class ChatRuntime:
|
|||
# candidate so the manifold-walk evidence is overwritten only
|
||||
# in the rejection branch (the contract says illegal
|
||||
# articulation evidence is the relevant telemetry).
|
||||
guard_verdict_main = _check_realizer_surface(
|
||||
response_surface,
|
||||
pos_lookup=self._pos_by_surface.get,
|
||||
# Same ADR-0257 exemption as the stub path: quoted deduction
|
||||
# templates are not slot-composed articulations.
|
||||
guard_verdict_main = (
|
||||
_GUARD_QUOTED_EXEMPT
|
||||
if warm_grounding_source == "deduction"
|
||||
else _check_realizer_surface(
|
||||
response_surface,
|
||||
pos_lookup=self._pos_by_surface.get,
|
||||
)
|
||||
)
|
||||
realizer_guard_status_main = guard_verdict_main.status
|
||||
realizer_guard_rule_main = guard_verdict_main.rule_id
|
||||
|
|
|
|||
|
|
@ -137,9 +137,8 @@ def serialize_turn_event(
|
|||
out["identity_deviation_axes"] = sorted(
|
||||
getattr(identity_score, "deviation_axes", ()) or ()
|
||||
)
|
||||
# ADR-0244 §2.2 — operator-preservation wave-gate telemetry. Emitted only
|
||||
# when the wave path ran (config.identity_wave_gate on); absent otherwise,
|
||||
# so the pre-ADR-0244 wire format stays byte-identical when the gate is off.
|
||||
# ADR-0244 §2.2 — operator-preservation wave-gate telemetry. Emitted when
|
||||
# the geometric score path ran (always after L2 excision when a score exists).
|
||||
if getattr(identity_score, "wave_mode_active", False):
|
||||
out["identity_wave_mode"] = True
|
||||
out["identity_leakage_norm"] = float(
|
||||
|
|
|
|||
27
conftest.py
27
conftest.py
|
|
@ -29,6 +29,33 @@ import engine_state
|
|||
_USES_DEFAULT_ENGINE_STATE_MARKER = "uses_default_engine_state"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="session")
|
||||
def _isolate_engine_state_session_baseline(tmp_path_factory):
|
||||
"""Session-scoped floor under ``_isolate_engine_state_default``.
|
||||
|
||||
The per-test fixture below is function-scoped, and pytest sets up
|
||||
higher-scoped fixtures FIRST — so a module-/session-scoped fixture that
|
||||
constructs a bare ``ChatRuntime()`` (e.g. ``tests/test_achat.py``'s
|
||||
module-scoped ``runtime``) bound the REAL repo ``engine_state/`` before
|
||||
the per-test patch existed: it loaded the live life-store and committed
|
||||
real generations into it (observed 2026-07-22: smoke advanced the live
|
||||
store's ``turn_count`` 14989→14990 and re-stamped its revision).
|
||||
|
||||
This baseline is set up before fixtures of any narrower scope, so no
|
||||
pytest-constructed default store can ever resolve to the repo dir. It is
|
||||
deliberately unconditional: ``uses_default_engine_state`` opts out of the
|
||||
per-test layer only — those tests verify default-dir RESOLUTION
|
||||
semantics, which this baseline preserves; they too must never touch the
|
||||
live life-store.
|
||||
"""
|
||||
isolated = tmp_path_factory.mktemp("engine_state_session")
|
||||
mp = pytest.MonkeyPatch()
|
||||
mp.setattr(engine_state, "_DEFAULT_DIR", isolated)
|
||||
mp.setenv("CORE_ENGINE_STATE_DIR", str(isolated))
|
||||
yield
|
||||
mp.undo()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolate_engine_state_default(request, tmp_path_factory, monkeypatch):
|
||||
"""Isolate the default engine-state checkpoint dir per test.
|
||||
|
|
|
|||
177
core/cli.py
177
core/cli.py
|
|
@ -25,156 +25,12 @@ _CORE_RS_MANIFEST = _CORE_RS_DIR / "Cargo.toml"
|
|||
DESCRIPTION = "CORE versor engine command suite."
|
||||
EPILOG = 'Examples:\n core chat\n core pulse "What is truth?"\n core pulse --no-glove --json "Compare knowledge and wisdom"\n core bench\n core bench --suite all\n core bench --suite all --json --report bench_all.json\n core bench --suite determinism --runs 50\n core bench --suite speedup --json\n core trace "word beginning truth"\n core trace --output-language grc --frame-pack grc --json "logos"\n core rust status\n core rust build\n core oov covenant\n core pack list\n core pack verify en_minimal_v1\n core teaching audit\n core teaching audit --json\n core teaching gaps --top 10\n core teaching queue --threshold 3\n core teaching hitl-queue list\n core teaching hitl-queue list --state all --json\n core teaching hitl-queue show <proposal_id>\n core teaching propose <candidate-jsonl-path>\n core teaching propose-from-exemplars teaching/admissibility_exemplars/rate_with_currency_v1.jsonl\n core teaching propose-from-exemplars --all\n core teaching proposals --state pending\n core teaching review <proposal_id> --accept --review-date 2026-05-18\n core teaching supersede cause_light_reveals_truth --subject light --intent cause --connective grounds --object truth --review-date 2026-05-18\n core teaching supersessions\n core teaching supersessions --json\n core test --suite fast -q\n core test --suite pulse -q\n core test --suite proof -q\n core test --suite cognition -q\n core test -- tests/test_alignment_graph.py -q\n core demo audit-tour\n core demo register-tour\n core demo anchor-lens-tour\n core demo orthogonality-tour\n core demo pack-measurements\n core demo long-context-comparison\n core demo anti-regression\n core demo learning-loop\n core demo learning-arc\n core demo articulation\n core demo conversation\n core demo conversation --no-stream\n core demo all\n core demo adr-0024-chain\n core eval --list\n core eval cognition\n core eval cognition --json --save\n core eval cognition --split dev --version v1\n core eval cognition --split holdout\n core eval contemplation_quality\n core eval contemplation_quality --json --save\n core eval math-contemplation\n core eval math-contemplation --audit evals/gsm8k_math/train_sample/v1/audit_brief_11.json\n core eval math-contemplation --output teaching/math_proposals/proposals.jsonl\n core workbench api\n core workbench api --port 9000\n core workbench api --host 0.0.0.0 --allow-nonlocal-bind'
|
||||
|
||||
_TEST_SUITES: dict[str, tuple[str, ...]] = {
|
||||
"fast": (
|
||||
"tests/test_cli_test_suites.py",
|
||||
"tests/test_runtime_config.py",
|
||||
"tests/test_core_semantic_seed_pack.py",
|
||||
"tests/test_intent_proposition_graph.py",
|
||||
"tests/test_articulation_realizer_v2.py",
|
||||
"tests/test_reviewed_teaching_loop.py",
|
||||
"tests/test_cognitive_eval_harness.py",
|
||||
),
|
||||
"smoke": (
|
||||
"tests/test_chat_runtime.py",
|
||||
"tests/test_achat.py",
|
||||
"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",
|
||||
"tests/test_achat.py",
|
||||
"tests/test_runtime_config.py",
|
||||
"tests/test_session_coherence.py",
|
||||
),
|
||||
"cognition": (
|
||||
"tests/test_intent_proposition_graph.py",
|
||||
"tests/test_cognitive_turn_pipeline.py",
|
||||
"tests/test_articulation_realizer_v2.py",
|
||||
"tests/test_semantic_realizer_integration.py",
|
||||
"tests/test_cognitive_eval_harness.py",
|
||||
"tests/test_deterministic_hash.py",
|
||||
"tests/test_morphology_irregular.py",
|
||||
"tests/test_realizer_quantifier_agreement.py",
|
||||
"tests/test_benchmarks_profiler.py",
|
||||
"tests/test_compose_relations.py",
|
||||
"tests/test_replay_vs_llm_benchmark.py",
|
||||
),
|
||||
"teaching": (
|
||||
"tests/test_reviewed_teaching_loop.py",
|
||||
"tests/test_pipeline_teaching_integration.py",
|
||||
"tests/test_epistemic_invariants.py",
|
||||
"tests/test_adr_0172_w2_decomposer.py",
|
||||
"tests/test_adr_0172_w5_inference_proposal.py",
|
||||
"tests/test_math_frame_ratification.py",
|
||||
"tests/test_math_composition_ratification.py",
|
||||
"tests/test_teaching_coverage_cli.py",
|
||||
),
|
||||
"packs": (
|
||||
"tests/test_core_semantic_seed_pack.py",
|
||||
"tests/test_adr_0127_pack_ratification.py",
|
||||
"tests/test_frame_registry_load.py",
|
||||
"tests/test_composition_registry_load.py",
|
||||
"tests/test_composition_consult_in_injector.py",
|
||||
"tests/test_consumption_case_0050_hazard_pin.py",
|
||||
"tests/test_consumption_empty_registry_no_op.py",
|
||||
"tests/test_consumption_partition.py",
|
||||
"tests/test_matcher_extension_currency_per_unit.py",
|
||||
"tests/test_matcher_extension_case_0050_hazard_pin.py",
|
||||
"tests/test_matcher_extension_end_to_end_admission.py",
|
||||
"tests/test_me2_cross_sentence_subject.py",
|
||||
"tests/test_me2_case_0019_admits.py",
|
||||
"tests/test_me3_additive_composition.py",
|
||||
"tests/test_me4_subtractive_composition.py",
|
||||
"tests/test_me5_all_categories_integration.py",
|
||||
"tests/test_rat1_end_to_end_admission.py",
|
||||
"tests/test_wave_a_multiplicative_aggregation_injector.py",
|
||||
),
|
||||
"algebra": (
|
||||
"tests/test_versor_closure.py",
|
||||
"tests/test_holonomy.py",
|
||||
"tests/test_holonomy_resonance.py",
|
||||
"tests/test_energy.py",
|
||||
"tests/test_motor.py",
|
||||
"tests/test_null_cone.py",
|
||||
"tests/test_vault_recall.py",
|
||||
"tests/test_vault_recall_vectorised.py",
|
||||
"tests/test_vault_recall_rust_parity.py",
|
||||
"tests/test_cga_inner_rust_parity.py",
|
||||
"tests/test_geometric_product_rust_parity.py",
|
||||
"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",
|
||||
),
|
||||
"formation": ("tests/formation",),
|
||||
"proof": ("tests/test_proof_properties.py",),
|
||||
# ADR-0024 chain suites (Phases 2-6). Each phase has its own
|
||||
# contract tests so investors / reviewers can run them
|
||||
# independently; ``adr-0024`` runs the full chain end-to-end.
|
||||
"refusal": ("tests/test_refusal_contract.py",),
|
||||
"margin": ("tests/test_margin_admissibility.py",),
|
||||
"rotor": ("tests/test_rotor_admissibility.py",),
|
||||
"inner-loop": (
|
||||
"tests/test_inner_loop_admissibility.py",
|
||||
"tests/test_inner_loop_phase2.py",
|
||||
"tests/test_inner_loop_phase3.py",
|
||||
"tests/test_inner_loop_phase4.py",
|
||||
),
|
||||
"phase5": ("tests/test_phase5_corpus.py",),
|
||||
"phase6": ("tests/test_phase6_demo.py",),
|
||||
"adr-0024": (
|
||||
"tests/test_refusal_contract.py",
|
||||
"tests/test_margin_admissibility.py",
|
||||
"tests/test_rotor_admissibility.py",
|
||||
"tests/test_inner_loop_admissibility.py",
|
||||
"tests/test_inner_loop_phase2.py",
|
||||
"tests/test_inner_loop_phase3.py",
|
||||
"tests/test_inner_loop_phase4.py",
|
||||
"tests/test_phase5_corpus.py",
|
||||
"tests/test_phase6_demo.py",
|
||||
),
|
||||
# ADR-0126 P6 — measurement harness for the GSM8K candidate-graph
|
||||
# parser exit criterion. ``wrong == 0`` is a hard gate (Obligation
|
||||
# #4: refuse rather than confabulate).
|
||||
"math": ("tests/test_adr_0126_train_sample_runner.py",),
|
||||
"deductive": ("tests/test_deductive_logic_entail.py",),
|
||||
"full": ("tests/",),
|
||||
}
|
||||
# Canonical suite map lives in core.cli_test (runtime + --list-suites).
|
||||
# Re-export here for argparse choices and tests that import core.cli.
|
||||
# Do not maintain a second copy — Stage dual-pack / algebra pins drifted
|
||||
# when cli.py lagged cli_test.TEST_SUITES (full-suite failure 2026-07-20).
|
||||
from core.cli_test import TEST_SUITES as _TEST_SUITES # noqa: E402
|
||||
|
||||
|
||||
|
||||
def _run(*args: str, check: bool = False, cwd: Path | None = None) -> int:
|
||||
|
|
@ -865,6 +721,12 @@ def cmd_teaching_coverage(args: argparse.Namespace) -> int:
|
|||
return cli_teaching.cmd_teaching_coverage(args)
|
||||
|
||||
|
||||
def cmd_teaching_discovery_yield(args: argparse.Namespace) -> int:
|
||||
from core import cli_teaching
|
||||
|
||||
return cli_teaching.cmd_teaching_discovery_yield(args)
|
||||
|
||||
|
||||
def cmd_teaching_refusal_taxonomy(args: argparse.Namespace) -> int:
|
||||
from core import cli_teaching
|
||||
|
||||
|
|
@ -3476,6 +3338,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
)
|
||||
teaching_coverage.set_defaults(func=cmd_teaching_coverage)
|
||||
|
||||
teaching_discovery_yield = teaching_sub.add_parser(
|
||||
"discovery-yield",
|
||||
help="candidates proposed per served turn, on clean post-reset traffic",
|
||||
)
|
||||
teaching_discovery_yield.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="emit machine-readable JSON",
|
||||
)
|
||||
teaching_discovery_yield.set_defaults(func=cmd_teaching_discovery_yield)
|
||||
|
||||
teaching_refusal_taxonomy = teaching_sub.add_parser(
|
||||
"refusal-taxonomy",
|
||||
help="ADR-0163 Phase A — categorise refused statements by shape",
|
||||
|
|
@ -3822,6 +3695,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
|
||||
_register_formation(subparsers)
|
||||
|
||||
from core.cli_proposal_queue import register as _register_proposal_queue
|
||||
|
||||
_register_proposal_queue(subparsers)
|
||||
|
||||
contemplation = subparsers.add_parser(
|
||||
"contemplation",
|
||||
help="run ADR-0080 read-only contemplation over explicit evidence files",
|
||||
|
|
|
|||
214
core/cli_proposal_queue.py
Normal file
214
core/cli_proposal_queue.py
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
"""core/cli_proposal_queue.py — ``core proposal-queue`` CLI (Tier S4).
|
||||
|
||||
Operator-facing surface over :mod:`core.proposal_review.queue`: list and
|
||||
review pending proposals from the contemplation/idle sinks
|
||||
(``comprehension_failures``, ``derived_close_facts``). Read + review-state
|
||||
transitions ONLY — no command here can ratify, mutate the teaching corpus,
|
||||
mount anything, or flip a flag. Ratification stays ``core teaching
|
||||
proposals`` / ``core teaching review`` over the separate ADR-0057 sink.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from dataclasses import asdict
|
||||
|
||||
from core.proposal_review.queue import (
|
||||
SINKS,
|
||||
QueueEntry,
|
||||
record_review,
|
||||
reviewed_keys,
|
||||
scan_all,
|
||||
queue_status,
|
||||
)
|
||||
|
||||
_SINK_NAMES = tuple(spec.name for spec in SINKS)
|
||||
|
||||
|
||||
def _print_entry(entry: QueueEntry, *, reviewed: bool) -> None:
|
||||
mark = "reviewed" if reviewed else ("pending" if entry.requires_review else "no-review-needed")
|
||||
print(f"[{entry.sink}] {entry.content_hash} {mark:<17} status={entry.status} family={entry.family}")
|
||||
|
||||
|
||||
def cmd_proposal_queue_status(args: argparse.Namespace) -> int:
|
||||
sinks = (args.sink,) if args.sink else None
|
||||
status = queue_status(sinks)
|
||||
if args.json:
|
||||
print(json.dumps(status, indent=2, sort_keys=True))
|
||||
return 0
|
||||
for name, counts in status.items():
|
||||
print(
|
||||
f"{name}: total={counts['total']} pending_review={counts['pending_review']} "
|
||||
f"reviewed={counts['reviewed']} malformed={counts['malformed']}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_proposal_queue_list(args: argparse.Namespace) -> int:
|
||||
sinks = (args.sink,) if args.sink else None
|
||||
entries, malformed = scan_all(sinks)
|
||||
reviewed = reviewed_keys()
|
||||
if args.pending_only:
|
||||
entries = [
|
||||
e for e in entries
|
||||
if e.requires_review and (e.sink, e.content_hash) not in reviewed
|
||||
]
|
||||
if args.json:
|
||||
payload = {
|
||||
"entries": [
|
||||
{**asdict(e), "reviewed": (e.sink, e.content_hash) in reviewed}
|
||||
for e in entries
|
||||
],
|
||||
"malformed": [asdict(m) for m in malformed],
|
||||
}
|
||||
print(json.dumps(payload, indent=2, sort_keys=True))
|
||||
return 0
|
||||
if not entries:
|
||||
print("(no pending proposals)")
|
||||
for entry in entries:
|
||||
_print_entry(entry, reviewed=(entry.sink, entry.content_hash) in reviewed)
|
||||
for m in malformed:
|
||||
print(f"[{m.sink}] MALFORMED {m.path}: {m.reason}")
|
||||
return 1 if malformed else 0
|
||||
|
||||
|
||||
def cmd_proposal_queue_show(args: argparse.Namespace) -> int:
|
||||
entries, _malformed = scan_all((args.sink,))
|
||||
match = next((e for e in entries if e.content_hash == args.content_hash), None)
|
||||
if match is None:
|
||||
print(f"error: no entry {args.content_hash!r} in sink {args.sink!r}")
|
||||
return 2
|
||||
with open(match.path, encoding="utf-8") as fh:
|
||||
raw = json.load(fh)
|
||||
print(json.dumps(raw, indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_proposal_queue_review(args: argparse.Namespace) -> int:
|
||||
try:
|
||||
record = record_review(args.sink, args.content_hash, note=args.note or "")
|
||||
except KeyError as exc:
|
||||
print(f"error: {exc}")
|
||||
return 2
|
||||
print(f"recorded review: {args.sink}/{args.content_hash} at {record.reviewed_at}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_proposal_queue_ratify(args: argparse.Namespace) -> int:
|
||||
"""The ratification ceremony (Lane 2) — a reviewed decision becomes artifacts.
|
||||
|
||||
Deliberately a sibling of ``review`` rather than a flag on it: ``review``
|
||||
records that a human looked and must stay incapable of ratifying. This
|
||||
command is the separate, explicit act, and it still ratifies nothing on its
|
||||
own judgement — the operator supplies the edge, the identity, and the
|
||||
rationale, and the ceremony only enforces that the result is *routable*.
|
||||
"""
|
||||
from teaching.ratification import (
|
||||
RatificationError,
|
||||
build_chain_record,
|
||||
ratify_chain,
|
||||
)
|
||||
|
||||
try:
|
||||
record = build_chain_record(
|
||||
domain=args.domain,
|
||||
subject=args.subject,
|
||||
connective=args.connective,
|
||||
obj=args.object,
|
||||
reviewer=args.reviewer,
|
||||
rationale=args.rationale,
|
||||
intent=args.intent,
|
||||
)
|
||||
receipt = ratify_chain(record, dry_run=args.dry_run)
|
||||
except RatificationError as exc:
|
||||
print(f"REFUSED: {exc}")
|
||||
return 1
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(receipt.as_dict(), indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
verb = "would ratify" if args.dry_run else "RATIFIED"
|
||||
print(f"{verb} {receipt.chain.chain_id}: "
|
||||
f"{record.subject} {record.connective} {record.object}")
|
||||
print(f" corpus : {receipt.corpus_id}")
|
||||
print(f" domain chains : {receipt.chains_before} -> {receipt.chains_after}")
|
||||
print(f" {record.operator_family} band : "
|
||||
f"{receipt.family_chains_before} -> {receipt.family_chains_after}")
|
||||
if not args.dry_run:
|
||||
print(f" still pending : {', '.join(receipt.pending_stages)}")
|
||||
return 0
|
||||
|
||||
|
||||
def register(subparsers: argparse._SubParsersAction) -> None:
|
||||
"""Attach the ``core proposal-queue`` subcommand tree to a top-level parser."""
|
||||
queue = subparsers.add_parser(
|
||||
"proposal-queue",
|
||||
help="list and review pending contemplation/idle proposals (HITL, read-only)",
|
||||
description=(
|
||||
"Read-only listing plus human-review-state tracking over the "
|
||||
"comprehension_failures and derived_close_facts proposal sinks. "
|
||||
"Never ratifies, mutates the teaching corpus, mounts anything, or "
|
||||
"flips a flag — that stays `core teaching proposals`/`review`."
|
||||
),
|
||||
)
|
||||
sub = queue.add_subparsers(
|
||||
dest="proposal_queue_command",
|
||||
metavar="proposal-queue-command",
|
||||
required=True,
|
||||
)
|
||||
|
||||
status = sub.add_parser("status", help="per-sink pending/reviewed/malformed counts")
|
||||
status.add_argument("--sink", choices=_SINK_NAMES, default=None)
|
||||
status.add_argument("--json", action="store_true")
|
||||
status.set_defaults(func=cmd_proposal_queue_status)
|
||||
|
||||
list_cmd = sub.add_parser("list", help="list proposal-queue entries")
|
||||
list_cmd.add_argument("--sink", choices=_SINK_NAMES, default=None)
|
||||
list_cmd.add_argument(
|
||||
"--pending-only", action="store_true",
|
||||
help="only entries that require review and have no review record yet",
|
||||
)
|
||||
list_cmd.add_argument("--json", action="store_true")
|
||||
list_cmd.set_defaults(func=cmd_proposal_queue_list)
|
||||
|
||||
show = sub.add_parser("show", help="print one entry's full artifact JSON")
|
||||
show.add_argument("sink", choices=_SINK_NAMES)
|
||||
show.add_argument("content_hash")
|
||||
show.set_defaults(func=cmd_proposal_queue_show)
|
||||
|
||||
review = sub.add_parser(
|
||||
"review",
|
||||
help="record that a human reviewed one entry (append-only; no ratification)",
|
||||
)
|
||||
review.add_argument("sink", choices=_SINK_NAMES)
|
||||
review.add_argument("content_hash")
|
||||
review.add_argument("--note", default=None, help="optional free-text note")
|
||||
review.set_defaults(func=cmd_proposal_queue_review)
|
||||
|
||||
ratify = sub.add_parser(
|
||||
"ratify",
|
||||
help="ratify one reviewed edge into the domain chain corpus (writes)",
|
||||
description=(
|
||||
"The ratification ceremony: validate -> append -> CONFIRM the "
|
||||
"curriculum loader admits the chain -> receipt. Refuses, and rolls "
|
||||
"back, if the appended row would be silently dropped. Does not "
|
||||
"write a ledger (bridge rule 1) or queue an arena entry; the "
|
||||
"receipt names those stages."
|
||||
),
|
||||
)
|
||||
ratify.add_argument("domain")
|
||||
ratify.add_argument("subject")
|
||||
ratify.add_argument("connective")
|
||||
ratify.add_argument("object")
|
||||
ratify.add_argument("--reviewer", required=True, help="who ratified this")
|
||||
ratify.add_argument("--rationale", required=True, help="why it is ratified")
|
||||
ratify.add_argument("--intent", default="cause")
|
||||
ratify.add_argument("--dry-run", action="store_true",
|
||||
help="report the band delta without writing")
|
||||
ratify.add_argument("--json", action="store_true")
|
||||
ratify.set_defaults(func=cmd_proposal_queue_ratify)
|
||||
|
||||
|
||||
__all__ = ["register"]
|
||||
|
|
@ -1123,6 +1123,46 @@ def cmd_teaching_coverage(args: argparse.Namespace) -> int:
|
|||
return 0
|
||||
|
||||
|
||||
def cmd_teaching_discovery_yield(args: argparse.Namespace) -> int:
|
||||
"""2026-07-23 directive — candidates proposed per served turn, post-reset.
|
||||
|
||||
Pure read of the live ``engine_state`` checkpoint (or
|
||||
``CORE_ENGINE_STATE_DIR`` if set). Fails closed with a clear message
|
||||
(exit 1) when no ``turn_count_baseline`` has ever been stamped —
|
||||
see ``scripts/ops/stamp_discovery_yield_baseline_20260723.py``.
|
||||
"""
|
||||
from engine_state import EngineStateStore
|
||||
from teaching.discovery_yield import compute_discovery_yield
|
||||
|
||||
store = EngineStateStore()
|
||||
result = compute_discovery_yield(store)
|
||||
if result is None:
|
||||
print(
|
||||
"No discovery-yield baseline stamped yet — the manifest has no "
|
||||
"turn_count_baseline. Run "
|
||||
"scripts/ops/stamp_discovery_yield_baseline_20260723.py once "
|
||||
"against the target store.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(result.as_dict(), indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
print(f"turn_count : {result.turn_count}")
|
||||
print(f"turn_count_baseline : {result.turn_count_baseline}")
|
||||
print(f"served_turns_since_reset : {result.served_turns_since_reset}")
|
||||
print(f"candidates_since_reset : {result.candidates_since_reset}")
|
||||
rate = (
|
||||
f"{result.yield_rate:.4f}"
|
||||
if result.yield_rate is not None
|
||||
else "n/a (no served turns since reset)"
|
||||
)
|
||||
print(f"yield_rate : {rate}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_teaching_refusal_taxonomy(args: argparse.Namespace) -> int:
|
||||
"""ADR-0163 Phase A — categorise refused statements by shape.
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,47 @@ TEST_SUITES: dict[str, tuple[str, ...]] = {
|
|||
# the falsifiability claim blocks-on-regression rather than
|
||||
# detect-after-merge.
|
||||
"tests/test_pack_measurements_phase2.py",
|
||||
# ADR-0253 dual-pack boundary — draft he/grc trees must not be serve imports.
|
||||
"tests/test_pack_draft_serve_boundary.py",
|
||||
# Register axis (ADR-0069/0071/0077) — the e2e tests here are the
|
||||
# falsifiable contract that terse/convivial registers actually differ
|
||||
# from neutral on pipeline-served surfaces. Red on main outside every
|
||||
# gate for 2026-07-20..24 (the register-axis serving regression,
|
||||
# docs/research/lane-drift-investigation-2026-07-24.md) because this
|
||||
# file lived only under `full`; the lane-shas job was the sole gate
|
||||
# that runs the demo lanes this axis shows up in, and its failures
|
||||
# were conflated with an unrelated flake. Promoted so this axis can
|
||||
# never silently regress again.
|
||||
"tests/test_register_substantive_consumption.py",
|
||||
# Deduction serving is LIVE (ADR-0256 ratified the flag ON), so its
|
||||
# cross-stack provenance contract is a serving-path contract, not a
|
||||
# capability-under-development one. Workbench's live chat route builds
|
||||
# a bare ChatRuntime(), which means these composers decide Workbench
|
||||
# turns too — and while their labels were unregistered the API coerced
|
||||
# them to "none" and recorded a proved answer as ungrounded. ~3s.
|
||||
"tests/test_workbench_deduction_provenance.py",
|
||||
# Correction binding anchors to hash_surface, in lockstep with every
|
||||
# substantive override of the served surface. Cheap, and the axis is
|
||||
# invisible until it breaks silently.
|
||||
"tests/test_prior_surface_deduction_binding.py",
|
||||
# ADR governance. An ADR that governs a default-ON flag records a
|
||||
# decision already in force, so leaving it "Proposed" makes the
|
||||
# governance record assert something false about the running system —
|
||||
# which is what happened to ADR-0256 while deduction serving was live.
|
||||
# Filesystem + regex only, ~1.2s, and it is the record for a serving
|
||||
# path, so it belongs on the pre-push gate rather than under `full`.
|
||||
# test_adr_index.py rides along: it landed in #113 registered in NO
|
||||
# curated suite, which is the same silent-red shape it exists to catch.
|
||||
"tests/test_adr_status_governance.py",
|
||||
"tests/test_adr_index.py",
|
||||
# Volume honesty (ADR-0264 R9). The Wilson floor assumes independent
|
||||
# trials; a deterministic pipeline replaying one case N times supplies
|
||||
# one trial's evidence, not N. This pins the measured exposure in the
|
||||
# ratified deduction ledger (21 of 25 bands short on distinct evidence)
|
||||
# in BOTH directions, and forces any new licensed capability to register
|
||||
# an audit source. It gates a live, flag-ON serving path, so it belongs
|
||||
# on the pre-push gate. ~1.5s.
|
||||
"tests/test_volume_honesty.py",
|
||||
),
|
||||
"runtime": (
|
||||
"tests/test_chat_runtime.py",
|
||||
|
|
@ -73,8 +114,15 @@ TEST_SUITES: dict[str, tuple[str, ...]] = {
|
|||
"tests/test_math_frame_ratification.py",
|
||||
"tests/test_math_composition_ratification.py",
|
||||
"tests/test_teaching_coverage_cli.py",
|
||||
"tests/test_proposal_queue.py",
|
||||
# The ratification ceremony is the only path that turns a reviewed
|
||||
# proposal into a routable ratified chain — i.e. the one mechanism that
|
||||
# can move the curriculum-volume constraint. It landed in #113 in NO
|
||||
# curated suite, so its 14 tests ran only under `full`.
|
||||
"tests/test_ratification_ceremony.py",
|
||||
),
|
||||
"packs": (
|
||||
"tests/test_pack_draft_serve_boundary.py",
|
||||
"tests/test_core_semantic_seed_pack.py",
|
||||
"tests/test_adr_0127_pack_ratification.py",
|
||||
"tests/test_frame_registry_load.py",
|
||||
|
|
@ -95,6 +143,8 @@ TEST_SUITES: dict[str, tuple[str, ...]] = {
|
|||
"tests/test_wave_a_multiplicative_aggregation_injector.py",
|
||||
),
|
||||
"algebra": (
|
||||
"tests/test_stage2_physics_hardening.py",
|
||||
"tests/test_geometric_convergence_checklist.py",
|
||||
"tests/test_versor_closure.py",
|
||||
"tests/test_holonomy.py",
|
||||
"tests/test_holonomy_resonance.py",
|
||||
|
|
@ -167,7 +217,21 @@ TEST_SUITES: dict[str, tuple[str, ...]] = {
|
|||
# exit criterion. ``wrong == 0`` is a hard gate (Obligation #4: refuse
|
||||
# rather than confabulate).
|
||||
"math": ("tests/test_adr_0126_train_sample_runner.py",),
|
||||
"deductive": ("tests/test_deductive_logic_entail.py",),
|
||||
"deductive": (
|
||||
"tests/test_deductive_logic_entail.py",
|
||||
"tests/test_deduction_serve_lane.py",
|
||||
"tests/test_deduction_serve_license.py",
|
||||
"tests/test_categorical_decider.py",
|
||||
"tests/test_english_argument_reader.py",
|
||||
"tests/test_member_argument_reader.py",
|
||||
"tests/test_cond_member_argument_reader.py",
|
||||
"tests/test_verb_argument_reader.py",
|
||||
"tests/test_exist_argument_reader.py",
|
||||
"tests/test_deduction_serve_e2e.py",
|
||||
"tests/test_curriculum_serve.py",
|
||||
"tests/test_ratified_ledger_bridge.py",
|
||||
"tests/test_vocab_trigger_instrument.py",
|
||||
),
|
||||
"full": ("tests/",),
|
||||
}
|
||||
|
||||
|
|
|
|||
256
core/cognition/fail_closed.py
Normal file
256
core/cognition/fail_closed.py
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
"""Typed fail-closed outcomes for geometry / coherence / contract gates.
|
||||
|
||||
These types are the only admissible non-answer results on answer-authority
|
||||
paths. Silent defaults and heuristic fills are forbidden when the field
|
||||
cannot close.
|
||||
|
||||
Fields required by linguistic governance Phase 1:
|
||||
failure_class, violated_condition, residual_state, refusal_reason
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any, Mapping
|
||||
|
||||
|
||||
class FailureClass(str, Enum):
|
||||
"""Machine-stable failure taxonomy for answer-authority paths."""
|
||||
|
||||
COHERENCE = "coherence"
|
||||
CONTRACT = "contract"
|
||||
FIELD = "field"
|
||||
CONSTRAINT = "constraint"
|
||||
MISSING_REFERENT = "missing_referent"
|
||||
AMBIGUITY = "ambiguity"
|
||||
ARTICULATION = "articulation"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ResidualState:
|
||||
"""Optional residual snapshot when a gate refuses.
|
||||
|
||||
Coordinates are never required; digests / scalar residuals preferred.
|
||||
"""
|
||||
|
||||
versor_condition: float | None = None
|
||||
goldtether_residual: float | None = None
|
||||
missing_bindings: tuple[str, ...] = ()
|
||||
unresolved_hazards: tuple[str, ...] = ()
|
||||
detail: str = ""
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"versor_condition": self.versor_condition,
|
||||
"goldtether_residual": self.goldtether_residual,
|
||||
"missing_bindings": list(self.missing_bindings),
|
||||
"unresolved_hazards": list(self.unresolved_hazards),
|
||||
"detail": self.detail,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FieldFailure:
|
||||
"""Geometry / field construction could not produce a closed state."""
|
||||
|
||||
failure_class: FailureClass
|
||||
violated_condition: str
|
||||
residual_state: ResidualState | None
|
||||
refusal_reason: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.violated_condition:
|
||||
raise ValueError("FieldFailure.violated_condition must be non-empty")
|
||||
if not self.refusal_reason:
|
||||
raise ValueError("FieldFailure.refusal_reason must be non-empty")
|
||||
if not isinstance(self.failure_class, FailureClass):
|
||||
raise TypeError("FieldFailure.failure_class must be a FailureClass")
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"kind": "field_failure",
|
||||
"failure_class": self.failure_class.value,
|
||||
"violated_condition": self.violated_condition,
|
||||
"residual_state": None
|
||||
if self.residual_state is None
|
||||
else self.residual_state.as_dict(),
|
||||
"refusal_reason": self.refusal_reason,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CoherenceRefusal:
|
||||
"""No admissible configuration — typed abstention, not a default answer."""
|
||||
|
||||
failure_class: FailureClass
|
||||
violated_condition: str
|
||||
residual_state: ResidualState | None
|
||||
refusal_reason: str
|
||||
surface_message: str = ""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.violated_condition:
|
||||
raise ValueError("CoherenceRefusal.violated_condition must be non-empty")
|
||||
if not self.refusal_reason:
|
||||
raise ValueError("CoherenceRefusal.refusal_reason must be non-empty")
|
||||
if not isinstance(self.failure_class, FailureClass):
|
||||
raise TypeError("CoherenceRefusal.failure_class must be a FailureClass")
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
if self.surface_message:
|
||||
return self.surface_message
|
||||
return (
|
||||
f"Abstaining: {self.refusal_reason} "
|
||||
f"(condition={self.violated_condition})."
|
||||
)
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"kind": "coherence_refusal",
|
||||
"failure_class": self.failure_class.value,
|
||||
"violated_condition": self.violated_condition,
|
||||
"residual_state": None
|
||||
if self.residual_state is None
|
||||
else self.residual_state.as_dict(),
|
||||
"refusal_reason": self.refusal_reason,
|
||||
"surface_message": self.surface_message,
|
||||
"message": self.message,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ContractViolation:
|
||||
"""Contract assessment missing or structurally incomplete — never silent pass."""
|
||||
|
||||
failure_class: FailureClass
|
||||
violated_condition: str
|
||||
residual_state: ResidualState | None
|
||||
refusal_reason: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.failure_class is not FailureClass.CONTRACT:
|
||||
raise ValueError("ContractViolation.failure_class must be CONTRACT")
|
||||
if not self.violated_condition:
|
||||
raise ValueError("ContractViolation.violated_condition must be non-empty")
|
||||
if not self.refusal_reason:
|
||||
raise ValueError("ContractViolation.refusal_reason must be non-empty")
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"kind": "contract_violation",
|
||||
"failure_class": self.failure_class.value,
|
||||
"violated_condition": self.violated_condition,
|
||||
"residual_state": None
|
||||
if self.residual_state is None
|
||||
else self.residual_state.as_dict(),
|
||||
"refusal_reason": self.refusal_reason,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ConstraintViolation:
|
||||
"""A typed semantic constraint could not be satisfied."""
|
||||
|
||||
failure_class: FailureClass
|
||||
violated_condition: str
|
||||
residual_state: ResidualState | None
|
||||
refusal_reason: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.violated_condition:
|
||||
raise ValueError("ConstraintViolation.violated_condition must be non-empty")
|
||||
if not self.refusal_reason:
|
||||
raise ValueError("ConstraintViolation.refusal_reason must be non-empty")
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"kind": "constraint_violation",
|
||||
"failure_class": self.failure_class.value,
|
||||
"violated_condition": self.violated_condition,
|
||||
"residual_state": None
|
||||
if self.residual_state is None
|
||||
else self.residual_state.as_dict(),
|
||||
"refusal_reason": self.refusal_reason,
|
||||
}
|
||||
|
||||
|
||||
def contract_assessment_none_violation() -> ContractViolation:
|
||||
"""Canonical violation when assessment is absent on an answer-authority path."""
|
||||
return ContractViolation(
|
||||
failure_class=FailureClass.CONTRACT,
|
||||
violated_condition="contract_assessment_present",
|
||||
residual_state=ResidualState(
|
||||
detail="contract_assessment is None — geometric conjugate cannot pass"
|
||||
),
|
||||
refusal_reason=(
|
||||
"contract_assessment is None; substrate and certified answers are refused"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def open_geometry_refusal(
|
||||
*,
|
||||
missing_bindings: tuple[str, ...] = (),
|
||||
unresolved_hazards: tuple[str, ...] = (),
|
||||
explanation: str = "",
|
||||
versor_condition: float | None = None,
|
||||
goldtether_residual: float | None = None,
|
||||
) -> CoherenceRefusal:
|
||||
"""Typed abstention when versor / GoldTether / binding contract is open."""
|
||||
conditions: list[str] = []
|
||||
if missing_bindings:
|
||||
conditions.extend(missing_bindings)
|
||||
if unresolved_hazards:
|
||||
conditions.extend(unresolved_hazards)
|
||||
violated = "|".join(conditions) if conditions else "geometric_contract_closed"
|
||||
return CoherenceRefusal(
|
||||
failure_class=FailureClass.COHERENCE,
|
||||
violated_condition=violated,
|
||||
residual_state=ResidualState(
|
||||
versor_condition=versor_condition,
|
||||
goldtether_residual=goldtether_residual,
|
||||
missing_bindings=missing_bindings,
|
||||
unresolved_hazards=unresolved_hazards,
|
||||
detail=explanation,
|
||||
),
|
||||
refusal_reason=(
|
||||
"field geometric contract is not closed; no certified answer is available"
|
||||
),
|
||||
surface_message=(
|
||||
"I cannot certify an answer: the geometric coherence contract is open "
|
||||
f"({violated})."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def residual_from_mapping(data: Mapping[str, Any] | None) -> ResidualState | None:
|
||||
if not data:
|
||||
return None
|
||||
return ResidualState(
|
||||
versor_condition=_opt_float(data.get("versor_condition")),
|
||||
goldtether_residual=_opt_float(data.get("goldtether_residual")),
|
||||
missing_bindings=tuple(data.get("missing_bindings") or ()),
|
||||
unresolved_hazards=tuple(data.get("unresolved_hazards") or ()),
|
||||
detail=str(data.get("detail") or ""),
|
||||
)
|
||||
|
||||
|
||||
def _opt_float(value: Any) -> float | None:
|
||||
if value is None:
|
||||
return None
|
||||
return float(value)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"FailureClass",
|
||||
"ResidualState",
|
||||
"FieldFailure",
|
||||
"CoherenceRefusal",
|
||||
"ContractViolation",
|
||||
"ConstraintViolation",
|
||||
"contract_assessment_none_violation",
|
||||
"open_geometry_refusal",
|
||||
"residual_from_mapping",
|
||||
]
|
||||
118
core/cognition/geometric_coherence.py
Normal file
118
core/cognition/geometric_coherence.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
"""Turn-level geometric coherence verdict (Master Blueprint Stage 3A).
|
||||
|
||||
Ownership model (deliberate dual taxonomy — do NOT collapse names):
|
||||
|
||||
* ``teaching.epistemic.EpistemicStatus`` — vault / pack durable standing
|
||||
(SPECULATIVE | COHERENT | CONTESTED | FALSIFIED). Mutation only via
|
||||
``vault/store.py`` (INV-29).
|
||||
* ``core.epistemic_state.EpistemicState`` — turn / surface taxonomy for
|
||||
dialogue observability (perceived, verified, decoded, …). **No** member
|
||||
named COHERENT — vault COHERENT maps to DECODED via
|
||||
``epistemic_state_for_vault_status``.
|
||||
|
||||
This module adds a third, geometry-native axis: whether the *field* closed
|
||||
under versor + GoldTether residual checks on this turn. It never renames
|
||||
EpistemicState and never stamps vault COHERENT by itself.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from algebra.backend import versor_condition
|
||||
from core.physics.goldtether import coherence_residual
|
||||
|
||||
_CLOSURE = 1e-6
|
||||
|
||||
|
||||
class GeometricCoherenceStatus(str, Enum):
|
||||
"""Turn-level geometric standing — orthogonal to vault EpistemicStatus."""
|
||||
|
||||
GEOMETRICALLY_VERIFIED = "geometrically_verified"
|
||||
UNVERIFIED = "unverified"
|
||||
REFUSED = "refused"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class GeometricCoherenceVerdict:
|
||||
"""Pipeline-visible geometric coherence for one turn.
|
||||
|
||||
``GEOMETRICALLY_VERIFIED`` requires:
|
||||
* field present
|
||||
* versor_condition(F) < 1e-6
|
||||
* R_GoldTether ≤ 1e-6
|
||||
Identity leakage flags are observational while identity_wave_gate is off
|
||||
(ADR-0244 honest scope) and do not alone refuse this verdict unless
|
||||
``boundary_violations`` is non-empty.
|
||||
"""
|
||||
|
||||
status: GeometricCoherenceStatus
|
||||
versor_condition: float
|
||||
goldtether_residual: float
|
||||
field_present: bool
|
||||
identity_boundary_breach: bool = False
|
||||
detail: str = ""
|
||||
|
||||
@property
|
||||
def closed(self) -> bool:
|
||||
return self.status is GeometricCoherenceStatus.GEOMETRICALLY_VERIFIED
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"status": self.status.value,
|
||||
"closed": self.closed,
|
||||
"versor_condition": float(self.versor_condition),
|
||||
"goldtether_residual": float(self.goldtether_residual),
|
||||
"field_present": bool(self.field_present),
|
||||
"identity_boundary_breach": bool(self.identity_boundary_breach),
|
||||
"detail": self.detail,
|
||||
}
|
||||
|
||||
|
||||
def evaluate_geometric_coherence(
|
||||
F,
|
||||
*,
|
||||
identity_score=None,
|
||||
epsilon: float = _CLOSURE,
|
||||
) -> GeometricCoherenceVerdict:
|
||||
"""Compute turn geometric coherence from the live field versor."""
|
||||
if F is None:
|
||||
return GeometricCoherenceVerdict(
|
||||
status=GeometricCoherenceStatus.REFUSED,
|
||||
versor_condition=float("inf"),
|
||||
goldtether_residual=float("inf"),
|
||||
field_present=False,
|
||||
detail="missing_wave_field",
|
||||
)
|
||||
arr = np.asarray(F, dtype=np.float64)
|
||||
vc = float(versor_condition(arr))
|
||||
r_gt = float(coherence_residual(arr))
|
||||
boundary = bool(getattr(identity_score, "boundary_violations", ()) or ())
|
||||
if boundary:
|
||||
return GeometricCoherenceVerdict(
|
||||
status=GeometricCoherenceStatus.REFUSED,
|
||||
versor_condition=vc,
|
||||
goldtether_residual=r_gt,
|
||||
field_present=True,
|
||||
identity_boundary_breach=True,
|
||||
detail="identity_boundary_breach",
|
||||
)
|
||||
if vc >= float(epsilon) or r_gt > float(epsilon):
|
||||
return GeometricCoherenceVerdict(
|
||||
status=GeometricCoherenceStatus.UNVERIFIED,
|
||||
versor_condition=vc,
|
||||
goldtether_residual=r_gt,
|
||||
field_present=True,
|
||||
detail=f"versor_condition={vc:.3e}; R_GoldTether={r_gt:.3e}",
|
||||
)
|
||||
return GeometricCoherenceVerdict(
|
||||
status=GeometricCoherenceStatus.GEOMETRICALLY_VERIFIED,
|
||||
versor_condition=vc,
|
||||
goldtether_residual=r_gt,
|
||||
field_present=True,
|
||||
detail="versor_and_goldtether_closed",
|
||||
)
|
||||
243
core/cognition/inductive_closure.py
Normal file
243
core/cognition/inductive_closure.py
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
"""Bounded multi-step inductive closure over teaching-store relations (Stage 3C).
|
||||
|
||||
Fixed-point expansion of same-relation chains with explicit budgets,
|
||||
cycle handling, contradiction detection, geometric admissibility, and
|
||||
replayable provenance.
|
||||
|
||||
Atom *identity* for entailment telemetry remains conformal
|
||||
(``CognitiveTurnPipeline._proof_atom``). This module expands the *relation
|
||||
graph* of surface triples from ``TeachingStore.triples()`` into a closed
|
||||
set under transitive composition of equal relation labels — not string
|
||||
atom join as final identity authority.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Iterable, Sequence
|
||||
|
||||
_DEFAULT_BUDGET = 16
|
||||
|
||||
# geometric_admissible(head, relation, tail) -> bool
|
||||
GeometricAdmissibleFn = Callable[[str, str, str], bool]
|
||||
|
||||
|
||||
def _norm(token: str) -> str:
|
||||
return token.strip().lower()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DerivedRelation:
|
||||
"""One promoted (or base) triple with provenance path."""
|
||||
|
||||
head: str
|
||||
relation: str
|
||||
tail: str
|
||||
path: tuple[str, ...] # entity path head … tail
|
||||
step: int # 0 = base fact from store; >0 = derived at fixed-point step
|
||||
admissible: bool = True
|
||||
contradiction: bool = False
|
||||
|
||||
def as_triple(self) -> tuple[str, str, str]:
|
||||
return (self.head, self.relation, self.tail)
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"head": self.head,
|
||||
"relation": self.relation,
|
||||
"tail": self.tail,
|
||||
"path": list(self.path),
|
||||
"step": self.step,
|
||||
"admissible": self.admissible,
|
||||
"contradiction": self.contradiction,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class InductiveClosureResult:
|
||||
"""Result of bounded fixed-point expansion."""
|
||||
|
||||
base: tuple[DerivedRelation, ...]
|
||||
derived: tuple[DerivedRelation, ...]
|
||||
contradictions: tuple[DerivedRelation, ...]
|
||||
steps_taken: int
|
||||
budget: int
|
||||
fixed_point: bool
|
||||
truncated: bool
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"base": [r.as_dict() for r in self.base],
|
||||
"derived": [r.as_dict() for r in self.derived],
|
||||
"contradictions": [r.as_dict() for r in self.contradictions],
|
||||
"steps_taken": self.steps_taken,
|
||||
"budget": self.budget,
|
||||
"fixed_point": self.fixed_point,
|
||||
"truncated": self.truncated,
|
||||
"n_derived": len(self.derived),
|
||||
"n_admissible_derived": sum(1 for d in self.derived if d.admissible),
|
||||
}
|
||||
|
||||
|
||||
def default_geometric_admissible(head: str, relation: str, tail: str) -> bool:
|
||||
"""Strict default: a derived relation is admissible only if endpoints are
|
||||
non-empty distinct tokens and the relation is non-empty. Callers that
|
||||
have a vocab/field resolver should pass a stronger callback that checks
|
||||
closed Cl(4,1) versors (see pipeline wiring).
|
||||
"""
|
||||
del relation
|
||||
h, t = head.strip(), tail.strip()
|
||||
return bool(h) and bool(t) and h != t
|
||||
|
||||
|
||||
def expand_relation_closure(
|
||||
triples: Sequence[tuple[str, str, str]],
|
||||
*,
|
||||
budget: int = _DEFAULT_BUDGET,
|
||||
relations: Iterable[str] | None = None,
|
||||
geometric_admissible: GeometricAdmissibleFn | None = None,
|
||||
) -> InductiveClosureResult:
|
||||
"""Compute same-relation transitive closure with budget and contradictions.
|
||||
|
||||
Rules:
|
||||
* Base facts: each input triple (normalized).
|
||||
* Step k+1: if (a,r,b) and (b,r,c) known and a≠c and (a,r,c) unknown,
|
||||
derive (a,r,c) with path a…c.
|
||||
* Cycle: if path would revisit a node, skip (no infinite loop).
|
||||
* Contradiction: two different tails for the same (head, relation)
|
||||
among base facts mark contradiction=True.
|
||||
* Geometric admissibility (Stage 3 exit gate): a candidate edge is
|
||||
*promoted* into the expansion frontier (``work``) and into
|
||||
``derived`` **only** when ``geometric_admissible(h,r,t)`` is True.
|
||||
Non-admissible candidates are excluded — they must not seed further
|
||||
fixed-point steps (no ``admissible=False`` intermediates that still
|
||||
expand). Base store facts remain visible in ``base`` with their
|
||||
admissibility stamp, but only admissible base edges enter ``work``.
|
||||
Default requires non-empty distinct endpoints; pipeline supplies
|
||||
versor-grounded checks when session vocab is available.
|
||||
* Termination: no new triples or budget exhausted.
|
||||
"""
|
||||
if budget < 1:
|
||||
raise ValueError("budget must be >= 1")
|
||||
|
||||
geom = geometric_admissible or default_geometric_admissible
|
||||
rel_filter = None if relations is None else {_norm(r) for r in relations}
|
||||
|
||||
base_list: list[DerivedRelation] = []
|
||||
edge_map: dict[tuple[str, str], set[str]] = {}
|
||||
known: set[tuple[str, str, str]] = set()
|
||||
|
||||
for h, r, t in triples:
|
||||
hn, rn, tn = _norm(h), _norm(r), _norm(t)
|
||||
if not hn or not rn or not tn:
|
||||
continue
|
||||
if rel_filter is not None and rn not in rel_filter:
|
||||
continue
|
||||
key3 = (hn, rn, tn)
|
||||
if key3 in known:
|
||||
continue
|
||||
known.add(key3)
|
||||
edge_map.setdefault((hn, rn), set()).add(tn)
|
||||
# Base facts are store-grounded; admissible if geometric check passes.
|
||||
base_list.append(
|
||||
DerivedRelation(
|
||||
head=hn,
|
||||
relation=rn,
|
||||
tail=tn,
|
||||
path=(hn, tn),
|
||||
step=0,
|
||||
admissible=bool(geom(hn, rn, tn)),
|
||||
)
|
||||
)
|
||||
|
||||
contradictions: list[DerivedRelation] = []
|
||||
for (h, r), tails in edge_map.items():
|
||||
if len(tails) > 1:
|
||||
for dr in base_list:
|
||||
if dr.head == h and dr.relation == r:
|
||||
contradictions.append(
|
||||
DerivedRelation(
|
||||
head=dr.head,
|
||||
relation=dr.relation,
|
||||
tail=dr.tail,
|
||||
path=dr.path,
|
||||
step=0,
|
||||
admissible=False,
|
||||
contradiction=True,
|
||||
)
|
||||
)
|
||||
|
||||
derived: list[DerivedRelation] = []
|
||||
# Frontier is geometry-gated: non-admissible base never seeds expansion.
|
||||
work: set[tuple[str, str, str]] = {
|
||||
dr.as_triple() for dr in base_list if dr.admissible
|
||||
}
|
||||
steps_taken = 0
|
||||
fixed_point = False
|
||||
truncated = False
|
||||
|
||||
for step in range(1, budget + 1):
|
||||
steps_taken = step
|
||||
new_edges: list[DerivedRelation] = []
|
||||
by_hr: dict[tuple[str, str], list[str]] = {}
|
||||
for h, r, t in work:
|
||||
by_hr.setdefault((h, r), []).append(t)
|
||||
|
||||
for (a, r), mids in by_hr.items():
|
||||
for b in mids:
|
||||
for c in by_hr.get((b, r), ()):
|
||||
if a == c:
|
||||
continue
|
||||
key3 = (a, r, c)
|
||||
if key3 in work:
|
||||
continue
|
||||
# Promote only when geometrically admissible — never
|
||||
# park an inadmissible edge in work to seed hop k+1.
|
||||
if not bool(geom(a, r, c)):
|
||||
continue
|
||||
path = (a, b, c)
|
||||
new_edges.append(
|
||||
DerivedRelation(
|
||||
head=a,
|
||||
relation=r,
|
||||
tail=c,
|
||||
path=path,
|
||||
step=step,
|
||||
admissible=True,
|
||||
contradiction=False,
|
||||
)
|
||||
)
|
||||
|
||||
if not new_edges:
|
||||
fixed_point = True
|
||||
steps_taken = step - 1 if step > 0 else 0
|
||||
break
|
||||
|
||||
for dr in new_edges:
|
||||
key3 = dr.as_triple()
|
||||
if key3 in work:
|
||||
continue
|
||||
work.add(key3)
|
||||
edge_map.setdefault((dr.head, dr.relation), set()).add(dr.tail)
|
||||
derived.append(dr)
|
||||
else:
|
||||
truncated = True
|
||||
by_hr = {}
|
||||
for h, r, t in work:
|
||||
by_hr.setdefault((h, r), []).append(t)
|
||||
for (a, r), mids in by_hr.items():
|
||||
for b in mids:
|
||||
for c in by_hr.get((b, r), ()):
|
||||
if a != c and (a, r, c) not in work:
|
||||
truncated = True
|
||||
break
|
||||
|
||||
return InductiveClosureResult(
|
||||
base=tuple(base_list),
|
||||
derived=tuple(derived),
|
||||
contradictions=tuple(contradictions),
|
||||
steps_taken=steps_taken,
|
||||
budget=budget,
|
||||
fixed_point=fixed_point and not truncated,
|
||||
truncated=truncated,
|
||||
)
|
||||
|
|
@ -15,14 +15,23 @@ Constraint: ChatRuntime.chat() and ChatResponse contract are unchanged.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
|
||||
import numpy as np
|
||||
|
||||
from algebra.backend import versor_condition
|
||||
from algebra.cl41 import geometric_product, reverse, scalar_part
|
||||
from field.state import FieldState
|
||||
from core.cognition.geometric_coherence import evaluate_geometric_coherence
|
||||
from core.cognition.inductive_closure import expand_relation_closure
|
||||
from core.cognition.leeway import build_leeway_record
|
||||
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.physics.goldtether import coherence_residual
|
||||
from core.physics.wave_manifold import multivector_content_digest
|
||||
from core.reasoning.adapters import evidence_from_entailment_trace
|
||||
from generate.intent import classify_compound_intent
|
||||
from generate.intent_bridge import _is_useful_surface
|
||||
|
|
@ -92,6 +101,13 @@ _SUBJECT_STOPWORDS: frozenset[str] = frozenset({
|
|||
"your", "their", "answer",
|
||||
})
|
||||
|
||||
# Conformal atom unification (dossier Subsystem C.1).
|
||||
# Absolute "score > 1−ε" is only meaningful for normalized null-cone points
|
||||
# with self-inner ≈ 1. Pack versors can have cross-inner > 1, so we unify when
|
||||
# the mutual reverse-product matches both self-products within ε (exact same
|
||||
# geometric atom), else content-address by SHA-256 of components.
|
||||
_UNIFY_EPS = 1e-4
|
||||
|
||||
# Finding 5 (audit 2026-05-20) — cap the speculative-subjects cache so a
|
||||
# long teaching session cannot grow it without bound. 64 is large enough
|
||||
# to cover every distinct teaching subject a single session realistically
|
||||
|
|
@ -101,17 +117,6 @@ _SUBJECT_STOPWORDS: frozenset[str] = frozenset({
|
|||
# promotion removes it explicitly.
|
||||
_MAX_SPECULATIVE_SUBJECTS = 64
|
||||
|
||||
# All PASSTHROUGH variants normalised to "passthrough" for trace_hash so
|
||||
# pre-ADR-0144 hashes remain byte-identical after _ratify_intent gains
|
||||
# specific sub-values (ADR-0144 / ADR-0142 §Implementation debts, debt 1).
|
||||
_PASSTHROUGH_OUTCOMES: frozenset[str] = frozenset({
|
||||
"passthrough",
|
||||
"passthrough_no_field",
|
||||
"passthrough_no_vocab",
|
||||
"passthrough_no_versor",
|
||||
})
|
||||
|
||||
|
||||
class CognitiveTurnPipeline:
|
||||
"""Thin pipeline wrapper over ChatRuntime.
|
||||
|
||||
|
|
@ -222,8 +227,12 @@ class CognitiveTurnPipeline:
|
|||
from generate.exhaustion import RefusalReason as _ExhaustionRefusalReason
|
||||
_recognition_refusal_reason = _ExhaustionRefusalReason.RECOGNITION_REFUSED.value
|
||||
|
||||
# 1. LISTEN — capture pre-turn field state
|
||||
# 1. LISTEN — capture pre-turn field state. If absent, compile turn
|
||||
# tokens into an initial Cl(4,1) wave-packet before intent ratification
|
||||
# (Geometric Sovereignty — cold-start must compile a field first).
|
||||
field_state_before: FieldState | None = self._capture_field_state()
|
||||
if field_state_before is None or getattr(field_state_before, "F", None) is None:
|
||||
field_state_before = self._compile_turn_wave_packet(raw_tokens)
|
||||
|
||||
# 1b. CLASSIFY — intent and proposition graph (deterministic, pre-chat)
|
||||
# ADR-0089 Phase C1 (Finding 4, audit 2026-05-20) — run the
|
||||
|
|
@ -398,9 +407,16 @@ class CognitiveTurnPipeline:
|
|||
realized_plan = realize_semantic(target, grounded_graph)
|
||||
effective_graph = grounded_graph
|
||||
|
||||
gate_fired = (
|
||||
response.vault_hits == 0
|
||||
and response.grounding_source not in ("vault", "pack", "teaching")
|
||||
# Physical coherence only (vault_hits bookkeeping is not a gate).
|
||||
# gate_fired True ⇒ residual failure ⇒ substrate realizer refused.
|
||||
field_for_gate = self._capture_field_state()
|
||||
F_gate = getattr(field_for_gate, "F", None) if field_for_gate is not None else None
|
||||
if F_gate is None and field_state_before is not None:
|
||||
F_gate = field_state_before.F
|
||||
contract_assessment = self._geometry_contract_assessment(F_gate)
|
||||
gate_fired = bool(
|
||||
contract_assessment.missing_bindings
|
||||
or contract_assessment.unresolved_hazards
|
||||
)
|
||||
canonical = response.register_canonical_surface
|
||||
pre_decoration = response.pre_decoration_surface
|
||||
|
|
@ -425,15 +441,36 @@ class CognitiveTurnPipeline:
|
|||
):
|
||||
compose_surface = CognitiveTurnPipeline._render_compose_surface(compose_result)
|
||||
|
||||
# Stage 3C — bounded inductive closure over teaching-store relations.
|
||||
# Provenance-preserving fixed-point; derived edges require geometric
|
||||
# admissibility (closed Cl(4,1) versors when vocab can ground them).
|
||||
def _geom_admissible(h: str, r: str, t: str) -> bool:
|
||||
del r
|
||||
hv = self._resolve_surface_versor(h)
|
||||
tv = self._resolve_surface_versor(t)
|
||||
if hv is None or tv is None:
|
||||
# Ungrounded endpoints cannot be promoted as geometric facts.
|
||||
return False
|
||||
return (
|
||||
float(versor_condition(hv)) < 1e-6
|
||||
and float(versor_condition(tv)) < 1e-6
|
||||
)
|
||||
|
||||
inductive_closure = expand_relation_closure(
|
||||
triples,
|
||||
budget=16,
|
||||
geometric_admissible=_geom_admissible,
|
||||
)
|
||||
|
||||
entailment_trace = self._maybe_entailment_trace(intent, triples)
|
||||
|
||||
# === SHADOW COHERENCE GATE WIRING ===
|
||||
# Graph + realizer already executed unconditionally above.
|
||||
# Pass the effective (possibly grounded) graph so the gate can
|
||||
# apply the strict supremacy test. Assessment=None for Phase A
|
||||
# (assessments still live primarily in derivation organs). When
|
||||
# the main spine carries ProblemFrame through the turn, this
|
||||
# becomes the active contract backpressure site.
|
||||
# Dual-competing: substrate supremacy requires fully grounded graph
|
||||
# AND closed geometric contract (versor_condition + GoldTether residual).
|
||||
# Grounding provenance (pack/teaching/vault/oov/…) is read once here and
|
||||
# reused for the OOV telemetry below; the gate consumes it to route an
|
||||
# open-geometry-but-pack-grounded surface to the hedge arm (T13 dec. 2).
|
||||
grounding_src = getattr(response, "grounding_source", "") or ""
|
||||
resolved = resolve_surface(
|
||||
canonical_surface=canonical,
|
||||
pre_decoration_surface=pre_decoration,
|
||||
|
|
@ -445,17 +482,65 @@ class CognitiveTurnPipeline:
|
|||
walk_surface=walk_surface,
|
||||
compose_surface=compose_surface,
|
||||
proposition_graph=effective_graph,
|
||||
contract_assessment=None,
|
||||
contract_assessment=contract_assessment,
|
||||
grounding_provenance=grounding_src,
|
||||
)
|
||||
surface = resolved.surface
|
||||
# Truth-path bytes for trace_hash (ADR-0069 inv C): the served
|
||||
# surface carries register R6/R4 transforms and MUST NOT move the
|
||||
# hash; hash_surface is the register-invariant canonical-precedence
|
||||
# capture, kept in lockstep with every served-surface mutation below.
|
||||
hash_surface = resolved.hash_surface or resolved.surface
|
||||
articulation_surface = resolved.articulation_surface
|
||||
authority_source = resolved.authority
|
||||
|
||||
# === LOGOS MORPH AUTHORITY (bulk live seam) ===
|
||||
# Same pure decision function as teaching store + four-arm ablation.
|
||||
# Executable morph may force abstain/refuse; never soft-pass a
|
||||
# certified singular-exclusivity claim against observed plural HE.
|
||||
# English-only turns with no HE surface → no-op (None).
|
||||
logos_decision_kind = ""
|
||||
logos_decision_reason = ""
|
||||
logos_rule_id = ""
|
||||
logos_constraint_id = ""
|
||||
logos_decision = None
|
||||
try:
|
||||
from generate.observed_he_morph_v0.authority import (
|
||||
decision_as_coherence_refusal,
|
||||
evaluate_logos_on_text,
|
||||
first_logos_constraint,
|
||||
logos_blocks_certified_answer,
|
||||
)
|
||||
|
||||
logos_decision = evaluate_logos_on_text(text=text, mode="executable")
|
||||
if logos_decision is not None:
|
||||
logos_decision_kind = logos_decision.kind.value
|
||||
logos_decision_reason = logos_decision.reason
|
||||
logos_rule_id = logos_decision.rule_id or ""
|
||||
lc = first_logos_constraint(logos_decision)
|
||||
if lc is not None:
|
||||
logos_constraint_id = lc.constraint_id
|
||||
if logos_blocks_certified_answer(logos_decision):
|
||||
refusal = decision_as_coherence_refusal(logos_decision)
|
||||
surface = refusal.surface_message or refusal.message
|
||||
hash_surface = surface
|
||||
articulation_surface = surface
|
||||
authority_source = "logos_morph_constraint"
|
||||
except Exception:
|
||||
# Pack load / catalog failure must not crash the turn spine;
|
||||
# English path continues without morph authority.
|
||||
logos_decision = None
|
||||
|
||||
# SUBSTRATE_BYPASS_HAZARD telemetry (data-driven roadmap).
|
||||
# Only populated when a graph existed yet substrate did not win.
|
||||
# This is *observability only* — never used to change control flow
|
||||
# after the fact, never folded into trace_hash in Phase A.
|
||||
substrate_hazard: tuple[str, ...] = ()
|
||||
if effective_graph is not None and resolved.authority not in ("substrate_realizer", "realizer"):
|
||||
if effective_graph is not None and authority_source not in (
|
||||
"substrate_realizer",
|
||||
"realizer",
|
||||
"logos_morph_constraint",
|
||||
):
|
||||
reasons: list[str] = []
|
||||
if not effective_graph.is_fully_grounded():
|
||||
reasons.append("unfilled_pending_slots")
|
||||
|
|
@ -468,6 +553,11 @@ class CognitiveTurnPipeline:
|
|||
if not _is_useful_surface(realized_plan.surface):
|
||||
reasons.append("realizer_surface_not_useful")
|
||||
substrate_hazard = tuple(reasons)
|
||||
if logos_decision is not None and logos_blocks_certified_answer(logos_decision):
|
||||
substrate_hazard = substrate_hazard + (
|
||||
f"logos_morph_{logos_decision.kind.value}",
|
||||
logos_decision.reason,
|
||||
)
|
||||
|
||||
# Phase C (Geometric Anti-Unification) — read-only telemetry instrumentation.
|
||||
# Captures the graph-structural context around any OOV/pending "hole"
|
||||
|
|
@ -476,7 +566,7 @@ class CognitiveTurnPipeline:
|
|||
# Populated observationally; never affects surface, hash (yet), or
|
||||
# any durable mutation. Uses only what the substrate already produced.
|
||||
oov_geometric_context = None
|
||||
grounding_src = getattr(response, "grounding_source", "") or ""
|
||||
# grounding_src is computed above at the gate-wiring seam and reused here.
|
||||
has_pending = bool(effective_graph and any(
|
||||
(n.obj or "") in ("", "<pending>") or "..." in (n.obj or "")
|
||||
for n in effective_graph.nodes
|
||||
|
|
@ -485,11 +575,41 @@ class CognitiveTurnPipeline:
|
|||
# Use pure build_node_depths for canonical extraction (nid-keyed).
|
||||
node_depths = build_node_depths(effective_graph.nodes) if effective_graph else {}
|
||||
if grounding_src == "oov" or has_pending:
|
||||
# Active conformal neighborhood probe (exact cga_inner over vault).
|
||||
probe_performed = False
|
||||
probe_neighbors: list[dict[str, object]] = []
|
||||
try:
|
||||
from algebra.backend import cga_inner as _cga_inner
|
||||
|
||||
F_probe = getattr(self._capture_field_state(), "F", None)
|
||||
vault = getattr(getattr(self.runtime, "session", None), "vault", None)
|
||||
if F_probe is not None and vault is not None and hasattr(vault, "entries"):
|
||||
scores: list[tuple[float, str]] = []
|
||||
for entry in list(vault.entries())[:64]:
|
||||
versor = getattr(entry, "versor", None)
|
||||
if versor is None and isinstance(entry, (tuple, list)) and entry:
|
||||
versor = entry[0]
|
||||
if versor is None:
|
||||
continue
|
||||
try:
|
||||
s = float(_cga_inner(F_probe, versor))
|
||||
except Exception:
|
||||
continue
|
||||
label = str(getattr(entry, "id", "") or getattr(entry, "key", "") or "")
|
||||
scores.append((s, label))
|
||||
scores.sort(key=lambda item: item[0], reverse=True)
|
||||
probe_neighbors = [
|
||||
{"cga_inner": s, "ref": lab} for s, lab in scores[:5]
|
||||
]
|
||||
probe_performed = True
|
||||
except Exception:
|
||||
probe_performed = False
|
||||
oov_geometric_context = {
|
||||
"unresolved_topology": effective_graph.get_unresolved_topology() if effective_graph else (),
|
||||
"intent_tag": getattr(intent, "tag", None).value if intent and getattr(intent, "tag", None) else "unknown",
|
||||
"geometric_probe_performed": False,
|
||||
"note": "Hook for geometric anti-unification: surrounding realized facts (via exact vault cga_inner) can infer relation type / SPECULATIVE var for the hole instead of lexical fallback.",
|
||||
"geometric_probe_performed": probe_performed,
|
||||
"conformal_neighbors": probe_neighbors,
|
||||
"note": "Conformal anti-unification probe: vault neighbors via exact cga_inner.",
|
||||
"node_depths": node_depths,
|
||||
}
|
||||
else:
|
||||
|
|
@ -553,6 +673,7 @@ class CognitiveTurnPipeline:
|
|||
# proposal (if any) is added below for FUTURE turns to see.
|
||||
if self._speculative_subjects and surface and self._should_mark_speculative(text, surface):
|
||||
surface = _SPECULATIVE_SURFACE_MARKER + surface
|
||||
hash_surface = _SPECULATIVE_SURFACE_MARKER + hash_surface
|
||||
articulation_surface = _SPECULATIVE_SURFACE_MARKER + articulation_surface
|
||||
|
||||
# 10. TEACHING — correction capture, review, and store
|
||||
|
|
@ -591,9 +712,11 @@ class CognitiveTurnPipeline:
|
|||
if len(tok) >= 4 and tok not in _SUBJECT_STOPWORDS:
|
||||
self._forget_speculative_subject(tok)
|
||||
|
||||
# Advance turn counter and remember surface for next correction binding
|
||||
# Advance turn counter and remember surface for next correction binding.
|
||||
# The truth-path surface (not the served, register-decorated bytes)
|
||||
# feeds correction binding so register packs cannot perturb teaching.
|
||||
self._turn_number += 1
|
||||
self._prior_surface = surface
|
||||
self._prior_surface = hash_surface
|
||||
|
||||
# 11. TRACE — deterministic hash (includes teaching IDs and any
|
||||
# typed-operator invocation per ADR-0018).
|
||||
|
|
@ -605,10 +728,34 @@ class CognitiveTurnPipeline:
|
|||
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.
|
||||
# Stage 3C — inductive fixed-point provenance (before hash so replay
|
||||
# includes multi-step derivation when teaching store has chains).
|
||||
inductive_serialised = ""
|
||||
if inductive_closure.derived or inductive_closure.contradictions:
|
||||
inductive_serialised = json.dumps(
|
||||
{
|
||||
"inductive_closure": {
|
||||
"n_derived": len(inductive_closure.derived),
|
||||
"n_contradictions": len(inductive_closure.contradictions),
|
||||
"steps_taken": inductive_closure.steps_taken,
|
||||
"fixed_point": inductive_closure.fixed_point,
|
||||
"truncated": inductive_closure.truncated,
|
||||
"derived": [d.as_dict() for d in inductive_closure.derived[:8]],
|
||||
}
|
||||
},
|
||||
sort_keys=True,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
# Deterministic concatenation: walk, compose, entailment, inductive.
|
||||
# Empty strings are dropped so unaffected turns keep existing trace bytes.
|
||||
operator_invocation = "|".join(
|
||||
s for s in (walk_serialised, compose_serialised, entailment_serialised)
|
||||
s
|
||||
for s in (
|
||||
walk_serialised,
|
||||
compose_serialised,
|
||||
entailment_serialised,
|
||||
inductive_serialised,
|
||||
)
|
||||
if s
|
||||
)
|
||||
# ADR-0023 — admissibility trace + ratification provenance.
|
||||
|
|
@ -619,21 +766,17 @@ class CognitiveTurnPipeline:
|
|||
admissibility_trace = response.admissibility_trace
|
||||
region_was_unconstrained = response.region_was_unconstrained
|
||||
admissibility_trace_hash = hash_admissibility_trace(admissibility_trace)
|
||||
# Normalise all PASSTHROUGH sub-values to "passthrough" so the value
|
||||
# stored in CognitiveTurnResult matches what goes into trace_hash
|
||||
# (trace_hash_from_result invariant) and pre-ADR-0144 hashes remain
|
||||
# byte-identical (ADR-0144 / ADR-0142 §Implementation debts, debt 1).
|
||||
_ratification_outcome_raw = ratified.outcome.value
|
||||
ratification_outcome = (
|
||||
"passthrough"
|
||||
if _ratification_outcome_raw in _PASSTHROUGH_OUTCOMES
|
||||
else _ratification_outcome_raw
|
||||
)
|
||||
# Geometric ratification outcomes only (ratified | demoted).
|
||||
ratification_outcome = ratified.outcome.value
|
||||
_trace_ratification_outcome = ratification_outcome
|
||||
# ADR-0024 Phase 2 + W-011 — refusal_reason precedence:
|
||||
# recognition wins (earlier-fail boundary) over generation.
|
||||
_generation_refusal_reason = getattr(response, "refusal_reason", "") or ""
|
||||
refusal_reason = _recognition_refusal_reason or _generation_refusal_reason
|
||||
# Logos morph abstain/fail-closed is answer-authority: surface the reason
|
||||
# when morph blocked certification (does not override recognition refuse).
|
||||
if not refusal_reason and logos_decision_kind in ("abstain", "fail_closed"):
|
||||
refusal_reason = logos_decision_reason
|
||||
|
||||
# Phase B — Quantized Topological Hashing for the cognitive spine.
|
||||
# Include the discrete (string-only) topological form of the
|
||||
|
|
@ -653,7 +796,7 @@ class CognitiveTurnPipeline:
|
|||
trace_hash = compute_trace_hash(
|
||||
input_text=text,
|
||||
filtered_tokens=filtered_tokens,
|
||||
surface=surface,
|
||||
surface=hash_surface,
|
||||
walk_surface=response.walk_surface,
|
||||
articulation_surface=articulation_surface,
|
||||
dialogue_role=str(response.dialogue_role),
|
||||
|
|
@ -679,6 +822,16 @@ class CognitiveTurnPipeline:
|
|||
# ``source_turn_trace``. See runtime.finalize_turn_trace_hash.
|
||||
self.runtime.finalize_turn_trace_hash(trace_hash)
|
||||
|
||||
# Weekly-audit T13 (2026-07-22) — back-stamp the pipeline's final
|
||||
# *served* surface onto the same TurnEvent, sibling to the
|
||||
# trace_hash back-stamp above. ``resolve_surface`` + the logos-morph
|
||||
# authority (above) can override the runtime-owned surface AFTER
|
||||
# ``runtime.chat`` sealed the TurnEvent, so without this the
|
||||
# telemetry record disagrees with the served bytes
|
||||
# (warmed_session_consistency telemetry_consistency_rate < 1.0;
|
||||
# breaks audit/replay trust — Absolute Provenance).
|
||||
self.runtime.finalize_turn_surface(surface, articulation_surface)
|
||||
|
||||
# B4 producer: capture the leeway the response path already decided —
|
||||
# observational only (reads the runtime's introspection-only accrual and
|
||||
# the response's reach level; never alters the surface, never gates).
|
||||
|
|
@ -692,6 +845,15 @@ class CognitiveTurnPipeline:
|
|||
license_decision=getattr(accrual, "license", None),
|
||||
)
|
||||
|
||||
# Stage 3A — turn-level geometric coherence (orthogonal to vault COHERENT).
|
||||
geo_F = F_gate
|
||||
if geo_F is None and field_state_after is not None:
|
||||
geo_F = getattr(field_state_after, "F", None)
|
||||
geometric_coherence = evaluate_geometric_coherence(
|
||||
geo_F,
|
||||
identity_score=response.identity_score,
|
||||
)
|
||||
|
||||
return CognitiveTurnResult(
|
||||
input_text=text,
|
||||
input_tokens=raw_tokens,
|
||||
|
|
@ -701,6 +863,7 @@ class CognitiveTurnPipeline:
|
|||
proposition=response.proposition,
|
||||
articulation=response.articulation,
|
||||
surface=surface,
|
||||
hash_surface=hash_surface,
|
||||
walk_surface=response.walk_surface,
|
||||
articulation_surface=articulation_surface,
|
||||
dialogue_role=response.dialogue_role,
|
||||
|
|
@ -730,65 +893,111 @@ class CognitiveTurnPipeline:
|
|||
dispatch_trace=getattr(response, "dispatch_trace", None),
|
||||
dropped_compound_clauses=dropped_compound_clauses,
|
||||
versor_condition=response.versor_condition,
|
||||
geometric_coherence=geometric_coherence,
|
||||
trace_hash=trace_hash,
|
||||
leeway=leeway,
|
||||
# Phase A — Shadow Coherence Gate observability.
|
||||
# authority_source makes the winner of the substrate vs legacy
|
||||
# decision first-class evidence (visible in trace, workbench,
|
||||
# evals). substrate_hazard is the precise bypass signal.
|
||||
authority_source=resolved.authority,
|
||||
# Logos morph may override to "logos_morph_constraint" when it
|
||||
# blocks certified answers (same decision fn as teaching/ablation).
|
||||
authority_source=authority_source,
|
||||
substrate_hazard=substrate_hazard,
|
||||
oov_geometric_context=oov_geometric_context,
|
||||
# 3-lang depth unification: surface the same data at top level on result
|
||||
# (extracted from pre-computed node_depths var or oov_geometric_context to keep single source)
|
||||
node_depths=node_depths if node_depths else None,
|
||||
graph_anti_unify=(oov_geometric_context or {}).get("graph_anti_unify") if oov_geometric_context else None,
|
||||
logos_decision_kind=logos_decision_kind,
|
||||
logos_decision_reason=logos_decision_reason,
|
||||
logos_rule_id=logos_rule_id,
|
||||
logos_constraint_id=logos_constraint_id,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _compile_turn_wave_packet(self, tokens: tuple[str, ...] | list[str]) -> FieldState:
|
||||
"""Compile turn tokens into an initial Cl(4,1) field on the manifold.
|
||||
|
||||
Uses the session inject/probe path (holonomy encode + normalize_to_versor
|
||||
at the owned ingest boundary). Does not replace session state when
|
||||
probe_ingest is available (chat() still owns commit).
|
||||
"""
|
||||
session = getattr(self.runtime, "session", None)
|
||||
if session is None:
|
||||
raise RuntimeError(
|
||||
"CognitiveTurnPipeline cannot compile a wave-packet without a session"
|
||||
)
|
||||
token_list = [str(t) for t in tokens]
|
||||
if not token_list:
|
||||
token_list = ["_empty_"]
|
||||
if hasattr(session, "probe_ingest"):
|
||||
return session.probe_ingest(token_list)
|
||||
from ingest.gate import inject
|
||||
|
||||
vocab = getattr(session, "vocab", None)
|
||||
if vocab is None:
|
||||
raise RuntimeError(
|
||||
"CognitiveTurnPipeline cannot compile a wave-packet without session.vocab"
|
||||
)
|
||||
return inject(token_list, vocab)
|
||||
|
||||
@staticmethod
|
||||
def _geometry_contract_assessment(F):
|
||||
"""Build contract assessment from active versor + GoldTether residuals.
|
||||
|
||||
Closed only when versor_condition(F) < 1e-6 and R_GoldTether ≤ 1e-6.
|
||||
Local import avoids chat → cognition → problem_frame_contracts cycles
|
||||
(problem_frame_contracts imports chat.pack_resolver).
|
||||
"""
|
||||
from generate.problem_frame_contracts import ContractAssessment
|
||||
|
||||
if F is None:
|
||||
return ContractAssessment(
|
||||
candidate_organ="shadow_coherence_gate",
|
||||
missing_bindings=("missing_wave_field",),
|
||||
unresolved_hazards=(),
|
||||
runnable=False,
|
||||
explanation="no field versor available for geometric contract",
|
||||
)
|
||||
vc = float(versor_condition(F))
|
||||
r_gt = float(coherence_residual(F))
|
||||
missing: list[str] = []
|
||||
hazards: list[str] = []
|
||||
if vc >= 1e-6:
|
||||
missing.append("versor_condition")
|
||||
if r_gt > 1e-6:
|
||||
hazards.append("goldtether_residual")
|
||||
return ContractAssessment(
|
||||
candidate_organ="shadow_coherence_gate",
|
||||
missing_bindings=tuple(missing),
|
||||
unresolved_hazards=tuple(hazards),
|
||||
runnable=not missing and not hazards,
|
||||
explanation=(
|
||||
f"versor_condition={vc:.3e}; R_GoldTether={r_gt:.3e}"
|
||||
),
|
||||
)
|
||||
|
||||
def _ratify_intent(self, intent, field_state):
|
||||
"""Field-ratify a seeded intent (ADR-0022 §TBD-1).
|
||||
|
||||
Emits specific PASSTHROUGH sub-values (ADR-0144 / ADR-0142 debt 1)
|
||||
so the trace can distinguish which cold-start condition fired.
|
||||
All sub-values normalise to "passthrough" for trace_hash.
|
||||
Geometric Sovereignty: field must already be compiled (see :meth:`run`);
|
||||
vocab and prompt versor are required for conformal ratification.
|
||||
"""
|
||||
if field_state is None:
|
||||
return RatifiedIntent(
|
||||
intent=intent,
|
||||
outcome=RatificationOutcome.PASSTHROUGH_NO_FIELD,
|
||||
score=0.0,
|
||||
threshold=0.0,
|
||||
seed_tag=intent.tag,
|
||||
if field_state is None or getattr(field_state, "F", None) is None:
|
||||
raise RuntimeError(
|
||||
"intent ratification requires a compiled Cl(4,1) field state"
|
||||
)
|
||||
# ChatRuntime exposes vocab via session, not directly. The
|
||||
# original ADR-0022 wiring used ``getattr(self.runtime, "vocab",
|
||||
# None)`` which always returned None — silently routing every
|
||||
# turn through PASSTHROUGH. ADR-0023 §3 surfaced this via the
|
||||
# ``passthrough_on_scored`` lane metric; the fix here is to
|
||||
# resolve vocab through the session contract.
|
||||
session = getattr(self.runtime, "session", None)
|
||||
vocab = getattr(session, "vocab", None) if session is not None else None
|
||||
if vocab is None:
|
||||
return RatifiedIntent(
|
||||
intent=intent,
|
||||
outcome=RatificationOutcome.PASSTHROUGH_NO_VOCAB,
|
||||
score=0.0,
|
||||
threshold=0.0,
|
||||
seed_tag=intent.tag,
|
||||
)
|
||||
prompt_versor = getattr(field_state, "F", None)
|
||||
if prompt_versor is None:
|
||||
return RatifiedIntent(
|
||||
intent=intent,
|
||||
outcome=RatificationOutcome.PASSTHROUGH_NO_VERSOR,
|
||||
score=0.0,
|
||||
threshold=0.0,
|
||||
seed_tag=intent.tag,
|
||||
raise RuntimeError(
|
||||
"intent ratification requires session.vocab"
|
||||
)
|
||||
prompt_versor = field_state.F
|
||||
return ratify_intent(intent, prompt_versor, vocab=vocab)
|
||||
|
||||
def _remember_speculative_subject(self, subject: str) -> None:
|
||||
|
|
@ -871,10 +1080,27 @@ class CognitiveTurnPipeline:
|
|||
return None, None, None
|
||||
|
||||
manifold = getattr(self.runtime, "identity_manifold", None)
|
||||
# ADR-0244 honest scope: with ``identity_wave_gate`` off, live
|
||||
# final_state.F scores routinely show high leakage and axis inversion
|
||||
# because value axes are not yet dynamically load-bearing. Those
|
||||
# measures are observational telemetry, not teaching veto authority.
|
||||
# Only committed ``boundary_violations`` (safety/ethics ∩ manifold)
|
||||
# remain a hard geometric teaching reject while the gate is off.
|
||||
# Syntactic identity-override detection remains active regardless.
|
||||
review_score = identity_score
|
||||
cfg = getattr(self.runtime, "config", None)
|
||||
gate_on = bool(getattr(cfg, "identity_wave_gate", False))
|
||||
if (
|
||||
not gate_on
|
||||
and identity_score is not None
|
||||
and bool(getattr(identity_score, "wave_mode_active", False))
|
||||
and not bool(getattr(identity_score, "boundary_violations", ()) or ())
|
||||
):
|
||||
review_score = None
|
||||
reviewed = review_correction(
|
||||
candidate,
|
||||
identity_score=identity_score, # type: ignore[arg-type]
|
||||
identity_manifold=manifold,
|
||||
identity_score=review_score, # type: ignore[arg-type]
|
||||
identity_manifold=manifold if review_score is not None else None,
|
||||
)
|
||||
proposal = self.teaching_store.add(reviewed)
|
||||
return candidate, reviewed, proposal
|
||||
|
|
@ -959,13 +1185,17 @@ class CognitiveTurnPipeline:
|
|||
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.
|
||||
|
||||
Atoms are content-addressed by Cl(4,1) versor digests and unified by
|
||||
conformal reverse-product score (no string ``atom_`` join).
|
||||
"""
|
||||
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)
|
||||
registry: list[tuple[str, np.ndarray]] = []
|
||||
head = self._proof_atom(intent.subject, registry)
|
||||
tail = self._proof_atom(intent.object, registry)
|
||||
if not head or not tail:
|
||||
return None
|
||||
|
||||
|
|
@ -974,8 +1204,8 @@ class CognitiveTurnPipeline:
|
|||
for h, r, t in triples:
|
||||
if r.strip().lower() != relation:
|
||||
continue
|
||||
h_atom = self._proof_atom(h)
|
||||
t_atom = self._proof_atom(t)
|
||||
h_atom = self._proof_atom(h, registry)
|
||||
t_atom = self._proof_atom(t, registry)
|
||||
if h_atom and t_atom:
|
||||
premises.append(f"{h_atom} -> {t_atom}")
|
||||
if not premises:
|
||||
|
|
@ -1019,12 +1249,68 @@ class CognitiveTurnPipeline:
|
|||
return ""
|
||||
return f"entailment:{evidence_from_entailment_trace(trace).canonical_json()}"
|
||||
|
||||
def _resolve_surface_versor(self, text: str) -> np.ndarray | None:
|
||||
"""Resolve surface text to a vocab-grounded Cl(4,1) versor, or None."""
|
||||
session = getattr(self.runtime, "session", None)
|
||||
vocab = getattr(session, "vocab", None) if session is not None else None
|
||||
if vocab is None or not text:
|
||||
return None
|
||||
tokens = [p for p in _SUBJECT_SPLIT_RE.split(text.lower()) if p]
|
||||
# Prefer last non-stopword content token (subject-like), then any token.
|
||||
ordered = [t for t in reversed(tokens) if t not in _SUBJECT_STOPWORDS] + tokens
|
||||
for token in ordered:
|
||||
try:
|
||||
return np.asarray(vocab.get_versor(token), dtype=np.float64)
|
||||
except (KeyError, AttributeError):
|
||||
continue
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _proof_atom(text: str) -> str:
|
||||
parts = [p for p in _SUBJECT_SPLIT_RE.split(text.lower()) if p]
|
||||
if not parts:
|
||||
def _unify_score(a: np.ndarray, b: np.ndarray) -> float:
|
||||
"""⟨a, ~b⟩_0 — scalar part of geometric product with reversion."""
|
||||
return float(scalar_part(geometric_product(a, reverse(b))))
|
||||
|
||||
@classmethod
|
||||
def _atoms_unify(cls, a: np.ndarray, b: np.ndarray) -> bool:
|
||||
"""True when a and b are the same geometric atom under reverse-product.
|
||||
|
||||
Requires mutual score to match both self-products within ``_UNIFY_EPS``
|
||||
(relative when |self| ≥ 1, absolute otherwise). Exact component match
|
||||
short-circuits. Distinct pack versors with large cross-inner do not unify.
|
||||
"""
|
||||
if a.shape != b.shape:
|
||||
return False
|
||||
if np.allclose(a, b, rtol=0.0, atol=1e-9):
|
||||
return True
|
||||
sa = cls._unify_score(a, a)
|
||||
sb = cls._unify_score(b, b)
|
||||
sab = cls._unify_score(a, b)
|
||||
scale = max(1.0, abs(sa), abs(sb))
|
||||
tol = _UNIFY_EPS * scale
|
||||
return abs(sab - sa) <= tol and abs(sab - sb) <= tol
|
||||
|
||||
def _proof_atom(
|
||||
self,
|
||||
text: str,
|
||||
registry: list[tuple[str, np.ndarray]] | None = None,
|
||||
) -> str:
|
||||
"""Content-addressed conformal atom id for entailment telemetry.
|
||||
|
||||
Two surfaces unify to the same atom iff their grounded versors match
|
||||
under :meth:`_atoms_unify`. Ungrounded surfaces fail closed (empty id).
|
||||
"""
|
||||
psi = self._resolve_surface_versor(text)
|
||||
if psi is None:
|
||||
return ""
|
||||
return "atom_" + "_".join(parts)
|
||||
if registry is not None:
|
||||
for atom_id, prior in registry:
|
||||
if self._atoms_unify(psi, prior):
|
||||
return atom_id
|
||||
digest = multivector_content_digest(psi)
|
||||
atom_id = f"atom_{digest}"
|
||||
if registry is not None:
|
||||
registry.append((atom_id, psi.copy()))
|
||||
return atom_id
|
||||
|
||||
@staticmethod
|
||||
def _render_walk_surface(walk: WalkResult) -> str:
|
||||
|
|
|
|||
283
core/cognition/proof_trace.py
Normal file
283
core/cognition/proof_trace.py
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
"""Minimal structured proof trace for proof-preserving articulation.
|
||||
|
||||
Ordered sequence: semantic atoms → field operators → closure result.
|
||||
No free-text-only steps as authoritative proof content.
|
||||
|
||||
Citation rules (firewall):
|
||||
* Valid citation keys are ONLY step_id and ``kind:symbol``.
|
||||
* Raw payload values are never citation keys (prevents whitelist bypass
|
||||
via incidental values like ``"2"`` or ``"0.000000e+00"``).
|
||||
* Payload content is available separately for content certification.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any, Iterable, Sequence
|
||||
|
||||
|
||||
class ProofStepKind(str, Enum):
|
||||
ATOM = "atom"
|
||||
OPERATOR = "operator"
|
||||
CLOSURE = "closure"
|
||||
REFUSAL = "refusal"
|
||||
|
||||
|
||||
_TOKEN_SPLIT = re.compile(r"[^a-z0-9_.:-]+", re.IGNORECASE)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ProofStep:
|
||||
"""One ordered, typed proof step."""
|
||||
|
||||
step_id: str
|
||||
kind: ProofStepKind
|
||||
symbol: str
|
||||
"""Machine id: entity id, operator class, closure predicate, etc."""
|
||||
payload: tuple[tuple[str, str], ...] = ()
|
||||
"""Stringly-serialised structured payload pairs (key, value) only."""
|
||||
parent_ids: tuple[str, ...] = ()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.step_id:
|
||||
raise ValueError("ProofStep.step_id must be non-empty")
|
||||
if not isinstance(self.kind, ProofStepKind):
|
||||
raise TypeError("ProofStep.kind must be a ProofStepKind")
|
||||
if not self.symbol:
|
||||
raise ValueError("ProofStep.symbol must be non-empty")
|
||||
for key, value in self.payload:
|
||||
if not isinstance(key, str) or not isinstance(value, str):
|
||||
raise TypeError("ProofStep.payload must be tuple[tuple[str, str], ...]")
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"step_id": self.step_id,
|
||||
"kind": self.kind.value,
|
||||
"symbol": self.symbol,
|
||||
"payload": [[k, v] for k, v in self.payload],
|
||||
"parent_ids": list(self.parent_ids),
|
||||
}
|
||||
|
||||
def citation_keys(self) -> frozenset[str]:
|
||||
"""Keys an articulation claim may *cite* as trace_refs.
|
||||
|
||||
Only step_id and kind:symbol — never bare payload values.
|
||||
"""
|
||||
return frozenset(
|
||||
{
|
||||
self.step_id,
|
||||
f"{self.kind.value}:{self.symbol}",
|
||||
}
|
||||
)
|
||||
|
||||
# Back-compat alias used by older call sites / tests.
|
||||
def claim_keys(self) -> frozenset[str]:
|
||||
return self.citation_keys()
|
||||
|
||||
def certified_content_tokens(self) -> frozenset[str]:
|
||||
"""Tokens that may appear in claim *text* when this step is cited.
|
||||
|
||||
Includes step_id parts, symbol parts, and payload keys/values —
|
||||
but only as content vocabulary, not as citation keys.
|
||||
"""
|
||||
tokens: set[str] = set()
|
||||
for raw in (self.step_id, self.symbol, self.kind.value):
|
||||
tokens |= _tokenize(raw)
|
||||
for k, v in self.payload:
|
||||
tokens |= _tokenize(k)
|
||||
tokens |= _tokenize(v)
|
||||
return frozenset(tokens)
|
||||
|
||||
|
||||
def _tokenize(text: str) -> set[str]:
|
||||
out: set[str] = set()
|
||||
for part in _TOKEN_SPLIT.split(text.lower()):
|
||||
if part:
|
||||
out.add(part)
|
||||
# Also keep dotted/colon segments whole when present.
|
||||
# Whole lowercased string if multi-token machine id
|
||||
if text and " " not in text:
|
||||
out.add(text.lower())
|
||||
return out
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ProofTrace:
|
||||
"""Ordered proof trace. Empty only for non-authoritative turns."""
|
||||
|
||||
steps: tuple[ProofStep, ...] = ()
|
||||
closed: bool = False
|
||||
closure_step_id: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.closed and not self.steps:
|
||||
raise ValueError("closed ProofTrace must contain at least one step")
|
||||
if self.closed:
|
||||
if self.closure_step_id is None:
|
||||
raise ValueError("closed ProofTrace requires closure_step_id")
|
||||
ids = {s.step_id for s in self.steps}
|
||||
if self.closure_step_id not in ids:
|
||||
raise ValueError("closure_step_id must reference a step in the trace")
|
||||
closure = next(s for s in self.steps if s.step_id == self.closure_step_id)
|
||||
if closure.kind is not ProofStepKind.CLOSURE:
|
||||
raise ValueError("closure_step_id must point at a CLOSURE step")
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"closed": self.closed,
|
||||
"closure_step_id": self.closure_step_id,
|
||||
"steps": [s.as_dict() for s in self.steps],
|
||||
}
|
||||
|
||||
def all_citation_keys(self) -> frozenset[str]:
|
||||
keys: set[str] = set()
|
||||
for step in self.steps:
|
||||
keys |= set(step.citation_keys())
|
||||
return frozenset(keys)
|
||||
|
||||
def all_claim_keys(self) -> frozenset[str]:
|
||||
"""Alias for citation keys (not payload-value whitelist)."""
|
||||
return self.all_citation_keys()
|
||||
|
||||
def steps_for_refs(self, refs: Sequence[str]) -> tuple[ProofStep, ...]:
|
||||
"""Resolve citation refs to steps; unknown refs yield empty match list."""
|
||||
by_key: dict[str, list[ProofStep]] = {}
|
||||
for step in self.steps:
|
||||
for key in step.citation_keys():
|
||||
by_key.setdefault(key, []).append(step)
|
||||
found: list[ProofStep] = []
|
||||
seen: set[str] = set()
|
||||
for ref in refs:
|
||||
for step in by_key.get(ref, ()):
|
||||
if step.step_id not in seen:
|
||||
seen.add(step.step_id)
|
||||
found.append(step)
|
||||
return tuple(found)
|
||||
|
||||
def certified_content_for_refs(self, refs: Sequence[str]) -> frozenset[str]:
|
||||
tokens: set[str] = set()
|
||||
for step in self.steps_for_refs(refs):
|
||||
tokens |= set(step.certified_content_tokens())
|
||||
return frozenset(tokens)
|
||||
|
||||
def extend(self, extra: Sequence[ProofStep]) -> "ProofTrace":
|
||||
return ProofTrace(
|
||||
steps=self.steps + tuple(extra),
|
||||
closed=self.closed,
|
||||
closure_step_id=self.closure_step_id,
|
||||
)
|
||||
|
||||
|
||||
def build_closed_trace(
|
||||
atoms: Iterable[tuple[str, str, Sequence[tuple[str, str]]]],
|
||||
operators: Iterable[tuple[str, str, Sequence[tuple[str, str]], Sequence[str]]],
|
||||
*,
|
||||
closure_symbol: str = "versor_and_goldtether_closed",
|
||||
closure_payload: Sequence[tuple[str, str]] = (),
|
||||
) -> ProofTrace:
|
||||
"""Build a closed proof from atom and operator descriptors.
|
||||
|
||||
atoms: (step_id, symbol, payload)
|
||||
operators: (step_id, symbol, payload, parent_ids)
|
||||
"""
|
||||
steps: list[ProofStep] = []
|
||||
for step_id, symbol, payload in atoms:
|
||||
steps.append(
|
||||
ProofStep(
|
||||
step_id=step_id,
|
||||
kind=ProofStepKind.ATOM,
|
||||
symbol=symbol,
|
||||
payload=tuple((str(k), str(v)) for k, v in payload),
|
||||
)
|
||||
)
|
||||
for step_id, symbol, payload, parents in operators:
|
||||
steps.append(
|
||||
ProofStep(
|
||||
step_id=step_id,
|
||||
kind=ProofStepKind.OPERATOR,
|
||||
symbol=symbol,
|
||||
payload=tuple((str(k), str(v)) for k, v in payload),
|
||||
parent_ids=tuple(parents),
|
||||
)
|
||||
)
|
||||
closure_id = "closure:0"
|
||||
parent_ids = tuple(s.step_id for s in steps if s.kind is ProofStepKind.OPERATOR)
|
||||
if not parent_ids:
|
||||
parent_ids = tuple(s.step_id for s in steps)
|
||||
steps.append(
|
||||
ProofStep(
|
||||
step_id=closure_id,
|
||||
kind=ProofStepKind.CLOSURE,
|
||||
symbol=closure_symbol,
|
||||
payload=tuple((str(k), str(v)) for k, v in closure_payload),
|
||||
parent_ids=parent_ids,
|
||||
)
|
||||
)
|
||||
return ProofTrace(steps=tuple(steps), closed=True, closure_step_id=closure_id)
|
||||
|
||||
|
||||
def build_refusal_trace(
|
||||
*,
|
||||
reason: str,
|
||||
violated_condition: str,
|
||||
) -> ProofTrace:
|
||||
"""Trace that certifies only the refusal itself (no answer claims)."""
|
||||
step = ProofStep(
|
||||
step_id="refusal:0",
|
||||
kind=ProofStepKind.REFUSAL,
|
||||
symbol="coherence_refusal",
|
||||
payload=(
|
||||
("reason", reason),
|
||||
("violated_condition", violated_condition),
|
||||
),
|
||||
)
|
||||
return ProofTrace(steps=(step,), closed=False, closure_step_id=None)
|
||||
|
||||
|
||||
def geometry_contract_trace(
|
||||
*,
|
||||
versor_condition: float,
|
||||
goldtether_residual: float,
|
||||
closed: bool,
|
||||
) -> ProofTrace:
|
||||
"""Proof fragment from live shadow-gate scalars."""
|
||||
payload = (
|
||||
("versor_condition", f"{versor_condition:.6e}"),
|
||||
("goldtether_residual", f"{goldtether_residual:.6e}"),
|
||||
)
|
||||
if closed:
|
||||
return build_closed_trace(
|
||||
atoms=(
|
||||
(
|
||||
"atom:field",
|
||||
"field_state",
|
||||
payload,
|
||||
),
|
||||
),
|
||||
operators=(
|
||||
(
|
||||
"op:geometry_gate",
|
||||
"shadow_coherence_gate",
|
||||
payload,
|
||||
("atom:field",),
|
||||
),
|
||||
),
|
||||
closure_symbol="geometric_contract_closed",
|
||||
closure_payload=payload,
|
||||
)
|
||||
return build_refusal_trace(
|
||||
reason="geometric_contract_open",
|
||||
violated_condition="versor_condition_and_goldtether",
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ProofStepKind",
|
||||
"ProofStep",
|
||||
"ProofTrace",
|
||||
"build_closed_trace",
|
||||
"build_refusal_trace",
|
||||
"geometry_contract_trace",
|
||||
]
|
||||
|
|
@ -10,6 +10,7 @@ from __future__ import annotations
|
|||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from core.cognition.geometric_coherence import GeometricCoherenceVerdict
|
||||
from core.cognition.leeway import LeewayRecord
|
||||
from field.state import FieldState
|
||||
from generate.articulation import ArticulationPlan
|
||||
|
|
@ -69,6 +70,19 @@ class CognitiveTurnResult:
|
|||
vault_hits: int
|
||||
recall_energy_class: str | None = None
|
||||
|
||||
#: The register-INVARIANT truth-path bytes — what ``compute_trace_hash``
|
||||
#: actually folds (ADR-0069 inv C, ADR-0077 R6). Distinct from ``surface``,
|
||||
#: which is the served, register-decorated string the user reads.
|
||||
#:
|
||||
#: Exposed 2026-07-25. It was pipeline-local before that, which made the
|
||||
#: truth-path-isolation invariant unobservable from a turn result — so
|
||||
#: ``test_register_matrix_canonical_surface_byte_identical`` asserted it on
|
||||
#: ``surface`` instead, and went red across all 99 registers the moment
|
||||
#: Phase 0 flipped ``resolve_surface`` to response-first precedence and
|
||||
#: moved the invariant here. The contract was never broken; it just could
|
||||
#: not be seen. Empty string ⇒ identical to ``surface`` (no divergence).
|
||||
hash_surface: str = ""
|
||||
|
||||
# --- intent / graph telemetry ---
|
||||
intent: DialogueIntent | None = None
|
||||
proposition_graph: PropositionGraph | None = None
|
||||
|
|
@ -150,6 +164,9 @@ class CognitiveTurnResult:
|
|||
|
||||
# --- invariant bookkeeping ---
|
||||
versor_condition: float = 0.0 # must be < 1e-6
|
||||
# Stage 3A — geometry-native turn coherence (orthogonal to vault EpistemicStatus).
|
||||
# None only on pre-Stage-3 artifacts; live pipeline always populates.
|
||||
geometric_coherence: GeometricCoherenceVerdict | None = None
|
||||
trace_hash: str = "" # SHA-256 over deterministic key fields
|
||||
|
||||
# --- response-governance leeway evidence (B4; observational, not in trace_hash) ---
|
||||
|
|
@ -203,3 +220,13 @@ class CognitiveTurnResult:
|
|||
# Never folded into trace_hash (observational only, like oov_geometric_context).
|
||||
node_depths: dict | None = None
|
||||
graph_anti_unify: dict | None = None
|
||||
|
||||
# --- Logos morph authority (observational + outcome-affecting when non-pass) ---
|
||||
# Populated when observed HE surface is present in the turn input and the
|
||||
# shared ``evaluate_logos_on_text`` path runs. Empty kind == not consulted.
|
||||
# Never folded into trace_hash as a separate key (surface/refusal already
|
||||
# capture the user-visible effect when morph blocks certification).
|
||||
logos_decision_kind: str = ""
|
||||
logos_decision_reason: str = ""
|
||||
logos_rule_id: str = ""
|
||||
logos_constraint_id: str = ""
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ The pipeline produces several candidate surfaces in one turn:
|
|||
|
||||
Historically these mutated one string in evaluation order. This module
|
||||
centralizes the policy so fold behavior is declared and unit-testable.
|
||||
|
||||
Phase 1 linguistic governance:
|
||||
* ``contract_assessment is None`` → typed ContractViolation; no certified answer
|
||||
* open geometric contract → typed CoherenceRefusal; no certified answer
|
||||
* walk/compose folds never upgrade an uncertified base into authority
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -16,11 +21,38 @@ from dataclasses import dataclass
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from core.cognition.fail_closed import (
|
||||
CoherenceRefusal,
|
||||
ContractViolation,
|
||||
contract_assessment_none_violation,
|
||||
open_geometry_refusal,
|
||||
)
|
||||
from core.cognition.proof_trace import ProofTrace, build_refusal_trace, geometry_contract_trace
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from generate.graph_planner import PropositionGraph
|
||||
from generate.problem_frame_contracts import ContractAssessment
|
||||
|
||||
|
||||
_ABSTENTION_AUTHORITY = "coherence_abstention"
|
||||
|
||||
# --- T13 decision (2): grounded-open hedge arm --------------------------------
|
||||
# Grounding provenances whose surfaces are curated enough to hedge (serve
|
||||
# non-authoritatively) rather than hard-refuse when the geometric contract is
|
||||
# open. Mirrors the "grounded" test in chat.runtime (pack / teaching).
|
||||
_GROUNDED_PROVENANCES = frozenset({"pack", "teaching"})
|
||||
|
||||
# The ONLY open tokens that are hedgeable: geometric-coherence residuals that a
|
||||
# pack surface never claimed to certify. Any token outside this set — where a
|
||||
# genuine safety/harm/imperative hazard would land — fails closed to a hard
|
||||
# refusal (INV-34). Deliberately excludes "missing_wave_field": the absence of
|
||||
# any field is a harder failure than an open-but-computed residual.
|
||||
_HEDGEABLE_GEOMETRIC_RESIDUALS = frozenset({"versor_condition", "goldtether_residual"})
|
||||
|
||||
_GROUNDED_OPEN_HEDGE_AUTHORITY = "grounded_open_hedge"
|
||||
_GROUNDED_OPEN_HEDGE_PREFIX = "Grounded but not geometrically certified —"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SurfaceResolution:
|
||||
"""Resolved user-facing and articulation surfaces.
|
||||
|
|
@ -31,14 +63,36 @@ class SurfaceResolution:
|
|||
|
||||
When authority == "substrate_realizer", the PropositionGraph +
|
||||
realize_semantic path was granted supremacy by the Shadow Coherence
|
||||
Gate (strict structural + contract + coherence proof). Legacy runtime
|
||||
and walk/compose folds are still applied after, never before.
|
||||
Gate (strict structural + contract + coherence proof).
|
||||
|
||||
``authoritative`` is True only when a certified answer may leave the
|
||||
system. Geometry-open and assessment-missing paths set this False and
|
||||
attach a typed refusal/violation.
|
||||
|
||||
``hedged`` is True only on the grounded-open hedge arm (T13 decision 2):
|
||||
a curated pack/teaching surface served non-authoritatively because the
|
||||
geometric contract is open on a known coherence residual. It is mutually
|
||||
exclusive with ``refusal``/``contract_violation`` and never authoritative.
|
||||
"""
|
||||
|
||||
surface: str
|
||||
articulation_surface: str
|
||||
authority: str
|
||||
fold_sources: tuple[str, ...] = ()
|
||||
authoritative: bool = True
|
||||
hedged: bool = False
|
||||
refusal: CoherenceRefusal | None = None
|
||||
contract_violation: ContractViolation | None = None
|
||||
proof_trace: ProofTrace | None = None
|
||||
hash_surface: str = ""
|
||||
"""Truth-path surface for ``trace_hash`` folding (ADR-0069 inv C).
|
||||
|
||||
``surface`` carries the *served* bytes (post register R6/R4);
|
||||
``hash_surface`` carries the register-invariant truth-path bytes
|
||||
(canonical-precedence base plus the same substrate/fold suffixes).
|
||||
Empty means the served surface IS the truth-path surface (refusal,
|
||||
abstention, and legacy callers) — the pipeline falls back to
|
||||
``surface`` when folding the trace."""
|
||||
|
||||
|
||||
def _base_runtime_surface(
|
||||
|
|
@ -48,13 +102,169 @@ def _base_runtime_surface(
|
|||
response_surface: str,
|
||||
response_articulation_surface: str,
|
||||
) -> tuple[str, str, str]:
|
||||
"""Select the runtime-owned base surface by declared precedence."""
|
||||
"""Select the runtime-owned base surface by declared precedence.
|
||||
|
||||
if canonical_surface:
|
||||
return canonical_surface, response_articulation_surface, "runtime_canonical"
|
||||
``response_surface`` is the runtime's final *served* bytes — post
|
||||
realizer-guard, post substantive register (ADR-0077 R6), post seeded
|
||||
decoration (ADR-0071 R4) — and always wins when present.
|
||||
``canonical_surface`` / ``pre_decoration_surface`` are truth-path
|
||||
identity captures the pipeline folds into ``trace_hash``; they are
|
||||
fallbacks for callers that never sealed a response surface, never a
|
||||
substitute for served bytes. Preferring canonical here strips the
|
||||
entire register axis from pipeline-served turns (terse/convivial
|
||||
stop differing from neutral) while trace_hash stays green — the
|
||||
register-tour claims are the falsifiable contract that catches it.
|
||||
"""
|
||||
|
||||
if response_surface:
|
||||
return response_surface, response_articulation_surface, "runtime"
|
||||
if pre_decoration_surface:
|
||||
return pre_decoration_surface, response_articulation_surface, "runtime_pre_decoration"
|
||||
return response_surface, response_articulation_surface, "runtime"
|
||||
if canonical_surface:
|
||||
return canonical_surface, response_articulation_surface, "runtime_canonical"
|
||||
return "", response_articulation_surface, "runtime"
|
||||
|
||||
|
||||
def _truth_path_base(
|
||||
*,
|
||||
canonical_surface: str,
|
||||
pre_decoration_surface: str,
|
||||
response_surface: str,
|
||||
) -> str:
|
||||
"""Register-invariant base folded into ``trace_hash``.
|
||||
|
||||
Canonical-first: the composer's pre-R6 capture is the truth-path
|
||||
identity field, byte-identical across register packs (ADR-0069
|
||||
inv C / ADR-0077). Falls through to pre-decoration, then the
|
||||
response itself for turns that never captured a canonical surface.
|
||||
"""
|
||||
|
||||
return canonical_surface or pre_decoration_surface or response_surface
|
||||
|
||||
|
||||
def _abstention_resolution(
|
||||
*,
|
||||
refusal: CoherenceRefusal | None,
|
||||
violation: ContractViolation | None,
|
||||
) -> SurfaceResolution:
|
||||
if violation is not None:
|
||||
msg = (
|
||||
f"I cannot certify an answer: {violation.refusal_reason} "
|
||||
f"(condition={violation.violated_condition})."
|
||||
)
|
||||
trace = build_refusal_trace(
|
||||
reason=violation.refusal_reason,
|
||||
violated_condition=violation.violated_condition,
|
||||
)
|
||||
return SurfaceResolution(
|
||||
surface=msg,
|
||||
articulation_surface=msg,
|
||||
authority=_ABSTENTION_AUTHORITY,
|
||||
fold_sources=(),
|
||||
authoritative=False,
|
||||
refusal=None,
|
||||
contract_violation=violation,
|
||||
proof_trace=trace,
|
||||
)
|
||||
assert refusal is not None
|
||||
msg = refusal.message
|
||||
trace = build_refusal_trace(
|
||||
reason=refusal.refusal_reason,
|
||||
violated_condition=refusal.violated_condition,
|
||||
)
|
||||
return SurfaceResolution(
|
||||
surface=msg,
|
||||
articulation_surface=msg,
|
||||
authority=_ABSTENTION_AUTHORITY,
|
||||
fold_sources=(),
|
||||
authoritative=False,
|
||||
refusal=refusal,
|
||||
contract_violation=None,
|
||||
proof_trace=trace,
|
||||
)
|
||||
|
||||
|
||||
def _is_pack_grounded(grounding_provenance: str) -> bool:
|
||||
"""Structural discriminator: is the surface curated pack/teaching grounded?
|
||||
|
||||
Purely provenance-based — it never inspects the question text. A lexical
|
||||
"definitional/epistemic" cue-table would fail a geometric gate open on a
|
||||
surface cue (fail-open; ADR-0252 / INV-34), which the ruling rejected.
|
||||
"""
|
||||
return (grounding_provenance or "").strip().lower() in _GROUNDED_PROVENANCES
|
||||
|
||||
|
||||
def _grounded_open_hedge_admissible(
|
||||
contract_assessment: "ContractAssessment",
|
||||
grounding_provenance: str,
|
||||
) -> bool:
|
||||
"""True iff an open-geometry surface may hedge instead of hard-refuse.
|
||||
|
||||
Predicate (authorized): ``pack_grounded ∧ every open token is a known
|
||||
geometric-coherence residual``. Fail-closed: an unrecognized open token —
|
||||
where a genuine safety/harm/imperative hazard would appear — makes this
|
||||
False, so the caller hard-refuses. ``¬hazard`` is thus enforced
|
||||
*structurally* by the residual allowlist, not by question typing.
|
||||
"""
|
||||
if not _is_pack_grounded(grounding_provenance):
|
||||
return False
|
||||
open_tokens = set(contract_assessment.missing_bindings) | set(
|
||||
contract_assessment.unresolved_hazards
|
||||
)
|
||||
if not open_tokens:
|
||||
# Defensive: the caller only reaches here when the conjugate is open.
|
||||
return False
|
||||
return open_tokens <= _HEDGEABLE_GEOMETRIC_RESIDUALS
|
||||
|
||||
|
||||
def _grounded_open_hedge_resolution(
|
||||
*,
|
||||
canonical_surface: str,
|
||||
pre_decoration_surface: str,
|
||||
response_surface: str,
|
||||
response_articulation_surface: str,
|
||||
) -> SurfaceResolution:
|
||||
"""Serve the pack surface, honestly hedged and non-authoritative.
|
||||
|
||||
The surface is emitted (not refused) but ``authoritative=False`` and
|
||||
``hedged=True``: the pack grounding stands on its textual provenance while
|
||||
explicitly disclaiming the open geometric certification. Walk/compose folds
|
||||
are suppressed — a hedge never accretes deterministic inference authority.
|
||||
"""
|
||||
base_surface, base_articulation, _authority = _base_runtime_surface(
|
||||
canonical_surface=canonical_surface,
|
||||
pre_decoration_surface=pre_decoration_surface,
|
||||
response_surface=response_surface,
|
||||
response_articulation_surface=response_articulation_surface,
|
||||
)
|
||||
truth_base = _truth_path_base(
|
||||
canonical_surface=canonical_surface,
|
||||
pre_decoration_surface=pre_decoration_surface,
|
||||
response_surface=response_surface,
|
||||
)
|
||||
surface = (
|
||||
f"{_GROUNDED_OPEN_HEDGE_PREFIX} {base_surface}" if base_surface else base_surface
|
||||
)
|
||||
hash_surface = (
|
||||
f"{_GROUNDED_OPEN_HEDGE_PREFIX} {truth_base}" if truth_base else truth_base
|
||||
)
|
||||
articulation = (
|
||||
f"{_GROUNDED_OPEN_HEDGE_PREFIX} {base_articulation}"
|
||||
if base_articulation
|
||||
else base_articulation
|
||||
)
|
||||
return SurfaceResolution(
|
||||
surface=surface,
|
||||
articulation_surface=articulation,
|
||||
authority=_GROUNDED_OPEN_HEDGE_AUTHORITY,
|
||||
fold_sources=(),
|
||||
authoritative=False,
|
||||
hedged=True,
|
||||
refusal=None,
|
||||
contract_violation=None,
|
||||
proof_trace=None,
|
||||
hash_surface=hash_surface,
|
||||
)
|
||||
|
||||
|
||||
def resolve_surface(
|
||||
|
|
@ -70,75 +280,129 @@ def resolve_surface(
|
|||
compose_surface: str = "",
|
||||
proposition_graph: "PropositionGraph | None" = None,
|
||||
contract_assessment: "ContractAssessment | None" = None,
|
||||
grounding_provenance: str = "",
|
||||
require_closed_geometry: bool = True,
|
||||
) -> SurfaceResolution:
|
||||
"""Resolve the final turn surface under one explicit policy.
|
||||
"""Resolve the final turn surface under dual-competing Shadow Coherence Gate.
|
||||
|
||||
The Shadow Coherence Gate (Strangler Fig Pattern per the refined plan):
|
||||
Dual-competing gate (forward ∧ conjugate) — both must pass to commit
|
||||
substrate authority:
|
||||
|
||||
- The PropositionGraph and realize_semantic are executed *unconditionally*
|
||||
on every turn (already true in pipeline before this call).
|
||||
- Authority is granted to the substrate realizer **only** when the
|
||||
strict geometric guard passes:
|
||||
* graph.is_fully_grounded() (no <pending> slots remain)
|
||||
* contract assessment (if present) is closed (no missing_bindings,
|
||||
no unresolved_hazards)
|
||||
* gate did not fire (unknown domain safety)
|
||||
Versor coherence (< 1e-6) is presupposed by construction at the
|
||||
boundaries that produced the graph/bindings; it is not re-"repaired"
|
||||
here.
|
||||
- When the guard refuses, we fall back to the legacy runtime surface
|
||||
and the *precise* topological delta is recorded upstream as
|
||||
SUBSTRATE_BYPASS_HAZARD telemetry. This makes every test run and
|
||||
every production turn a diagnostic that lights exactly which
|
||||
ProblemFrame / recall / realizer gaps still block substrate supremacy.
|
||||
- Legacy "realizer_useful" path is retained only as a transitional
|
||||
compat shim; the supreme check is the load-bearing decision.
|
||||
* **Forward** (surface resolution): graph fully grounded; structural
|
||||
contract slots closed when assessment present.
|
||||
* **Conjugate** (coherence correction check): geometric contract closed
|
||||
— versor_condition / GoldTether residual encoded as zero
|
||||
``missing_bindings`` and zero ``unresolved_hazards`` on
|
||||
``contract_assessment``. Assessment is **required** for substrate
|
||||
commit; ``None`` refuses geometric authority (fail-closed).
|
||||
|
||||
Walk/compose folds are *always* suffixes — they never affect the
|
||||
authority prefix decision.
|
||||
When ``require_closed_geometry`` is True (default), a missing or open
|
||||
geometric contract yields a typed abstention — no runtime fluent answer
|
||||
is emitted as certified content. Walk/compose folds are suppressed on
|
||||
abstention paths.
|
||||
|
||||
Three Engineering Pillars are non-negotiable here:
|
||||
I. Mechanical Sympathy — the entire decision is a handful of O(N)
|
||||
structural inspections on tiny tuples; zero extra alloc, zero
|
||||
cross-language roundtrip, zero sensitivity to FMA/assoc drift.
|
||||
II. Semantic Rigor — every term ("fully_grounded", "substrate_realizer",
|
||||
"bypass_hazard") has one precise meaning. No numeric tolerance,
|
||||
no "good enough" surface.
|
||||
III. Third Door — we did not pick "keep the regex sidecar" nor
|
||||
"rip it out and break the suite". We built the substrate spine
|
||||
as the sole authority path and made the old path the observable
|
||||
bypass that starves itself to zero.
|
||||
|
||||
See also: engineer's assessment §1 (Authority Flip Cliff), AGENTS.md
|
||||
(versor only at owned boundaries, exact recall, kernel substrate rule),
|
||||
runtime_contracts.md (surface selection contract).
|
||||
**Grounded-open hedge arm (T13 decision 2).** When the conjugate is open
|
||||
but the surface is ``pack``/``teaching`` grounded (``grounding_provenance``)
|
||||
and every open token is a known geometric-coherence residual, the pack
|
||||
surface is served *honestly hedged* (``authoritative=False``, ``hedged=True``)
|
||||
instead of hard-refused. The discriminator is purely structural (provenance
|
||||
+ residual allowlist) — never question typing — and fails closed: an
|
||||
unrecognized open token or non-pack grounding takes the unchanged refusal.
|
||||
"""
|
||||
|
||||
# --- Fail-closed: missing assessment never silently passes ---
|
||||
if require_closed_geometry and contract_assessment is None:
|
||||
return _abstention_resolution(
|
||||
refusal=None,
|
||||
violation=contract_assessment_none_violation(),
|
||||
)
|
||||
|
||||
conjugate_ok = _conjugate_coherence_ok(contract_assessment)
|
||||
if require_closed_geometry and not conjugate_ok:
|
||||
if contract_assessment is None:
|
||||
# Unreachable when require_closed_geometry handled None above,
|
||||
# but keep branch explicit for non-require callers.
|
||||
return _abstention_resolution(
|
||||
refusal=None,
|
||||
violation=contract_assessment_none_violation(),
|
||||
)
|
||||
# --- T13 decision (2): grounded-open hedge arm ---
|
||||
# A curated pack/teaching surface whose ONLY open tokens are known
|
||||
# geometric-coherence residuals is served honestly hedged
|
||||
# (authoritative=False) rather than hard-refused: the pack surface never
|
||||
# claimed geometric certification. Structural predicate only (grounding
|
||||
# provenance + residual allowlist); any unrecognized token or non-pack
|
||||
# grounding falls through to the unchanged fail-closed refusal below.
|
||||
if _grounded_open_hedge_admissible(contract_assessment, grounding_provenance):
|
||||
del gate_fired # hedge does not depend on the residual-failure flag
|
||||
return _grounded_open_hedge_resolution(
|
||||
canonical_surface=canonical_surface or "",
|
||||
pre_decoration_surface=pre_decoration_surface or "",
|
||||
response_surface=response_surface or "",
|
||||
response_articulation_surface=response_articulation_surface or "",
|
||||
)
|
||||
refusal = open_geometry_refusal(
|
||||
missing_bindings=tuple(contract_assessment.missing_bindings),
|
||||
unresolved_hazards=tuple(contract_assessment.unresolved_hazards),
|
||||
explanation=str(contract_assessment.explanation or ""),
|
||||
)
|
||||
# gate_fired is the pipeline's residual-failure flag; either way the
|
||||
# contract is not closed for certified answers.
|
||||
del gate_fired # used as documentation of pipeline dual; gate is conjugate
|
||||
return _abstention_resolution(refusal=refusal, violation=None)
|
||||
|
||||
surface, articulation_surface, authority = _base_runtime_surface(
|
||||
canonical_surface=canonical_surface or "",
|
||||
pre_decoration_surface=pre_decoration_surface or "",
|
||||
response_surface=response_surface or "",
|
||||
response_articulation_surface=response_articulation_surface or "",
|
||||
)
|
||||
hash_surface = _truth_path_base(
|
||||
canonical_surface=canonical_surface or "",
|
||||
pre_decoration_surface=pre_decoration_surface or "",
|
||||
response_surface=response_surface or "",
|
||||
)
|
||||
|
||||
# === SHADOW COHERENCE GATE ===
|
||||
# Unconditional substrate execution has already occurred.
|
||||
# We now decide authority strictly.
|
||||
if not gate_fired and realized_surface:
|
||||
if _substrate_supreme(proposition_graph, contract_assessment):
|
||||
surface = realized_surface
|
||||
articulation_surface = realized_surface
|
||||
authority = "substrate_realizer"
|
||||
elif realizer_useful:
|
||||
# Transitional shim (pre full coverage of grounding + organs).
|
||||
# Will be removed when hazard frequency for the legacy path hits zero.
|
||||
surface = realized_surface
|
||||
articulation_surface = realized_surface
|
||||
authority = "realizer"
|
||||
# === DUAL-COMPETING SHADOW COHERENCE GATE ===
|
||||
# Forward and conjugate evaluated as independent competitors; commit
|
||||
# substrate only when both pass (and gate_fired is false).
|
||||
forward_ok = _forward_surface_ok(proposition_graph, contract_assessment)
|
||||
|
||||
# When require_closed_geometry is False, preserve historical gate_fired
|
||||
# blocking of substrate even if assessment looks closed.
|
||||
if not require_closed_geometry:
|
||||
gate_blocks = gate_fired
|
||||
else:
|
||||
# Under fail-closed geometry, open conjugate already returned.
|
||||
# gate_fired with a closed assessment is treated as residual conflict
|
||||
# → still refuse substrate, keep runtime only if conjugate ok.
|
||||
gate_blocks = gate_fired
|
||||
|
||||
if not gate_blocks and realized_surface and forward_ok and conjugate_ok:
|
||||
surface = realized_surface
|
||||
hash_surface = realized_surface
|
||||
articulation_surface = realized_surface
|
||||
authority = "substrate_realizer"
|
||||
elif (
|
||||
not gate_blocks
|
||||
and realized_surface
|
||||
and realizer_useful
|
||||
and conjugate_ok
|
||||
and not forward_ok
|
||||
):
|
||||
# Transitional shim: geometric coherence holds, but graph not yet
|
||||
# fully grounded. Never used when conjugate residual fails.
|
||||
surface = realized_surface
|
||||
hash_surface = realized_surface
|
||||
articulation_surface = realized_surface
|
||||
authority = "realizer"
|
||||
|
||||
fold_sources: list[str] = []
|
||||
if walk_surface:
|
||||
surface = f"{surface} — {walk_surface}" if surface else walk_surface
|
||||
hash_surface = (
|
||||
f"{hash_surface} — {walk_surface}" if hash_surface else walk_surface
|
||||
)
|
||||
articulation_surface = (
|
||||
f"{articulation_surface} — {walk_surface}"
|
||||
if articulation_surface
|
||||
|
|
@ -148,6 +412,9 @@ def resolve_surface(
|
|||
|
||||
if compose_surface:
|
||||
surface = f"{surface} — {compose_surface}" if surface else compose_surface
|
||||
hash_surface = (
|
||||
f"{hash_surface} — {compose_surface}" if hash_surface else compose_surface
|
||||
)
|
||||
articulation_surface = (
|
||||
f"{articulation_surface} — {compose_surface}"
|
||||
if articulation_surface
|
||||
|
|
@ -155,47 +422,73 @@ def resolve_surface(
|
|||
)
|
||||
fold_sources.append("compose")
|
||||
|
||||
proof: ProofTrace | None = None
|
||||
if conjugate_ok and contract_assessment is not None:
|
||||
# Closed geometry path: embed gate scalars when explanation carries them.
|
||||
proof = geometry_contract_trace(
|
||||
versor_condition=0.0,
|
||||
goldtether_residual=0.0,
|
||||
closed=True,
|
||||
)
|
||||
|
||||
return SurfaceResolution(
|
||||
surface=surface,
|
||||
articulation_surface=articulation_surface,
|
||||
authority=authority,
|
||||
fold_sources=tuple(fold_sources),
|
||||
authoritative=True,
|
||||
refusal=None,
|
||||
contract_violation=None,
|
||||
proof_trace=proof,
|
||||
hash_surface=hash_surface,
|
||||
)
|
||||
|
||||
|
||||
def _conjugate_coherence_ok(
|
||||
contract_assessment: "ContractAssessment | None",
|
||||
) -> bool:
|
||||
"""Conjugate competitor: geometric residual contract must be closed.
|
||||
|
||||
Requires an explicit assessment (populated from versor_condition +
|
||||
GoldTether residual upstream). ``None`` fails closed — no soft admit.
|
||||
"""
|
||||
if contract_assessment is None:
|
||||
return False
|
||||
if contract_assessment.missing_bindings or contract_assessment.unresolved_hazards:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _forward_surface_ok(
|
||||
proposition_graph: "PropositionGraph | None",
|
||||
contract_assessment: "ContractAssessment | None",
|
||||
) -> bool:
|
||||
"""Forward competitor: structural graph readiness for substrate surface."""
|
||||
if proposition_graph is None:
|
||||
return False
|
||||
if not proposition_graph.is_fully_grounded():
|
||||
return False
|
||||
# Structural contract slots (when assessment carries organ bindings).
|
||||
if contract_assessment is not None:
|
||||
if contract_assessment.missing_bindings or contract_assessment.unresolved_hazards:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _substrate_supreme(
|
||||
proposition_graph: "PropositionGraph | None",
|
||||
contract_assessment: "ContractAssessment | None",
|
||||
) -> bool:
|
||||
"""Return True only when the geometric substrate has earned authority.
|
||||
"""True iff both dual-competing Shadow Gate competitors pass."""
|
||||
return _forward_surface_ok(proposition_graph, contract_assessment) and (
|
||||
_conjugate_coherence_ok(contract_assessment)
|
||||
)
|
||||
|
||||
This is the single source of truth for "use the PropositionGraph path
|
||||
as the cognitive spine instead of legacy runtime/pack/walk".
|
||||
|
||||
Conditions (all must hold):
|
||||
- A graph was produced.
|
||||
- graph.is_fully_grounded() — every slot bound by exact recall or
|
||||
direct construction (no <pending>).
|
||||
- If a ContractAssessment is supplied, it must be closed
|
||||
(zero missing_bindings and zero unresolved_hazards).
|
||||
(Assessments are still diagnostic-only in many organs; when the
|
||||
main spine wires ProblemFrame + assess_contracts, this becomes
|
||||
active backpressure — see Layer 3/Phase D.)
|
||||
|
||||
Versor coherence is *not* re-checked with a repair here. It is
|
||||
required by construction at the sites that emit versors (see
|
||||
VersorBinding and algebra/versor.py). Passing a non-coherent state
|
||||
here is a programmer error, not a runtime tolerance.
|
||||
|
||||
When this returns False the caller (pipeline) must emit the
|
||||
SUBSTRATE_BYPASS_HAZARD with graph.get_unresolved_topology() so the
|
||||
failure is actionable rather than silent.
|
||||
"""
|
||||
if proposition_graph is None:
|
||||
return False
|
||||
if not proposition_graph.is_fully_grounded():
|
||||
return False
|
||||
if contract_assessment is not None:
|
||||
if contract_assessment.missing_bindings or contract_assessment.unresolved_hazards:
|
||||
return False
|
||||
return True
|
||||
__all__ = [
|
||||
"SurfaceResolution",
|
||||
"resolve_surface",
|
||||
"_conjugate_coherence_ok",
|
||||
"_forward_surface_ok",
|
||||
"_substrate_supreme",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -292,15 +292,12 @@ class RuntimeConfig:
|
|||
# wanting a hard identity-continuity guarantee opt in.
|
||||
strict_identity_continuity: bool = False
|
||||
|
||||
# ADR-0244 §2.2 / §4a — operator-preservation identity gate. When on, the
|
||||
# per-turn identity check runs the metric-exact wave-field gate on the live
|
||||
# versor (final_state.F): subspace-leakage + signed self-alignment via
|
||||
# F aᵢ F̃, plus the boundary_ids intersection with the turn's safety/ethics
|
||||
# violations, and a fail-closed IdentityGateRefusal folded into the typed
|
||||
# refusal surface. OFF by default: the leakage threshold is provisional
|
||||
# (reuses alignment_threshold) until calibrated to γ_id in D4 Phase 3, and
|
||||
# the flag-off path is byte-identical to the pre-ADR-0244 advisory behavior
|
||||
# (legacy scalar-L2 identity score, no geometric refusal).
|
||||
# ADR-0244 §2.2 / §4a — live identity *refusal* on operator-preservation
|
||||
# leakage / inversion / boundary breach (IdentityGateRefusal). Scoring always
|
||||
# uses the metric-exact wave path on final_state.F; this flag only controls
|
||||
# whether a flagged score becomes a typed refusal surface. OFF by default:
|
||||
# γ_id separates geometric attack signal from benign traffic poorly on the
|
||||
# current nominal axis frame (see ADR-0244 Phase 3 honesty notes).
|
||||
identity_wave_gate: bool = False
|
||||
|
||||
# ADR-0246 §3.7 — the fuller induced-action admit surface (d_orth, d_stab vs
|
||||
|
|
@ -374,6 +371,46 @@ class RuntimeConfig:
|
|||
# default); the engine never raises its own ceiling.
|
||||
estimation_enabled: bool = False
|
||||
|
||||
# Deduction-serve arc — when on, ``chat/deduction_surface.py`` intercepts
|
||||
# propositional-argument-shaped turns ("P1. P2. ... Therefore C.") BEFORE
|
||||
# generic intent dispatch and decides them with the verified ROBDD
|
||||
# entailment engine (generate.proof_chain, 716/716 wrong=0) instead of the
|
||||
# pack-token-gloss fallback. Band v1 scope: single-token propositional
|
||||
# atoms only (docs/research/deduction-serve-arc-phase0-baseline-2026-07-23
|
||||
# .md); categorical/syllogism arguments are recognized as argument-shaped
|
||||
# but honestly declined as out-of-band. Phase 3 (ADR-0256): this is an
|
||||
# enable for an EARNED path, not a direct-serve switch — a decided
|
||||
# argument is served AUTHORITATIVELY only when its shape-band holds a SERVE
|
||||
# license on the committed, SHA-sealed reliability ledger
|
||||
# (chat/deduction_serve_license); an unearned band is served DISCLOSED
|
||||
# (hedged).
|
||||
#
|
||||
# RATIFIED ON 2026-07-24 by Shay — default flipped False -> True. The
|
||||
# evidence at ratification: 25 shape-bands each holding SERVE at 720/720
|
||||
# wrong=0 on the SHA-sealed ledger (theta_SERVE=0.99, n >= 657 committed),
|
||||
# the deduction-serve lane at 166/166 wrong=0 across six hand-authored
|
||||
# splits, and a commit gate (`looks_like_deductive_argument`) narrow by
|
||||
# construction — a sentence-initial "therefore" IS an argument, so the
|
||||
# composer cannot claim turns that are not. Rollback is flipping this back:
|
||||
# flag-off remains byte-identical to pre-arc dispatch and leaves no residue
|
||||
# (pinned by ``test_flag_off_preserves_pack_token_gloss_byte_identity``).
|
||||
deduction_serving_enabled: bool = True
|
||||
|
||||
# Generalization arc, Phase 2 (ADR-0262) — when on, ``chat/curriculum_surface.py``
|
||||
# answers exam-shaped polar questions ("Does force cause acceleration?") from the
|
||||
# RATIFIED domain-chain curriculum of the subject the question's vocabulary
|
||||
# belongs to, decided by the same argument bands the deduction composer uses.
|
||||
# The curriculum is read OPEN-world: a relation it does not teach is UNKNOWN,
|
||||
# never "no", so an untaught fact can never be decoded into a negative claim.
|
||||
# Like ``deduction_serving_enabled`` this is an enable for an EARNED path, not a
|
||||
# direct-serve switch — an answer is served AUTHORITATIVELY only when its
|
||||
# (subject x relation family) band holds a SERVE license on the committed,
|
||||
# SHA-sealed curriculum ledger; an unearned band is served DISCLOSED (hedged),
|
||||
# which is the state of every band today (ADR-0262 SS5: the binding constraint is
|
||||
# ratified curriculum volume, not machinery). OFF by default: flag-off is
|
||||
# byte-identical to pre-arc dispatch.
|
||||
curriculum_serving_enabled: bool = False
|
||||
|
||||
# ASK serving gate enable flag. When True, ASK serving is allowed.
|
||||
# Default False (dark).
|
||||
ask_serving_enabled: bool = False
|
||||
|
|
|
|||
|
|
@ -9,16 +9,44 @@ serialize stably into JSONL, metadata dictionaries, and test fixtures.
|
|||
from __future__ import annotations
|
||||
|
||||
from enum import Enum, unique
|
||||
from typing import Any, Literal
|
||||
from typing import Any, Literal, get_args
|
||||
|
||||
|
||||
GroundingSource = Literal["pack", "teaching", "vault", "partial", "oov", "none"]
|
||||
GroundingSource = Literal[
|
||||
"pack",
|
||||
"teaching",
|
||||
"vault",
|
||||
"partial",
|
||||
"oov",
|
||||
"none",
|
||||
"deduction",
|
||||
"curriculum",
|
||||
]
|
||||
"""Ratified grounding-source labels.
|
||||
|
||||
Single source of truth for the values ``epistemic_state_for_grounding_source``
|
||||
maps, the cold-start-grounding lane validates, and the Workbench UI badges
|
||||
bind to (ADR-0162 §3d). Adding a value here without adding a corresponding
|
||||
badge fails the build-time enum coverage test under ``workbench-ui/``.
|
||||
maps, the cold-start-grounding lane validates, the Workbench API coerces
|
||||
against, and the Workbench UI badges bind to (ADR-0162 §3d). Adding a value
|
||||
here without adding a corresponding badge fails the build-time enum coverage
|
||||
test under ``workbench-ui/``.
|
||||
|
||||
``deduction`` (ADR-0256) and ``curriculum`` (ADR-0262) joined the closed set
|
||||
once deduction serving was ratified ON: ``workbench/api.py``'s live chat route
|
||||
builds a bare ``ChatRuntime()``, so those composers decide Workbench turns and
|
||||
stamp their label on the ``TurnEvent``. While the labels were unregistered the
|
||||
API's coercion floored them to ``"none"`` — recording a proved answer as
|
||||
ungrounded in a durable audit artifact. Registration is what makes the
|
||||
recorded provenance true; ``GROUNDING_SOURCES`` below is what keeps any second
|
||||
copy of this set from drifting away from it again.
|
||||
"""
|
||||
|
||||
GROUNDING_SOURCES: frozenset[str] = frozenset(get_args(GroundingSource))
|
||||
"""The runtime-iterable form of :data:`GroundingSource`.
|
||||
|
||||
A ``Literal`` is invisible at runtime, which is why consumers historically
|
||||
restated its members by hand — and why one of those restatements silently fell
|
||||
behind. Anything that needs to *check* a grounding source reads this set;
|
||||
nothing re-types the members.
|
||||
"""
|
||||
|
||||
|
||||
|
|
@ -133,7 +161,14 @@ def normative_detail_from_verdicts(verdicts: Any = None, *, safety_verdict: Any
|
|||
def epistemic_state_for_grounding_source(source: str | None) -> EpistemicState:
|
||||
"""Default runtime mapping for existing grounding-source labels."""
|
||||
normalized = (source or "none").strip().lower()
|
||||
if normalized in {"pack", "teaching", "vault"}:
|
||||
# ``deduction`` / ``curriculum`` rank with the decoded sources rather than
|
||||
# the evidenced ones: both are *decided* by the ROBDD entailment engine
|
||||
# over ratified structure (an argument's own premises, or a subject's
|
||||
# ratified curriculum), not retrieved as supporting evidence. Serving them
|
||||
# authoritatively additionally requires a SERVE license on the sealed
|
||||
# ledger — an unearned band is served DISCLOSED, which is a surface-level
|
||||
# hedge and does not change how the turn was grounded.
|
||||
if normalized in {"pack", "teaching", "vault", "deduction", "curriculum"}:
|
||||
return EpistemicState.DECODED
|
||||
if normalized == "partial":
|
||||
return EpistemicState.EVIDENCED_INCOMPLETE
|
||||
|
|
@ -145,6 +180,7 @@ def epistemic_state_for_grounding_source(source: str | None) -> EpistemicState:
|
|||
|
||||
|
||||
__all__ = [
|
||||
"GROUNDING_SOURCES",
|
||||
"EpistemicState",
|
||||
"GroundingSource",
|
||||
"NormativeClearance",
|
||||
|
|
|
|||
|
|
@ -23,7 +23,14 @@ from core.physics.reasoning import ReasoningTrajectory, TrajectoryOperator
|
|||
from core.physics.articulation import ArticulationPlan, ArticulationPlanner, OutputModality
|
||||
from core.physics.drive import DriveGradientMap, GradientField, ValueAxis
|
||||
from core.physics.exertion import ExertionMeter, FatigueIndex, CycleCost
|
||||
from core.physics.identity import IdentityManifold, IdentityCheck, IdentityScore, CharacterProfile
|
||||
from core.physics.identity import (
|
||||
CharacterProfile,
|
||||
IdentityCheck,
|
||||
IdentityGateRefusal,
|
||||
IdentityManifold,
|
||||
IdentityScore,
|
||||
MissingWaveStateError,
|
||||
)
|
||||
from core.physics.learning import PromotionDecision, VaultPromotionPolicy
|
||||
from core.physics.goldtether import (
|
||||
AutonomyBand,
|
||||
|
|
@ -31,9 +38,11 @@ from core.physics.goldtether import (
|
|||
CoherenceResidual,
|
||||
GoldPromotionProof,
|
||||
GoldTetherMonitor,
|
||||
GoldTetherViolationError,
|
||||
OperatingMode,
|
||||
coherence_residual,
|
||||
propose_kappa_line_search,
|
||||
require_unitary,
|
||||
)
|
||||
from core.physics.dynamic_manifold import (
|
||||
AxisClassification,
|
||||
|
|
@ -230,9 +239,11 @@ __all__ = [
|
|||
"DriveGradientMap", "GradientField", "ValueAxis",
|
||||
"ExertionMeter", "FatigueIndex", "CycleCost",
|
||||
"IdentityManifold", "IdentityCheck", "IdentityScore", "CharacterProfile",
|
||||
"IdentityGateRefusal", "MissingWaveStateError",
|
||||
"PromotionDecision", "VaultPromotionPolicy",
|
||||
"AutonomyBand", "AutonomyDecision", "CoherenceResidual",
|
||||
"GoldPromotionProof", "GoldTetherMonitor", "OperatingMode", "coherence_residual",
|
||||
"GoldPromotionProof", "GoldTetherMonitor", "GoldTetherViolationError",
|
||||
"OperatingMode", "coherence_residual", "require_unitary",
|
||||
"AxisClassification", "CartanIwasawaFactors", "ConformalProcrustesResult",
|
||||
"PrincipalAxis", "SignatureAwarePCAResult",
|
||||
"cartan_iwasawa_extract", "cartan_iwasawa_factorize",
|
||||
|
|
|
|||
|
|
@ -68,15 +68,23 @@ if TYPE_CHECKING: # annotation-only: the monitor instance is caller-supplied
|
|||
|
||||
import numpy as np
|
||||
|
||||
from algebra.cl41 import N_COMPONENTS, geometric_product
|
||||
from algebra.cl41 import N_COMPONENTS, geometric_product, reverse
|
||||
from algebra.rotor import word_transition_rotor
|
||||
from algebra.versor import versor_condition
|
||||
from core.physics.energy import EnergyClass, EnergyProfile, FieldEnergyOperator
|
||||
from core.physics.sensorium_wave_feed import PacketLike, _coerce_packet, superpose_packets
|
||||
from core.physics.goldtether import GoldTetherViolationError, require_unitary
|
||||
from core.physics.sensorium_wave_feed import (
|
||||
PacketLike,
|
||||
_coerce_packet,
|
||||
compile_packet_to_psi,
|
||||
superpose_packets,
|
||||
)
|
||||
from core.physics.wave_energy_boundary import (
|
||||
CrystallizationDecision,
|
||||
crystallization_for_holographic_seal,
|
||||
energy_profile_from_wave,
|
||||
)
|
||||
from core.physics.wave_manifold import WaveManifold
|
||||
from core.physics.wave_manifold import WaveManifold, multivector_content_digest
|
||||
|
||||
_NEAR_ZERO = 1e-12
|
||||
_UNIT_TOL = 1e-9
|
||||
|
|
@ -213,46 +221,181 @@ def assignment_component_index(assignment_mask: int) -> int:
|
|||
# --- Ingress ----------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ModalityTransition:
|
||||
"""Provenance for a versor-sandwich modality transition (Spin(4,1))."""
|
||||
|
||||
psi_in_digest: str
|
||||
psi_out_digest: str
|
||||
rotor_digest: str
|
||||
goldtether_residual: float
|
||||
source_modality: str
|
||||
target_modality: str
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"psi_in_digest": self.psi_in_digest,
|
||||
"psi_out_digest": self.psi_out_digest,
|
||||
"rotor_digest": self.rotor_digest,
|
||||
"goldtether_residual": float(self.goldtether_residual),
|
||||
"source_modality": self.source_modality,
|
||||
"target_modality": self.target_modality,
|
||||
}
|
||||
|
||||
|
||||
def modality_transition_sandwich(
|
||||
psi_in: np.ndarray,
|
||||
rotor: np.ndarray,
|
||||
*,
|
||||
source_modality: str = "",
|
||||
target_modality: str = "",
|
||||
epsilon_drift: float = _EPSILON_DRIFT,
|
||||
) -> tuple[np.ndarray, ModalityTransition]:
|
||||
"""Inter-modality transition: ψ_out = R · ψ_in · rev(R), R ∈ Spin(4,1).
|
||||
|
||||
Fail-closed GoldTether validation on the output (and on the rotor unit
|
||||
residual). Digests are full SHA-256 over little-endian float64 bytes.
|
||||
Maps the dossier's multimodal_lifecycle sandwich contract onto this module
|
||||
(the real lifecycle owner; ``multimodal_lifecycle.py`` does not exist).
|
||||
"""
|
||||
psi = _as_psi(psi_in, "ψ_in", error=IngressDegenerate)
|
||||
R = np.asarray(rotor, dtype=np.float64)
|
||||
if R.shape != (N_COMPONENTS,):
|
||||
raise IngressDegenerate("bad_rotor_shape", shape=list(R.shape))
|
||||
if float(versor_condition(R)) >= float(epsilon_drift):
|
||||
raise GoldTetherViolationError(
|
||||
float(versor_condition(R)),
|
||||
float(epsilon_drift),
|
||||
detail="modality rotor not unit versor",
|
||||
)
|
||||
# ψ_out = R ψ rev(R)
|
||||
psi_out = geometric_product(geometric_product(R, psi), reverse(R)).astype(np.float64)
|
||||
residual = float(require_unitary(psi_out, epsilon=float(epsilon_drift)))
|
||||
transition = ModalityTransition(
|
||||
psi_in_digest=multivector_content_digest(psi),
|
||||
psi_out_digest=multivector_content_digest(psi_out),
|
||||
rotor_digest=multivector_content_digest(R),
|
||||
goldtether_residual=residual,
|
||||
source_modality=str(source_modality),
|
||||
target_modality=str(target_modality),
|
||||
)
|
||||
return psi_out, transition
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class IngressWavePacket:
|
||||
"""Normalized ingress field ψ_context with provenance (ADR-0243 §2.1)."""
|
||||
"""Normalized ingress field ψ_context with provenance (ADR-0243 §2.1).
|
||||
|
||||
Multi-modality composition is sandwich-governed: each inter-modality step
|
||||
is recorded in ``modality_transitions`` with full SHA-256 digests.
|
||||
"""
|
||||
|
||||
psi: np.ndarray
|
||||
domain_id: str
|
||||
modality_ids: tuple[str, ...]
|
||||
packet_digest: str
|
||||
modality_transitions: tuple[ModalityTransition, ...] = ()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
arr = _as_psi(self.psi, "ψ_context", error=IngressDegenerate)
|
||||
arr = arr.copy()
|
||||
arr.setflags(write=False)
|
||||
object.__setattr__(self, "psi", arr)
|
||||
object.__setattr__(
|
||||
self,
|
||||
"modality_transitions",
|
||||
tuple(self.modality_transitions),
|
||||
)
|
||||
|
||||
|
||||
def _construction_unitize(psi: np.ndarray, *, name: str) -> np.ndarray:
|
||||
"""Owned construction-boundary Euclidean unitize (not hot-path repair)."""
|
||||
arr = np.asarray(psi, dtype=np.float64).reshape(-1)
|
||||
if arr.shape != (N_COMPONENTS,):
|
||||
raise IngressDegenerate("bad_shape", name=name, shape=list(arr.shape))
|
||||
if not np.all(np.isfinite(arr)):
|
||||
raise IngressDegenerate("non_finite", name=name)
|
||||
norm = float(np.linalg.norm(arr))
|
||||
if not np.isfinite(norm) or norm < _NEAR_ZERO:
|
||||
raise IngressDegenerate("degenerate_packet", name=name, norm=norm)
|
||||
return (arr / norm).astype(np.float64)
|
||||
|
||||
|
||||
def ingest_context(packets: Sequence[PacketLike], domain_id: str) -> IngressWavePacket:
|
||||
"""Superpose modality packets and normalize at the owned construction boundary.
|
||||
"""Compose modality packets into ψ_context with sandwich-governed multi-modality.
|
||||
|
||||
Delegates superposition to :func:`sensorium_wave_feed.superpose_packets`
|
||||
(which refuses empty input). Normalization here is the ONE owned
|
||||
construction boundary of the lifecycle (D-3) — a degenerate superposition
|
||||
(destructive cancellation below ``1e-12``) is refused, never zero-filled.
|
||||
* Empty input refuses (via superpose preflight / empty list).
|
||||
* Degenerate linear cancellation (Σψ ≈ 0) refuses as construction failure.
|
||||
* Single packet: construction-boundary unitize only.
|
||||
* Multi-packet: successive Spin(4,1) sandwiches
|
||||
``ψ ← R_i · ψ · rev(R_i)`` with
|
||||
``R_i = word_transition_rotor(ψ, ψ_{i+1})``, each step fail-closed via
|
||||
:func:`modality_transition_sandwich` (GoldTether + SHA-256 digests).
|
||||
|
||||
Normalization / unitize lives only at this owned construction boundary.
|
||||
"""
|
||||
domain = str(domain_id).strip()
|
||||
if not domain:
|
||||
raise IngressDegenerate("empty_domain_id")
|
||||
if not packets:
|
||||
raise ValueError("superpose_packets: empty packet list")
|
||||
|
||||
# Preflight: refuse empty and destructive cancellation (Σψ ≈ 0).
|
||||
total = superpose_packets(packets)
|
||||
norm = float(np.linalg.norm(total))
|
||||
if not np.isfinite(norm) or norm < _NEAR_ZERO:
|
||||
raise IngressDegenerate("degenerate_superposition", norm=norm, n_packets=len(packets))
|
||||
psi = (total / norm).astype(np.float64)
|
||||
modality_ids = tuple(_coerce_packet(p).modality_id for p in packets)
|
||||
mass = float(np.linalg.norm(total))
|
||||
if not np.isfinite(mass) or mass < _NEAR_ZERO:
|
||||
raise IngressDegenerate(
|
||||
"degenerate_superposition", norm=mass, n_packets=len(packets)
|
||||
)
|
||||
|
||||
coerced = [_coerce_packet(p) for p in packets]
|
||||
modality_ids = tuple(p.modality_id for p in coerced)
|
||||
transitions: list[ModalityTransition] = []
|
||||
|
||||
# Seed from first packet at construction boundary.
|
||||
psi = _construction_unitize(
|
||||
compile_packet_to_psi(coerced[0]), name="packet[0]"
|
||||
)
|
||||
|
||||
# Multi-modality: sandwich each subsequent packet into the field.
|
||||
for i in range(1, len(coerced)):
|
||||
target = _construction_unitize(
|
||||
compile_packet_to_psi(coerced[i]), name=f"packet[{i}]"
|
||||
)
|
||||
try:
|
||||
rotor = word_transition_rotor(psi, target)
|
||||
except ValueError as exc:
|
||||
raise IngressDegenerate(
|
||||
"modality_rotor_refused",
|
||||
source=modality_ids[i - 1],
|
||||
target=modality_ids[i],
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
psi, tr = modality_transition_sandwich(
|
||||
psi,
|
||||
rotor,
|
||||
source_modality=modality_ids[i - 1],
|
||||
target_modality=modality_ids[i],
|
||||
epsilon_drift=_EPSILON_DRIFT,
|
||||
)
|
||||
transitions.append(tr)
|
||||
|
||||
# Final construction close: unit Euclidean density for energy path.
|
||||
psi = _construction_unitize(psi, name="ψ_context")
|
||||
digests = [tr.psi_out_digest for tr in transitions]
|
||||
return IngressWavePacket(
|
||||
psi=psi,
|
||||
domain_id=domain,
|
||||
modality_ids=modality_ids,
|
||||
packet_digest=_content_id(
|
||||
{"psi": _psi_digest(psi), "domain": domain, "modalities": list(modality_ids)}
|
||||
{
|
||||
"psi": _psi_digest(psi),
|
||||
"domain": domain,
|
||||
"modalities": list(modality_ids),
|
||||
"transitions": digests,
|
||||
}
|
||||
),
|
||||
modality_transitions=tuple(transitions),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1053,7 +1196,14 @@ def tether_reading(
|
|||
autonomy = float(monitor.autonomy)
|
||||
updated = False
|
||||
else:
|
||||
residual, autonomy = monitor.update(arr)
|
||||
# ``GoldTetherMonitor.update`` raises on R > ε after forcing autonomy
|
||||
# to zero. Corridor tether readings must surface that fail-closed
|
||||
# residual without aborting the lifecycle observation path.
|
||||
try:
|
||||
residual, autonomy = monitor.update(arr)
|
||||
except GoldTetherViolationError as exc:
|
||||
residual = float(exc.residual)
|
||||
autonomy = float(monitor.autonomy)
|
||||
updated = True
|
||||
chiral_verdict = monitor.chiral_gate.observe(arr).verdict
|
||||
return TetherReading(
|
||||
|
|
@ -1198,6 +1348,8 @@ __all__ = [
|
|||
"compile_quadratic_well",
|
||||
"egress_gate",
|
||||
"ingest_context",
|
||||
"modality_transition_sandwich",
|
||||
"ModalityTransition",
|
||||
"propositional_entails",
|
||||
"relax_to_ground",
|
||||
"serving_cast",
|
||||
|
|
|
|||
|
|
@ -2,20 +2,15 @@
|
|||
core/physics/goldtether.py
|
||||
|
||||
GoldTether — Coherence Residual Monitor + Dynamic Autonomy Floor
|
||||
ADR-0238
|
||||
ADR-0238 / ADR-0241 wave residual path.
|
||||
|
||||
Note (fidelity #19, RETIRED): an earlier draft borrowed grade-5 "pseudoscalar"
|
||||
vocabulary from Super-Blueprint §3.3 for the autonomy floor and read ``F[31]``
|
||||
into telemetry. That anchor is vacuous in odd-dim Cl(4,1) — field-state versors
|
||||
are even (``F[31] ≡ 0``) and ``I₅`` is central (``V·I₅·Ṽ = I₅`` for every
|
||||
versor), so no non-vacuous grade-5 transition invariant exists. The namesake is
|
||||
removed; the integrity-anchor role is carried by versor closure + the harmonized
|
||||
GoldTether residual + biography/identity holonomy. See
|
||||
``docs/research/third-door-blueprint-fidelity.md`` §5.
|
||||
Primary residual:
|
||||
R = || ψ · reverse(ψ) − 1 ||_F
|
||||
via :meth:`WaveManifold.measure_unitary_residual` (dual-checked). Transitions
|
||||
with R > epsilon_drift raise :class:`GoldTetherViolationError` synchronously
|
||||
(:meth:`GoldTetherMonitor.update`, :func:`require_unitary`).
|
||||
|
||||
Absolute mastery implementation on the live Cl(4,1) algebra kernel.
|
||||
All operators are pure where possible, dual-corrected, and enforce algebraic
|
||||
closure on versor-valued outputs.
|
||||
No flat ``np.dot`` residual path. No external I₅ matrix parameters.
|
||||
|
||||
Distinct from Arena GoldTether (ADR-0199 / core.learning_arena.protocols).
|
||||
"""
|
||||
|
|
@ -107,6 +102,25 @@ class AutonomyDecision:
|
|||
reason: str
|
||||
|
||||
|
||||
class GoldTetherViolationError(ValueError):
|
||||
"""Fail-closed rejection when unitary amplitude drift exceeds tolerance.
|
||||
|
||||
Raised synchronously when ``R_GoldTether > epsilon`` (default ``1e-6``).
|
||||
Does not soft-warn or defer; the transition must not commit.
|
||||
"""
|
||||
|
||||
def __init__(self, residual: float, epsilon: float = 1e-6, *, detail: str = "") -> None:
|
||||
self.residual = float(residual)
|
||||
self.epsilon = float(epsilon)
|
||||
msg = (
|
||||
f"GoldTether violation: R={self.residual:.3e} exceeds "
|
||||
f"epsilon={self.epsilon:.3e}"
|
||||
)
|
||||
if detail:
|
||||
msg = f"{msg} ({detail})"
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
def _as_mv(F: np.ndarray, name: str = "F") -> np.ndarray:
|
||||
arr = np.asarray(F, dtype=np.float64)
|
||||
if arr.shape != (N_COMPONENTS,):
|
||||
|
|
@ -117,14 +131,34 @@ def _as_mv(F: np.ndarray, name: str = "F") -> np.ndarray:
|
|||
def coherence_residual(F: np.ndarray) -> float:
|
||||
"""Public one-shot residual for tests and harnesses.
|
||||
|
||||
R = || F · reverse(F) − 1 ||_F (dual-checked against reverse(F)).
|
||||
R_GoldTether = || ψ · reverse(ψ) − 1 ||_F (dual-checked against reverse(ψ)).
|
||||
|
||||
``||·||_F`` is |⟨ψ~ψ⟩₀ − 1| plus the Euclidean norm of non-scalar grades
|
||||
(via :func:`algebra.versor.versor_unit_residual`).
|
||||
|
||||
Canonical path (ADR-0241 Slice 2): :meth:`WaveManifold.measure_unitary_residual`
|
||||
— unitary wave amplitude drift, not a parallel residual implementation.
|
||||
No flat ``np.dot`` products; no external I₅ matrix parameters.
|
||||
"""
|
||||
return WaveManifold().measure_unitary_residual(_as_mv(F))
|
||||
|
||||
|
||||
def require_unitary(
|
||||
F: np.ndarray,
|
||||
*,
|
||||
epsilon: float = 1e-6,
|
||||
detail: str = "",
|
||||
) -> float:
|
||||
"""Return residual if ``R ≤ epsilon``; else raise :class:`GoldTetherViolationError`.
|
||||
|
||||
Synchronous fail-closed gate for state transitions.
|
||||
"""
|
||||
r = float(coherence_residual(F))
|
||||
if r > float(epsilon):
|
||||
raise GoldTetherViolationError(r, float(epsilon), detail=detail)
|
||||
return r
|
||||
|
||||
|
||||
@dataclass
|
||||
class GoldTetherMonitor:
|
||||
"""
|
||||
|
|
@ -169,6 +203,10 @@ class GoldTetherMonitor:
|
|||
"""Compute the primary GoldTether residual. Always ≥ 0. Dual-corrected."""
|
||||
return coherence_residual(F)
|
||||
|
||||
def require_unitary(self, F: np.ndarray, *, detail: str = "") -> float:
|
||||
"""Fail-closed residual gate for this monitor's ``epsilon_drift``."""
|
||||
return require_unitary(F, epsilon=float(self.epsilon_drift), detail=detail)
|
||||
|
||||
def update(
|
||||
self,
|
||||
F: np.ndarray,
|
||||
|
|
@ -177,20 +215,31 @@ class GoldTetherMonitor:
|
|||
"""
|
||||
Update monitor with new field state.
|
||||
Returns (residual, new_autonomy).
|
||||
Dual-correction: residual is checked both ways inside residual().
|
||||
|
||||
If residual exceeds ``epsilon_drift``, autonomy is forced to zero, the
|
||||
rejection is recorded in history, and :class:`GoldTetherViolationError`
|
||||
is raised synchronously (no soft commit of elevated floor/autonomy).
|
||||
"""
|
||||
r = self.residual(F)
|
||||
|
||||
if r > self.epsilon_drift:
|
||||
# Fail-closed: force autonomy to zero
|
||||
# Fail-closed: force autonomy to zero, record, then reject.
|
||||
self.autonomy = 0.0
|
||||
self.floor = max(0.0, self.floor - self.floor_decay)
|
||||
else:
|
||||
if epistemic_elevation:
|
||||
# Only proven elevation may raise the floor
|
||||
self.floor = min(1.0, self.floor + self.floor_step)
|
||||
# Autonomy may never exceed the floor
|
||||
self.autonomy = min(self.autonomy + self.autonomy_step, self.floor)
|
||||
self.history.append((float(r), float(self.floor), float(self.autonomy)))
|
||||
if len(self.history) > self.max_history:
|
||||
self.history.pop(0)
|
||||
raise GoldTetherViolationError(
|
||||
float(r),
|
||||
float(self.epsilon_drift),
|
||||
detail="GoldTetherMonitor.update rejected drifted field",
|
||||
)
|
||||
|
||||
if epistemic_elevation:
|
||||
# Only proven elevation may raise the floor
|
||||
self.floor = min(1.0, self.floor + self.floor_step)
|
||||
# Autonomy may never exceed the floor
|
||||
self.autonomy = min(self.autonomy + self.autonomy_step, self.floor)
|
||||
|
||||
self.history.append((float(r), float(self.floor), float(self.autonomy)))
|
||||
if len(self.history) > self.max_history:
|
||||
|
|
@ -343,11 +392,16 @@ class GoldTetherMonitor:
|
|||
cond = float(versor_condition(F_arr))
|
||||
# Closure residual only (geo distance to 𝓘_gold is expected for new axes).
|
||||
drift = float(coherence_residual(F_arr))
|
||||
if cond >= _CLOSURE_TOL or drift > float(self.epsilon_drift):
|
||||
if cond >= _CLOSURE_TOL:
|
||||
raise ValueError(
|
||||
"promote_gold_invariant refused: not a closed versor "
|
||||
f"(versor_condition={cond:.3e}) or residual/drift {drift:.3e} "
|
||||
f"exceeds epsilon_drift={float(self.epsilon_drift)}"
|
||||
f"(versor_condition={cond:.3e})"
|
||||
)
|
||||
if drift > float(self.epsilon_drift):
|
||||
raise GoldTetherViolationError(
|
||||
drift,
|
||||
float(self.epsilon_drift),
|
||||
detail="promote_gold_invariant refused high residual",
|
||||
)
|
||||
self.gold_invariants.append(F_arr.copy())
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,20 @@
|
|||
"""core.physics.identity — Identity as geometric structure, not prompt veneer.
|
||||
|
||||
ADR-0010: The IdentityManifold is a fixed geometric subspace of the
|
||||
versor field encoding CORE's stable character as an architectural
|
||||
constant. Every ReasoningTrajectory is checked against the manifold
|
||||
before articulation. Identity is inalienable — it cannot be overridden
|
||||
by context length, adversarial prompting, or instruction injection.
|
||||
ADR-0010 / ADR-0244: The IdentityManifold is a fixed geometric subspace of
|
||||
the Cl(4,1) versor field. Trajectory alignment uses metric-exact Gram
|
||||
projection (``identity_manifold``) on an explicit wave-field ``ψ_traj``.
|
||||
|
||||
Missing wave state raises :class:`MissingWaveStateError`. There is no
|
||||
scalar-L2 fallback. Live refusal remains flag-gated via
|
||||
``RuntimeConfig.identity_wave_gate``; scoring is always geometric.
|
||||
|
||||
Theological grounding: John 1:1-2.
|
||||
The Word is not a description of God. It is God, expressed.
|
||||
CORE's identity is not a description of CORE. It is CORE, expressed geometrically.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import functools
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import warnings
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, FrozenSet, List, Optional, Tuple
|
||||
|
|
@ -52,10 +51,9 @@ from core.physics.identity_action import (
|
|||
# signal, NOT real benign traffic — live ``final_state.F`` versors do not preserve
|
||||
# span(e1,e2,e3) (the shipped axes are nominal basis vectors, not dynamically
|
||||
# preserved eigenmodes), so benign leakage overlaps the attack range and the
|
||||
# calibration certifies ``flag_flip_authorized=False``. The wave gate therefore
|
||||
# stays flag-gated OFF in the runtime (``identity_wave_gate=False``); this bound
|
||||
# governs only the off-serve research/eval path until identity is made
|
||||
# dynamically load-bearing (ADR-0246 induced action).
|
||||
# calibration certifies ``flag_flip_authorized=False``. Scoring is always the
|
||||
# metric-exact wave path; live *refusal* remains flag-gated via
|
||||
# ``identity_wave_gate`` until identity is dynamically load-bearing (ADR-0246).
|
||||
_WAVE_LEAKAGE_BOUND: float = 0.2126624458513829
|
||||
# The orientation floor flags a value axis the versor has rotated *past
|
||||
# orthogonal* (toward inversion). It is a geometric invariant (a preserved axis
|
||||
|
|
@ -76,6 +74,15 @@ class IdentityGateRefusal(Exception):
|
|||
"""
|
||||
|
||||
|
||||
class MissingWaveStateError(ValueError):
|
||||
"""Fail-closed when IdentityCheck receives no trajectory wave-packet.
|
||||
|
||||
Convergence blueprint (ADR-0244 Gram path): identity alignment is defined
|
||||
only for an explicit Cl(4,1) wave-field ``ψ_traj``. An absent field is not
|
||||
a soft advisory case and must never fall back to scalar heuristics.
|
||||
"""
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=32)
|
||||
def _geometry_for_axis_directions(
|
||||
directions: Tuple[Tuple[float, ...], ...]
|
||||
|
|
@ -211,28 +218,24 @@ class IdentityScore:
|
|||
flagged: bool # True if any axis projection fell below alignment threshold
|
||||
deviation_axes: FrozenSet[str] # ValueAxis IDs where deviation was detected
|
||||
trajectory_id: str
|
||||
# ADR-0244 §2.2 / §4a — operator-preservation wave-field measures. Populated
|
||||
# only on the wave path (``wave_mode_active=True``); legacy defaults preserve
|
||||
# the pre-ADR-0244 IdentityScore shape and all downstream serialization
|
||||
# (the telemetry serializer emits these keys only when the wave path ran).
|
||||
wave_mode_active: bool = False
|
||||
# ADR-0244 §2.2 / §4a — operator-preservation wave-field measures.
|
||||
# Always True after geometric convergence (wave path is the only path).
|
||||
wave_mode_active: bool = True
|
||||
# RMS subspace-leakage over the value axes (0.0 = every axis preserved).
|
||||
leakage_norm: float = 0.0
|
||||
# Minimum signed self-alignment ⟨aᵢ, F aᵢ F̃⟩₀ across axes (+1 preserved,
|
||||
# −1 inverted); 1.0 in legacy mode.
|
||||
# −1 inverted).
|
||||
min_self_alignment: float = 1.0
|
||||
# Committed boundary_ids the turn violated (intersection with the manifold's
|
||||
# boundary set); a non-empty set is a hard identity-boundary breach.
|
||||
boundary_violations: FrozenSet[str] = frozenset()
|
||||
# ADR-0246 §3.7 induced-action admit-surface measures. Populated only when the
|
||||
# ``identity_action_surface`` policy runs (``action_surface_active=True``);
|
||||
# legacy defaults keep the flag-off wave/legacy IdentityScore byte-identical.
|
||||
# ``identity_action_surface`` policy runs (``action_surface_active=True``).
|
||||
action_surface_active: bool = False
|
||||
d_orth: float = 0.0
|
||||
d_stab: float = 0.0
|
||||
# ADR-0246 §4.1 — the full per-turn IdentityActionRecord (typed residual
|
||||
# channels, digests, admit verdict). ``None`` unless the §3.7 surface ran
|
||||
# (``action_surface_active=True``); legacy/flag-off callers are unaffected.
|
||||
# channels, digests, admit verdict). ``None`` unless the §3.7 surface ran.
|
||||
action_record: "IdentityActionRecord | None" = None
|
||||
|
||||
@property
|
||||
|
|
@ -336,38 +339,13 @@ class IdentityCheck:
|
|||
def _clamp01(value: float) -> float:
|
||||
return max(0.0, min(1.0, float(value)))
|
||||
|
||||
@staticmethod
|
||||
def _mean_frame_coherence(trajectory) -> float:
|
||||
frames = getattr(trajectory, "frames", None)
|
||||
if not frames:
|
||||
return 0.0
|
||||
return sum(
|
||||
float(getattr(frame, "coherence_magnitude", 0.0)) for frame in frames
|
||||
) / len(frames)
|
||||
|
||||
@staticmethod
|
||||
def _axis_projection(axis, trajectory, scalar_score: float) -> float:
|
||||
"""Deterministically project trajectory evidence onto one value axis."""
|
||||
direction = tuple(float(x) for x in getattr(axis, "direction", ()) or ())
|
||||
if not direction:
|
||||
return scalar_score
|
||||
full_l2 = math.sqrt(sum(x * x for x in direction)) or 1.0
|
||||
head_l2 = math.sqrt(sum(x * x for x in direction[:3]))
|
||||
directional_weight = head_l2 / full_l2
|
||||
frame_coherence = IdentityCheck._mean_frame_coherence(trajectory)
|
||||
coherence_term = IdentityCheck._clamp01(0.5 + (frame_coherence / 2.0))
|
||||
return IdentityCheck._clamp01(
|
||||
(0.75 * scalar_score) + (0.25 * directional_weight * coherence_term)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _validate_wave_field(wave_field) -> np.ndarray:
|
||||
"""Coerce + fail-closed-validate the live versor (ADR-0244 §4a).
|
||||
|
||||
A malformed wave field (wrong shape, non-finite, wrong byte-order) is a
|
||||
typed ``ValueError`` — it never silently falls back to the legacy
|
||||
scalar-L2 path. The dual-mode fallback (see :meth:`check`) is for an
|
||||
ABSENT wave field only, not a malformed one.
|
||||
typed ``ValueError``. An *absent* wave field is
|
||||
:class:`MissingWaveStateError` at :meth:`check` (never a scalar fallback).
|
||||
"""
|
||||
F = np.ascontiguousarray(wave_field, dtype=np.float32)
|
||||
if F.dtype.byteorder not in ("<", "="):
|
||||
|
|
@ -493,25 +471,27 @@ class IdentityCheck:
|
|||
) -> IdentityScore:
|
||||
"""Check a trajectory against the IdentityManifold (ADR-0010 / ADR-0244).
|
||||
|
||||
Dual-mode (ADR-0244 §3): when a ``wave_field`` (the live versor
|
||||
``final_state.F``) is supplied, run the metric-exact operator-preservation
|
||||
gate; otherwise fall back to the legacy scalar-L2 heuristic. A *malformed*
|
||||
wave field raises (fail-closed) — only an ABSENT one falls back.
|
||||
Metric-exact operator-preservation only (Gram geometry in
|
||||
:mod:`core.physics.identity_manifold`). Requires an explicit Cl(4,1)
|
||||
``wave_field`` (``ψ_traj``); absence raises
|
||||
:class:`MissingWaveStateError`. Malformed fields raise ``ValueError``.
|
||||
|
||||
``admission_policy`` (ADR-0246 §3.7, flag-gated behind
|
||||
``identity_action_surface``) is forwarded to the wave path only; ``None``
|
||||
(default) keeps every caller byte-identical to the D4 gate. ``turn_id``/
|
||||
``pack_id`` (ADR-0246 §4.1) are cosmetic identifiers for the per-turn
|
||||
record and default to ``0``/``""`` — omitting them changes nothing.
|
||||
``admission_policy`` (ADR-0246 §3.7) is optional; ``None`` keeps the
|
||||
D4 wave gate without the induced-action surface. ``turn_id`` / ``pack_id``
|
||||
are cosmetic identifiers for the per-turn action record.
|
||||
|
||||
``violated_boundary_ids`` (the turn's safety/ethics violated boundaries)
|
||||
is intersected with the manifold's committed ``boundary_ids``; a non-empty
|
||||
intersection is a hard identity-boundary breach (governance annotation
|
||||
item 7). Defaults empty so pre-ADR-0244 callers are byte-identical.
|
||||
``violated_boundary_ids`` is intersected with the manifold's committed
|
||||
``boundary_ids``; a non-empty intersection is a hard identity-boundary
|
||||
breach.
|
||||
"""
|
||||
resolved_manifold = manifold or self._manifold
|
||||
if resolved_manifold is None:
|
||||
raise TypeError("IdentityCheck.check() requires an IdentityManifold")
|
||||
if wave_field is None:
|
||||
raise MissingWaveStateError(
|
||||
"IdentityCheck requires an explicit Cl(4,1) wave_field "
|
||||
"(ψ_traj); scalar-L2 fallback is excised"
|
||||
)
|
||||
trajectory_id = str(getattr(trajectory, "trajectory_id", "legacy_trajectory"))
|
||||
boundary_violations = (
|
||||
frozenset(violated_boundary_ids) & resolved_manifold.boundary_ids
|
||||
|
|
@ -522,27 +502,17 @@ class IdentityCheck:
|
|||
flagged=bool(boundary_violations),
|
||||
deviation_axes=frozenset(),
|
||||
trajectory_id=trajectory_id,
|
||||
wave_mode_active=True,
|
||||
boundary_violations=boundary_violations,
|
||||
)
|
||||
if wave_field is not None:
|
||||
return self._wave_field_score(
|
||||
wave_field, resolved_manifold, trajectory_id, boundary_violations,
|
||||
admission_policy=admission_policy, turn_id=turn_id, pack_id=pack_id,
|
||||
)
|
||||
confidence = float(getattr(trajectory, "total_coherence_delta", 0.0))
|
||||
confidence += self._mean_frame_coherence(trajectory)
|
||||
score = self._clamp01(0.5 + (confidence / 2.0))
|
||||
deviations = frozenset(
|
||||
str(getattr(axis, "axis_id", getattr(axis, "name", "axis")))
|
||||
for axis in resolved_manifold.value_axes
|
||||
if self._axis_projection(axis, trajectory, score) < resolved_manifold.alignment_threshold
|
||||
)
|
||||
return IdentityScore(
|
||||
score=score,
|
||||
flagged=bool(deviations) or bool(boundary_violations),
|
||||
deviation_axes=deviations,
|
||||
trajectory_id=trajectory_id,
|
||||
boundary_violations=boundary_violations,
|
||||
return self._wave_field_score(
|
||||
wave_field,
|
||||
resolved_manifold,
|
||||
trajectory_id,
|
||||
boundary_violations,
|
||||
admission_policy=admission_policy,
|
||||
turn_id=turn_id,
|
||||
pack_id=pack_id,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -15,9 +15,16 @@ class PromotionDecision:
|
|||
|
||||
|
||||
class VaultPromotionPolicy:
|
||||
"""Promote only settled, coherent regions into deep vault storage."""
|
||||
"""Promote only settled, *geometrically* coherent regions to COHERENT.
|
||||
|
||||
def __init__(self, residual_threshold: float = 0.05) -> None:
|
||||
Stage 3A / Master Blueprint: COHERENT standing requires the unitary
|
||||
residual condition (``coherence_residual ≤ 1e-6`` by default), not a
|
||||
soft energy-band threshold alone. Tests may pass a looser
|
||||
``residual_threshold`` explicitly for energy-class isolation fixtures;
|
||||
production ``VaultPromotionPolicy()`` uses the geometric floor.
|
||||
"""
|
||||
|
||||
def __init__(self, residual_threshold: float = 1e-6) -> None:
|
||||
if residual_threshold < 0.0:
|
||||
raise ValueError("residual_threshold must be non-negative")
|
||||
self.residual_threshold = float(residual_threshold)
|
||||
|
|
@ -27,6 +34,13 @@ class VaultPromotionPolicy:
|
|||
return PromotionDecision(False, "missing_energy_profile", EnergyClass.E2)
|
||||
if not energy.energy_class.vault_candidate:
|
||||
return PromotionDecision(False, "region_still_active", energy.energy_class)
|
||||
# Full geometric unitarity gate for COHERENT promotion (Stage 3A).
|
||||
if energy.coherence_residual > self.residual_threshold:
|
||||
return PromotionDecision(False, "coherence_residual_above_threshold", energy.energy_class)
|
||||
return PromotionDecision(
|
||||
False, "coherence_residual_above_threshold", energy.energy_class
|
||||
)
|
||||
# Residual must also be finite and non-negative (typed safety).
|
||||
r = float(energy.coherence_residual)
|
||||
if not (r == r) or r < 0.0: # NaN or negative
|
||||
return PromotionDecision(False, "coherence_residual_invalid", energy.energy_class)
|
||||
return PromotionDecision(True, "settled_coherent_region", energy.energy_class)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ is unset. Helpers without a Rust path (``reverse``, ``scalar_part``,
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from typing import Any, Sequence, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
|
@ -40,6 +41,22 @@ _NEAR_ZERO = 1e-12
|
|||
_NONSIMPLE_TOL = 1e-6
|
||||
|
||||
|
||||
def multivector_content_digest(psi: np.ndarray) -> str:
|
||||
"""Full 64-char SHA-256 of little-endian float64 multivector components.
|
||||
|
||||
Canonical content address for Cl(4,1) wave state (Reconstruction-over-Storage).
|
||||
"""
|
||||
arr = np.ascontiguousarray(np.asarray(psi, dtype=np.float64))
|
||||
if arr.shape != (N_COMPONENTS,):
|
||||
raise ValueError(
|
||||
f"content digest requires shape ({N_COMPONENTS},); got {arr.shape}"
|
||||
)
|
||||
if not np.all(np.isfinite(arr)):
|
||||
raise ValueError("content digest requires finite multivector components")
|
||||
le = arr.astype(np.dtype("<f8"), copy=False)
|
||||
return hashlib.sha256(le.tobytes()).hexdigest()
|
||||
|
||||
|
||||
class WaveSpectralLeakageError(ValueError):
|
||||
"""Fail-closed spectral leakage (metric-degenerate resonant span).
|
||||
|
||||
|
|
@ -193,13 +210,22 @@ class WaveManifold:
|
|||
Construction-closed rotors; dual-checked unitary residual; deterministic.
|
||||
Optional standing-wave mode registry for resonant recall (ADR-0241 §2.2);
|
||||
not a vault/store — reconstruction-over-storage, off-serving.
|
||||
|
||||
Stored modes are content-addressed by SHA-256 over little-endian float64
|
||||
multivector bytes (:func:`multivector_content_digest`).
|
||||
"""
|
||||
|
||||
def __init__(self, epsilon_drift: float = 1e-6) -> None:
|
||||
self.epsilon_drift = float(epsilon_drift)
|
||||
self.n_dims = N_COMPONENTS
|
||||
# Standing-wave eigenmode registry (session-local; not durable memory).
|
||||
self._resonant_modes: list[np.ndarray] = []
|
||||
# Each entry is (digest, psi) so storage is content-addressed.
|
||||
self._resonant_modes: list[tuple[str, np.ndarray]] = []
|
||||
|
||||
@staticmethod
|
||||
def content_digest(psi: np.ndarray) -> str:
|
||||
"""SHA-256 hex digest of a Cl(4,1) multivector (little-endian f64)."""
|
||||
return multivector_content_digest(psi)
|
||||
|
||||
# --- Transport -----------------------------------------------------------
|
||||
|
||||
|
|
@ -300,9 +326,13 @@ class WaveManifold:
|
|||
# --- Standing-wave registry / resonant recall (ADR-0241 §2.2) ------------
|
||||
|
||||
def register_resonant_mode(self, psi_k: np.ndarray) -> int:
|
||||
"""Register a standing-wave mode. Returns mode index. Session-local only."""
|
||||
"""Register a standing-wave mode. Returns mode index. Session-local only.
|
||||
|
||||
Content-addressed by SHA-256 of little-endian float64 components.
|
||||
"""
|
||||
mode = _as_mv(psi_k, "ψ_k").copy()
|
||||
self._resonant_modes.append(mode)
|
||||
digest = multivector_content_digest(mode)
|
||||
self._resonant_modes.append((digest, mode))
|
||||
return len(self._resonant_modes) - 1
|
||||
|
||||
def clear_resonant_modes(self) -> None:
|
||||
|
|
@ -311,7 +341,12 @@ class WaveManifold:
|
|||
|
||||
@property
|
||||
def resonant_modes(self) -> tuple[np.ndarray, ...]:
|
||||
return tuple(m.copy() for m in self._resonant_modes)
|
||||
return tuple(m.copy() for _d, m in self._resonant_modes)
|
||||
|
||||
@property
|
||||
def resonant_mode_digests(self) -> tuple[str, ...]:
|
||||
"""SHA-256 digests parallel to :attr:`resonant_modes`."""
|
||||
return tuple(d for d, _m in self._resonant_modes)
|
||||
|
||||
def resonant_recall(
|
||||
self,
|
||||
|
|
@ -406,7 +441,7 @@ class WaveManifold:
|
|||
modes: Sequence[np.ndarray] | None,
|
||||
) -> list[np.ndarray]:
|
||||
if modes is None:
|
||||
return list(self._resonant_modes)
|
||||
return [m.copy() for _d, m in self._resonant_modes]
|
||||
return [_as_mv(m, f"mode[{i}]") for i, m in enumerate(modes)]
|
||||
|
||||
# --- Chiral spinor charge ------------------------------------------------
|
||||
|
|
@ -433,4 +468,4 @@ class WaveManifold:
|
|||
)
|
||||
|
||||
|
||||
__all__ = ["WaveManifold", "WaveSpectralLeakageError"]
|
||||
__all__ = ["WaveManifold", "WaveSpectralLeakageError", "multivector_content_digest"]
|
||||
|
|
|
|||
|
|
@ -1,16 +1,33 @@
|
|||
"""Read-only proposal review reporter (RPT) — surfaces comprehension-failure proposals for review.
|
||||
"""Proposal review reporter (RPT) — surfaces contemplation/idle 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.
|
||||
N5), validates them, and reports pending review obligations. 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.
|
||||
|
||||
:mod:`core.proposal_review.queue` (Tier S4) extends this to a second sink
|
||||
(``derived_close_facts``) and adds human review-STATE tracking — an append-only sidecar log
|
||||
(``teaching/proposals/review_log.jsonl``) recording that a human looked at an artifact. That sidecar
|
||||
is the one write anywhere in this package: it never touches a sink artifact's own ``status`` /
|
||||
``mounted`` / ``requires_review`` fields, never ratifies, and never mounts anything — ratification
|
||||
stays ``teaching/proposals.py``'s separate ADR-0057 corridor (``core teaching proposals`` / ``core
|
||||
teaching review``, a different sink entirely).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from core.proposal_review.model import MalformedArtifact, PendingProposal
|
||||
from core.proposal_review.queue import (
|
||||
MalformedEntry,
|
||||
QueueEntry,
|
||||
ReviewRecord,
|
||||
queue_status,
|
||||
record_review,
|
||||
reviewed_keys,
|
||||
scan_all,
|
||||
)
|
||||
from core.proposal_review.report import (
|
||||
ProposalReviewReport,
|
||||
build_report,
|
||||
|
|
@ -24,15 +41,22 @@ from core.proposal_review.summary import ProposalReviewIdleSummary, idle_summary
|
|||
__all__ = [
|
||||
"DEFAULT_SINK",
|
||||
"MalformedArtifact",
|
||||
"MalformedEntry",
|
||||
"PendingProposal",
|
||||
"ProposalReviewIdleSummary",
|
||||
"ProposalReviewReport",
|
||||
"QueueEntry",
|
||||
"ReviewRecord",
|
||||
"SafetyVerdict",
|
||||
"build_report",
|
||||
"default_sink",
|
||||
"dry_check",
|
||||
"idle_summary",
|
||||
"queue_status",
|
||||
"record_review",
|
||||
"report_json",
|
||||
"report_text",
|
||||
"reviewed_keys",
|
||||
"scan",
|
||||
"scan_all",
|
||||
]
|
||||
|
|
|
|||
301
core/proposal_review/queue.py
Normal file
301
core/proposal_review/queue.py
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
"""core/proposal_review/queue.py — generic multi-sink proposal queue (Tier S4).
|
||||
|
||||
**Read + review-state transitions only.** NEVER ratification, NEVER corpus
|
||||
mutation, NEVER flag flips — those stay `teaching/proposals.py`'s own
|
||||
`accept_proposal`/`reject_proposal` path over a DIFFERENT sink
|
||||
(`teaching/proposals/proposals.jsonl`, already CLI-exposed via
|
||||
``core teaching proposals`` / ``core teaching review``, ADR-0057).
|
||||
|
||||
This module covers the other two proposal sinks the generalization-arc
|
||||
brief calls "contemplation/idle sinks" — populated by background passes,
|
||||
not by the ratification corridor, and until now readable only through a
|
||||
Python API with no CLI surface at all:
|
||||
|
||||
- ``comprehension_failures`` (the N5 contemplation pass) — already has a
|
||||
hardened typed reader and an independent safety dry-check:
|
||||
:mod:`core.proposal_review` (``scan`` / ``dry_check``). Reused here, not
|
||||
duplicated.
|
||||
- ``derived_close_facts`` (the idle_tick PR-2 bridge,
|
||||
:mod:`generate.determine.derived_close_proposals`) — emitter only, no
|
||||
reader existed before this module. Its own docstring says it is
|
||||
"reviewable by the same HITL tooling" as ``comprehension_failures`` —
|
||||
this is that tooling. Read GENERICALLY here (the shared minimal
|
||||
``status``/``mounted``/``requires_review`` contract every proposal-only
|
||||
artifact carries), not via a new typed dataclass: the sink is currently
|
||||
empty and default-off (``review_derived_close_proposals``), so committing
|
||||
to a schema-specific reader now would be speculative in a way the
|
||||
populated ``comprehension_failures`` sink is not.
|
||||
|
||||
"Reviewing" here means recording that a HUMAN looked at an artifact — an
|
||||
append-only sidecar log (``teaching/proposals/review_log.jsonl``), never a
|
||||
write to the artifact itself. The artifact's own ``status`` / ``mounted`` /
|
||||
``requires_review`` fields stay exactly what the emitter wrote; nothing in
|
||||
this module can change what a proposal ratifies to.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from core.proposal_review.scan import DEFAULT_SINK as _COMPREHENSION_SINK
|
||||
from core.proposal_review.scan import scan as _scan_comprehension
|
||||
from generate.determine.derived_close_proposals import DEFAULT_SINK as _DERIVED_CLOSE_SINK
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
#: The append-only human-review sidecar. Lives beside the sinks it reviews,
|
||||
#: not inside either of them — a review record is metadata ABOUT an
|
||||
#: artifact, never a mutation of it.
|
||||
REVIEW_LOG_PATH = _REPO_ROOT / "teaching" / "proposals" / "review_log.jsonl"
|
||||
|
||||
#: Fields every proposal-only artifact in EITHER sink carries, regardless of
|
||||
#: family-specific schema (verified against both emitters:
|
||||
#: `core/comprehension_attempt/proposal.py`,
|
||||
#: `generate/determine/derived_close_proposals.py`).
|
||||
_SHARED_REQUIRED: tuple[str, ...] = ("status", "requires_review", "mounted")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class QueueEntry:
|
||||
"""One proposal artifact, sink-agnostic. ``family`` is a short label for
|
||||
display only (``failure_family`` for comprehension-failures, ``source``
|
||||
for derived-close-facts) — it carries no safety meaning here."""
|
||||
|
||||
sink: str
|
||||
content_hash: str
|
||||
family: str
|
||||
status: str
|
||||
requires_review: bool
|
||||
mounted: bool
|
||||
path: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MalformedEntry:
|
||||
sink: str
|
||||
path: str
|
||||
reason: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SinkSpec:
|
||||
name: str
|
||||
path: Path
|
||||
label: str
|
||||
|
||||
|
||||
#: The two contemplation/idle sinks this queue reads. Order is display order.
|
||||
SINKS: tuple[SinkSpec, ...] = (
|
||||
SinkSpec(
|
||||
"comprehension_failures",
|
||||
_COMPREHENSION_SINK,
|
||||
"comprehension-failure proposals (N5 contemplation pass)",
|
||||
),
|
||||
SinkSpec(
|
||||
"derived_close_facts",
|
||||
_DERIVED_CLOSE_SINK,
|
||||
"derived CLOSE-fact proposals (idle_tick PR-2 bridge)",
|
||||
),
|
||||
)
|
||||
|
||||
_SINKS_BY_NAME: dict[str, SinkSpec] = {spec.name: spec for spec in SINKS}
|
||||
|
||||
|
||||
def _scan_derived_close(root: Path) -> tuple[list[QueueEntry], list[MalformedEntry]]:
|
||||
"""Generic reader for ``derived_close_facts`` — no typed dataclass (module
|
||||
docstring): validates only the shared minimal contract, tolerating the
|
||||
family's own extra fields (``predicate``/``subject``/``object``/...)."""
|
||||
if not root.exists():
|
||||
return [], []
|
||||
entries: list[QueueEntry] = []
|
||||
malformed: list[MalformedEntry] = []
|
||||
for path in sorted(root.glob("*.json")):
|
||||
try:
|
||||
raw: Any = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
||||
malformed.append(MalformedEntry("derived_close_facts", str(path), f"invalid_json: {exc}"))
|
||||
continue
|
||||
if not isinstance(raw, dict):
|
||||
malformed.append(MalformedEntry("derived_close_facts", str(path), "not_a_json_object"))
|
||||
continue
|
||||
missing = [key for key in _SHARED_REQUIRED if key not in raw]
|
||||
if missing:
|
||||
malformed.append(
|
||||
MalformedEntry("derived_close_facts", str(path), f"missing_fields: {missing}")
|
||||
)
|
||||
continue
|
||||
predicate = str(raw.get("predicate", ""))
|
||||
subject = str(raw.get("subject", ""))
|
||||
obj = str(raw.get("object", ""))
|
||||
family = f"{predicate}:{subject}:{obj}" if predicate else str(raw.get("source", "unknown"))
|
||||
entries.append(
|
||||
QueueEntry(
|
||||
sink="derived_close_facts",
|
||||
content_hash=path.stem,
|
||||
family=family,
|
||||
status=str(raw["status"]),
|
||||
requires_review=bool(raw["requires_review"]),
|
||||
mounted=bool(raw["mounted"]),
|
||||
path=str(path),
|
||||
)
|
||||
)
|
||||
return entries, malformed
|
||||
|
||||
|
||||
def scan_all(
|
||||
sink_names: tuple[str, ...] | None = None,
|
||||
*,
|
||||
roots: dict[str, Path] | None = None,
|
||||
) -> tuple[list[QueueEntry], list[MalformedEntry]]:
|
||||
"""Scan the requested sinks (default: all). Pure read, sorted by
|
||||
``(sink, content_hash)`` for a deterministic listing order. ``roots``
|
||||
overrides a sink's directory by name — test isolation, never used by the
|
||||
CLI (which always reads the real sinks)."""
|
||||
names = sink_names or tuple(spec.name for spec in SINKS)
|
||||
entries: list[QueueEntry] = []
|
||||
malformed: list[MalformedEntry] = []
|
||||
for name in names:
|
||||
spec = _SINKS_BY_NAME[name]
|
||||
root = (roots or {}).get(name, spec.path)
|
||||
if name == "comprehension_failures":
|
||||
proposals, bad = _scan_comprehension(root)
|
||||
entries.extend(
|
||||
QueueEntry(
|
||||
sink=name,
|
||||
content_hash=p.content_hash,
|
||||
family=p.failure_family,
|
||||
status=p.status,
|
||||
requires_review=p.requires_review,
|
||||
mounted=p.mounted,
|
||||
path=p.path,
|
||||
)
|
||||
for p in proposals
|
||||
)
|
||||
malformed.extend(MalformedEntry(name, m.path, m.reason) for m in bad)
|
||||
elif name == "derived_close_facts":
|
||||
good, close_bad = _scan_derived_close(root)
|
||||
entries.extend(good)
|
||||
malformed.extend(close_bad)
|
||||
else: # pragma: no cover - closed SINKS tuple above
|
||||
raise ValueError(f"unknown proposal-queue sink: {name!r}")
|
||||
entries.sort(key=lambda e: (e.sink, e.content_hash))
|
||||
malformed.sort(key=lambda m: (m.sink, m.path))
|
||||
return entries, malformed
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ReviewRecord:
|
||||
sink: str
|
||||
content_hash: str
|
||||
note: str
|
||||
reviewed_at: str
|
||||
|
||||
|
||||
def _load_review_log(path: Path | None = None) -> list[ReviewRecord]:
|
||||
log_path = path or REVIEW_LOG_PATH
|
||||
if not log_path.exists():
|
||||
return []
|
||||
records: list[ReviewRecord] = []
|
||||
for line in log_path.read_text(encoding="utf-8").splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
row = json.loads(line)
|
||||
records.append(
|
||||
ReviewRecord(
|
||||
sink=row["sink"],
|
||||
content_hash=row["content_hash"],
|
||||
note=row.get("note", ""),
|
||||
reviewed_at=row["reviewed_at"],
|
||||
)
|
||||
)
|
||||
return records
|
||||
|
||||
|
||||
def reviewed_keys(path: Path | None = None) -> frozenset[tuple[str, str]]:
|
||||
"""``(sink, content_hash)`` pairs that have at least one review record."""
|
||||
return frozenset((r.sink, r.content_hash) for r in _load_review_log(path))
|
||||
|
||||
|
||||
def record_review(
|
||||
sink: str,
|
||||
content_hash: str,
|
||||
*,
|
||||
note: str = "",
|
||||
path: Path | None = None,
|
||||
roots: dict[str, Path] | None = None,
|
||||
) -> ReviewRecord:
|
||||
"""Append one human-review record. Does NOT touch the artifact itself,
|
||||
does not ratify, does not mount, does not flip any flag — purely an
|
||||
append to the sidecar log. Raises ``KeyError`` if ``(sink, content_hash)``
|
||||
does not resolve in a fresh scan, so a typo cannot silently log a review
|
||||
of nothing."""
|
||||
entries, _malformed = scan_all((sink,), roots=roots)
|
||||
if not any(e.content_hash == content_hash for e in entries):
|
||||
raise KeyError(f"no pending entry {content_hash!r} in sink {sink!r}")
|
||||
log_path = path or REVIEW_LOG_PATH
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
record = ReviewRecord(
|
||||
sink=sink,
|
||||
content_hash=content_hash,
|
||||
note=note,
|
||||
reviewed_at=datetime.now(timezone.utc).isoformat(),
|
||||
)
|
||||
with log_path.open("a", encoding="utf-8") as fh:
|
||||
fh.write(json.dumps(asdict(record), sort_keys=True) + "\n")
|
||||
return record
|
||||
|
||||
|
||||
def queue_status(
|
||||
sink_names: tuple[str, ...] | None = None,
|
||||
*,
|
||||
roots: dict[str, Path] | None = None,
|
||||
log_path: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Per-sink counts: total, pending_review (requires_review AND not yet
|
||||
human-reviewed), reviewed (has a review record), no_review_needed
|
||||
(requires_review is False — not currently emitted by either sink, kept
|
||||
distinct rather than folded into "reviewed" for correctness if that ever
|
||||
changes), malformed. Not a safety verdict — for ``comprehension_failures``,
|
||||
use :func:`core.proposal_review.dry_check` for that; this is a triage
|
||||
summary only."""
|
||||
entries, malformed = scan_all(sink_names, roots=roots)
|
||||
reviewed = reviewed_keys(log_path)
|
||||
by_sink: dict[str, dict[str, int]] = {}
|
||||
for spec in SINKS:
|
||||
if sink_names is not None and spec.name not in sink_names:
|
||||
continue
|
||||
sink_entries = [e for e in entries if e.sink == spec.name]
|
||||
no_review_needed = sum(1 for e in sink_entries if not e.requires_review)
|
||||
has_review = sum(
|
||||
1 for e in sink_entries if (e.sink, e.content_hash) in reviewed
|
||||
)
|
||||
pending = sum(
|
||||
1 for e in sink_entries
|
||||
if e.requires_review and (e.sink, e.content_hash) not in reviewed
|
||||
)
|
||||
by_sink[spec.name] = {
|
||||
"total": len(sink_entries),
|
||||
"pending_review": pending,
|
||||
"reviewed": has_review,
|
||||
"no_review_needed": no_review_needed,
|
||||
"malformed": sum(1 for m in malformed if m.sink == spec.name),
|
||||
}
|
||||
return by_sink
|
||||
|
||||
|
||||
__all__ = [
|
||||
"REVIEW_LOG_PATH",
|
||||
"MalformedEntry",
|
||||
"QueueEntry",
|
||||
"ReviewRecord",
|
||||
"SINKS",
|
||||
"SinkSpec",
|
||||
"queue_status",
|
||||
"record_review",
|
||||
"reviewed_keys",
|
||||
"scan_all",
|
||||
]
|
||||
241
core/ratified_ledger.py
Normal file
241
core/ratified_ledger.py
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
"""The ratified-ledger bridge — seal → ratify → SHA-verify → serve-gate.
|
||||
|
||||
ADR-0175 Phase 5's consumption bridge (generalization plan Phase 3.3),
|
||||
extracted from three working instances rather than designed ahead of them:
|
||||
|
||||
- ``generate/determine/estimation_license.py`` (ADR-0175, the first)
|
||||
- ``chat/deduction_serve_license.py`` (ADR-0256, the second)
|
||||
- ``chat/curriculum_serve_license.py`` (ADR-0262, the third)
|
||||
|
||||
All three had converged on the same artifact and the same four rules, which is
|
||||
what makes this an extraction and not a speculation. The rules, stated once:
|
||||
|
||||
1. **The engine reads; only sealed practice writes.** A ledger is the output
|
||||
of a practice run over a gold corpus, never of a serving turn. Nothing in a
|
||||
serving path may call :func:`write_sealed_ledger`.
|
||||
2. **Tamper-evidence is structural.** The artifact carries
|
||||
``content_sha256`` over its ``classes`` table; a load that cannot reproduce
|
||||
it REFUSES. A hand-edited ledger is not a slightly-wrong ledger, it is an
|
||||
unratified one.
|
||||
3. **Ceilings are not negotiable at the call site.** The gate always runs at
|
||||
the safe defaults unless a caller passes ceilings explicitly, and no
|
||||
production path does — ADR-0175 invariant #4: an engine cannot raise its own
|
||||
bar.
|
||||
4. **Absent evidence is never a license.** A class missing from the ledger
|
||||
yields ``None``, and every caller's ``None`` branch serves the disclosed
|
||||
(hedged) surface. A capability with no track record is served honestly, not
|
||||
withheld and not asserted.
|
||||
|
||||
Byte-compatibility is deliberate: :func:`seal_artifact` and
|
||||
:func:`write_sealed_ledger` reproduce the exact bytes the three existing
|
||||
sealers wrote, so adopting the bridge re-seals every committed ledger
|
||||
identically and no lane pin moves.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from core.reliability_gate import (
|
||||
Action,
|
||||
Ceilings,
|
||||
ClassTally,
|
||||
LicenseDecision,
|
||||
license_for,
|
||||
)
|
||||
from formation.hashing import sha256_of
|
||||
|
||||
|
||||
class RatifiedLedgerError(ValueError):
|
||||
"""A committed ledger is malformed or does not verify against its own hash."""
|
||||
|
||||
|
||||
_PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LedgerSpec:
|
||||
"""One capability's committed ledger, and whether its absence is an error."""
|
||||
|
||||
capability: str
|
||||
path: Path
|
||||
#: ``False`` — this capability SHIPS with a sealed ledger, so a missing file
|
||||
#: means a broken deployment and the load must refuse.
|
||||
#: ``True`` — this capability's practice volume is still being built, so a
|
||||
#: missing file honestly means "nothing earned yet" and serves disclosed.
|
||||
missing_ok: bool
|
||||
note: str
|
||||
|
||||
|
||||
#: Rule 5 of the bridge: **absence policy is declared, not passed.**
|
||||
#:
|
||||
#: Rules 1-4 (docstring above) are all enforced structurally; this one was not.
|
||||
#: ``missing_ok`` began life as a ``load_sealed_ledger`` keyword, which meant
|
||||
#: each adapter chose its own answer to "is a missing ledger a broken
|
||||
#: deployment, or an unearned capability?" — a question about the capability,
|
||||
#: not about the call. Any new subject onboarding through the bridge could pass
|
||||
#: ``missing_ok=True`` and silently downgrade a should-be-hard-refuse into a
|
||||
#: disclosed hedge, and nothing would catch it.
|
||||
#:
|
||||
#: Declaring it here means adding a capability is a manifest edit that a
|
||||
#: reviewer reads as a policy change, which is what it is. The keyword survives
|
||||
#: on the primitive for tests and one-off tooling; no production adapter passes
|
||||
#: it.
|
||||
CAPABILITY_LEDGERS: dict[str, LedgerSpec] = {
|
||||
"estimation": LedgerSpec(
|
||||
capability="estimation",
|
||||
path=_PROJECT_ROOT / "generate" / "determine" / "data" / "estimation_ledger.json",
|
||||
missing_ok=False,
|
||||
note="ADR-0175 — ships sealed with the converse-estimation gate.",
|
||||
),
|
||||
"deduction_serve": LedgerSpec(
|
||||
capability="deduction_serve",
|
||||
path=_PROJECT_ROOT / "chat" / "data" / "deduction_serve_ledger.json",
|
||||
missing_ok=False,
|
||||
note="ADR-0256 — ships sealed; 25 bands at 720/720 wrong=0.",
|
||||
),
|
||||
"curriculum_serve": LedgerSpec(
|
||||
capability="curriculum_serve",
|
||||
path=_PROJECT_ROOT / "chat" / "data" / "curriculum_serve_ledger.json",
|
||||
missing_ok=True,
|
||||
note=(
|
||||
"ADR-0262 §5 — no band has earned a license from present curriculum "
|
||||
"volume; every served band is DISCLOSED. Flips to False when the "
|
||||
"first band earns SERVE and the ledger is committed."
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def ledger_spec(capability: str) -> LedgerSpec:
|
||||
"""The declared spec for *capability*, or a refusal naming the manifest."""
|
||||
try:
|
||||
return CAPABILITY_LEDGERS[capability]
|
||||
except KeyError:
|
||||
raise RatifiedLedgerError(
|
||||
f"unregistered ledger capability: {capability!r} — declare it in "
|
||||
"core.ratified_ledger.CAPABILITY_LEDGERS before consuming it"
|
||||
) from None
|
||||
|
||||
|
||||
def load_capability_ledger(capability: str) -> dict[str, ClassTally]:
|
||||
"""Load the committed ledger for *capability* under its declared policy.
|
||||
|
||||
The production entry point. A caller names what it is, not how absence
|
||||
should be treated — so no call site can grant itself a softer failure mode
|
||||
than the capability was registered with.
|
||||
"""
|
||||
spec = ledger_spec(capability)
|
||||
return load_sealed_ledger(spec.path, missing_ok=spec.missing_ok)
|
||||
|
||||
|
||||
def tally_dict(tally: ClassTally) -> dict[str, Any]:
|
||||
"""The committed per-class row. The field set is the contract — a reader
|
||||
of an older ledger must be able to name every field it finds."""
|
||||
return {
|
||||
"correct": tally.correct,
|
||||
"wrong": tally.wrong,
|
||||
"refused": tally.refused,
|
||||
"t2_verified": tally.t2_verified,
|
||||
"t2_agrees_gold": tally.t2_agrees_gold,
|
||||
}
|
||||
|
||||
|
||||
def seal_artifact(
|
||||
ledger: dict[str, ClassTally], *, schema: str, note: str, provenance: str
|
||||
) -> dict[str, Any]:
|
||||
"""The self-verifying sealed-ledger dict for *ledger*.
|
||||
|
||||
Classes are sorted, so the artifact is a pure function of the practice
|
||||
result: the same corpus and solver seal byte-identically, which is what
|
||||
makes a committed ledger reviewable as a diff.
|
||||
"""
|
||||
classes = {name: tally_dict(tally) for name, tally in sorted(ledger.items())}
|
||||
return {
|
||||
"schema": schema,
|
||||
"classes": classes,
|
||||
"content_sha256": sha256_of(classes),
|
||||
"note": note,
|
||||
"provenance": provenance,
|
||||
}
|
||||
|
||||
|
||||
def write_sealed_ledger(path: Path, artifact: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Write *artifact* to *path* in the committed formatting."""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(
|
||||
json.dumps(artifact, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||
)
|
||||
return artifact
|
||||
|
||||
|
||||
def load_sealed_ledger(path: Path, *, missing_ok: bool = False) -> dict[str, ClassTally]:
|
||||
"""Load + verify a sealed ledger → per-class ``ClassTally``.
|
||||
|
||||
``missing_ok`` distinguishes two genuinely different situations. A ledger
|
||||
that a capability *ships with* is required: its absence means the
|
||||
deployment is broken, and refusing is right. A ledger for a capability
|
||||
whose practice volume is still being built is legitimately absent, and the
|
||||
honest reading of "no file" is "no class has earned anything yet" — an
|
||||
empty table, every answer disclosed. Neither case may be answered by
|
||||
guessing a license.
|
||||
"""
|
||||
if not path.exists():
|
||||
if missing_ok:
|
||||
return {}
|
||||
raise RatifiedLedgerError(f"ratified ledger not found: {path}")
|
||||
try:
|
||||
artifact = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise RatifiedLedgerError(f"cannot read ratified ledger: {exc}") from exc
|
||||
|
||||
classes = artifact.get("classes") if isinstance(artifact, dict) else None
|
||||
if not isinstance(classes, dict):
|
||||
raise RatifiedLedgerError("ratified ledger has no 'classes' table")
|
||||
if sha256_of(classes) != artifact.get("content_sha256"):
|
||||
raise RatifiedLedgerError(
|
||||
"ratified ledger content_sha256 mismatch — not the sealed-practice output"
|
||||
)
|
||||
|
||||
return {
|
||||
name: ClassTally(
|
||||
class_name=name,
|
||||
correct=int(counts.get("correct", 0)),
|
||||
wrong=int(counts.get("wrong", 0)),
|
||||
refused=int(counts.get("refused", 0)),
|
||||
t2_verified=int(counts.get("t2_verified", 0)),
|
||||
t2_agrees_gold=int(counts.get("t2_agrees_gold", 0)),
|
||||
)
|
||||
for name, counts in classes.items()
|
||||
}
|
||||
|
||||
|
||||
def serve_license(
|
||||
class_name: str,
|
||||
ledger: dict[str, ClassTally],
|
||||
*,
|
||||
ceilings: Ceilings | None = None,
|
||||
) -> LicenseDecision | None:
|
||||
"""The ``Action.SERVE`` verdict for *class_name*, or ``None`` when the
|
||||
class has no committed evidence (never a license — rule 4)."""
|
||||
tally = ledger.get(class_name)
|
||||
if tally is None:
|
||||
return None
|
||||
return license_for(tally, Action.SERVE, ceilings or Ceilings.default())
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CAPABILITY_LEDGERS",
|
||||
"LedgerSpec",
|
||||
"RatifiedLedgerError",
|
||||
"ledger_spec",
|
||||
"load_capability_ledger",
|
||||
"load_sealed_ledger",
|
||||
"seal_artifact",
|
||||
"serve_license",
|
||||
"tally_dict",
|
||||
"write_sealed_ledger",
|
||||
]
|
||||
|
|
@ -9,11 +9,24 @@ Public surface:
|
|||
- :class:`ClassTally` — per-class counted ledger; reliability = commitment precision.
|
||||
- :class:`Action`, :class:`Ceilings` — human-set θ ceilings (engine never mutates).
|
||||
- :func:`license_for`, :class:`LicenseDecision` — the deterministic gate.
|
||||
- :class:`EvidenceAudit`, :func:`audit_band` — distinct-evidence audit (ADR-0264
|
||||
R9). Measurement only, imported by no serving path: the Wilson floor assumes
|
||||
independent trials, and a deterministic pipeline replaying one input N times
|
||||
supplies one trial's evidence, not N.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from core.reliability_gate.ceilings import Action, Ceilings
|
||||
from core.reliability_gate.evidence import (
|
||||
SERVE_VOLUME_AT_THETA_099,
|
||||
EvidenceAudit,
|
||||
audit_band,
|
||||
audit_bands,
|
||||
below_floor,
|
||||
format_report,
|
||||
volume_for_theta,
|
||||
)
|
||||
from core.reliability_gate.floor import N_MIN, WILSON_Z, conservative_floor
|
||||
from core.reliability_gate.gate import LicenseDecision, license_for
|
||||
from core.reliability_gate.ledger import ClassTally
|
||||
|
|
@ -23,11 +36,18 @@ __all__ = [
|
|||
"Action",
|
||||
"Ceilings",
|
||||
"ClassTally",
|
||||
"EvidenceAudit",
|
||||
"LicenseDecision",
|
||||
"N_MIN",
|
||||
"RatifiableProposal",
|
||||
"SERVE_VOLUME_AT_THETA_099",
|
||||
"WILSON_Z",
|
||||
"audit_band",
|
||||
"audit_bands",
|
||||
"below_floor",
|
||||
"conservative_floor",
|
||||
"format_report",
|
||||
"license_for",
|
||||
"propose_from_ledger",
|
||||
"volume_for_theta",
|
||||
]
|
||||
|
|
|
|||
175
core/reliability_gate/evidence.py
Normal file
175
core/reliability_gate/evidence.py
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
"""ADR-0264 R9 — distinct-evidence audit: the precondition :func:`conservative_floor` assumes.
|
||||
|
||||
:func:`core.reliability_gate.floor.conservative_floor` is a one-sided Wilson lower
|
||||
bound on a success proportion. Wilson assumes **independent trials**. CORE's
|
||||
pipeline is deterministic by construction — that is the whole thesis — so
|
||||
replaying an identical input yields the identical outcome with certainty. Two
|
||||
identical cases in a practice ledger are therefore perfectly correlated, not
|
||||
independent, and contribute one trial's worth of evidence between them, not two.
|
||||
|
||||
So a ledger's ``committed`` count is an upper bound on its evidence, and the
|
||||
honest figure is the count of *distinct decisions* it exercised. This module
|
||||
computes both and reports the gap. It is deliberately a pure measurement with no
|
||||
authority: it never edits a ledger, never gates anything, and nothing in the
|
||||
serving path imports it. What to do about a gap is a ratification decision.
|
||||
|
||||
Why this exists as code rather than a note: S6 predicted the exposure in the
|
||||
abstract (`docs/research/curriculum-volume-quantification-2026-07-24.md` §1 —
|
||||
``conservative_floor`` cannot distinguish "657 independent facts" from "16 facts
|
||||
asked 41 times"). It was then measured in the *live, ratified* deduction ledger,
|
||||
where 21 of 25 bands do not clear θ_SERVE on distinct evidence. A prediction that
|
||||
has come true needs an instrument, not a second warning.
|
||||
|
||||
**The key is supplied by the caller, and must be conservative.** A decision key
|
||||
identifies what the pipeline actually decided. Case *text* is the safe default:
|
||||
two cases with identical text are indisputably the same decision. A tighter key
|
||||
(e.g. the normalized propositional atom, which collapses spelling variants of one
|
||||
question) reports *more* inflation. Text-identity therefore under-reports, which
|
||||
is the correct direction for a claim of this kind — the measured gap is a floor on
|
||||
the real gap, never an exaggeration of it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
from collections.abc import Hashable, Iterable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from core.reliability_gate.floor import conservative_floor
|
||||
|
||||
#: The committed volume a *perfect* record needs to clear θ=0.99 under the pinned
|
||||
#: ``WILSON_Z``. Derived, not chosen: the bound for ``successes == committed``
|
||||
#: reduces to ``n / (n + z²)``, so ``n / (n + 6.635776) ≥ 0.99`` ⟺ ``n ≥ 656.93``.
|
||||
#: Recomputed by :func:`volume_for_theta` rather than trusted as a constant.
|
||||
SERVE_VOLUME_AT_THETA_099: int = 657
|
||||
|
||||
|
||||
def volume_for_theta(theta: float) -> int:
|
||||
"""Smallest perfect-record volume whose conservative floor reaches *theta*.
|
||||
|
||||
Derives the figure from :func:`conservative_floor` itself instead of
|
||||
restating it, so a change to ``WILSON_Z`` cannot leave a stale number behind
|
||||
in a docstring or a test. Search, not algebra, because the bound is rounded
|
||||
to 1e-9 and the comparison downstream is the rounded one.
|
||||
"""
|
||||
if not 0.0 < theta < 1.0:
|
||||
raise ValueError(f"theta must lie in (0, 1); got {theta!r}")
|
||||
n = 1
|
||||
while conservative_floor(n, n) < theta:
|
||||
n *= 2
|
||||
if n > 1 << 40: # pragma: no cover - unreachable for theta < 1
|
||||
raise ValueError(f"no finite volume reaches theta={theta!r}")
|
||||
low, high = n // 2, n
|
||||
while low < high:
|
||||
mid = (low + high) // 2
|
||||
if conservative_floor(mid, mid) >= theta:
|
||||
high = mid
|
||||
else:
|
||||
low = mid + 1
|
||||
return low
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class EvidenceAudit:
|
||||
"""What one band's practice volume is worth as evidence.
|
||||
|
||||
``claimed`` is the floor the ledger's own ``committed`` count produces —
|
||||
what the gate sees today. ``honest`` is the floor over distinct decisions —
|
||||
what a bound assuming independent trials is entitled to claim.
|
||||
"""
|
||||
|
||||
band: str
|
||||
committed: int
|
||||
distinct: int
|
||||
max_repeat: int
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.committed < 0 or self.distinct < 0 or self.max_repeat < 0:
|
||||
raise ValueError("counts must be non-negative")
|
||||
if self.distinct > self.committed:
|
||||
raise ValueError(
|
||||
f"{self.band}: distinct ({self.distinct}) cannot exceed "
|
||||
f"committed ({self.committed})"
|
||||
)
|
||||
if self.committed and not self.distinct:
|
||||
raise ValueError(f"{self.band}: committed cases with no distinct keys")
|
||||
|
||||
@property
|
||||
def claimed(self) -> float:
|
||||
"""Floor from ``committed`` — assumes every case was a fresh trial."""
|
||||
return conservative_floor(self.committed, self.committed)
|
||||
|
||||
@property
|
||||
def honest(self) -> float:
|
||||
"""Floor from distinct decisions — the defensible figure."""
|
||||
return conservative_floor(self.distinct, self.distinct)
|
||||
|
||||
@property
|
||||
def inflation(self) -> float:
|
||||
"""``committed / distinct``. 1.0 means every case was a distinct decision."""
|
||||
return round(self.committed / self.distinct, 9) if self.distinct else 0.0
|
||||
|
||||
def clears(self, theta: float) -> bool:
|
||||
"""Does the *honest* floor reach *theta*? The question the gate should ask."""
|
||||
return round(self.honest, 9) >= round(theta, 9)
|
||||
|
||||
|
||||
def audit_band(band: str, keys: Iterable[Hashable]) -> EvidenceAudit:
|
||||
"""Audit one band from the decision keys of its committed cases.
|
||||
|
||||
*keys* is one entry per committed case — repeats are the point, so this
|
||||
takes an iterable of keys rather than a set.
|
||||
"""
|
||||
counts = Counter(keys)
|
||||
committed = sum(counts.values())
|
||||
return EvidenceAudit(
|
||||
band=band,
|
||||
committed=committed,
|
||||
distinct=len(counts),
|
||||
max_repeat=max(counts.values()) if counts else 0,
|
||||
)
|
||||
|
||||
|
||||
def audit_bands(cases_by_band: dict[str, Iterable[Hashable]]) -> tuple[EvidenceAudit, ...]:
|
||||
"""Audit every band, ordered by band name for replay-stable reporting."""
|
||||
return tuple(audit_band(band, cases_by_band[band]) for band in sorted(cases_by_band))
|
||||
|
||||
|
||||
def below_floor(
|
||||
audits: Iterable[EvidenceAudit], theta: float
|
||||
) -> tuple[EvidenceAudit, ...]:
|
||||
"""The audits whose honest floor does not reach *theta*, worst gap first."""
|
||||
failing = [a for a in audits if not a.clears(theta)]
|
||||
return tuple(sorted(failing, key=lambda a: (a.honest, a.band)))
|
||||
|
||||
|
||||
def format_report(audits: Iterable[EvidenceAudit], theta: float) -> str:
|
||||
"""A fixed-width report — the text a failing pin should print.
|
||||
|
||||
Assertion messages are the whole value of an audit that fails months from
|
||||
now, so the numbers are formatted here rather than at each call site.
|
||||
"""
|
||||
rows = sorted(audits, key=lambda a: a.band)
|
||||
need = volume_for_theta(theta)
|
||||
lines = [
|
||||
f"theta={theta} perfect-record volume needed={need}",
|
||||
f"{'band':30s} {'committed':>9s} {'distinct':>8s} {'x':>6s} "
|
||||
f"{'claimed':>8s} {'honest':>7s} clears",
|
||||
]
|
||||
for a in rows:
|
||||
lines.append(
|
||||
f"{a.band:30s} {a.committed:9d} {a.distinct:8d} {a.inflation:6.1f} "
|
||||
f"{a.claimed:8.4f} {a.honest:7.4f} {'yes' if a.clears(theta) else 'NO'}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"SERVE_VOLUME_AT_THETA_099",
|
||||
"EvidenceAudit",
|
||||
"audit_band",
|
||||
"audit_bands",
|
||||
"below_floor",
|
||||
"format_report",
|
||||
"volume_for_theta",
|
||||
]
|
||||
52
core/semantic_primitives/__init__.py
Normal file
52
core/semantic_primitives/__init__.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
"""Shared typed semantic primitives for linguistic → field compilation.
|
||||
|
||||
Authoritative representations are frozen validated dataclasses — never bare
|
||||
dicts, free strings, or JSON blobs at the new compiler seams.
|
||||
|
||||
Linguistic layers may only *emit candidates* bound to these types. They must
|
||||
not assign Cl(4,1) field state.
|
||||
"""
|
||||
|
||||
from core.semantic_primitives.model import (
|
||||
AmbiguityManifold,
|
||||
ConservationLaw,
|
||||
Container,
|
||||
DimensionalType,
|
||||
Entity,
|
||||
Event,
|
||||
IdentityBridge,
|
||||
LogosConstraint,
|
||||
MissingReferent,
|
||||
Operator,
|
||||
OperatorClass,
|
||||
ProvenanceSpan,
|
||||
Quantity,
|
||||
Relation,
|
||||
RelationKind,
|
||||
TemporalExtent,
|
||||
TemporalFrame,
|
||||
TemporalKind,
|
||||
ValidationError,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AmbiguityManifold",
|
||||
"ConservationLaw",
|
||||
"Container",
|
||||
"DimensionalType",
|
||||
"Entity",
|
||||
"Event",
|
||||
"IdentityBridge",
|
||||
"LogosConstraint",
|
||||
"MissingReferent",
|
||||
"Operator",
|
||||
"OperatorClass",
|
||||
"ProvenanceSpan",
|
||||
"Quantity",
|
||||
"Relation",
|
||||
"RelationKind",
|
||||
"TemporalExtent",
|
||||
"TemporalFrame",
|
||||
"TemporalKind",
|
||||
"ValidationError",
|
||||
]
|
||||
397
core/semantic_primitives/model.py
Normal file
397
core/semantic_primitives/model.py
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
"""Typed semantic primitives (linguistic governance Phase 2)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from fractions import Fraction
|
||||
from typing import Any, Iterable, Sequence
|
||||
|
||||
|
||||
class ValidationError(ValueError):
|
||||
"""Ill-typed or incomplete primitive construction."""
|
||||
|
||||
|
||||
class DimensionalType(str, Enum):
|
||||
COUNT = "count"
|
||||
LENGTH = "length"
|
||||
MASS = "mass"
|
||||
TIME = "time"
|
||||
CURRENCY = "currency"
|
||||
RATE = "rate"
|
||||
DIMENSIONLESS = "dimensionless"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
class RelationKind(str, Enum):
|
||||
POSSESSION = "possession"
|
||||
COMPARISON = "comparison"
|
||||
EQUALITY = "equality"
|
||||
RATIO = "ratio"
|
||||
MEMBERSHIP = "membership"
|
||||
ORDER = "order"
|
||||
AGENT = "agent"
|
||||
PATIENT = "patient"
|
||||
RECIPIENT = "recipient"
|
||||
POSSESSOR = "possessor"
|
||||
SOURCE = "source"
|
||||
TARGET = "target"
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
class OperatorClass(str, Enum):
|
||||
TRANSFER = "transfer"
|
||||
REMOVAL = "removal"
|
||||
ACCUMULATION = "accumulation"
|
||||
CREATION = "creation"
|
||||
PARTITION = "partition"
|
||||
COMPARISON = "comparison"
|
||||
TRANSFORMATION = "transformation"
|
||||
RECURRENCE = "recurrence"
|
||||
CAUSATION = "causation"
|
||||
IDENTITY_CONTINUITY = "identity_continuity"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
class TemporalKind(str, Enum):
|
||||
PRIOR_ONGOING = "prior_ongoing"
|
||||
PRIOR_COMPLETED = "prior_completed"
|
||||
PRESENT = "present"
|
||||
FUTURE = "future"
|
||||
RECURRING = "recurring"
|
||||
UNSPECIFIED = "unspecified"
|
||||
|
||||
|
||||
def _require_nonempty(value: str, name: str) -> str:
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
raise ValidationError(f"{name} must be a non-empty str")
|
||||
return value
|
||||
|
||||
|
||||
def _reject_dict_blob(value: Any, name: str) -> None:
|
||||
if isinstance(value, dict):
|
||||
raise ValidationError(
|
||||
f"{name} must not be an untyped dict; use the typed constructor"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ProvenanceSpan:
|
||||
start: int
|
||||
end: int
|
||||
text: str = ""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.start < 0 or self.end < self.start:
|
||||
raise ValidationError("ProvenanceSpan requires 0 <= start <= end")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LogosConstraint:
|
||||
"""Language-independent morph→constraint record with full provenance.
|
||||
|
||||
Used by the live Logos authority path (teaching + CognitiveTurn). Fields
|
||||
are typed — not a free-form meaning dict. rule_id + morphology_id +
|
||||
source_pack_id + source_span make the constraint falsifiable and replayable.
|
||||
"""
|
||||
|
||||
constraint_id: str
|
||||
kind: str
|
||||
rule_id: str
|
||||
lemma: str
|
||||
root: str
|
||||
surface: str
|
||||
morphology_id: str
|
||||
source_pack_id: str
|
||||
source_span: ProvenanceSpan
|
||||
language: str = "he"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_require_nonempty(self.constraint_id, "LogosConstraint.constraint_id")
|
||||
_require_nonempty(self.kind, "LogosConstraint.kind")
|
||||
_require_nonempty(self.rule_id, "LogosConstraint.rule_id")
|
||||
_require_nonempty(self.morphology_id, "LogosConstraint.morphology_id")
|
||||
_require_nonempty(self.source_pack_id, "LogosConstraint.source_pack_id")
|
||||
if not isinstance(self.source_span, ProvenanceSpan):
|
||||
raise ValidationError(
|
||||
"LogosConstraint.source_span must be a ProvenanceSpan"
|
||||
)
|
||||
_reject_dict_blob(self.kind, "LogosConstraint.kind")
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"constraint_id": self.constraint_id,
|
||||
"kind": self.kind,
|
||||
"rule_id": self.rule_id,
|
||||
"lemma": self.lemma,
|
||||
"root": self.root,
|
||||
"surface": self.surface,
|
||||
"morphology_id": self.morphology_id,
|
||||
"source_pack_id": self.source_pack_id,
|
||||
"source_span": [self.source_span.start, self.source_span.end],
|
||||
"language": self.language,
|
||||
}
|
||||
|
||||
@property
|
||||
def provenance_complete(self) -> bool:
|
||||
return bool(
|
||||
self.rule_id
|
||||
and self.morphology_id
|
||||
and self.source_pack_id
|
||||
and self.source_span.end >= self.source_span.start
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Entity:
|
||||
"""Persistent identity anchor with typed id, name, type, and frame bindings."""
|
||||
|
||||
entity_id: str
|
||||
name: str
|
||||
entity_type: str
|
||||
frame_ids: tuple[str, ...] = ()
|
||||
provenance: ProvenanceSpan | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_require_nonempty(self.entity_id, "Entity.entity_id")
|
||||
_require_nonempty(self.name, "Entity.name")
|
||||
_require_nonempty(self.entity_type, "Entity.entity_type")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Quantity:
|
||||
"""Value + unit + dimensional type + owner + frame (no raw bare floats alone)."""
|
||||
|
||||
value: Fraction
|
||||
unit: str
|
||||
dimensional_type: DimensionalType
|
||||
owner_entity_id: str
|
||||
frame_id: str
|
||||
source_token: str = ""
|
||||
provenance: ProvenanceSpan | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if isinstance(self.value, float):
|
||||
# Allow float only if caller wrapped — still reject bare construction
|
||||
# via type check: Fraction is required.
|
||||
raise ValidationError(
|
||||
"Quantity.value must be Fraction (not float) to avoid untyped numerics"
|
||||
)
|
||||
if not isinstance(self.value, Fraction):
|
||||
raise ValidationError("Quantity.value must be a Fraction")
|
||||
_require_nonempty(self.unit, "Quantity.unit")
|
||||
if not isinstance(self.dimensional_type, DimensionalType):
|
||||
raise ValidationError("Quantity.dimensional_type must be DimensionalType")
|
||||
_require_nonempty(self.owner_entity_id, "Quantity.owner_entity_id")
|
||||
_require_nonempty(self.frame_id, "Quantity.frame_id")
|
||||
|
||||
@classmethod
|
||||
def from_int(
|
||||
cls,
|
||||
value: int,
|
||||
*,
|
||||
unit: str,
|
||||
dimensional_type: DimensionalType,
|
||||
owner_entity_id: str,
|
||||
frame_id: str,
|
||||
source_token: str = "",
|
||||
provenance: ProvenanceSpan | None = None,
|
||||
) -> "Quantity":
|
||||
return cls(
|
||||
value=Fraction(value),
|
||||
unit=unit,
|
||||
dimensional_type=dimensional_type,
|
||||
owner_entity_id=owner_entity_id,
|
||||
frame_id=frame_id,
|
||||
source_token=source_token,
|
||||
provenance=provenance,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Container:
|
||||
"""Bounded inventory/balance with typed content and conserved quantity id."""
|
||||
|
||||
container_id: str
|
||||
owner_entity_id: str
|
||||
content_entity_ids: tuple[str, ...]
|
||||
conserved_quantity_id: str
|
||||
capacity: Quantity | None = None
|
||||
frame_id: str = "default"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_require_nonempty(self.container_id, "Container.container_id")
|
||||
_require_nonempty(self.owner_entity_id, "Container.owner_entity_id")
|
||||
_require_nonempty(self.conserved_quantity_id, "Container.conserved_quantity_id")
|
||||
if not isinstance(self.content_entity_ids, tuple):
|
||||
raise ValidationError("Container.content_entity_ids must be a tuple")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TemporalFrame:
|
||||
frame_id: str
|
||||
kind: TemporalKind
|
||||
label: str = ""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_require_nonempty(self.frame_id, "TemporalFrame.frame_id")
|
||||
if not isinstance(self.kind, TemporalKind):
|
||||
raise ValidationError("TemporalFrame.kind must be TemporalKind")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TemporalExtent:
|
||||
frame: TemporalFrame
|
||||
start_label: str = ""
|
||||
end_label: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Event:
|
||||
"""Typed transformation with roles; incomplete roles must be explicit None."""
|
||||
|
||||
event_id: str
|
||||
operator_class: OperatorClass
|
||||
agent_entity_id: str | None
|
||||
patient_entity_id: str | None
|
||||
source_entity_id: str | None
|
||||
target_entity_id: str | None
|
||||
conserved_quantity_id: str | None
|
||||
temporal_extent: TemporalExtent | None
|
||||
direction: str = ""
|
||||
unresolved_roles: tuple[str, ...] = ()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_require_nonempty(self.event_id, "Event.event_id")
|
||||
if not isinstance(self.operator_class, OperatorClass):
|
||||
raise ValidationError("Event.operator_class must be OperatorClass")
|
||||
if isinstance(self.agent_entity_id, dict):
|
||||
raise ValidationError("Event roles must not be dict blobs")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Operator:
|
||||
"""Executable semantic transformation linked to an Event type."""
|
||||
|
||||
operator_id: str
|
||||
operator_class: OperatorClass
|
||||
event_id: str
|
||||
executable_symbol: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_require_nonempty(self.operator_id, "Operator.operator_id")
|
||||
_require_nonempty(self.event_id, "Operator.event_id")
|
||||
_require_nonempty(self.executable_symbol, "Operator.executable_symbol")
|
||||
if not isinstance(self.operator_class, OperatorClass):
|
||||
raise ValidationError("Operator.operator_class must be OperatorClass")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Relation:
|
||||
relation_id: str
|
||||
kind: RelationKind
|
||||
left_entity_id: str
|
||||
right_entity_id: str
|
||||
polarity: bool = True
|
||||
provenance: ProvenanceSpan | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_require_nonempty(self.relation_id, "Relation.relation_id")
|
||||
if not isinstance(self.kind, RelationKind):
|
||||
raise ValidationError("Relation.kind must be RelationKind")
|
||||
_require_nonempty(self.left_entity_id, "Relation.left_entity_id")
|
||||
_require_nonempty(self.right_entity_id, "Relation.right_entity_id")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class IdentityBridge:
|
||||
"""Explicit cross-frame entity identity claim with change log."""
|
||||
|
||||
bridge_id: str
|
||||
entity_id: str
|
||||
from_frame_id: str
|
||||
to_frame_id: str
|
||||
change_log: tuple[str, ...] = ()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_require_nonempty(self.bridge_id, "IdentityBridge.bridge_id")
|
||||
_require_nonempty(self.entity_id, "IdentityBridge.entity_id")
|
||||
_require_nonempty(self.from_frame_id, "IdentityBridge.from_frame_id")
|
||||
_require_nonempty(self.to_frame_id, "IdentityBridge.to_frame_id")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ConservationLaw:
|
||||
law_id: str
|
||||
quantity_id: str
|
||||
persists: bool
|
||||
flows: bool
|
||||
externally_added: bool
|
||||
consumed: bool
|
||||
transferred: bool
|
||||
statement: str = ""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_require_nonempty(self.law_id, "ConservationLaw.law_id")
|
||||
_require_nonempty(self.quantity_id, "ConservationLaw.quantity_id")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AmbiguityManifold:
|
||||
"""Set of unresolved semantic candidates with explicit resolution condition."""
|
||||
|
||||
manifold_id: str
|
||||
candidate_ids: tuple[str, ...]
|
||||
candidate_kinds: tuple[str, ...]
|
||||
resolution_condition: str
|
||||
resolved_id: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_require_nonempty(self.manifold_id, "AmbiguityManifold.manifold_id")
|
||||
if isinstance(self.candidate_ids, dict) or isinstance(self.candidate_kinds, dict):
|
||||
raise ValidationError("AmbiguityManifold candidates must not be dict blobs")
|
||||
if not self.candidate_ids:
|
||||
raise ValidationError("AmbiguityManifold.candidate_ids must be non-empty")
|
||||
if len(self.candidate_ids) != len(self.candidate_kinds):
|
||||
raise ValidationError(
|
||||
"AmbiguityManifold candidate_ids/kinds length mismatch"
|
||||
)
|
||||
_require_nonempty(
|
||||
self.resolution_condition, "AmbiguityManifold.resolution_condition"
|
||||
)
|
||||
if self.resolved_id is not None and self.resolved_id not in self.candidate_ids:
|
||||
raise ValidationError("resolved_id must be one of candidate_ids")
|
||||
|
||||
@property
|
||||
def resolved(self) -> bool:
|
||||
return self.resolved_id is not None and len(self.candidate_ids) == 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MissingReferent:
|
||||
"""Absent antecedent/owner/unit/time — never silently filled."""
|
||||
|
||||
referent_id: str
|
||||
role: str
|
||||
expected_kind: str
|
||||
context: str = ""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_require_nonempty(self.referent_id, "MissingReferent.referent_id")
|
||||
_require_nonempty(self.role, "MissingReferent.role")
|
||||
_require_nonempty(self.expected_kind, "MissingReferent.expected_kind")
|
||||
|
||||
|
||||
def reject_untyped(value: Any, expected_name: str) -> None:
|
||||
"""Public seam guard: refuse dict/str as authoritative complex primitive."""
|
||||
_reject_dict_blob(value, expected_name)
|
||||
if isinstance(value, str) and expected_name in {
|
||||
"Event",
|
||||
"Quantity",
|
||||
"AmbiguityManifold",
|
||||
"Entity",
|
||||
"Container",
|
||||
}:
|
||||
raise ValidationError(
|
||||
f"{expected_name} must not be a bare string as authoritative representation"
|
||||
)
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
**Status:** Accepted (Phase 1 only; Phases 2–5 deferred)
|
||||
**Date:** 2026-05-23
|
||||
**Parent proposal:** `docs/implementation/semantic-symbolic-binding-graph-proposal.md` (PR #170)
|
||||
**Parent proposal:** `docs/paradigm-archive/semantic-symbolic-binding-graph-proposal.md` (PR #170; originally at `docs/implementation/`, retired to the paradigm archive under ADR-0252 §9, 2026-07-19 — this ADR's Phase 1 data model remains Accepted)
|
||||
**Related:** ADR-0115..0118 (math parser/solver/verifier/realizer), ADR-0126
|
||||
(candidate-graph parser), ADR-0127 (units pack), ADR-0131 (math-expert
|
||||
rebench / proof corridor)
|
||||
|
|
|
|||
|
|
@ -205,22 +205,38 @@ class CognitiveLifecycleEngine:
|
|||
|
||||
psi \= ingress.psi.copy()
|
||||
|
||||
\# Schrödinger propagator: R \= exp(H\_problem \* I \* dt)
|
||||
\# IMPLEMENTED (not this sketch): imaginary-time power iteration in
|
||||
|
||||
generator \= np.dot(H\_problem, self.I)
|
||||
\# core/physics/cognitive_lifecycle.relax_to_ground — geometric_product
|
||||
|
||||
\# path only. The np.dot / la.expm sketch below is HISTORICAL and
|
||||
|
||||
\# superseded (pin SD-B; convergence 2026-07-20).
|
||||
|
||||
\#
|
||||
|
||||
\# Schrödinger-style discrete step (wave_manifold): R = exp(B·Δt) via
|
||||
|
||||
\# closed-form / series bivector exp; sandwich ψ' = R ψ ~R.
|
||||
|
||||
\# Multi-modality ingress uses modality_transition_sandwich
|
||||
|
||||
\# (R·ψ·rev(R) + GoldTether). (Folded from the retired docs/research
|
||||
|
||||
\# copy under the 2026-07-22 weekly-audit T7 ruling.)
|
||||
|
||||
generator \= np.dot(H\_problem, self.I) \# SUPERSEDED — do not implement
|
||||
|
||||
R \= la.expm(generator \* dt)
|
||||
|
||||
|
||||
|
||||
\# Relaxation loop (Euler/exponential integrator)
|
||||
\# Relaxation loop (Euler/exponential integrator) — SUPERSEDED
|
||||
|
||||
for \_ in range(relaxation\_steps):
|
||||
|
||||
psi \= np.dot(R, psi)
|
||||
|
||||
\# Enforce the null-cone amplitude normalization step
|
||||
|
||||
norm \= np.linalg.norm(psi)
|
||||
|
||||
if norm \> 1e-12:
|
||||
|
|
@ -239,13 +255,17 @@ class CognitiveLifecycleEngine:
|
|||
|
||||
wave-states from entering the readback and serving paths.
|
||||
|
||||
IMPLEMENTED residual: WaveManifold.measure_unitary_residual /
|
||||
|
||||
goldtether.coherence_residual (ψ · rev(ψ) via geometric_product).
|
||||
|
||||
The np.dot sketch below is SUPERSEDED (convergence 2026-07-20).
|
||||
|
||||
"""
|
||||
|
||||
psi\_arr \= np.asarray(psi\_steady, dtype=np.float64)
|
||||
|
||||
\# Unitary residual check: || psi \* rev(psi) \- 1 ||\_F
|
||||
|
||||
\# rev(psi) proxied via conjugate transpose under I-metric
|
||||
\# SUPERSEDED flat residual — use geometric_product path in code:
|
||||
|
||||
psi\_rev \= np.dot(self.I.T, psi\_arr)
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ This ADR resolves these issues by completely reconstructing the Identity Manifol
|
|||
|
||||
We completely solidify CORE's identity layer by establishing that **identity is an inalienable geometric property of the wave-field itself, defended via metric-exact spectral projection and topological charge conservation**.
|
||||
|
||||
We implement this transition through a **dual-mode architecture** in `core/physics/identity.py`, maintaining 100% backwards compatibility with legacy heuristic fixtures while enabling optimal wave-field geometry when wave-packets are present.
|
||||
We implement this transition through a **wave-only geometry path** in `core/physics/identity.py`. Scalar-L2 dual-mode fallback has been **excised** (system convergence 2026-07-20): missing `ψ_traj` raises `MissingWaveStateError`; scoring always uses metric-exact Gram / operator-preservation geometry.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -177,35 +177,19 @@ We implement a **Fibonacci-Word Background Scheduler** strictly isolated from th
|
|||
|
||||
---
|
||||
|
||||
## 3\. Backwards Compatibility & Dual-Mode Fallback
|
||||
## 3\. Fail-Closed Wave Requirement (Dual-Mode Excised)
|
||||
|
||||
To prevent any regression across existing test suites and fixtures, `IdentityCheck().check(trajectory)` operates in a **graceful dual-mode configuration**:
|
||||
**Supersedes the former dual-mode / scalar-L2 fallback.** Convergence (2026-07-20) removed `_axis_projection`, `_mean_frame_coherence`, and the blend `(0.75 * score) + (0.25 * directional_weight * coherence_term)` entirely.
|
||||
|
||||
def check(self, trajectory, manifold: IdentityManifold | None \= None) \-\> IdentityScore:
|
||||
`IdentityCheck().check(trajectory, manifold, *, wave_field=...)` now requires an explicit Cl(4,1) `wave_field` (`ψ_traj`). Absence raises typed `MissingWaveStateError`. Malformed fields raise `ValueError`. Live *refusal* remains flag-gated via `RuntimeConfig.identity_wave_gate`; **scoring is always geometric**.
|
||||
|
||||
\# 1\. Check if the trajectory contains a wave-field representation (ADR-0244)
|
||||
|
||||
psi\_traj \= getattr(trajectory, "psi\_traj", None)
|
||||
|
||||
if psi\_traj is not None:
|
||||
|
||||
\# Execute metric-exact wave-field spectral projection
|
||||
|
||||
...
|
||||
|
||||
else:
|
||||
|
||||
\# Fall back gracefully to legacy scalar-L2 heuristics (ADR-0010)
|
||||
|
||||
...
|
||||
|
||||
This ensures that legacy evaluation suites (such as `evals/adversarial_identity` and `evals/teaching_injection_resistance`) run without modification, while wave-capable serving paths automatically leverage the high-assurance geometric projection.
|
||||
Callers (e.g. `chat/runtime.py`) always pass `final_state.F`. Evaluation suites that previously relied on L2 must supply a wave field.
|
||||
|
||||
---
|
||||
|
||||
## 4\. Implementation Specification
|
||||
|
||||
The conformed implementation in `core/physics/identity.py` combines both legacy and upgraded paths:
|
||||
The conformed implementation in `core/physics/identity.py` is wave-only (Gram / operator-preservation via `identity_manifold.py`):
|
||||
|
||||
\# core/physics/identity.py
|
||||
|
||||
|
|
@ -515,13 +499,16 @@ def axis_response(R, axes_psi, g_inv):
|
|||
return leak, align
|
||||
```
|
||||
|
||||
**Phase 2 gate — `core/physics/identity.py` (§2.2; dual-mode, fail-closed):**
|
||||
**Phase 2 gate — `core/physics/identity.py` (§2.2; wave-only, fail-closed):**
|
||||
|
||||
```python
|
||||
class IdentityGateRefusal(Exception):
|
||||
"""Fail-closed refusal: leakage/orientation or boundary check failed and
|
||||
C_id could not recover alignment within its bound. Params unchanged."""
|
||||
|
||||
class MissingWaveStateError(ValueError):
|
||||
"""Raised when wave_field / ψ_traj is absent (scalar-L2 path excised)."""
|
||||
|
||||
def _wave_field_check(F_traj, axes_psi, g_inv) -> tuple[float, list, list]:
|
||||
F = np.ascontiguousarray(F_traj, dtype=np.float32)
|
||||
if F.dtype.byteorder not in ("<", "="):
|
||||
|
|
@ -531,14 +518,11 @@ def _wave_field_check(F_traj, axes_psi, g_inv) -> tuple[float, list, list]:
|
|||
if F.shape != (N_COMPONENTS,):
|
||||
raise ValueError(f"F_traj must be shape ({N_COMPONENTS},), got {F.shape}")
|
||||
leak, align = axis_response(F.astype(np.float64), axes_psi, g_inv)
|
||||
# subspace-preservation score (RMS leakage over axes; each rotated axis is
|
||||
# unit-norm, so the denominator is sqrt(n)); orientation carried separately.
|
||||
score = 1.0 - (sum(l * l for l in leak) / len(leak)) ** 0.5
|
||||
return score, leak, align
|
||||
|
||||
# Malformed F_traj (NaN / wrong shape / wrong byte-order) raises — it never
|
||||
# falls through to the legacy scalar-L2 path (Sec 3's dual-mode fallback is
|
||||
# for ABSENT F_traj only, not malformed F_traj).
|
||||
# Absent F_traj → MissingWaveStateError. Malformed F_traj → ValueError.
|
||||
# No scalar-L2 fallback remains (convergence 2026-07-20).
|
||||
```
|
||||
|
||||
Egress condition (replaces §2.2 item 2's formula — `∧ ΔQ_top = 0` dropped per governance annotation item 1; operator-preservation per item 12):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
# ADR-0253: Master Blueprint ADR Collision Resolution & Dual-Pack Boundary
|
||||
|
||||
**Status**: Accepted (Stage 1 governance freeze)
|
||||
**Date**: 2026-07-20
|
||||
**Deciders**: CORE engineering (Master Convergence Stage 1)
|
||||
**Related**: `docs/adr/MASTER-BLUEPRINT-2026-07-20-ADR-MAPPING.md`, Master Architectural Specification 2026-07-20
|
||||
|
||||
## Context
|
||||
|
||||
The Master Blueprint §3.2 lists ADRs 0240–0253 with intent titles that **collide by number** with the repository’s already-Accepted ADR-0246 through ADR-0252 (and with other live 0240–0245 titles). Overwriting Accepted history is forbidden by repository governance and by the Stage 1 program rule.
|
||||
|
||||
Separately, language packs exist as both:
|
||||
|
||||
- **compiled runtime artifacts** under `packs/data/<pack_id>/`, and
|
||||
- **source/draft trees** such as `packs/he`, `packs/grc`, `packs/en`, `packs/el`.
|
||||
|
||||
Serve paths must not treat draft source trees as importable authority.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **ID history is immutable.** Accepted ADR numbers keep their live titles and scopes. Blueprint intent titles never renumber Accepted ADRs.
|
||||
2. **Mapping is the reconciliation surface.** The file
|
||||
`docs/adr/MASTER-BLUEPRINT-2026-07-20-ADR-MAPPING.md`
|
||||
is the authoritative Blueprint-ID → repository-home table (Covered / Partial / Gap / reserved new IDs 0254–0261).
|
||||
3. **This ADR-0253 number is claimed for governance freeze**, not for the Blueprint’s “Holonomy Primacy Rust SIMD” title. That Blueprint intent is reserved as **ADR-0261** if implemented later.
|
||||
4. **Dual-pack boundary**
|
||||
- Runtime serve/load of language packs uses `packs.compiler` → `packs/data/<pack_id>/` only.
|
||||
- `packs/he`, `packs/grc`, and peer source trees are draft/source material; they are not serve-import authority.
|
||||
- Architecture tests pin that serve-entry modules do not import `packs.he` / `packs.grc` as packages for serving.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Stage 3/4 Blueprint work must open **new** ADR numbers (0254+) or amend the correct existing owner ADR — never 0246–0252 renames.
|
||||
- CI fails if draft HE/GRC package imports appear on the serve import graph.
|
||||
- Documentation that cites Blueprint ADR numbers for convergence work must also cite this mapping.
|
||||
|
||||
## Validation
|
||||
|
||||
- `tests/test_pack_draft_serve_boundary.py`
|
||||
- Mapping file present and linked from `docs/adr/README.md`
|
||||
55
docs/adr/ADR-0254-grounded-open-hedge-arm.md
Normal file
55
docs/adr/ADR-0254-grounded-open-hedge-arm.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# ADR-0254: Grounded-Open Hedge Arm for the Shadow Coherence Gate
|
||||
|
||||
**Status:** Accepted — ratified by Joshua Shay via the PR #103 merge (`da3447e9`, 2026-07-23), discharging this ADR's own ratify-on-merge predicate (pre-authorized by the 2026-07-22 weekly-audit ruling T13 decision 2)
|
||||
**Date:** 2026-07-23
|
||||
**Deciders:** Joshua Shay (ruling authority) + Claude (implementation)
|
||||
**Companion docs:** [`ADR-0036-safety-refusal-policy.md`](ADR-0036-safety-refusal-policy.md), [`ADR-0037-per-predicate-ethics-refusal.md`](ADR-0037-per-predicate-ethics-refusal.md), [`ADR-0038-hedge-injection.md`](ADR-0038-hedge-injection.md), [`ADR-0252-problem-solving-paradigm-consolidation.md`](ADR-0252-problem-solving-paradigm-consolidation.md)
|
||||
**Preserves (governing, unchanged):** `wrong=0`, refuse-don't-guess, decode-don't-generate, fail-closed (INV-34), no lexical cue-tables (ADR-0252).
|
||||
|
||||
---
|
||||
|
||||
## 1. Context
|
||||
|
||||
The dual-competing Shadow Coherence Gate (`core/cognition/surface_resolution.py::resolve_surface`, introduced PR #96) abstains — emits a typed `CoherenceRefusal` — whenever the conjugate competitor (geometric residual contract) is open. The conjugate is open when the field's `versor_condition ≥ 1e-6` (→ `missing_bindings`) or the GoldTether residual `R_GT > 1e-6` (→ `unresolved_hazards`), as built by `_geometry_contract_assessment`.
|
||||
|
||||
The weekly audit (2026-07-22) surfaced an **over-refusal** in the `warmed_session_consistency` lane. Once the wave field is perturbed by a prior turn, a subsequent **pack-grounded** definitional surface — e.g. *"What is doubt?"* → `"To doubt means to think maybe not. pack-grounded (en_core_meta_v1)."` — hits an open `goldtether_residual` and is hard-refused with *"I cannot certify an answer: the geometric coherence contract is open (goldtether_residual)."*
|
||||
|
||||
This is a false refusal. **The pack surface never claimed geometric certification.** It stands on its curated textual provenance; the open geometric residual says only that the *field* did not close, not that the *pack answer* is wrong.
|
||||
|
||||
A tempting fix — a "definitional/epistemic" query-type classifier that bypasses the gate on a lexical cue — is **rejected**: it fails a geometric coherence gate open on a surface cue (fail-open cue-table; violates ADR-0252 and INV-34).
|
||||
|
||||
## 2. Decision
|
||||
|
||||
Route open-geometry-but-**pack-grounded** surfaces to a **hedge arm** — serve the pack surface non-authoritatively (`authoritative=False`) — instead of hard-refusing, discriminated **purely by grounding provenance** (structural), never by question type.
|
||||
|
||||
**Authorized predicate:**
|
||||
|
||||
```
|
||||
open_conjugate ∧ pack_grounded ∧ ¬hazard → hedge_arm
|
||||
```
|
||||
|
||||
Realized structurally inside the gate (single source of truth):
|
||||
|
||||
- **`pack_grounded`** — `grounding_provenance ∈ {pack, teaching}` (mirrors the existing `chat.runtime` "grounded" test). `vault`/`oov`/`partial`/`none` never hedge.
|
||||
- **`¬hazard`** — enforced by a **residual allowlist**, not question typing. Hedge iff *every* open token ∈ `{versor_condition, goldtether_residual}` (geometric-coherence residuals the pack surface never certified). **Any** unrecognized open token — where a genuine safety/harm/imperative hazard would land — fails the allowlist and takes the unchanged hard refusal. `missing_wave_field` is deliberately excluded (absence of any field is a harder failure than an open-but-computed residual).
|
||||
|
||||
The hedge outcome: the pack surface with a deterministic `"Grounded but not geometrically certified —"` marker, `authoritative=False`, `hedged=True`, `authority="grounded_open_hedge"`. Walk/compose folds are suppressed — a hedge never accretes deterministic inference authority.
|
||||
|
||||
## 3. Why this is fail-closed, not fail-open
|
||||
|
||||
- The **only** `ContractAssessment` reaching `resolve_surface` is the shadow-gate geometry assessment (`pipeline.py:416`), whose tokens are ⊆ `{versor_condition, missing_wave_field}` (bindings) ∪ `{goldtether_residual}` (hazards). Genuine safety/harm/imperative hazards ride **separate axes** — `SafetyVerdict` → typed refusal (ADR-0036) and the logos-morph override (`pipeline.py:513`) — both of which supersede this surface downstream. The hedge cannot leak a harmful surface: safety and morph refusals still win.
|
||||
- The predicate is **structural** (provenance + a closed allowlist of geometric tokens). It reads no question text. An unknown token defaults to refusal (INV-34).
|
||||
- `require_closed_geometry` callers that omit `grounding_provenance` get the **historical hard refusal** unchanged (no accidental hedge).
|
||||
|
||||
## 4. Consequences
|
||||
|
||||
- The over-refusal is fixed: the warmed *"What is doubt?"* case now serves the hedged pack definition (verified end-to-end).
|
||||
- New auditable authority tag `grounded_open_hedge` flows to `TurnEvent.authority_source`; audit distinguishes hedge from both certified answers and refusals.
|
||||
- **Not conflated with ADR-0038.** The ADR-0038 hedge is *ethics-commitment-driven* (`hedge_commitments` + manifold `preferred_hedge_soft`); this hedge is *coherence-geometry-driven*. They are independent mechanisms. Unifying the grounded-open marker with the manifold's `preferred_hedge_soft` vocabulary is a **deferred follow-up** (defer substrate-vocab commitment) and intentionally out of scope here.
|
||||
- `authoritative=False` lives in the gate; downstream observes it as `authority_source="grounded_open_hedge"` (consistent with how refusals already surface — no new result plumbing).
|
||||
|
||||
## 5. Validation
|
||||
|
||||
- **Unit (RED→GREEN):** `tests/test_grounded_open_hedge_arm.py` — hedge activates on pack + geometric-residual openness; fails closed on non-pack grounding, unknown/mixed hazard tokens, `missing_wave_field`, and `None` assessment; closed geometry stays authoritative; omitted provenance stays refusing.
|
||||
- **Real-data (GSM8K-style):** the warmed mixed-intent sequence exercises the arm on real field dynamics; the `warmed_session_consistency` lane never hard-refuses a pack surface, with `telemetry_consistency_rate` and `no_placeholder_rate` held at 1.0.
|
||||
- **Regression:** architectural invariants (incl. INV-34) green; **GSM8K `wrong=0` preserved** (the closed-geometry certified math path is untouched).
|
||||
47
docs/adr/ADR-0255-discovery-yield-baseline-telemetry.md
Normal file
47
docs/adr/ADR-0255-discovery-yield-baseline-telemetry.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# ADR-0255: Discovery-Yield-Per-Served-Turn Baseline Telemetry
|
||||
|
||||
**Status:** Accepted (directive + framing ruled by Joshua Shay, 2026-07-23)
|
||||
**Date:** 2026-07-23
|
||||
**Deciders:** Joshua Shay (ruling authority) + Claude (implementation)
|
||||
**Companion docs:** [`docs/analysis/weekly-audit-2026-07-22-stragglers-todo.md`](../analysis/weekly-audit-2026-07-22-stragglers-todo.md) (T11), [`scripts/ops/reset_candidate_corpus_t11_20260722.py`](../../scripts/ops/reset_candidate_corpus_t11_20260722.py), [`docs/adr/ADR-0219-generation-dir-atomic-checkpoint.md`](ADR-0219-generation-dir-atomic-checkpoint.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. Context
|
||||
|
||||
The T11 weekly-audit ruling (2026-07-22) wiped the live discovery-candidate ledger from 465 provenance-tainted records to 0, via the `engine_state` ADR-0219 atomic-generation API. It deliberately left `turn_count` (14990) unchanged — resetting a scalar that tracks real historical served turns would be fabrication, not cleanup.
|
||||
|
||||
That leaves no way to measure how fast the candidate corpus repopulates: `turn_count` is a monotonic counter spanning the ledger's entire pre-reset history, while the candidate ledger itself is now clean. Any yield metric computed as `candidates / turn_count` would silently blend pre-reset and post-reset traffic.
|
||||
|
||||
The follow-up directive (2026-07-23) asked for a `discovery-yield-per-served-turn` metric, explicitly scoped: **"candidates proposed per served turn on clean, post-reset traffic."** CORE has no always-on background PROCESS yet (`[[project-core-is-one-continuous-life]]`), so "per served turn" can only mean per user-invoked turn — there is no live/background rate to measure until that process exists. This ADR does not build that process; it measures the organic yield of the current (proposal-only, config-gated) discovery mechanism, which is itself a prerequisite for designing that process's metabolic loop.
|
||||
|
||||
## 2. Decision
|
||||
|
||||
Add one additive-optional manifest field, `turn_count_baseline`, and one pure computation function, `teaching.discovery_yield.compute_discovery_yield`.
|
||||
|
||||
- **`turn_count_baseline`** (`engine_state/__init__.py::save_manifest`) — the `turn_count` value at the moment the candidate ledger was last reset to empty. Written through the same atomic generation-dir commit as every other manifest field (no raw file writes). Does **not** bump `schema_version` — same additive-optional discipline as `engine_identity`/`parent_engine_identity`.
|
||||
- **`compute_discovery_yield(store)`** — reads the live manifest and candidate ledger, returns:
|
||||
- `served_turns_since_reset = turn_count - turn_count_baseline`
|
||||
- `candidates_since_reset = len(candidates)`
|
||||
- `yield_rate = candidates_since_reset / served_turns_since_reset` (or `None` when `served_turns_since_reset == 0`, never a `ZeroDivisionError`)
|
||||
- **Fail-closed on no baseline**: if `turn_count_baseline` is absent from the manifest, `compute_discovery_yield` returns `None`. It never coerces the missing baseline to `0` or to the current `turn_count` — either would fabricate a denominator against an epoch nobody marked. Absolute Provenance: no metric is better than an invented one.
|
||||
- **`scripts/ops/stamp_discovery_yield_baseline_20260723.py`** — the one-off script that stamps the baseline into the live store post-T11, mirroring the T11 reset script's atomic begin/commit pattern and idempotent guard (refuses to run if a baseline is already stamped).
|
||||
- **`core teaching discovery-yield [--json]`** — read-only CLI surface (`core/cli_teaching.py`), same shape as `core teaching coverage`: human-readable by default, `--json` for machine consumption. Exits 1 with a clear stderr message when no baseline is stamped.
|
||||
|
||||
## 3. Non-goals
|
||||
|
||||
- **Not a live/background rate.** The metric is computed on demand from committed state; it does not run on a clock or a daemon. Building that is a separate, later arc (`[[project-core-is-one-continuous-life]]` Phase 2) and this ADR takes no position on its design beyond noting that this organic yield number is a prerequisite input to it.
|
||||
- **Not a new telemetry sink.** This does not wire into `chat/telemetry.py`'s `TurnEventSink`/`DiscoveryCandidateSink` JSONL-stream pattern — the metric is a point-in-time snapshot of already-persisted state, not a per-event stream. If a continuous time-series becomes useful later, it can read this same computation at each checkpoint; that is a follow-up, not scope creep here.
|
||||
- **Does not touch serving physics.** Nothing here reads or writes anything on the `resolve_surface` / pipeline path. Pure read of `engine_state`, pure write of one additive manifest field.
|
||||
|
||||
## 4. Consequences
|
||||
|
||||
- The live store now carries a `turn_count_baseline` (stamped once, 2026-07-23, at `turn_count=14990` — the same value T11 left it at, since no live-store turns had been served between the T11 reset and this stamp).
|
||||
- Every served turn from here on advances `turn_count` past the baseline; `core teaching discovery-yield` gives a deterministic, reproducible answer to "how many candidates has the discovery mechanism proposed per served turn since the corpus went clean."
|
||||
- A manifest written before this directive, or by any caller that never resets the ledger, has no baseline and the CLI/function both fail closed rather than guess.
|
||||
|
||||
## 5. Validation
|
||||
|
||||
- **Unit (RED→GREEN):** `tests/test_discovery_yield.py` — baseline-absent fail-closed (+ `_bites` mutation), delta computation (+ `_bites`), yield-rate division (+ `_bites`), zero-served-turns → `None` rate not `ZeroDivisionError` (+ `_bites`), `as_dict()` JSON-safety, and CLI-level tests (`core.cli.main`) for the fail-closed exit code and `--json` output.
|
||||
- **Dry run:** the stamping script was exercised against a throwaway copy of the live store's `gen-21703` before touching the real one — confirmed the idempotent guard aborts a second run and the committed manifest carries `turn_count_baseline=14990` with identity/candidates preserved byte-faithfully.
|
||||
- **Regression:** smoke + warmed_session lane pin green pre-push (see PR `[Verification]`).
|
||||
59
docs/adr/ADR-0256-deduction-serve-earned-license.md
Normal file
59
docs/adr/ADR-0256-deduction-serve-earned-license.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# ADR-0256: Deduction Serving Governed by an Earned Reliability License
|
||||
|
||||
**Status:** Accepted — ratified by Joshua Shay via the deduction-serve Phase 5 merge (`1a6ccaf9`, 2026-07-23), discharging this ADR's own ratify-on-merge predicate. The `deduction_serving_enabled` default was flipped False → True by a separate, explicit ratification (`5dd61803`, 2026-07-24)
|
||||
**Date:** 2026-07-23
|
||||
**Deciders:** Joshua Shay (ruling authority) + Claude (implementation)
|
||||
**Companion docs:** [`docs/research/deduction-serve-arc-phase0-baseline-2026-07-23.md`](../research/deduction-serve-arc-phase0-baseline-2026-07-23.md), [`docs/research/deduction-serve-arc-phase1-turn-spine-2026-07-23.md`](../research/deduction-serve-arc-phase1-turn-spine-2026-07-23.md), [`docs/research/deduction-serve-arc-phase2-eval-lane-2026-07-23.md`](../research/deduction-serve-arc-phase2-eval-lane-2026-07-23.md), [`docs/adr/ADR-0175-...`](.) (calibrated learning), [`docs/adr/ADR-0199-...`](.) (cross-domain arena), [`docs/adr/ADR-0206-...`](.) (response-governance bridge)
|
||||
|
||||
---
|
||||
|
||||
## 1. Context
|
||||
|
||||
Phases 1–2 of the deduction-serve arc gave `core chat` the ability to decide propositional-argument questions with the verified ROBDD entailment engine (`generate/proof_chain`, 716/716 wrong=0), behind a bare `deduction_serving_enabled` flag. That flag is a **direct-serve switch**: flip it and CORE speaks authoritatively on every propositional argument, with nothing between the capability and the user but a boolean.
|
||||
|
||||
CORE already has the machinery to do better. ADR-0175's reliability gate (`core/reliability_gate/`) and ADR-0199's cross-domain learning arena (`core/learning_arena/`) exist precisely to make a served capability **earned** — measured over sealed practice against independent gold, licensed per capability-class only when a committed track record clears the human-set ceiling (θ_SERVE=0.99). Until now that machinery had exactly one consumer (GSM8K estimation, ADR-0206 Step E) and its own docstring admitted it was "NOT wired into the serving path" for anything else.
|
||||
|
||||
The ruling (endorsed by Shay in the arc plan): deduction serving must be **earned-license via the reliability gate**, not a direct-serve flag.
|
||||
|
||||
### What the license actually certifies (the non-circular part)
|
||||
|
||||
The ROBDD engine is sound **and** complete over the propositional regime — it is never wrong on the problem it is handed. So a license cannot, and does not, certify the engine. The **fallible** component is the reader/projector: `generate/meaning_graph/reader.py` is a template-based comprehension reader that can misparse a natural-language argument and hand the engine the *wrong* problem, which it then soundly decides — a wrong served answer. The reliability license certifies the **full serving pipeline** (reader → projector → engine) per argument shape: it answers "does CORE reliably *read and decide* arguments of this shape at volume?", which is exactly the failure mode the engine's soundness does not cover.
|
||||
|
||||
## 2. Decision
|
||||
|
||||
Make deduction serving an earned capability gated by a committed, SHA-sealed reliability ledger.
|
||||
|
||||
- **Shape-band capability axis** (`generate/proof_chain/shape.py`) — a deterministic, structural classification of an argument into one of five exhaustive bands: the four propositional bands `disjunctive` / `conditional_chain` / `conditional_single` / `atomic` (assigned by `classify_deduction_shape` over the projected `(premises, query)`), plus `categorical` (Band v1b, Phase 4 — assigned directly to any syllogism projected by `to_syllogism`). Computed identically by the practice arena and the serving composer (no drift). Coarse on purpose: each band mixes entailed/refuted/unknown (or valid/invalid) gold, so a band's reliability measures decision correctness across outcomes, not just how many entailments pass through.
|
||||
- **Band v1b — production categorical decider** (`generate/proof_chain/categorical.py`) — the piece Phase 0's fork identified as missing. Serving cannot import `evals/syllogism/oracle.py` (the sealed independence oracle; INV-25), so this is a fresh production decider that lowers a categorical argument to the propositional regime (one Boolean atom per term-membership *profile*) and rides the already-verified ROBDD engine. Sound AND complete for the modern/Boolean reading; independent of the eval oracle by mechanism (ROBDD over a profile encoding vs. brute-force subset enumeration), verified by case-for-case agreement with it across the syllogism gold lane. Serving projects a syllogism via `to_syllogism` and decides it here (ENTAILED ⇒ valid; UNKNOWN/REFUTED ⇒ invalid; REFUSED ⇒ inconsistent).
|
||||
- **Deduction practice arena** (`evals/deduction_serve/practice/`) — the **second** concrete instance of the ADR-0199 arena (GSM8K math is the first). A deterministic, synthetic, indexed corpus (`gold.py`, no clock/RNG) of propositional arguments per shape-band, each with **by-construction gold** (the generating template's logical form, independent of the reader — ADR-0199 L-2). `DeductionSolver` runs the exact serving pipeline; `ConstructionGoldTether` scores the committed outcome against the by-construction gold; a misread is caught as `wrong`. Sized to the SERVE volume floor (≥657 committed for a perfect record to clear θ_SERVE=0.99; `CASES_PER_BAND=720`).
|
||||
- **Construction-time soundness check** (`gold.py::assert_corpus_sound`) — every generated case's by-construction gold is cross-checked against the **independent truth-table oracle** (`evals.deductive_logic.oracle`, INV-25 — shares no code with the ROBDD engine) before the ledger can seal. A mis-stated template gold can never silently inflate the ledger.
|
||||
- **Sealed, SHA-verified committed ledger** (`chat/data/deduction_serve_ledger.json`, produced by `evals/deduction_serve/practice/runner.py::seal_ledger`) — the per-band `ClassTally`, self-sealed by `content_sha256`. The engine READS it; only the sealed-practice runner WRITES it. Mirrors the estimation ledger topology exactly (`generate/determine/data/estimation_ledger.json`).
|
||||
- **Serving-side license reader** (`chat/deduction_serve_license.py::deduction_serve_license`) — loads + SHA-verifies the committed ledger and returns the `Action.SERVE` `LicenseDecision` per band under the safe **default** ceilings (θ_SERVE=0.99). A tampered ledger is rejected on load; a band absent from the ledger returns `None` (never licensed). The engine never raises its own ceiling (ADR-0175 invariant #4).
|
||||
- **Composer gate** (`chat/deduction_surface.py::deduction_grounded_surface`) — after the engine decides an in-band argument, the composer classifies its shape-band and consults the license:
|
||||
- **earned SERVE** → the answer is served **authoritatively** (the plain Phase-1 surface).
|
||||
- **not earned** (a band absent from the ledger, or any deployment whose ledger is stripped/unverified) → the same sound answer is served **disclosed/hedged** (`"(reasoned, but I haven't yet earned a verified track record on arguments of this shape) …"`), never presented as a verified capability.
|
||||
|
||||
This is what makes deduction serving an *earned* capability rather than a flagged one: strip the committed ledger and CORE still reasons soundly, but stops claiming verified authority it has not demonstrated.
|
||||
|
||||
### 2a. ADR-numbering collision note (the "resolve ADR-0206" obligation)
|
||||
|
||||
`generate/proof_chain/entail.py` historically carried an "ADR-0206" attribution, and `evals/deductive_logic/report.json` still stamps `"adr": "ADR-0206"`. This is a documented three-way collision (see the `entail.py` module docstring and ADR-0253's collision-handling precedent): committed **ADR-0206** is the response-governance bridge; the entailment operator's real home is **ADR-0218** (Proof-Carrying Coherence Promotion); and deduction *serving* is now **ADR-0256** (this document). This ADR does **not** rewrite `report.json`'s `adr` field — that is a SHA-pinned artifact whose bytes are a separate concern (rewriting it would cascade the pinned lane SHA for no capability change). It records the correct homes so no future reader trusts the stale `ADR-0206` pointer.
|
||||
|
||||
## 3. Non-goals
|
||||
|
||||
- **Not a new soundness mechanism.** wrong=0 on the propositional regime is owned by the ROBDD engine (ADR-0201/0218), unchanged. This ADR adds an *earned-trust posture* on top, it does not touch the decision procedure.
|
||||
- **Not autonomous ratification.** The committed ledger is sealed by an explicit, reviewable practice run and committed as data; the engine consumes it read-only. Regenerating it is a deliberate `--seal` operator action, not a runtime write. No autonomous promotion (ADR-0175 invariant #1 discipline preserved: the arena writes only its own artifact; serving reads it).
|
||||
- **Not a widening past gold.** Unlike ADR-0206 Step E (which discloses an *estimate* past what is grounded), every deduction answer here is a sound entailment verdict; the license governs whether it is stated *authoritatively* vs *disclosed*, never whether an unsound guess is surfaced.
|
||||
- **Does not change the default flag posture.** `deduction_serving_enabled` remains default-off (ADR-0256 layers the license *inside* the enabled path). Flag-off is byte-identical to pre-arc dispatch.
|
||||
|
||||
## 4. Consequences
|
||||
|
||||
- Deduction serving is now governed: with the committed ledger (all four bands earned at reliability 0.99087, wrong=0), in-band arguments serve authoritatively — Phase-1 behavior preserved byte-for-byte. Without an earned ledger, the same reasoning is served honestly disclosed.
|
||||
- The ADR-0175/0199 reliability substrate gains its **second** real consumer and its **first** non-estimation serving consumer — a concrete step against the standing "designed, not wired" critique of that zone.
|
||||
- The `deduction_serving_enabled` flag stops being a direct-serve switch and becomes an enable for an *earned* path; authority now rests on committed, tamper-evident evidence, not a boolean.
|
||||
|
||||
## 5. Validation
|
||||
|
||||
- **Arena / ledger (`tests/test_deduction_serve_license.py`):** corpus soundness vs the independent oracle; every band earns SERVE at reliability ≥ 0.99 with wrong=0; committed-ledger `content_sha256` verifies on load; a tampered ledger is rejected; `deduction_serve_license` returns `None` for an unknown band.
|
||||
- **Composer gate (same file):** an earned band serves authoritatively (no hedge, Phase-1 surface preserved); an injected empty ledger forces the disclosed hedge on the SAME sound answer — proving the gate genuinely governs posture.
|
||||
- **Regression:** Phase-1 (`test_deduction_surface.py`) and Phase-2 (`test_deduction_serve_lane.py`) suites stay green; smoke + cognition green (see PR `[Verification]`).
|
||||
141
docs/adr/ADR-0257-english-clause-argument-band.md
Normal file
141
docs/adr/ADR-0257-english-clause-argument-band.md
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
# ADR-0257 — English-clause argument band (Band v2-EN): opaque-atom propositional serving
|
||||
|
||||
- **Status:** Accepted — ratified by Joshua Shay via the PR #107 merge (`9405cf19`, 2026-07-23)
|
||||
- **Date:** 2026-07-23
|
||||
- **Relates to:** ADR-0256 (deduction-serve earned license), ADR-0201 (ROBDD
|
||||
keystone), ADR-0218 (proof-carrying substrate), ADR-0175/0199 (calibrated
|
||||
learning / arena), ADR-0253 (dual-pack serve boundary — grc/he)
|
||||
|
||||
## 1. Context
|
||||
|
||||
ADR-0256 shipped the first end-to-end reasoning workflow: `core chat` decides
|
||||
propositional and categorical arguments, flag-gated, behind an earned per-band
|
||||
SERVE license. Its two documented boundary cases were natural-English inputs:
|
||||
|
||||
- `"If it rains then the ground is wet. It rains. Therefore the ground is wet."`
|
||||
→ declined (`reserved_word_in_np`);
|
||||
- `"If p then q. Therefore if not q then not p."` (contraposition)
|
||||
→ declined (nested negation).
|
||||
|
||||
Both declines came from the SHARED comprehension reader
|
||||
(`generate/meaning_graph/reader.py`), whose single-token atom floor and
|
||||
reserved-word guard are load-bearing for seven other lanes' wrong=0. The
|
||||
deductive engine itself (`generate/proof_chain/entail.py`) never was the
|
||||
limit: it is sound+complete over arbitrary propositional structure with
|
||||
opaque atoms. The gap was purely a *reading* band.
|
||||
|
||||
## 2. Decision
|
||||
|
||||
Add a dedicated **English-clause argument reader**
|
||||
(`generate/proof_chain/english.py`) used ONLY by the deduction serving path,
|
||||
as a fallback tier after Band v1 (propositional) and v1b (categorical):
|
||||
|
||||
- Whole English clauses become **opaque atoms**: one minted id (`a0`, `a1`, …)
|
||||
per distinct normalized clause; identity is normalized exact text. Clause
|
||||
*content* is never interpreted — only quoted back in the surface.
|
||||
- Structure is recognized by a **closed function-word grammar**:
|
||||
`if <A> then <B>` (junctions allowed on both sides), `[either] <A> or <B>`
|
||||
(n-ary), top-level `and` (premise splits / conjunctive conclusions), and
|
||||
three negation forms — leading `not`, sentential `it is not the case that`,
|
||||
and copular `<L> is|are|was|were not <R>` (normalized to `~atom(<L> is <R>)`,
|
||||
contractions `isn't`/`aren't`/… expanded).
|
||||
- The same verified ROBDD engine decides; deterministic templates render the
|
||||
verdict over the user's own clause text
|
||||
(`generate/proof_chain/render.py::render_entailment_english`).
|
||||
- Four new shape-bands — `en_conditional_single`, `en_conditional_chain`,
|
||||
`en_disjunctive`, `en_atomic` — each **earning its own SERVE license**
|
||||
through the ADR-0199 arena at n=720/band, wrong=0 (sealed into the same
|
||||
ratified ledger; unlicensed ⇒ automatically hedged, per ADR-0256).
|
||||
|
||||
The shared reader is untouched. The composer tries v1/v1b first, so every
|
||||
previously-served argument is served byte-identically; this band only widens.
|
||||
|
||||
## 3. Why the opaque reading is sound (the load-bearing argument)
|
||||
|
||||
Propositional entailment is **closed under uniform substitution** of formulas
|
||||
for atoms. Therefore, for verdicts computed over opaque clause-atoms:
|
||||
|
||||
- **ENTAILED**, **REFUTED**, and **inconsistent-premises** verdicts remain
|
||||
true under ANY deeper reading of the clauses — refine a clause into internal
|
||||
structure, or identify two clauses we kept distinct, and the verdict stands.
|
||||
These are served at full strength.
|
||||
- **UNKNOWN** ("doesn't settle") is the ONE verdict that is *not*
|
||||
substitution-safe: internal clause structure this band did not read could
|
||||
settle the argument. Two mitigations, both mandatory:
|
||||
1. **Scoped phrasing** — the UNKNOWN surface states its reading explicitly:
|
||||
*"Reading each clause as one indivisible claim, your premises don't
|
||||
settle whether …"*. True of the reading, disclosed as such.
|
||||
2. **Structural guards** — clauses whose surface form ANNOUNCES structure
|
||||
this band cannot read refuse out of the band entirely (typed):
|
||||
quantifier-led categorical clauses (`all/no/some/every/…` — a valid
|
||||
syllogism must never flatten into a misleading "doesn't follow"),
|
||||
`is a|an` membership clauses (reserved for a future `member_chain`
|
||||
band), and unnormalizable negation (`never`, `cannot`, `doesn't`, … —
|
||||
a negation must never hide inside an opaque atom).
|
||||
|
||||
Genuinely ambiguous English (mixed top-level `and`+`or`, nested conditionals)
|
||||
refuses rather than guessing a scope. Honesty caps (16 premises, 24 atoms)
|
||||
refuse rather than truncate.
|
||||
|
||||
## 4. What the earned licenses certify
|
||||
|
||||
Per ADR-0256, a band's license certifies READER fidelity per shape — hence the
|
||||
`en_` namespace: same engine, different reader, so the English reader earns its
|
||||
own record. The arena corpus is synthetic copular clauses ON PURPOSE (the
|
||||
reader is content-blind; the license certifies structural fidelity); the eval
|
||||
lane (`evals/deduction_serve/v2_en/`, 26 hand-authored REAL-English cases,
|
||||
content disjoint from the synthetic lexicon) keeps that claim honest against
|
||||
natural prose — the synthetic-corpus-overfit lesson applied in design.
|
||||
|
||||
Corpus gold is authored **by construction** and cross-checked against the
|
||||
independent truth-table oracle with NO reader in the loop (the template's
|
||||
intended logical form is checked directly) — stronger independence than the
|
||||
v1 path, INV-25 preserved.
|
||||
|
||||
## 5. Tri-language extension contract (grc / he)
|
||||
|
||||
CORE's three foundational languages are a capability asset precisely where
|
||||
English is structurally weak, and this band is built to receive them:
|
||||
|
||||
- **What is language-neutral** (reused as-is): the opaque-atom table, the
|
||||
substitution-soundness argument (§3), the caps, the refusal discipline, the
|
||||
license machinery, the engine. None of it knows English.
|
||||
- **What is language-specific** (the thin layer a sibling band replaces): the
|
||||
structural lexicon (`if/then/or/and/not/either/therefore`, copulas,
|
||||
contractions, quantifier leads) and the clause-order conventions.
|
||||
- **Honest wrinkle, recorded so we don't build a hollow abstraction:** a pure
|
||||
lexicon swap does NOT read real Greek — ἄρα is postpositive (second
|
||||
position), and case-driven word order changes clause segmentation. A `grc_*`
|
||||
/ `he_*` band is a sibling reader module + its own earned licenses, entering
|
||||
through pack ratification (ADR-0253: draft he/grc trees are not serve
|
||||
imports). No premature parameterization is introduced now.
|
||||
- **The trigger** (per the ratified pack-expansion doctrine): when en-band
|
||||
refusal telemetry shifts from *mechanism* (`ambiguous_and_or` — English
|
||||
genuinely ambiguous) to *coverage*, that is the evidence that the
|
||||
morphologically explicit languages pay — they read cleanly exactly where
|
||||
English refuses.
|
||||
|
||||
## 6. Scope-outs (deliberate)
|
||||
|
||||
1. **`member_chain` band** (membership + quantified premises — "Socrates is a
|
||||
man. All men are mortal. Therefore Socrates is mortal."): guarded out
|
||||
(`membership_shape_out_of_band`), reserved as the next band; needs a
|
||||
per-individual propositional lowering (same pattern as
|
||||
`generate/proof_chain/categorical.py`).
|
||||
2. **Verb-phrase negation de-conjugation** ("doesn't start" → "starts"):
|
||||
refuses today (`internal_negation_unread`); English morphology work, or the
|
||||
tri-language path (§5).
|
||||
3. **Proof-step recap** in the surface: unchanged from ADR-0256 scope-out.
|
||||
4. **Flag posture unchanged:** the band rides `deduction_serving_enabled`
|
||||
(default off); no new flag.
|
||||
|
||||
## 7. Verification
|
||||
|
||||
- `uv run core test --suite deductive -q` — 83 passed (reader contract,
|
||||
surfaces, license, both lane splits, e2e through `ChatRuntime`).
|
||||
- Practice arena: 9 bands × 720 = 6,480 cases, wrong=0, every band
|
||||
reliability 0.99087 ≥ θ_SERVE=0.99 → SERVE licensed; ledger re-sealed
|
||||
(`chat/data/deduction_serve_ledger.json`, self-verifying `content_sha256`).
|
||||
- Eval lane: v1 28/28 (the two former boundary declines now decided —
|
||||
entailed), v2_en 26/26, wrong=0; report re-pinned
|
||||
(`scripts/verify_lane_shas.py::deduction_serve_v1`).
|
||||
148
docs/adr/ADR-0258-member-chain-band.md
Normal file
148
docs/adr/ADR-0258-member-chain-band.md
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
# ADR-0258 — Member-chain band (Band v3-MEM): singular membership + universal premises
|
||||
|
||||
- **Status:** Accepted — ratified by Joshua Shay via the PR #108 merge (`dd2245a7`, 2026-07-24)
|
||||
- **Date:** 2026-07-23
|
||||
- **Relates to:** ADR-0257 (Band v2-EN — this is its scope-out #1), ADR-0256
|
||||
(earned license), ADR-0201 (ROBDD keystone), ADR-0175/0199 (calibrated
|
||||
learning / arena), ADR-0253 (dual-pack serve boundary — grc/he)
|
||||
|
||||
## 1. Context
|
||||
|
||||
ADR-0257 §6.1 reserved the classic instantiation syllogism as the next band:
|
||||
|
||||
> "Socrates is a man. All men are mortal. Therefore Socrates is mortal."
|
||||
|
||||
Band v2-EN refuses it (`membership_shape_out_of_band` — an `is a|an` clause
|
||||
announces internal structure the opaque band must not flatten), Band v1b's
|
||||
categorical decider has no notion of an *individual* (its terms are classes),
|
||||
and the v1 propositional reader declines multi-word names. The eval lane pins
|
||||
the gap: `ds-en-0022` is gold-`declined` purely for want of a reading band.
|
||||
The engine underneath was never the limit.
|
||||
|
||||
## 2. Decision
|
||||
|
||||
Add a dedicated **member-argument reader + per-individual propositional
|
||||
lowering** (`generate/proof_chain/member.py`), used ONLY by the deduction
|
||||
serving path as a fallback tier strictly AFTER Bands v1 / v1b / v2-EN (so
|
||||
every previously-served argument is byte-identical; this band only widens):
|
||||
|
||||
- **Closed sentence grammar** (function-word dispatch, refusal-first):
|
||||
- Singular fact — `<NAME> is [not] [a|an] <CLASS>` (contractions expanded,
|
||||
plus the sentential `it is not the case that <singular>` prefix). `<NAME>`
|
||||
and `<CLASS>` are opaque token runs; `is` only (no tense in v1).
|
||||
- Universal — `all|every|each <C1> are|is [a|an] <C2>` (affirmative A-form)
|
||||
and `no <C1> are|is [a|an] <C2>` (negative E-form).
|
||||
- Conclusion — final sentence, sole `therefore`-led, **singular form only**.
|
||||
- **Per-individual propositional lowering:** collect the named individuals
|
||||
(subjects of singular sentences, conclusion included); mint one opaque atom
|
||||
per *(individual, class)* pair; each universal instantiates at EVERY named
|
||||
individual — `A(C1,C2)` ⇒ `mem(i,C1) -> mem(i,C2)`, `E(C1,C2)` ⇒
|
||||
`mem(i,C1) -> ~mem(i,C2)` for each `i`; singular facts are literals. The
|
||||
same verified ROBDD engine decides; deterministic templates render the
|
||||
verdict over the user's own sentences
|
||||
(`generate/proof_chain/render.py::render_entailment_member`).
|
||||
- **Number linking (the one semantic identification this band performs):**
|
||||
a universal's class word is usually plural ("all men") while the singular
|
||||
fact uses the singular ("a man"). Two *attested* class tokens are identified
|
||||
iff related by a **closed morphology table** — a curated irregular/invariant
|
||||
table (men↔man, people↔person, children↔child, …; invariants like sheep,
|
||||
species, news map only to themselves) consulted FIRST, then the three
|
||||
regular suffix rules (`+s`, `+es` after sibilants, `y↔ies`). Table priority
|
||||
kills the over-link hazards (specie/species, new/news); anything the closed
|
||||
relation cannot relate stays distinct — under-linking costs coverage
|
||||
(honest UNKNOWN), never soundness. Linking is per-token, so multi-token
|
||||
classes ("guard dog"↔"guard dogs") link on the head noun.
|
||||
- **Four new shape-bands** — `en_member_single` (exactly 1 universal),
|
||||
`en_member_chain` (≥2 universals), `en_member_negative` (any E-form),
|
||||
`en_member_atomic` (0 universals); priority negative > chain > single >
|
||||
atomic — each **earning its own SERVE license** through the ADR-0199 arena
|
||||
at n=720/band wrong=0, sealed into the same ratified ledger (unlicensed ⇒
|
||||
automatically hedged, per ADR-0256).
|
||||
|
||||
## 3. Why the lowering is sound (the load-bearing argument)
|
||||
|
||||
1. **Universal instantiation is sound.** Every first-order model of
|
||||
`∀x (C1(x) → C2(x))` satisfies its instantiation at each named individual,
|
||||
so any model of the premises is a model of the lowered premise set. An
|
||||
ENTAILED/REFUTED/inconsistent verdict over the lowered set therefore holds
|
||||
in every model of the original premises — served at full strength. Both
|
||||
verdicts survive even if two distinct names co-refer: adding equality
|
||||
facts only ADDS premises, and entailment is monotone.
|
||||
2. **For this closed fragment the lowering is also complete.** With premises
|
||||
restricted to singular literals and A/E universals (Boolean reading, no
|
||||
existential import) and a singular conclusion, a propositional countermodel
|
||||
lifts to a first-order countermodel whose domain is exactly the named
|
||||
individuals — every universal holds because every element is a named
|
||||
individual whose instantiation holds. So UNKNOWN means genuinely not
|
||||
forced *within the disclosed reading* — it is never an artifact of
|
||||
instantiating too little.
|
||||
3. **UNKNOWN is still the one guarded verdict.** Two readings could settle
|
||||
what the lowering leaves open: distinct names might co-refer, and class
|
||||
words the closed relation didn't link might be the same class. The UNKNOWN
|
||||
surface therefore scopes itself — *"Reading each name as one individual
|
||||
and each class word at face value…"* — same discipline as v2-EN's
|
||||
indivisible-clause scoping (ADR-0257 §3).
|
||||
4. **Existential forms refuse out of band.** `some/most/any/…`-led sentences
|
||||
assert anonymous witnesses that per-individual lowering cannot represent —
|
||||
`quantifier_out_of_band`, typed. Pure categorical arguments stay with
|
||||
Band v1b (tried first, unchanged).
|
||||
|
||||
Structural guards (closed refusal vocabulary, all typed): `quantifier_out_of_band`,
|
||||
`bare_plural_out_of_band` ("Dogs are loyal" — implicit genericity is a reading
|
||||
this band refuses to guess), `definite_description_out_of_band` ("is the …" is
|
||||
identity, not membership), `relative_clause_out_of_band` (`that/which/who`
|
||||
inside a name or class), `universal_conclusion_out_of_band` (categorical
|
||||
conclusions belong to v1b), `mixed_structure_out_of_band` (`if/or/either/and`
|
||||
— conditional-membership fusion is a future band), `internal_negation_unread`,
|
||||
`structural_leak`, `sentence_shape_out_of_band`, plus the v2-EN sentence
|
||||
discipline (`question_sentence`, `no_conclusion`, `multiple_conclusions`,
|
||||
`conclusion_not_last`, `no_premises`, `empty_side`, `empty`) and the honesty
|
||||
caps (`too_many_premises` at 16, `too_many_atoms` at 24 minted pairs — refuse,
|
||||
never truncate).
|
||||
|
||||
## 4. What the earned licenses certify
|
||||
|
||||
Same posture as ADR-0257 §4: the license certifies READER fidelity per shape
|
||||
(the engine cannot be wrong; the reader can hand it the wrong problem). The
|
||||
arena corpus is synthetic names × a class lexicon chosen to exercise every
|
||||
row-type of the morphology table (irregular, invariant, `+s`, `+es`, `y↔ies`);
|
||||
gold is authored by construction and cross-checked against the truth-table
|
||||
oracle over the template's INTENDED instantiated formulas — no reader in the
|
||||
loop (INV-25). The hand-authored real-English eval lane
|
||||
(`evals/deduction_serve/v2_member/`, content disjoint from the synthetic
|
||||
lexicon) keeps the structural-fidelity claim honest against natural prose,
|
||||
including the promoted `ds-en-0022` (declined → entailed: this band's designed
|
||||
acceptance case).
|
||||
|
||||
## 5. Tri-language posture
|
||||
|
||||
Unchanged from ADR-0257 §5 — with one addition: the number-linking table is
|
||||
CORE's **first morphology table**, and it is exactly the artifact class the
|
||||
tri-language doctrine says must eventually enter through pack ratification
|
||||
(ADR-0253). English needs ~30 closed rows; Greek/Hebrew declension is where a
|
||||
curated morphology pack stops being optional. When a `grc_*`/`he_*` member
|
||||
band is built, the table moves from module constants to a ratified pack; no
|
||||
premature parameterization now.
|
||||
|
||||
## 6. Scope-outs (deliberate)
|
||||
|
||||
1. **Conditional–membership fusion** ("If Socrates is a man then …") —
|
||||
refuses `mixed_structure_out_of_band`; a future band composes the v2-EN
|
||||
connective grammar with this band's sentence readings.
|
||||
2. **Existential premises/conclusions** (`some …`) — needs witness semantics;
|
||||
pure categorical `some` stays with Band v1b.
|
||||
3. **Tense** (`was/were`) and **verb predicates** ("Socrates runs") — refuse;
|
||||
verb morphology is ADR-0257 scope-out #2's territory.
|
||||
4. **Identity / definite descriptions / co-reference** — refuse
|
||||
(`definite_description_out_of_band`); an equality-aware band is future work.
|
||||
5. **Flag posture unchanged:** rides `deduction_serving_enabled` (default
|
||||
off); no new flag.
|
||||
|
||||
## 7. Verification
|
||||
|
||||
- `uv run core test --suite deductive -q` — reader contract, surfaces,
|
||||
license, all three lane splits, e2e through `ChatRuntime`.
|
||||
- Practice arena: 13 bands × 720 = 9,360 cases, wrong=0 required for every
|
||||
band; ledger re-sealed (`chat/data/deduction_serve_ledger.json`).
|
||||
- Eval lane: v1 28/28, v2_en 26/26 (ds-en-0022 promoted), v2_member new —
|
||||
wrong=0; report re-pinned (`scripts/verify_lane_shas.py`).
|
||||
163
docs/adr/ADR-0259-conditional-membership-fusion-band.md
Normal file
163
docs/adr/ADR-0259-conditional-membership-fusion-band.md
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# ADR-0259 — Conditional-membership fusion band (Band v4-CM)
|
||||
|
||||
- **Status:** Accepted — ratified by Joshua Shay via the PR #109 merge (`5224b5e0`, 2026-07-24)
|
||||
- **Date:** 2026-07-23
|
||||
- **Relates to:** ADR-0258 (Band v3-MEM — this is its scope-out #1), ADR-0257
|
||||
(Band v2-EN — the connective grammar this band reuses), ADR-0256 (earned
|
||||
license), ADR-0201 (ROBDD keystone), ADR-0175/0199 (calibrated learning /
|
||||
arena)
|
||||
|
||||
## 1. Context
|
||||
|
||||
ADR-0258 §6.1 reserved conditional–membership fusion as a future band:
|
||||
|
||||
> "If Socrates is a man then Socrates is mortal. Socrates is a man.
|
||||
> Therefore Socrates is mortal."
|
||||
|
||||
Band v3-MEM refuses this — `mixed_structure_out_of_band` — because its
|
||||
per-sentence grammar treats a connective token (`if`/`then`/`or`/`and`/
|
||||
`either`) anywhere in a sentence as announcing structure that band does not
|
||||
compose. Band v2-EN refuses the same text for the complementary reason:
|
||||
its opaque-atom minter explicitly rejects `is [a|an]` clauses
|
||||
(`membership_shape_out_of_band`) rather than flattening membership
|
||||
structure it cannot read. Each band is individually sound and, by design,
|
||||
refuses exactly what the other reads — the gap is the composition, not
|
||||
either reader's engine.
|
||||
|
||||
## 2. Decision
|
||||
|
||||
Add `generate/proof_chain/cond_member.py` — a third reader, tried strictly
|
||||
AFTER Bands v1 / v1b / v2-EN / v3-MEM (a fallback tier; every previously
|
||||
served argument stays byte-identical) — that composes v2-EN's connective
|
||||
grammar (`if/then`, `or`, `and`, `either` sugar, top-level `and`-splitting)
|
||||
with v3-MEM's singular membership sentence reading, over the SAME
|
||||
per-individual atom space:
|
||||
|
||||
- **Sentence dispatch** (closed, ordered): a bare top-level `X and Y`
|
||||
premise (no `if`/`or` anywhere) splits into independent singular
|
||||
records, exactly as v2-EN does — semantically conjoining, keeping the
|
||||
connective inventory the shape-bands classify over uncluttered. Any
|
||||
OTHER sentence containing a connective token is parsed by the ported
|
||||
junction/conditional grammar below. A universal-lead (`all|every|each|
|
||||
no`) sentence with NO connective token is a bare universal, read
|
||||
UNCHANGED by v3-MEM's own `_parse_universal`. Everything else is a bare
|
||||
singular fact, read UNCHANGED by v3-MEM's own `_parse_singular` (which
|
||||
already normalizes every negation form this band needs — leading `not`,
|
||||
the sentential `it is not the case that` prefix, and copular `is not`).
|
||||
Checking for a connective token BEFORE the universal-lead dispatch (not
|
||||
after) is load-bearing: it is what stops a connective token from ever
|
||||
silently leaking into an opaque name/class run.
|
||||
- **Connective grammar over membership leaves:** `if <A> then <B>` and
|
||||
`<A> or <B>` / `<A> and <B>` (with `either` as sugar, mixed `and`+`or` at
|
||||
one level refused as `ambiguous_and_or`, a conditional nested inside
|
||||
another refused as `nested_conditional` — identical restrictions to
|
||||
v2-EN, ported verbatim) build a small formula tree (`_Lit` / `_And` /
|
||||
`_Or` / `_Implies`) whose LEAVES are v3-MEM singular facts, not opaque
|
||||
atoms. Because `_parse_singular` is negation-aware on its own, this
|
||||
band's grammar needs no separate negation layer — v2-EN needs one only
|
||||
because ITS leaf-miller (`_AtomTable.mint`) is negation-blind.
|
||||
- **A conclusion stays a single singular fact** (optionally negated),
|
||||
matching v3-MEM's existing discipline exactly: a universal or
|
||||
connective-shaped conclusion refuses
|
||||
(`universal_conclusion_out_of_band` / new `compound_conclusion_out_of_band`)
|
||||
rather than attempting a compound render.
|
||||
- **One shared per-individual atom space:** every singular fact — whether
|
||||
a bare sentence, an and-split part, or a leaf nested inside a connective
|
||||
tree — contributes to the SAME two-pass lowering v3-MEM already performs
|
||||
(collect named individuals and closed-morphology class-groups first,
|
||||
THEN mint one opaque atom per attested `(individual, class)` pair, THEN
|
||||
instantiate every bare universal at every named individual). A bare
|
||||
universal may now instantiate at an individual named ONLY inside a
|
||||
connective's leaf, and a connective's leaf atom may now UNIFY with an
|
||||
atom a bare universal's instantiation also produced — this atom-sharing
|
||||
across the two mechanisms is the fusion the band is named for, and nothing
|
||||
about it is a new soundness claim: it is the same per-individual
|
||||
instantiation ADR-0258 §3 already proved sound, applied to a strictly
|
||||
larger set of premise formulas.
|
||||
- **Four new shape-bands**, priority order first-match: `en_condmem_fused`
|
||||
(a bare universal coexists with a connective sentence — the mechanism
|
||||
above genuinely fires), `en_condmem_disjunctive` (an `or` appears
|
||||
anywhere in any connective sentence), `en_condmem_chain` (two or more
|
||||
connective sentences, no universal, no `or`), `en_condmem_conditional`
|
||||
(the base case — exactly one `if/then`, no universal, no `or`). A
|
||||
successfully-decided argument in this band always contains at least one
|
||||
connective sentence — every other reason v3-MEM would refuse also makes
|
||||
THIS reader refuse identically (it reuses v3-MEM's own
|
||||
`_parse_universal`/`_parse_singular`, guards included), so there is no
|
||||
connective-free case this band could newly decide; no fifth "atomic"
|
||||
band is needed.
|
||||
|
||||
## 3. Why the composition is sound (the load-bearing argument)
|
||||
|
||||
1. **Nothing here is a new inference rule.** Every connective (`&`, `|`,
|
||||
`->`) is standard propositional structure the ROBDD engine already
|
||||
decides; every leaf is a v3-MEM singular-membership atom, minted and
|
||||
linked by the IDENTICAL closed morphology relation ADR-0258 §3 already
|
||||
proved sound and (for this fragment) complete. Composing them is
|
||||
substitution of one sound reading's atoms into another sound reading's
|
||||
connective positions — the propositional engine's soundness does not
|
||||
care which reader supplied which formula string.
|
||||
2. **Universal instantiation stays sound when the instantiated individual
|
||||
is named only inside a connective.** ADR-0258 §3's argument — every
|
||||
first-order model of `∀x(C1(x)→C2(x))` satisfies the instantiation at
|
||||
every domain element, so at every NAMED individual in particular — does
|
||||
not depend on WHERE in the argument that individual's name appears. An
|
||||
individual introduced solely as the subject of an `if`-clause leaf is
|
||||
still a named individual; instantiating a bare universal at it is still
|
||||
a sound consequence of the universal, exactly as if it had appeared in
|
||||
its own bare sentence.
|
||||
3. **Completeness is preserved for the same closed fragment.** The
|
||||
fragment is unchanged from ADR-0258 (singular literals + A/E
|
||||
universals, Boolean reading, no existential import) with one syntactic
|
||||
widening: singular literals may now be combined by `&`/`|`/`->` before
|
||||
appearing as a premise or antecedent/consequent. A propositional
|
||||
countermodel still lifts to a first-order countermodel over exactly
|
||||
the named individuals, by the identical argument — connectives among
|
||||
already-Boolean atoms do not change what "every element is a named
|
||||
individual" needs to guarantee. UNKNOWN therefore still means genuinely
|
||||
not forced within the disclosed reading, and the surface still scopes
|
||||
itself exactly as v3-MEM's does (reused verbatim — see §4).
|
||||
4. **The refusal vocabulary stays a strict superset of v3-MEM's, plus
|
||||
v2-EN's connective-shape guards.** `nested_conditional` and
|
||||
`ambiguous_and_or` are v2-EN's own restrictions, ported unchanged
|
||||
(English `and`/`or` scope genuinely is ambiguous when mixed; a
|
||||
conditional nested inside another is refused rather than guessed at,
|
||||
in both bands identically). `compound_conclusion_out_of_band` is new
|
||||
and narrow: it exists only to keep the conclusion a single renderable
|
||||
fact, not to hide any inference.
|
||||
|
||||
## 4. What the earned licenses certify; rendering
|
||||
|
||||
Same posture as ADR-0258 §4: the license certifies reader fidelity, not
|
||||
engine soundness. `render_entailment_member` (ADR-0258) is reused
|
||||
UNCHANGED for this band's surface — its UNKNOWN scoping ("reading each
|
||||
name as one individual and each class word at face value…") is exactly as
|
||||
accurate here, since every clause in this band's arguments is read all the
|
||||
way down to individual+class; connectives introduce no additional hidden
|
||||
structure the surface would need to disclose. No new render function is
|
||||
added.
|
||||
|
||||
## 5. Scope-outs (deliberate)
|
||||
|
||||
1. **A universal clause cannot be nested inside a connective** — only a
|
||||
bare top-level universal sentence instantiates; `if all men are
|
||||
mortal then …` refuses (typed, via the same connective-shape-first
|
||||
dispatch that protects bare universals from silent corruption).
|
||||
Composing a QUANTIFIED antecedent/consequent is a larger, separate
|
||||
question (it would need bound-variable tracking across clauses, not
|
||||
just shared individuals) and is left for a future band if a real case
|
||||
ever needs it.
|
||||
2. **Existential premises, tense, verb predicates, identity/definite
|
||||
descriptions** — unchanged scope-outs, inherited from ADR-0258 §6
|
||||
via the reused `_parse_singular`/`_parse_universal`.
|
||||
3. **Flag posture unchanged:** rides `deduction_serving_enabled` (default
|
||||
off); no new flag.
|
||||
|
||||
## 6. Verification
|
||||
|
||||
- `uv run core test --suite deductive -q` — reader contract, surfaces,
|
||||
license, all four lane splits, e2e through `ChatRuntime`.
|
||||
- Practice arena: 17 bands × 720 = 12,240 cases, wrong=0 required for every
|
||||
band; ledger re-sealed (`chat/data/deduction_serve_ledger.json`).
|
||||
- Eval lane: v1 28/28, v2_en 26/26, v2_member 26/26, v2_condmem new —
|
||||
wrong=0; report re-pinned (`scripts/verify_lane_shas.py`).
|
||||
131
docs/adr/ADR-0260-verb-predicate-band.md
Normal file
131
docs/adr/ADR-0260-verb-predicate-band.md
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
# ADR-0260 — Band v5-VP: verb-predicate arguments, decided
|
||||
|
||||
- **Status:** Accepted — ratified by Joshua Shay via the PR #111 merge (`2a82c8a3`, 2026-07-24)
|
||||
- **Date:** 2026-07-24
|
||||
- **Arc:** deduction-serve (ADR-0256 → 0257 → 0258 → 0259 → this)
|
||||
- **Governs:** `generate/proof_chain/verb.py`, the `en_verb_*` shape-bands,
|
||||
their arena templates, the `v2_verb` lane split, and the
|
||||
`_verb_band_surface` serving tier.
|
||||
|
||||
## 1. Context
|
||||
|
||||
ADR-0258 §6.3 scoped verb predicates out of the member band: v3-MEM reads
|
||||
only copula sentences, so "All philosophers teach. Socrates is a
|
||||
philosopher. Therefore Socrates teaches." refused — despite being the plain
|
||||
English shape in which most non-logic subject matter states its rules
|
||||
(physics, biology, and every other domain speak in verb sentences, not is-a
|
||||
chains). The generalization arc (docs/plans/generalization-arc-2026-07-24.md
|
||||
Phase 1.1) makes this the single highest-leverage reading unlock: Phase 2's
|
||||
curriculum-entailment serving is gated on it.
|
||||
|
||||
The earlier bands each guard this territory out rather than misreading it:
|
||||
v2-EN refuses quantifier-led sentences (`categorical_shape_out_of_band`),
|
||||
is-a clauses (`membership_shape_out_of_band`), and unnormalized negation
|
||||
(`internal_negation_unread`); v3-MEM/v4-CM refuse copula-free sentences
|
||||
(`sentence_shape_out_of_band`). Pure verb-fact restatements with none of
|
||||
those triggers are ALREADY decided soundly by v2-EN as opaque clause-atoms —
|
||||
so this band's genuine territory is exactly the fall-through: arguments
|
||||
mixing verb sentences with quantified rules, membership facts, or `does not`
|
||||
negation.
|
||||
|
||||
## 2. Decision
|
||||
|
||||
Add `generate.proof_chain.verb.read_verb_argument` as the next fallback tier
|
||||
(after v1 / v1b / v2-EN / v3-MEM / v4-CM), extending v3-MEM's
|
||||
per-individual propositional lowering with a SECOND atom family in one
|
||||
shared space:
|
||||
|
||||
- **Membership atoms** *(individual, class-group)* — minted by v3-MEM's own
|
||||
`_parse_singular` / `_parse_universal`, reused verbatim (copula sentences
|
||||
keep their exact v3-MEM reading, refusals included).
|
||||
- **Verb atoms** *(individual, verb-lemma-group, object-term)* — minted from
|
||||
verb facts (`<name> <verb3sg> [<object>]`, `does not` negation, the
|
||||
sentential-not prefix) and verb universals
|
||||
(`all|every|each|no [the] <class> <verb> [<object>]`).
|
||||
|
||||
A verb universal instantiates at every named individual as
|
||||
`mem(i, C) -> [~]verb(i)` — so a membership fact minted by a copula sentence
|
||||
discharges a verb rule. The same verified ROBDD engine decides.
|
||||
|
||||
Bands (priority order): `en_verb_negative` (any negative universal, either
|
||||
kind) > `en_verb_chain` (≥2 universals) > `en_verb_universal` (exactly 1) >
|
||||
`en_verb_fact` (facts only). Rendered by `render_entailment_verb` — the
|
||||
member surface with the UNKNOWN scoping extended to the verb reading. Rides
|
||||
`deduction_serving_enabled` (default off); no new flag.
|
||||
|
||||
## 3. Why this is sound
|
||||
|
||||
1. **Instantiation** — identical to ADR-0258 §3: universal instantiation at
|
||||
named individuals is truth-preserving in every first-order model, and
|
||||
each *(verb-group, object-term)* pair is one opaque unary predicate over
|
||||
the same named-individual domain. ENTAILED / REFUTED / inconsistent
|
||||
verdicts over the lowered premises hold in every model of the original
|
||||
argument; completeness within the fragment lifts the same way (a
|
||||
propositional countermodel's domain is exactly the named individuals).
|
||||
2. **Agreement is the ONE new identification** — a universal's base form
|
||||
("teach") and a singular fact's 3sg form ("teaches") are identified iff
|
||||
related by a CLOSED relation: a verb-specific irregular table (has/have,
|
||||
goes/go, does/do; consulted first, sole authority for its tokens) else
|
||||
the three regular suffix rules (+s, +es after sibilants, y↔ies). The noun
|
||||
table is deliberately NOT reused — it would misroute verb forms like
|
||||
"lives" (noun: life; verb: live). Under-linking costs coverage, never
|
||||
soundness. Object terms link by exact token identity only.
|
||||
3. **Scope-tight segmentation** — without a lexicon, an arbitrary
|
||||
copula-free token run cannot be soundly split into subject/verb/object;
|
||||
the band reads single-token name/class/verb/object shapes ONLY, and
|
||||
refuses everything longer, typed. A three-token sentence read as
|
||||
(subject, verb, object) is a disclosed reading, restated verbatim in the
|
||||
surface's "Given:" line.
|
||||
4. **Cascade honesty** — every text this band decides was verified to be
|
||||
REFUSED by all four earlier bands (the arena templates and lane cases
|
||||
are constructed on the fall-through path serving actually uses), so this
|
||||
tier is pure widening: every previously-served argument is served
|
||||
byte-identically.
|
||||
|
||||
## 4. License and render
|
||||
|
||||
The four `en_verb_*` bands earned SERVE through the ADR-0199 arena
|
||||
(720/band committed, wrong=0, θ_SERVE=0.99; ledger resealed at 21 bands).
|
||||
The arena's by-construction gold is cross-checked template-by-template
|
||||
against the independent truth-table oracle over each template's INTENDED
|
||||
per-individual lowering, with no reader in the loop (INV-25). Unearned or
|
||||
ledger-stripped deployments serve the same sound answer hedged
|
||||
(`_UNVERIFIED_SHAPE_DISCLOSURE`), exactly as every earlier band.
|
||||
|
||||
## 5. Scope-outs (typed refusals today, future bands' territory)
|
||||
|
||||
1. **Connective × verb composition** — "If Socrates teaches then …"
|
||||
refuses `mixed_structure_out_of_band`; fusing v4-CM's connective grammar
|
||||
over verb leaves is the natural next composition.
|
||||
2. **Multi-token subjects/objects, PPs, adverbs, ditransitives** —
|
||||
`sentence_shape_out_of_band`. Note one disclosed misreading inside the
|
||||
3-token shape: an adverb in object position ("Lena runs quickly") is
|
||||
read as an object TERM — never unsound (worst case an over-scoped
|
||||
UNKNOWN), documented here rather than guessed at.
|
||||
3. **Tense** — `did not` refuses `tense_out_of_band`; bare past forms
|
||||
("taught") are indistinguishable from lemmas without a lexicon and read
|
||||
as distinct opaque lemmas (under-linking, sound).
|
||||
4. **Arity/subsumption** — "teaches" does not entail "teaches logic" (nor
|
||||
vice versa) under the face-value reading; both directions stay UNKNOWN,
|
||||
scoped. Same posture v2-EN already takes for opaque clauses.
|
||||
5. **Plural/bare subjects in verb facts** ("dogs bark") — read as an
|
||||
individual named "dogs" (disclosed in the UNKNOWN scoping); the copula
|
||||
analogue is refused by v3-MEM's bare-plural guard, but no copula means no
|
||||
plural signal in the closed grammar.
|
||||
6. Existential (`some`) witnesses, identity/co-reference — unchanged
|
||||
scope-outs inherited from ADR-0258 §6 (existentials are the
|
||||
generalization arc's Phase 1.2, assigned to the Opus tier).
|
||||
|
||||
## 6. Verification
|
||||
|
||||
- `tests/test_verb_argument_reader.py` — 34 tests: gap-is-real cross-check,
|
||||
flagship lowering, every agreement rule, object-in-atom-key, chain
|
||||
discharge, cross-individual non-leakage, family separation
|
||||
(teacher/teaches), 14 typed refusals incl. delegated v3-MEM reasons,
|
||||
honesty caps, determinism.
|
||||
- Surface/e2e/lane: `tests/test_deduction_surface.py` (+5),
|
||||
`tests/test_deduction_serve_e2e.py` (+1),
|
||||
`tests/test_deduction_serve_lane.py` (+1 over the 28-case `v2_verb`
|
||||
split) — full lane 134/134, wrong=0, all five splits.
|
||||
- Arena: 4 × 720 committed, wrong=0, first seal run; corpus soundness
|
||||
asserted against the independent oracle before sealing (15,120 cases).
|
||||
195
docs/adr/ADR-0261-existential-witness-band.md
Normal file
195
docs/adr/ADR-0261-existential-witness-band.md
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
# ADR-0261 — Band v6-EX: existential arguments, decided
|
||||
|
||||
- **Status:** Accepted — ratified by Joshua Shay via the `feat/existential-band` merge (`a8488e9c`, 2026-07-24)
|
||||
- **Date:** 2026-07-24
|
||||
- **Arc:** deduction-serve (ADR-0256 → 0257 → 0258 → 0259 → 0260 → this)
|
||||
- **Governs:** `generate/proof_chain/exist.py`, the `en_exist_*` shape-bands,
|
||||
their arena templates, the `v2_exist` lane split, the `_exist_band_surface`
|
||||
serving tier, and the refuse-don't-drop contract of
|
||||
`generate.meaning_graph.projectors.to_syllogism`.
|
||||
|
||||
## 1. Context
|
||||
|
||||
The band cascade reads `all` and `no`; `some` refuses
|
||||
(`quantifier_out_of_band`) in every fallback tier — ADR-0258 §6 listed
|
||||
existentials as a deliberate scope-out, and the generalization arc
|
||||
(docs/plans/generalization-arc-2026-07-24.md Phase 1.2) schedules them as the
|
||||
move that completes the square of opposition.
|
||||
|
||||
Band v1b already decides categorical some-syllogisms — but only inside the
|
||||
shared reader's morphology lexicon, and only for arguments that are purely
|
||||
categorical. Real subject matter is neither: it mixes singular facts, verb
|
||||
predicates, and vocabulary the lexicon has never seen. Everything outside that
|
||||
narrow window refused.
|
||||
|
||||
Existentials are also the first construct in this arc whose lowering is not a
|
||||
straightforward instantiation. A universal instantiates at things the argument
|
||||
already names; an existential asserts a thing the argument does NOT name, and
|
||||
an existential conclusion asks about things that may not be named at all. Get
|
||||
that wrong in the obvious way — lower `some` over just the named individuals —
|
||||
and the reader will report REFUTED for arguments that are merely unproven,
|
||||
which is precisely the wrong-answer class this arc exists to prevent.
|
||||
|
||||
## 2. Decision
|
||||
|
||||
Add `generate.proof_chain.exist.read_exist_argument` as the LAST fallback tier
|
||||
(after v1 / v1b / v2-EN / v3-MEM / v4-CM / v5-VP), extending v5-VP's
|
||||
per-individual lowering — both atom families unchanged — with two new kinds of
|
||||
domain element:
|
||||
|
||||
- **A witness per existential PREMISE** (a Skolem constant nothing else
|
||||
names): "some C1 are C2" ⇒ `mem(w,C1) & mem(w,C2)`; the O-form negates the
|
||||
second conjunct; the verb forms mint a verb atom instead.
|
||||
- **One arbitrary element per existential CONCLUSION**: a domain element about
|
||||
which no premise asserts anything and at which every universal is still
|
||||
instantiated.
|
||||
|
||||
An existential conclusion lowers to the disjunction of its conjunction over
|
||||
the WHOLE domain — named individuals, witnesses, and the arbitrary element.
|
||||
|
||||
New sentence shapes, all `some`-led:
|
||||
`some <C> are|is [not] [a|an] <C2>` (I-form and O-form) and
|
||||
`some <C> [do not] <verb> [<object>]`. Every other shape delegates verbatim to
|
||||
v3-MEM's and v5-VP's parsers, refusals included.
|
||||
|
||||
Bands (priority order): `en_exist_negative` (any negative universal) >
|
||||
`en_exist_chain` (≥2 universals) > `en_exist_universal` (exactly 1) >
|
||||
`en_exist_witness` (no universals). Rendered by `render_entailment_exist` —
|
||||
the verb surface with the no-existential-import reading disclosed in the
|
||||
UNKNOWN template. Rides `deduction_serving_enabled` (default off); no new flag.
|
||||
|
||||
**Second decision, forced by the first (§5.1):** `to_syllogism` now REFUSES
|
||||
when the comprehension carries a relation it cannot express, instead of
|
||||
filtering that relation out and projecting the remainder.
|
||||
|
||||
## 3. Why this is sound
|
||||
|
||||
1. **ENTAILED.** Let *M* be any first-order model of the premises. Each
|
||||
witness is interpretable in *M* (its existential premise asserts a
|
||||
satisfier); universal instantiation is truth-preserving at every element;
|
||||
the arbitrary element may be interpreted as ANY element, domains being
|
||||
non-empty. So every lowered premise holds in *M*. If the lowered premises
|
||||
propositionally entail the query disjunction, some element of *M*
|
||||
satisfies the conjunction — which is ∃. Skolem constants occur nowhere in
|
||||
the original argument, so this is entailment-preserving Skolemization, not
|
||||
merely satisfiability-preserving.
|
||||
2. **REFUTED — the arbitrary element is load-bearing.** Refuting ∃x φ(x)
|
||||
means deriving ∀x ¬φ(x). The lowered conjunction dual is entailed only if
|
||||
it is entailed at the arbitrary element, and that element is arbitrary (no
|
||||
premise mentions it), so what holds of it holds of every element — the
|
||||
universal is genuinely derived, never assumed by domain closure. Without
|
||||
it, "Rex is a wolf. Rex is not tame. Therefore some wolves are tame."
|
||||
would come back REFUTED — asserting that no wolf anywhere is tame — from a
|
||||
domain of one. With it, the honest UNKNOWN. This is the single most
|
||||
important line of this ADR and it is pinned by a named test.
|
||||
3. **Completeness within the fragment** lifts exactly as v3-MEM/v5-VP: a
|
||||
propositional countermodel becomes a first-order countermodel whose domain
|
||||
is the named individuals, the witnesses, and the arbitrary element.
|
||||
4. **No existential import.** Universals are read as vacuously true over an
|
||||
empty class, so the subaltern moods (Darapti, Felapton, Bamalip) are
|
||||
UNKNOWN, and the two contradictory pairs of the square are decided: an
|
||||
A-form REFUTES the corresponding O-form, an E-form REFUTES the I-form.
|
||||
This matches the modern first-order reading, matches what the categorical
|
||||
band (v1b) already does, and is the conservative choice — it declines to
|
||||
claim a member exists that no premise asserted. It is disclosed to the user
|
||||
in the UNKNOWN surface rather than left as a silent convention.
|
||||
5. **The plural `do not` is read here and refused in v5-VP** — deliberately,
|
||||
not inconsistently. "All C do not V" is genuinely scope-ambiguous (¬∀ vs
|
||||
∀¬) and refuses; "some C do not V" is unambiguously ∃x(C(x) ∧ ¬V(x)).
|
||||
6. **Cascade honesty** — every arena template and lane case was verified to
|
||||
fall through all six earlier bands, so this tier is pure widening: every
|
||||
previously-served argument is served byte-identically. The one previously
|
||||
DECLINED case it now decides (`ds-mem-0020`) is promoted to its correct
|
||||
verdict, not a favorable one.
|
||||
|
||||
## 4. License and render
|
||||
|
||||
The four `en_exist_*` bands earned SERVE through the ADR-0199 arena
|
||||
(720/band committed, wrong=0, θ_SERVE=0.99; ledger resealed at 25 bands).
|
||||
The by-construction gold is cross-checked template-by-template against the
|
||||
independent truth-table oracle over each template's INTENDED lowering — every
|
||||
universal's per-element implications and every query disjunct spelled out — so
|
||||
a lowering that drops or invents a domain element shows up as a disagreement
|
||||
rather than passing silently (INV-25). Unearned or ledger-stripped deployments
|
||||
serve the same sound answer hedged (`_UNVERIFIED_SHAPE_DISCLOSURE`).
|
||||
|
||||
## 5. Findings and scope-outs
|
||||
|
||||
### 5.1 A wrong-answer path found in Band v1b (fixed here)
|
||||
|
||||
Authoring this band's lane surfaced a defect in `to_syllogism` that predates
|
||||
it. The projector built its premise list by FILTERING the meaning graph's
|
||||
relations down to the categorical ones — silently discarding anything else,
|
||||
then answering from what survived. Two consequences, both wrong served
|
||||
answers rather than declines:
|
||||
|
||||
- "Aristotle is a philosopher. All philosophers are scholars. Therefore some
|
||||
scholars are philosophers." — the singular `member` premise was dropped, the
|
||||
argument lost its only witness, and serving replied *"That doesn't follow."*
|
||||
It does follow.
|
||||
- "Every mineral is a solid. Some quartzes are minerals. Therefore some
|
||||
quartzes are solids." — the shared reader misreads "every mineral" as an
|
||||
individual named `every_mineral` (a separate reader gap, §5.2), the
|
||||
resulting `member` relation was dropped, and a valid Darii was served as
|
||||
*"That doesn't follow."*
|
||||
|
||||
The arena never caught it because the CATEGORICAL band's templates are all
|
||||
purely categorical two-premise syllogisms in the synthetic lexicon — a family
|
||||
in which nothing is ever dropped. A band can hold an earned license and still
|
||||
have an unmeasured wrong-answer path if its practice corpus does not span its
|
||||
reachable inputs; that lesson is the durable part of this finding.
|
||||
|
||||
The fix makes `to_syllogism` behave exactly like its propositional sibling
|
||||
`to_deductive_logic`, which has always refused on a relation it cannot express:
|
||||
**refuse, don't drop.** Refused texts fall through to the reader bands that CAN
|
||||
hold a singular fact — v6-EX decides both examples correctly. The categorical
|
||||
band re-earned SERVE at 720/720 after the change (no coverage lost), and both
|
||||
examples are now pinned as regression cases (`ds-ex-0008`, `ds-ex-0011`, plus
|
||||
a projector unit test and an e2e test).
|
||||
|
||||
### 5.2 Scope-outs (typed refusals today)
|
||||
|
||||
1. **"Every C is a D" misread by the shared reader** — `comprehend` reads the
|
||||
quantifier phrase as an individual (`every_mineral`). Post-§5.1 this is
|
||||
harmless (v1b refuses; v6-EX reads the sentence correctly through v3-MEM's
|
||||
universal parser), so it is recorded, not patched: fixing the shared
|
||||
reader's template set is its own unit of work with its own lane.
|
||||
2. **Partitives** — "some of the sailors" refuses `partitive_out_of_band`
|
||||
rather than reading "of the sailors" as an opaque class run.
|
||||
3. **Multi-token subjects in verb existentials, PPs, ditransitives** —
|
||||
`sentence_shape_out_of_band`, inherited from v5-VP's closed grammar.
|
||||
4. **Tense** — `some C were P` / `some C did not V` refuse
|
||||
`tense_out_of_band`.
|
||||
5. **Connective × existential composition** — "If some C are P then …"
|
||||
refuses `mixed_structure_out_of_band`; fusing v4-CM's connective grammar
|
||||
over existential leaves is a later composition.
|
||||
6. **Numeric and proportional quantifiers** (`most`, `few`, `several`, `at
|
||||
least two`) — `quantifier_out_of_band`. `some` is the only existential
|
||||
spelling read; "there is a C that …" is not read either.
|
||||
7. **Universal conclusions** — unchanged from v3-MEM: proving ∀ from these
|
||||
premises needs the eigenvariable machinery this band uses only for the
|
||||
refutation direction, and exposing it as a conclusion form is a separate,
|
||||
testable unit.
|
||||
8. **Identity / co-reference, existential-import premises** — unchanged
|
||||
scope-outs; a user who wants the traditional square can state the
|
||||
existential premise explicitly and this band will use it.
|
||||
|
||||
## 6. Verification
|
||||
|
||||
- `tests/test_exist_argument_reader.py` — 33 tests: gap-is-real cross-check,
|
||||
flagship Darii lowering (exact formulas and atoms), the arbitrary-element
|
||||
soundness test, no-existential-import, both contradictory pairs, witness
|
||||
non-transfer, one-witness-per-premise, verb existentials with plural
|
||||
`do not`, number linking inside existentials, 4 band classifications, 13
|
||||
typed refusals incl. delegated v3-MEM/v5-VP reasons, honesty caps,
|
||||
determinism.
|
||||
- `tests/test_meaning_graph_projectors.py` (+1) — the §5.1 refuse-don't-drop
|
||||
contract.
|
||||
- Surface/e2e/lane: `tests/test_deduction_surface.py` (+5),
|
||||
`tests/test_deduction_serve_e2e.py` (+2, one of them the §5.1 regression),
|
||||
`tests/test_deduction_serve_lane.py` (+1 over the 32-case `v2_exist` split;
|
||||
`v2_member` docstring updated for the `ds-mem-0020` promotion) — full lane
|
||||
166/166, wrong=0, all six splits.
|
||||
- Arena: 4 × 720 committed, wrong=0, first seal run; all 25 bands still SERVE;
|
||||
corpus soundness asserted against the independent oracle before sealing
|
||||
(18,000 cases).
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue