feat(adr-0021): epistemic_status surface wired across teaching + trace

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%.
This commit is contained in:
Shay 2026-05-16 20:20:35 -07:00
parent e36998d25d
commit ef95d3e609
10 changed files with 320 additions and 4 deletions

View file

@ -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,
)

View file

@ -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,
)

View file

@ -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:

View file

@ -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"),
)

View file

@ -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)

View file

@ -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",
]

56
teaching/epistemic.py Normal file
View file

@ -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

View file

@ -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,
)

View file

@ -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

View file

@ -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