Lands the three load-bearing pieces of ADR-0055 Phase A so later phases (DiscoveryCandidate, TeachingChainProposal) have a safe substrate to write into. - teaching/audit.py: pure, deterministic re-parse of the reviewed corpus with same gates as the runtime loader but keeps drop reasons (invalid_json, missing_required_field:*, unsupported_intent, pack_missing_subject, pack_missing_object, superseded_by:*). - teaching/provenance.py: typed Provenance(adr_id, source, review_date, raw); legacy "reviewed" maps to "hand_authored" so current corpus reports the canonical enum without a file rewrite. - chat/teaching_grounding._corpus_index honors superseded_by — active view drops superseded entries while disk preserves history. - core teaching audit CLI subcommand (--json optional); exits 1 on any drop so CI catches silent corpus shrinkage from pack swaps. Observable behaviour unchanged: corpus is 10/10 loaded, all five core lanes green (smoke 67, cognition 121, runtime 19, teaching 17, packs 6), cognition eval metrics identical on dev / public / holdout splits. versor_condition < 1e-6 invariant untouched. Tests: tests/test_teaching_audit.py — 23 tests covering provenance parser, real-corpus determinism, every drop-reason path, supersession semantics, runtime/audit parity, read-only contract.
88 lines
3 KiB
Python
88 lines
3 KiB
Python
"""ADR-0055 Phase A — typed provenance parser for reviewed teaching entries.
|
|
|
|
Today's corpus provenance is a free-text string like
|
|
``"adr-0052:reviewed:2026-05-17"``. This module parses that shape
|
|
into a typed ``Provenance`` without rewriting the JSONL on disk.
|
|
Existing entries keep their raw string; the parser is lenient — if
|
|
the string does not match the expected shape, the typed fields fall
|
|
back to ``None`` / ``"unknown"`` and the raw is preserved.
|
|
|
|
Future entries written by ``TeachingChainProposal`` (Phase C) should
|
|
emit the canonical shape directly.
|
|
|
|
Source enum follows ADR-0055 §A3:
|
|
|
|
- ``"hand_authored"`` — written by an operator in an ADR PR
|
|
- ``"discovery_promoted"`` — accepted ``TeachingChainProposal`` (Phase C+)
|
|
- ``"imported"`` — bulk-imported from an external reviewed source
|
|
|
|
Anything else maps to ``"unknown"``. The legacy ``"reviewed"`` token
|
|
seen in existing entries is treated as ``"hand_authored"`` so today's
|
|
corpus reports the right enum without a file rewrite.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
|
|
_KNOWN_SOURCES: frozenset[str] = frozenset({
|
|
"hand_authored",
|
|
"discovery_promoted",
|
|
"imported",
|
|
})
|
|
|
|
# Legacy free-text tokens that have appeared in the corpus prior to
|
|
# Phase A. Each maps to its canonical typed enum value.
|
|
_LEGACY_SOURCE_ALIASES: dict[str, str] = {
|
|
"reviewed": "hand_authored",
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Provenance:
|
|
adr_id: str | None
|
|
source: str
|
|
review_date: str | None
|
|
raw: str
|
|
|
|
|
|
def _coerce_source(token: str) -> str:
|
|
token = token.strip().lower()
|
|
if token in _KNOWN_SOURCES:
|
|
return token
|
|
if token in _LEGACY_SOURCE_ALIASES:
|
|
return _LEGACY_SOURCE_ALIASES[token]
|
|
return "unknown"
|
|
|
|
|
|
def parse_provenance(value: Any) -> Provenance:
|
|
"""Parse a free-text provenance string into the typed shape.
|
|
|
|
Recognised shape: ``"<adr_id>:<source>:<review_date>"``
|
|
where each segment is non-empty. Extra trailing segments are
|
|
folded into ``review_date`` so ``"adr-0052:reviewed:2026-05-17"``
|
|
parses cleanly even if a future writer adds a fourth field by
|
|
accident (defensive — the parser does not crash on drift).
|
|
|
|
Non-string or empty input produces a ``Provenance`` with all
|
|
typed fields ``None`` / ``"unknown"`` and ``raw=""``.
|
|
"""
|
|
if not isinstance(value, str):
|
|
return Provenance(adr_id=None, source="unknown", review_date=None, raw="")
|
|
raw = value.strip()
|
|
if not raw:
|
|
return Provenance(adr_id=None, source="unknown", review_date=None, raw="")
|
|
parts = raw.split(":")
|
|
if len(parts) < 3:
|
|
return Provenance(adr_id=None, source="unknown", review_date=None, raw=raw)
|
|
adr_id = parts[0].strip() or None
|
|
source = _coerce_source(parts[1])
|
|
review_date_token = ":".join(parts[2:]).strip() or None
|
|
return Provenance(
|
|
adr_id=adr_id,
|
|
source=source,
|
|
review_date=review_date_token,
|
|
raw=raw,
|
|
)
|