From ef95d3e60968af0a858eed68453f78edea9c1673 Mon Sep 17 00:00:00 2001 From: Shay Date: Sat, 16 May 2026 20:20:35 -0700 Subject: [PATCH] feat(adr-0021): epistemic_status surface wired across teaching + trace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-0021 v1 schema land. epistemic_status is a position in the revision graph, not a source-trust tier — coherence is the only admission signal. Surfaces: - teaching/epistemic.py: EpistemicStatus enum (COHERENT, CONTESTED, SPECULATIVE, FALSIFIED); ADMISSIBLE_AS_EVIDENCE = {COHERENT}. - PackMutationProposal.epistemic_status (default SPECULATIVE) + immutable with_status() updater. - ReviewedTeachingExample.epistemic_status (default SPECULATIVE); orthogonal to acceptance per ADR §Schema impact. - LexicalEntry.epistemic_status (default "coherent" for seed; absent in JSONL is treated as the seed default — no retroactive tagging). - compute_trace_hash + trace_hash_from_result + pipeline.py fold the load-bearing proposal's epistemic_status into the trace hash so replay detects different epistemic frames. Non-hardening invariant (ADR-0021 §2): tests/test_epistemic_invariants.py asserts no final/frozen/axiom/permanent flag on PackMutationProposal or ReviewedTeachingExample, and EpistemicStatus contains no source-trust tier names. Docs: docs/runtime_contracts.md gains an Epistemic surface section. Lanes green: smoke 27/27, teaching 10/10, packs 6/6, runtime 19/19, cognition eval 100%. --- core/cognition/pipeline.py | 2 + core/cognition/trace.py | 14 +++ docs/runtime_contracts.md | 49 ++++++++++ language_packs/compiler.py | 1 + language_packs/schema.py | 11 ++- teaching/__init__.py | 10 +- teaching/epistemic.py | 56 +++++++++++ teaching/review.py | 15 +++ teaching/store.py | 22 +++++ tests/test_epistemic_invariants.py | 144 +++++++++++++++++++++++++++++ 10 files changed, 320 insertions(+), 4 deletions(-) create mode 100644 teaching/epistemic.py create mode 100644 tests/test_epistemic_invariants.py diff --git a/core/cognition/pipeline.py b/core/cognition/pipeline.py index 1b9fb34e..c42fc048 100644 --- a/core/cognition/pipeline.py +++ b/core/cognition/pipeline.py @@ -117,6 +117,7 @@ class CognitiveTurnPipeline: # typed-operator invocation per ADR-0018). review_hash = reviewed_example.review_hash if reviewed_example is not None else "" proposal_id = proposal.proposal_id if proposal is not None else "" + epistemic_status = proposal.epistemic_status.value if proposal is not None else "" operator_invocation = self._serialize_walk(walk_result) trace_hash = compute_trace_hash( input_text=text, @@ -130,6 +131,7 @@ class CognitiveTurnPipeline: intent_tag=intent.tag.value, teaching_review_hash=review_hash, teaching_proposal_id=proposal_id, + teaching_epistemic_status=epistemic_status, operator_invocation=operator_invocation, ) diff --git a/core/cognition/trace.py b/core/cognition/trace.py index 32dd2af5..78667c66 100644 --- a/core/cognition/trace.py +++ b/core/cognition/trace.py @@ -36,6 +36,7 @@ def compute_trace_hash( intent_tag: str = "unknown", teaching_review_hash: str = "", teaching_proposal_id: str = "", + teaching_epistemic_status: str = "", operator_invocation: str = "", ) -> str: """Return a deterministic SHA-256 hex digest over the turn's key outputs. @@ -48,6 +49,12 @@ def compute_trace_hash( string when no operator ran. Folding it explicitly makes operator invocation a load-bearing part of replay equality, not just an indirect consequence of surface-change. + + ``teaching_epistemic_status`` is the serialised EpistemicStatus of the + pack mutation proposal load-bearing in this turn — empty string when + no proposal was emitted. Folded per ADR-0021 §Consequences so replay + detects when a downstream surface was produced under a different + epistemic frame than at the time of recall. """ payload = { "input_text": input_text, @@ -61,6 +68,7 @@ def compute_trace_hash( "intent_tag": intent_tag, "teaching_review_hash": teaching_review_hash, "teaching_proposal_id": teaching_proposal_id, + "teaching_epistemic_status": teaching_epistemic_status, "operator_invocation": operator_invocation, } serialized = json.dumps(payload, sort_keys=True, ensure_ascii=False) @@ -80,6 +88,11 @@ def trace_hash_from_result(result: "CognitiveTurnResult") -> str: if result.pack_mutation_proposal is not None else "" ) + epistemic_status = ( + result.pack_mutation_proposal.epistemic_status.value + if result.pack_mutation_proposal is not None + else "" + ) return compute_trace_hash( input_text=result.input_text, filtered_tokens=result.filtered_tokens, @@ -92,5 +105,6 @@ def trace_hash_from_result(result: "CognitiveTurnResult") -> str: intent_tag=intent_tag, teaching_review_hash=review_hash, teaching_proposal_id=proposal_id, + teaching_epistemic_status=epistemic_status, operator_invocation=result.operator_invocation, ) diff --git a/docs/runtime_contracts.md b/docs/runtime_contracts.md index d25053e1..35bbc32c 100644 --- a/docs/runtime_contracts.md +++ b/docs/runtime_contracts.md @@ -106,6 +106,55 @@ Tests should protect load-bearing behavior: Avoid tests that preserve stale constructors, private helper shapes, or exact formatting that is not part of a documented contract. +## Epistemic surface (ADR-0021) + +CORE exposes a typed `epistemic_status` on the teaching and lexicon +surfaces. The status is a **position in the revision graph**, not a +source-trust tier: + +| Status | Meaning | +|---------------|------------------------------------------------------------------------------------------| +| `COHERENT` | Fits current field geometry; no incoherence with reviewed claims detected at admission. | +| `CONTESTED` | Incoherent with at least one reviewed claim; review pending; not load-bearing. | +| `SPECULATIVE` | Proposed; not yet reviewed for coherence; admissible only as a candidate. | +| `FALSIFIED` | Incoherent under accumulated evidence; eligible for Stage-3 inversion; retained. | + +### Non-hardening invariant + +No reviewed claim or proposition-graph edge ever becomes unrevisable. +No `final`, `frozen`, `axiom`, or `permanent` flag exists or may be +added on the runtime data model. The closest such property in the +architecture is the *mathematical* closure check +`versor_condition(F) < 1e-6` — never an epistemic seal on a claim. +The invariant is enforced by `tests/test_epistemic_invariants.py`. + +### Curator review rule + +`epistemic_status` transitions are computed from coherence with the +existing reviewed field — not asserted by source authority. At v1 the +judgment is curator-mediated, with one rule: + +> The curator's only admissible reasoning is *geometric*: does the +> claim cohere with already-reviewed claims, or does it produce +> incoherence? Source credentials, popularity, or institutional +> position must not be invoked as justification. + +### Schema surfaces + +| Surface | Field | Default at creation | +|-----------------------------------------|----------------------------------------|-----------------------| +| `teaching.PackMutationProposal` | `epistemic_status: EpistemicStatus` | `SPECULATIVE` | +| `teaching.ReviewedTeachingExample` | `epistemic_status: EpistemicStatus` | `SPECULATIVE` | +| `language_packs.schema.LexicalEntry` | `epistemic_status: str` | `"coherent"` (seed) | +| `core.cognition.trace.compute_trace_hash` | `teaching_epistemic_status: str` | `""` if no proposal | + +Promotion of a proposal's status uses the immutable updater +`PackMutationProposal.with_status(...)` — original is never mutated. + +The status of the load-bearing proposal in a turn is folded into +`trace_hash` so replay detects when a downstream surface was produced +under a different epistemic frame than at the time of recall. + ## Test organization target Future test moves should follow this taxonomy: diff --git a/language_packs/compiler.py b/language_packs/compiler.py index 8f680007..287df293 100644 --- a/language_packs/compiler.py +++ b/language_packs/compiler.py @@ -328,6 +328,7 @@ def _parse_entry(payload: dict) -> LexicalEntry: semantic_domains=tuple(payload.get("semantic_domains", [])), manifold_point_checksum=payload.get("manifold_point_checksum"), provenance_ids=tuple(payload.get("provenance_ids", [])), + epistemic_status=payload.get("epistemic_status", "coherent"), ) diff --git a/language_packs/schema.py b/language_packs/schema.py index ee8766c3..e080745f 100644 --- a/language_packs/schema.py +++ b/language_packs/schema.py @@ -98,7 +98,15 @@ class MorphologyEntry: @dataclass(frozen=True, slots=True) class LexicalEntry: - """One surface/lemma entry in a compiled linguistic manifold.""" + """One surface/lemma entry in a compiled linguistic manifold. + + `epistemic_status` follows ADR-0021: it is a *position in the + revision graph*, not a source-trust tier. Seed vocabulary defaults + to ``"coherent"`` per ADR §Schema impact; bumps to other statuses + require a deliberate curator review at pack version bumps. Absent + from the JSONL is treated as the seed default — no retroactive + tagging without review. + """ entry_id: str surface: str @@ -111,6 +119,7 @@ class LexicalEntry: semantic_domains: tuple[str, ...] = field(default_factory=tuple) manifold_point_checksum: str | None = None provenance_ids: tuple[str, ...] = field(default_factory=tuple) + epistemic_status: str = "coherent" @dataclass(frozen=True, slots=True) diff --git a/teaching/__init__.py b/teaching/__init__.py index eac7c8cb..ff79b46c 100644 --- a/teaching/__init__.py +++ b/teaching/__init__.py @@ -13,15 +13,19 @@ approval before they touch the vocabulary manifold. """ from teaching.correction import CorrectionCandidate, extract_correction +from teaching.epistemic import ADMISSIBLE_AS_EVIDENCE, EpistemicStatus, parse_status from teaching.review import ReviewedTeachingExample, ReviewOutcome, review_correction from teaching.store import TeachingStore, PackMutationProposal __all__ = [ + "ADMISSIBLE_AS_EVIDENCE", "CorrectionCandidate", - "extract_correction", + "EpistemicStatus", + "PackMutationProposal", "ReviewedTeachingExample", "ReviewOutcome", - "review_correction", "TeachingStore", - "PackMutationProposal", + "extract_correction", + "parse_status", + "review_correction", ] diff --git a/teaching/epistemic.py b/teaching/epistemic.py new file mode 100644 index 00000000..37b26313 --- /dev/null +++ b/teaching/epistemic.py @@ -0,0 +1,56 @@ +"""Epistemic Grade Policy — typed status surface per ADR-0021. + +`EpistemicStatus` is a *position in the revision graph*, not a trust tier. +Source labels (peer_consensus, outsider_empirical, established, +unauthoritative) are deliberately not part of this enum — they would +re-import the bias ADR-0021 refuses. + +The four positions form an open lattice under review. No member carries +a "hardened" or "permanent" flag (non-hardening invariant, ADR-0021 §2). +Every claim remains revisable; a Stage-3 inversion path is always +available for `FALSIFIED` claims. +""" + +from __future__ import annotations + +from enum import Enum, unique + + +@unique +class EpistemicStatus(Enum): + """Position of a claim in the reviewed revision graph. + + Coherence is the only admission signal (ADR-0021 §3). Transitions + between statuses are *computed from coherence with the existing + reviewed field*, not asserted by source authority. + """ + + COHERENT = "coherent" + CONTESTED = "contested" + SPECULATIVE = "speculative" + FALSIFIED = "falsified" + + +# Statuses that admit a claim as evidence in downstream inference. +# `SPECULATIVE` is admissible only as a candidate, not as evidence. +# `CONTESTED` is admissible but cannot drive inferences depending on its truth. +# `FALSIFIED` is retained for provenance and Stage-3 inversion, not evidence. +ADMISSIBLE_AS_EVIDENCE: frozenset[EpistemicStatus] = frozenset({ + EpistemicStatus.COHERENT, +}) + + +def parse_status(value: str | None) -> EpistemicStatus: + """Parse a serialised status string, defaulting to SPECULATIVE. + + SPECULATIVE is the safe default at proposal creation per ADR-0021 + §Schema impact: "transitions to COHERENT / CONTESTED / FALSIFIED only + via the review path." An absent or unknown value must not silently + promote a claim to COHERENT. + """ + if value is None or value == "": + return EpistemicStatus.SPECULATIVE + for status in EpistemicStatus: + if status.value == value: + return status + return EpistemicStatus.SPECULATIVE diff --git a/teaching/review.py b/teaching/review.py index 4d8ea8c9..ba02c038 100644 --- a/teaching/review.py +++ b/teaching/review.py @@ -21,6 +21,7 @@ from enum import Enum, unique from core.physics.identity import IdentityCheck, IdentityManifold, IdentityScore from teaching.correction import CorrectionCandidate +from teaching.epistemic import EpistemicStatus @unique @@ -224,9 +225,20 @@ def _review_hash(candidate: CorrectionCandidate, outcome: ReviewOutcome) -> str: @dataclass(frozen=True, slots=True) class ReviewedTeachingExample: + """A correction that has passed the review gate (or been rejected). + + `epistemic_status` is orthogonal to `outcome` per ADR-0021 + §Schema impact: "Accepting a proposal is not the same as ratifying + it as COHERENT — the two are orthogonal and both required for + admission as evidence." At v1 the default is SPECULATIVE for any + review outcome; promotion to COHERENT / CONTESTED / FALSIFIED is a + separate curator-mediated coherence judgment, not implied by + acceptance. + """ candidate: CorrectionCandidate outcome: ReviewOutcome review_hash: str + epistemic_status: EpistemicStatus = EpistemicStatus.SPECULATIVE @property def accepted(self) -> bool: @@ -237,6 +249,7 @@ class ReviewedTeachingExample: "candidate": self.candidate.as_dict(), "outcome": self.outcome.value, "review_hash": self.review_hash, + "epistemic_status": self.epistemic_status.value, } @@ -245,6 +258,7 @@ def review_correction( *, identity_score: IdentityScore | None = None, identity_manifold: IdentityManifold | None = None, + epistemic_status: EpistemicStatus = EpistemicStatus.SPECULATIVE, ) -> ReviewedTeachingExample: """Review a correction candidate and produce a teaching example. @@ -271,4 +285,5 @@ def review_correction( candidate=candidate, outcome=outcome, review_hash=_review_hash(candidate, outcome), + epistemic_status=epistemic_status, ) diff --git a/teaching/store.py b/teaching/store.py index 7ef0b090..0812459d 100644 --- a/teaching/store.py +++ b/teaching/store.py @@ -13,6 +13,7 @@ import json from dataclasses import dataclass from teaching.correction import CorrectionCandidate +from teaching.epistemic import EpistemicStatus from teaching.review import ReviewedTeachingExample @@ -25,6 +26,13 @@ class PackMutationProposal: stored alongside the opaque text so the inference operators in ``generate.operators`` can walk the typed-relation graph that the teaching store represents (ADR-0018). + + `epistemic_status` is set to SPECULATIVE at creation per ADR-0021 + §Schema impact: "transitions to COHERENT / CONTESTED / FALSIFIED + only via the review path." It is a *position in the revision + graph*, not a source-trust tier. No `final`, `frozen`, `axiom`, or + `permanent` flag exists or may be added (non-hardening invariant, + ADR-0021 §2). """ proposal_id: str candidate_id: str @@ -33,6 +41,7 @@ class PackMutationProposal: prior_surface: str applied: bool = False triple: tuple[str, str, str] | None = None + epistemic_status: EpistemicStatus = EpistemicStatus.SPECULATIVE def as_dict(self) -> dict[str, object]: return { @@ -43,8 +52,20 @@ class PackMutationProposal: "prior_surface": self.prior_surface, "applied": self.applied, "triple": list(self.triple) if self.triple is not None else None, + "epistemic_status": self.epistemic_status.value, } + def with_status(self, status: EpistemicStatus) -> "PackMutationProposal": + """Return a new proposal with `epistemic_status` set to `status`. + + Immutable update — never mutates the original. This is the only + admissible transition path for a proposal's epistemic status; it + must be driven by a coherence judgment, not by source authority + (ADR-0021 §3). + """ + from dataclasses import replace + return replace(self, epistemic_status=status) + def _proposal_id(candidate: CorrectionCandidate) -> str: payload = json.dumps( @@ -96,6 +117,7 @@ class TeachingStore: correction_text=example.candidate.correction_text, prior_surface=example.candidate.prior_surface, triple=triple, + epistemic_status=example.epistemic_status, ) self._proposals.append(proposal) return proposal diff --git a/tests/test_epistemic_invariants.py b/tests/test_epistemic_invariants.py new file mode 100644 index 00000000..4885b4f8 --- /dev/null +++ b/tests/test_epistemic_invariants.py @@ -0,0 +1,144 @@ +"""ADR-0021 non-hardening invariant tests. + +The Epistemic Grade Policy commits to: + +1. `epistemic_status` is a position in the revision graph, not a trust + tier. +2. No reviewed claim, relation, or proposition-graph edge ever becomes + unrevisable — no `final`, `frozen`, `axiom`, or `permanent` flag may + exist or be added on the runtime data model. +3. Coherence is the only admission signal — source-trust labels must not + be part of the schema. + +These tests are the structural checks behind those commitments. They +are intentionally simple and read like contract assertions, not +behaviour tests. +""" + +from __future__ import annotations + +import dataclasses + +from teaching import EpistemicStatus, PackMutationProposal, ReviewedTeachingExample +from teaching.correction import CorrectionCandidate +from teaching.review import ReviewOutcome + + +_FORBIDDEN_HARDENING_NAMES: frozenset[str] = frozenset({ + "final", + "frozen", + "axiom", + "permanent", + "immutable_truth", + "sealed", +}) + +# Source-trust tier names that ADR-0021 §3 forbids in the schema. +_FORBIDDEN_TRUST_TIER_NAMES: frozenset[str] = frozenset({ + "peer_consensus", + "outsider_empirical", + "established", + "unauthoritative", + "credentialed", + "source_trust", + "authority", + "trust_tier", +}) + + +def _field_names(cls: type) -> set[str]: + return {f.name for f in dataclasses.fields(cls)} + + +def _enum_value_strings(enum_cls) -> set[str]: + return {member.value for member in enum_cls} + + +def test_pack_mutation_proposal_has_no_hardening_flag(): + """PackMutationProposal must not expose any name implying permanence.""" + names = _field_names(PackMutationProposal) + forbidden = names & _FORBIDDEN_HARDENING_NAMES + assert forbidden == set(), ( + f"PackMutationProposal exposes hardening flag(s) {forbidden}; " + "ADR-0021 §2 forbids non-revisable state on the runtime data model." + ) + + +def test_reviewed_teaching_example_has_no_hardening_flag(): + """ReviewedTeachingExample must not expose any name implying permanence.""" + names = _field_names(ReviewedTeachingExample) + forbidden = names & _FORBIDDEN_HARDENING_NAMES + assert forbidden == set(), ( + f"ReviewedTeachingExample exposes hardening flag(s) {forbidden}; " + "ADR-0021 §2 forbids non-revisable state on the runtime data model." + ) + + +def test_epistemic_status_enum_carries_no_trust_tier_names(): + """EpistemicStatus must describe revision-graph position, not source trust.""" + values = _enum_value_strings(EpistemicStatus) + forbidden = values & _FORBIDDEN_TRUST_TIER_NAMES + assert forbidden == set(), ( + f"EpistemicStatus enum contains source-trust tier value(s) {forbidden}; " + "ADR-0021 §3 forbids credentialing the schema." + ) + + +def test_epistemic_status_enum_has_exactly_the_four_positions(): + """ADR-0021 §1 names the enum members precisely; no silent additions.""" + assert _enum_value_strings(EpistemicStatus) == { + "coherent", + "contested", + "speculative", + "falsified", + } + + +def test_proposal_default_is_speculative(): + """ADR-0021 §Schema impact: proposals start SPECULATIVE; promotion is + a separate, review-mediated transition.""" + proposal = PackMutationProposal( + proposal_id="x", + candidate_id="y", + subject="z", + correction_text="a knows b", + prior_surface="", + ) + assert proposal.epistemic_status is EpistemicStatus.SPECULATIVE + + +def test_proposal_with_status_returns_new_immutable_proposal(): + """Immutable update — the original is never mutated.""" + original = PackMutationProposal( + proposal_id="x", + candidate_id="y", + subject="z", + correction_text="a knows b", + prior_surface="", + ) + promoted = original.with_status(EpistemicStatus.COHERENT) + + assert promoted.epistemic_status is EpistemicStatus.COHERENT + assert original.epistemic_status is EpistemicStatus.SPECULATIVE + assert promoted is not original + + +def test_reviewed_example_default_is_speculative_for_accepted_outcome(): + """Acceptance is orthogonal to coherence ratification per ADR-0021 + §Schema impact: 'Accepting a proposal is not the same as ratifying + it as COHERENT.'""" + from generate.intent import DialogueIntent, IntentTag + + intent = DialogueIntent(tag=IntentTag.DEFINITION, subject="a") + candidate = CorrectionCandidate( + candidate_id="c1", + intent=intent, + correction_text="a is b", + prior_surface="", + prior_turn=0, + ) + from teaching import review_correction + + reviewed = review_correction(candidate) + assert reviewed.outcome is ReviewOutcome.ACCEPTED + assert reviewed.epistemic_status is EpistemicStatus.SPECULATIVE