core/tests/test_epistemic_invariants.py
Shay b24796386e feat(teaching): implement ADR-0094 — Proposal Source Provenance
Sealed ProposalSource type widening TeachingChainProposal and
PackMutationProposal schemas with typed (kind, source_id,
emitted_at_revision) provenance. Schema-only widening; no runtime
behavior changes. Unblocks ADR-0095 miner-sourced proposals.

- new teaching/source.py: frozen ProposalSource dataclass with sealed
  ProposalKind Literal["operator","miner","curriculum"], runtime
  invariants (operator → empty source_id; miner/curriculum → non-empty),
  serialize() ("operator" / "miner:<id>" / "curriculum:<id>"),
  as_dict/from_dict round-trip, ProposalSource.operator() helper
- TeachingChainProposal.source field added (proposals.py)
- PackMutationProposal.source field added (store.py)
- build_proposal() accepts optional source kwarg; default uses
  _default_operator_source() pinned at cached git HEAD SHA
- ProposalLog.current_state() now strictly requires source on every
  created event; raises ProposalError with migration pointer if missing;
  validates via ProposalSource.from_dict so malformed payloads reject
- teaching/migrate_proposals_source_field.py: deterministic one-shot
  migration script using PRE_MIGRATION_SENTINEL ("pre-adr-0094-migration")
  as the emitted_at_revision so re-runs across commits produce identical
  bytes
- migration applied to live proposals.jsonl: 11 created events gained
  source field; 33 non-created events untouched; idempotent verified
- 29 unit tests in test_proposal_source.py covering construction,
  serialization, exhaustive-match pattern with assert_never,
  migration determinism (3 idempotence/cross-run tests), strict-parse
  rejection, live-log loads
- 2 test fixes in test_epistemic_invariants.py for new required source param
- smoke 67/67, teaching 17/17, cognition 120/121 (1 pre-existing skip),
  runtime 19/19; cognition eval byte-identical 100/100/100/100
2026-05-21 18:11:09 -07:00

147 lines
4.8 KiB
Python

"""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
from teaching.source import ProposalSource
_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="",
source=ProposalSource.operator(emitted_at_revision="test"),
)
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="",
source=ProposalSource.operator(emitted_at_revision="test"),
)
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