feat: scaffold core_ingest/ governance layer (ADR-0012)
This commit is contained in:
parent
6010924405
commit
7737f8ca66
7 changed files with 1455 additions and 0 deletions
58
core_ingest/__init__.py
Normal file
58
core_ingest/__init__.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
"""
|
||||
core_ingest — Universal input-to-pressure boundary.
|
||||
|
||||
The sole job of this package is converting external information from any
|
||||
source into CORE-native pressure: typed, content-addressed, provenance-
|
||||
anchored candidate packets that can be validated and, if accepted, exported
|
||||
as LearningArtifact objects for the train/ layer.
|
||||
|
||||
Two paths:
|
||||
runtime — transient working pressure for active cognition
|
||||
(short-circuit: bypasses governance, feeds ingest/gate.py directly)
|
||||
durable — validated candidate pressure for learning and update paths
|
||||
(full three-gate IngestCompiler pipeline)
|
||||
|
||||
The gate (ingest/gate.py) is NEVER imported or modified from this package.
|
||||
This package is upstream of the gate, not a replacement for it.
|
||||
"""
|
||||
|
||||
from core_ingest.types import (
|
||||
Modality,
|
||||
DeterminismClass,
|
||||
ReviewLevel,
|
||||
SourceSpan,
|
||||
FrontendTrace,
|
||||
CandidateGeometricPressure,
|
||||
ValidationResult,
|
||||
ValidationReport,
|
||||
LearningArtifact,
|
||||
ReviewDecision,
|
||||
)
|
||||
from core_ingest.pressure import make_pressure_id, make_semantic_key
|
||||
from core_ingest.compiler import IngestCompiler
|
||||
from core_ingest.segmenter import StructuralSegmenter, SegmentKind
|
||||
from core_ingest.manifold import SegmentManifold
|
||||
|
||||
__all__ = [
|
||||
# types
|
||||
"Modality",
|
||||
"DeterminismClass",
|
||||
"ReviewLevel",
|
||||
"SourceSpan",
|
||||
"FrontendTrace",
|
||||
"CandidateGeometricPressure",
|
||||
"ValidationResult",
|
||||
"ValidationReport",
|
||||
"LearningArtifact",
|
||||
"ReviewDecision",
|
||||
# pressure addressing
|
||||
"make_pressure_id",
|
||||
"make_semantic_key",
|
||||
# compiler
|
||||
"IngestCompiler",
|
||||
# segmenter
|
||||
"StructuralSegmenter",
|
||||
"SegmentKind",
|
||||
# manifold index
|
||||
"SegmentManifold",
|
||||
]
|
||||
303
core_ingest/compiler.py
Normal file
303
core_ingest/compiler.py
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
"""
|
||||
IngestCompiler — deterministic three-gate validation pipeline.
|
||||
|
||||
Gate order (sequential; a packet failing any gate does not proceed):
|
||||
1. ProvenanceGate — SourceSpan integrity: non-empty text, valid SHA-256,
|
||||
byte offsets non-negative and ordered
|
||||
2. SemanticGate — span completeness: non-empty payload, balanced
|
||||
delimiters (for code/math), minimum length
|
||||
3. GovernanceGate — ReviewLevel vs DeterminismClass consistency;
|
||||
ReviewDecision override check
|
||||
|
||||
The compiler:
|
||||
- performs structural deduplication by pressure_id (exact structural identity)
|
||||
- tracks semantic convergence by semantic_key and annotates packets that
|
||||
assert the same claim as prior packets with a warning
|
||||
- applies ReviewDecision overrides without mutating the original packet
|
||||
- produces a ValidationReport alongside the original immutable packets
|
||||
- exports accepted packets as LearningArtifact objects
|
||||
|
||||
Ingest/gate.py is NOT imported or called here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from typing import Sequence
|
||||
|
||||
from core_ingest.types import (
|
||||
CandidateGeometricPressure,
|
||||
DeterminismClass,
|
||||
GateDisposition,
|
||||
LearningArtifact,
|
||||
ReviewDecision,
|
||||
ReviewLevel,
|
||||
ValidationReport,
|
||||
ValidationResult,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Individual gates
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class ProvenanceGate:
|
||||
"""
|
||||
Gate 1: SourceSpan integrity.
|
||||
|
||||
Checks:
|
||||
- At least one SourceSpan (enforced at packet construction; double-checked here)
|
||||
- source_sha256 is 64 hex characters
|
||||
- byte_start >= 0
|
||||
- byte_end > byte_start
|
||||
"""
|
||||
|
||||
def check(self, packet: CandidateGeometricPressure) -> str | None:
|
||||
"""Return a failure reason string, or None if the packet passes."""
|
||||
if not packet.provenance:
|
||||
return "provenance is empty"
|
||||
for span in packet.provenance:
|
||||
if span.byte_start < 0:
|
||||
return f"negative byte_start ({span.byte_start}) in SourceSpan"
|
||||
if span.byte_end <= span.byte_start:
|
||||
return (
|
||||
f"byte_end ({span.byte_end}) <= byte_start ({span.byte_start})"
|
||||
)
|
||||
if len(span.source_sha256) != 64:
|
||||
return (
|
||||
f"source_sha256 is {len(span.source_sha256)} chars, expected 64"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class SemanticGate:
|
||||
"""
|
||||
Gate 2: Span completeness.
|
||||
|
||||
Checks:
|
||||
- payload_json is non-empty (not just '{}')
|
||||
- lemma is non-empty for TEXT and SCRIPTURE modalities
|
||||
- For CODE modality: no unbalanced braces/brackets/parens in lemma
|
||||
- Minimum combined span length of 1 byte
|
||||
"""
|
||||
_PAIRS = {"{": "}", "[": "]", "(": ")", "<": ">"}
|
||||
|
||||
def check(self, packet: CandidateGeometricPressure) -> str | None:
|
||||
import json
|
||||
from core_ingest.types import Modality
|
||||
|
||||
if packet.payload_json in ("{}", ""):
|
||||
return "payload_json is empty — no content to ingest"
|
||||
|
||||
# Require non-empty lemma for text / scripture
|
||||
if packet.modality in (Modality.TEXT, Modality.SCRIPTURE) and not packet.lemma:
|
||||
return (
|
||||
f"modality {packet.modality.value} requires a non-empty lemma"
|
||||
)
|
||||
|
||||
# CODE: balanced delimiter check on lemma
|
||||
if packet.modality == Modality.CODE and packet.lemma:
|
||||
stack: list[str] = []
|
||||
for ch in packet.lemma:
|
||||
if ch in self._PAIRS:
|
||||
stack.append(self._PAIRS[ch])
|
||||
elif ch in self._PAIRS.values():
|
||||
if not stack or stack[-1] != ch:
|
||||
return f"unbalanced delimiter '{ch}' in code lemma"
|
||||
stack.pop()
|
||||
if stack:
|
||||
return f"unclosed delimiter(s) in code lemma: {''.join(stack)}"
|
||||
|
||||
# Minimum span length
|
||||
total_bytes = sum(s.byte_end - s.byte_start for s in packet.provenance)
|
||||
if total_bytes < 1:
|
||||
return "total span length is 0 bytes"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class GovernanceGate:
|
||||
"""
|
||||
Gate 3: ReviewLevel and DeterminismClass consistency.
|
||||
|
||||
Checks:
|
||||
- AUTO_ACCEPT_ELIGIBLE is only claimed by D0/D1 frontends
|
||||
(enforced at packet construction; double-checked here)
|
||||
- AUTO_REJECT packets are always rejected
|
||||
- D2–D4 packets not covered by a ReviewDecision land in review_required
|
||||
"""
|
||||
|
||||
def check(
|
||||
self,
|
||||
packet: CandidateGeometricPressure,
|
||||
authorized_ids: frozenset[str],
|
||||
) -> GateDisposition:
|
||||
if packet.review_level == ReviewLevel.AUTO_REJECT:
|
||||
return GateDisposition.REJECTED_GOVERNANCE
|
||||
|
||||
# Override: an authorized ReviewDecision accepts regardless of level
|
||||
if packet.pressure_id in authorized_ids:
|
||||
return GateDisposition.OVERRIDE_ACCEPTED
|
||||
|
||||
if packet.review_level == ReviewLevel.AUTO_ACCEPT_ELIGIBLE:
|
||||
# Invariant already enforced at construction — D0/D1 only
|
||||
return GateDisposition.ACCEPTED
|
||||
|
||||
# OPERATOR_REVIEW_REQUIRED or ARCHITECT_REVIEW_REQUIRED without override
|
||||
return GateDisposition.REVIEW_REQUIRED
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Compiler
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class IngestCompiler:
|
||||
"""
|
||||
Processes batches of CandidateGeometricPressure packets through the
|
||||
three-gate pipeline and produces a ValidationReport.
|
||||
|
||||
Usage
|
||||
-----
|
||||
compiler = IngestCompiler()
|
||||
report, artifacts = compiler.compile(packets, review_decision=decision)
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._provenance_gate = ProvenanceGate()
|
||||
self._semantic_gate = SemanticGate()
|
||||
self._governance_gate = GovernanceGate()
|
||||
|
||||
def compile(
|
||||
self,
|
||||
packets: Sequence[CandidateGeometricPressure],
|
||||
review_decision: ReviewDecision | None = None,
|
||||
) -> tuple[ValidationReport, list[LearningArtifact]]:
|
||||
"""
|
||||
Validate a batch of candidate packets.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
packets : The batch to validate (any sequence).
|
||||
review_decision : Optional operator/architect authorization. Packets
|
||||
whose pressure_id appears in
|
||||
review_decision.authorized_ids are accepted
|
||||
regardless of their ReviewLevel.
|
||||
|
||||
Returns
|
||||
-------
|
||||
(ValidationReport, list[LearningArtifact])
|
||||
"""
|
||||
authorized_ids: frozenset[str] = (
|
||||
review_decision.authorized_ids
|
||||
if review_decision is not None
|
||||
else frozenset()
|
||||
)
|
||||
|
||||
# Deduplication: track seen pressure_ids (structural) and semantic_keys
|
||||
seen_pressure_ids: set[str] = set()
|
||||
semantic_counts: dict[str, int] = defaultdict(int)
|
||||
|
||||
results: list[ValidationResult] = []
|
||||
artifacts: list[LearningArtifact] = []
|
||||
|
||||
accepted_ids: set[str] = set()
|
||||
rejected_ids: set[str] = set()
|
||||
review_ids: set[str] = set()
|
||||
|
||||
for packet in packets:
|
||||
pid = packet.pressure_id
|
||||
sk = packet.semantic_key
|
||||
|
||||
# Structural deduplication
|
||||
if pid in seen_pressure_ids:
|
||||
results.append(ValidationResult(
|
||||
pressure_id=pid,
|
||||
semantic_key=sk,
|
||||
disposition=GateDisposition.REJECTED_PROVENANCE,
|
||||
gate_failed="provenance",
|
||||
failure_reason="duplicate pressure_id — structural duplicate",
|
||||
))
|
||||
rejected_ids.add(pid)
|
||||
continue
|
||||
seen_pressure_ids.add(pid)
|
||||
|
||||
# Convergent-evidence tracking
|
||||
prior_count = semantic_counts[sk]
|
||||
semantic_counts[sk] += 1
|
||||
convergence_warning: tuple[str, ...] = ()
|
||||
if prior_count > 0:
|
||||
convergence_warning = (
|
||||
f"semantic_convergence:{prior_count}_prior_sources",
|
||||
)
|
||||
|
||||
# Gate 1: Provenance
|
||||
prov_failure = self._provenance_gate.check(packet)
|
||||
if prov_failure is not None:
|
||||
results.append(ValidationResult(
|
||||
pressure_id=pid,
|
||||
semantic_key=sk,
|
||||
disposition=GateDisposition.REJECTED_PROVENANCE,
|
||||
gate_failed="provenance",
|
||||
failure_reason=prov_failure,
|
||||
warnings=convergence_warning,
|
||||
))
|
||||
rejected_ids.add(pid)
|
||||
continue
|
||||
|
||||
# Gate 2: Semantic
|
||||
sem_failure = self._semantic_gate.check(packet)
|
||||
if sem_failure is not None:
|
||||
results.append(ValidationResult(
|
||||
pressure_id=pid,
|
||||
semantic_key=sk,
|
||||
disposition=GateDisposition.REJECTED_SEMANTIC,
|
||||
gate_failed="semantic",
|
||||
failure_reason=sem_failure,
|
||||
warnings=convergence_warning,
|
||||
))
|
||||
rejected_ids.add(pid)
|
||||
continue
|
||||
|
||||
# Gate 3: Governance
|
||||
disposition = self._governance_gate.check(packet, authorized_ids)
|
||||
|
||||
if disposition == GateDisposition.REVIEW_REQUIRED:
|
||||
results.append(ValidationResult(
|
||||
pressure_id=pid,
|
||||
semantic_key=sk,
|
||||
disposition=disposition,
|
||||
warnings=convergence_warning,
|
||||
))
|
||||
review_ids.add(pid)
|
||||
continue
|
||||
|
||||
if disposition == GateDisposition.REJECTED_GOVERNANCE:
|
||||
results.append(ValidationResult(
|
||||
pressure_id=pid,
|
||||
semantic_key=sk,
|
||||
disposition=disposition,
|
||||
gate_failed="governance",
|
||||
failure_reason="AUTO_REJECT review level",
|
||||
warnings=convergence_warning,
|
||||
))
|
||||
rejected_ids.add(pid)
|
||||
continue
|
||||
|
||||
# ACCEPTED or OVERRIDE_ACCEPTED
|
||||
result = ValidationResult(
|
||||
pressure_id=pid,
|
||||
semantic_key=sk,
|
||||
disposition=disposition,
|
||||
warnings=convergence_warning,
|
||||
)
|
||||
results.append(result)
|
||||
accepted_ids.add(pid)
|
||||
artifacts.append(LearningArtifact(packet=packet, result=result))
|
||||
|
||||
report = ValidationReport(
|
||||
results=tuple(results),
|
||||
accepted_ids=frozenset(accepted_ids),
|
||||
rejected_ids=frozenset(rejected_ids),
|
||||
review_ids=frozenset(review_ids),
|
||||
)
|
||||
return report, artifacts
|
||||
98
core_ingest/manifold.py
Normal file
98
core_ingest/manifold.py
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
"""
|
||||
SegmentManifold — reconstruction index for pre-injection provenance.
|
||||
|
||||
Maps semantic_key -> list of SourceSpan positions in source documents.
|
||||
|
||||
Purpose: given a vault recall hit on a semantic_key, recover the exact
|
||||
provenance spans in the original source documents. This extends
|
||||
Reconstruction-over-Storage to the pre-injection layer — we do not store
|
||||
full document copies; we store enough structured provenance to reconstruct
|
||||
the relevant span on demand.
|
||||
|
||||
The manifold is append-only. Entries are never modified or deleted.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Sequence
|
||||
|
||||
from core_ingest.types import CandidateGeometricPressure, SourceSpan
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ManifoldEntry:
|
||||
"""
|
||||
A single provenance record in the SegmentManifold.
|
||||
|
||||
semantic_key — SHA-256 over semantic fields; shared by packets asserting
|
||||
the same claim
|
||||
pressure_id — SHA-256 over the full packet; unique per structural variant
|
||||
spans — provenance spans from the packet
|
||||
instrument_id — identity of the proposing instrument
|
||||
"""
|
||||
semantic_key: str
|
||||
pressure_id: str
|
||||
spans: tuple[SourceSpan, ...]
|
||||
instrument_id: str
|
||||
|
||||
|
||||
class SegmentManifold:
|
||||
"""
|
||||
Append-only index: semantic_key -> list[ManifoldEntry].
|
||||
|
||||
Usage
|
||||
-----
|
||||
manifold = SegmentManifold()
|
||||
manifold.register(packets) # register a batch after compilation
|
||||
entries = manifold.lookup(sk) # retrieve all entries for a semantic_key
|
||||
spans = manifold.spans_for(sk) # retrieve all SourceSpans for a key
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._index: dict[str, list[ManifoldEntry]] = defaultdict(list)
|
||||
|
||||
def register(
|
||||
self,
|
||||
packets: Sequence[CandidateGeometricPressure],
|
||||
) -> None:
|
||||
"""
|
||||
Register a batch of packets into the manifold.
|
||||
|
||||
Typically called with the accepted packets after IngestCompiler.compile().
|
||||
Can also be called with the full batch to index all attempted ingest;
|
||||
the caller decides the registration policy.
|
||||
"""
|
||||
for packet in packets:
|
||||
entry = ManifoldEntry(
|
||||
semantic_key=packet.semantic_key,
|
||||
pressure_id=packet.pressure_id,
|
||||
spans=packet.provenance,
|
||||
instrument_id=packet.frontend.instrument_id,
|
||||
)
|
||||
self._index[packet.semantic_key].append(entry)
|
||||
|
||||
def lookup(self, semantic_key: str) -> list[ManifoldEntry]:
|
||||
"""
|
||||
Return all ManifoldEntry records for a given semantic_key.
|
||||
Returns an empty list if the key is not indexed.
|
||||
"""
|
||||
return list(self._index.get(semantic_key, []))
|
||||
|
||||
def spans_for(self, semantic_key: str) -> list[SourceSpan]:
|
||||
"""
|
||||
Return all SourceSpan records for a given semantic_key,
|
||||
flattened across all ManifoldEntry records.
|
||||
"""
|
||||
spans: list[SourceSpan] = []
|
||||
for entry in self._index.get(semantic_key, []):
|
||||
spans.extend(entry.spans)
|
||||
return spans
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return the number of distinct semantic_keys indexed."""
|
||||
return len(self._index)
|
||||
|
||||
def __contains__(self, semantic_key: str) -> bool:
|
||||
return semantic_key in self._index
|
||||
115
core_ingest/pressure.py
Normal file
115
core_ingest/pressure.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
"""
|
||||
Content-addressed identifier computation for CandidateGeometricPressure.
|
||||
|
||||
Two functions:
|
||||
make_pressure_id — SHA-256 over the full canonical packet (structural identity)
|
||||
make_semantic_key — SHA-256 over semantic fields only (claim-level identity)
|
||||
|
||||
This module transparently attempts to import core_ingest_rs (the PyO3/Rust
|
||||
backend) for these operations. If the Rust extension is not built, pure-Python
|
||||
fallbacks handle every case with identical behavior.
|
||||
|
||||
The Rust path is a compilation target chosen after the data model was locked —
|
||||
Axiom 6, Compilation-Last.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from core_ingest_rs import sha256_hex, canonical_json # type: ignore[import]
|
||||
_RUST = True
|
||||
except ImportError:
|
||||
_RUST = False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pure-Python fallbacks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _sha256_hex(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def _canonical_json(obj: Any) -> str:
|
||||
return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def make_pressure_id(
|
||||
*,
|
||||
kind: str,
|
||||
modality: str,
|
||||
provenance: list[tuple[int, int, str]], # [(byte_start, byte_end, sha256), ...]
|
||||
frontend_id: str,
|
||||
determinism: str,
|
||||
review_level: str,
|
||||
confidence: float,
|
||||
uncertainty: float,
|
||||
lemma: str,
|
||||
subject: str,
|
||||
verb: str,
|
||||
object_: str,
|
||||
payload_json: str,
|
||||
) -> str:
|
||||
"""
|
||||
SHA-256 over all canonical packet fields.
|
||||
|
||||
Two packets are structurally identical (same pressure_id) iff every field
|
||||
is identical, including provenance. Two packets asserting the same claim
|
||||
from different sources will have different pressure_ids but the same
|
||||
semantic_key.
|
||||
"""
|
||||
payload = _canonical_json({
|
||||
"kind": kind,
|
||||
"modality": modality,
|
||||
"provenance": sorted(provenance),
|
||||
"frontend_id": frontend_id,
|
||||
"determinism": determinism,
|
||||
"review_level": review_level,
|
||||
"confidence": confidence,
|
||||
"uncertainty": uncertainty,
|
||||
"lemma": lemma,
|
||||
"subject": subject,
|
||||
"verb": verb,
|
||||
"object": object_,
|
||||
"payload_json": payload_json,
|
||||
})
|
||||
fn = sha256_hex if _RUST else _sha256_hex
|
||||
return fn(payload.encode("utf-8"))
|
||||
|
||||
|
||||
def make_semantic_key(
|
||||
*,
|
||||
kind: str,
|
||||
modality: str,
|
||||
lemma: str,
|
||||
subject: str,
|
||||
verb: str,
|
||||
object_: str,
|
||||
payload_json: str,
|
||||
) -> str:
|
||||
"""
|
||||
SHA-256 over semantic fields only.
|
||||
|
||||
Two packets with the same semantic_key assert the same claim regardless
|
||||
of where or by whom they were proposed. The IngestCompiler uses this for
|
||||
convergent-evidence detection.
|
||||
"""
|
||||
payload = _canonical_json({
|
||||
"kind": kind,
|
||||
"modality": modality,
|
||||
"lemma": lemma,
|
||||
"subject": subject,
|
||||
"verb": verb,
|
||||
"object": object_,
|
||||
"payload_json": payload_json,
|
||||
})
|
||||
fn = sha256_hex if _RUST else _sha256_hex
|
||||
return fn(payload.encode("utf-8"))
|
||||
228
core_ingest/segmenter.py
Normal file
228
core_ingest/segmenter.py
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
"""
|
||||
StructuralSegmenter — D0 form-boundary segmentation.
|
||||
|
||||
Carves source documents at deterministic structural signals:
|
||||
prose — blank-line paragraph breaks, ATX/Setext headings
|
||||
scripture — verse markers (canonical chapter:verse format)
|
||||
code — fenced code blocks (``` or ~~~)
|
||||
math — LaTeX display environments (\\[...\\], $$...$$, \\begin{...})
|
||||
|
||||
The segmenter operates on the *form* of the source, not its content.
|
||||
It does NOT interpret meaning. Meaning stays inside the versor field.
|
||||
|
||||
All segmenters produce SourceSpan objects with a determinism class of D0:
|
||||
fully deterministic given the same source bytes, no external state.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, auto
|
||||
from typing import Iterator
|
||||
|
||||
from core_ingest.types import SourceSpan
|
||||
|
||||
|
||||
class SegmentKind(str, Enum):
|
||||
"""Structural role of a segment within its source document."""
|
||||
HEADING = "heading"
|
||||
BODY = "body"
|
||||
VERSE = "verse"
|
||||
CODE = "code"
|
||||
MATH = "math"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Segment:
|
||||
"""
|
||||
A single structural unit extracted from a source document.
|
||||
|
||||
span — SourceSpan with byte offsets and source SHA-256
|
||||
kind — structural role of this segment
|
||||
text — decoded surface text of the segment
|
||||
"""
|
||||
span: SourceSpan
|
||||
kind: SegmentKind
|
||||
text: str
|
||||
|
||||
|
||||
class StructuralSegmenter:
|
||||
"""
|
||||
Entry point for D0 structural segmentation.
|
||||
|
||||
Usage
|
||||
-----
|
||||
segmenter = StructuralSegmenter()
|
||||
for segment in segmenter.segment(source_bytes, modality_hint="prose"):
|
||||
...
|
||||
|
||||
modality_hint selects the sub-segmenter:
|
||||
"prose" — blank-line paragraph + heading detection
|
||||
"scripture" — verse-marker based (chapter:verse or book.chapter.verse)
|
||||
"code" — fenced code block extraction
|
||||
"math" — LaTeX display math extraction
|
||||
"""
|
||||
|
||||
_SEGMENTERS = {
|
||||
"prose": "_segment_prose",
|
||||
"scripture": "_segment_scripture",
|
||||
"code": "_segment_code",
|
||||
"math": "_segment_math",
|
||||
}
|
||||
|
||||
def segment(
|
||||
self,
|
||||
source: bytes,
|
||||
modality_hint: str = "prose",
|
||||
) -> list[Segment]:
|
||||
"""
|
||||
Segment `source` according to `modality_hint`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
source : Raw source bytes (UTF-8 expected).
|
||||
modality_hint : One of 'prose', 'scripture', 'code', 'math'.
|
||||
|
||||
Returns
|
||||
-------
|
||||
List of Segment objects in document order.
|
||||
"""
|
||||
source_sha = hashlib.sha256(source).hexdigest()
|
||||
method_name = self._SEGMENTERS.get(modality_hint, "_segment_prose")
|
||||
method = getattr(self, method_name)
|
||||
return list(method(source, source_sha))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Prose segmenter
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
_HEADING_RE = re.compile(
|
||||
rb"^(?P<hashes>#{1,6})\s+(?P<text>.+)$|^(?P<text2>.+)\n[=\-]{2,}$",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
def _segment_prose(self, source: bytes, source_sha: str) -> Iterator[Segment]:
|
||||
"""
|
||||
Split on blank lines. Classify leading ATX/Setext headings.
|
||||
Each non-empty block becomes one Segment.
|
||||
"""
|
||||
# Split on one or more blank lines
|
||||
blocks = re.split(rb"\n{2,}", source)
|
||||
offset = 0
|
||||
for block in blocks:
|
||||
block = block.strip()
|
||||
if not block:
|
||||
offset += len(block) + 2 # account for separator
|
||||
continue
|
||||
start = source.find(block, offset)
|
||||
end = start + len(block)
|
||||
text = block.decode("utf-8", errors="replace")
|
||||
|
||||
# Heading detection
|
||||
kind = SegmentKind.BODY
|
||||
if self._HEADING_RE.match(block):
|
||||
kind = SegmentKind.HEADING
|
||||
|
||||
yield Segment(
|
||||
span=SourceSpan(
|
||||
byte_start=start,
|
||||
byte_end=end,
|
||||
source_sha256=source_sha,
|
||||
region=kind.value,
|
||||
),
|
||||
kind=kind,
|
||||
text=text,
|
||||
)
|
||||
offset = end
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Scripture segmenter
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
# Matches: GEN.1.1, Gen 1:1, 1 Cor 13:4, etc.
|
||||
_VERSE_RE = re.compile(
|
||||
rb"(?:(?:[1-3]\s)?[A-Za-z]+\.?\s*\d{1,3}[:.\s]\d{1,3})",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
def _segment_scripture(
|
||||
self, source: bytes, source_sha: str
|
||||
) -> Iterator[Segment]:
|
||||
"""
|
||||
Split scripture source at verse boundaries.
|
||||
Each verse becomes one Segment with region='verse:<ref>'.
|
||||
"""
|
||||
matches = list(self._VERSE_RE.finditer(source))
|
||||
for i, match in enumerate(matches):
|
||||
start = match.start()
|
||||
end = matches[i + 1].start() if i + 1 < len(matches) else len(source)
|
||||
block = source[start:end].strip()
|
||||
if not block:
|
||||
continue
|
||||
ref = match.group(0).decode("utf-8", errors="replace").strip()
|
||||
yield Segment(
|
||||
span=SourceSpan(
|
||||
byte_start=start,
|
||||
byte_end=start + len(block),
|
||||
source_sha256=source_sha,
|
||||
region=f"verse:{ref}",
|
||||
),
|
||||
kind=SegmentKind.VERSE,
|
||||
text=block.decode("utf-8", errors="replace"),
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Code segmenter
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
_FENCE_RE = re.compile(
|
||||
rb"^(?P<fence>`{3,}|~{3,})(?P<lang>[^\n]*)\n(?P<body>.*?)^(?P=fence)\s*$",
|
||||
re.MULTILINE | re.DOTALL,
|
||||
)
|
||||
|
||||
def _segment_code(self, source: bytes, source_sha: str) -> Iterator[Segment]:
|
||||
"""Extract fenced code blocks (``` or ~~~)."""
|
||||
for match in self._FENCE_RE.finditer(source):
|
||||
start = match.start()
|
||||
end = match.end()
|
||||
body = match.group("body")
|
||||
lang = match.group("lang").decode("utf-8", errors="replace").strip()
|
||||
region = f"code_block:{lang}" if lang else "code_block"
|
||||
yield Segment(
|
||||
span=SourceSpan(
|
||||
byte_start=start,
|
||||
byte_end=end,
|
||||
source_sha256=source_sha,
|
||||
region=region,
|
||||
),
|
||||
kind=SegmentKind.CODE,
|
||||
text=body.decode("utf-8", errors="replace"),
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Math segmenter
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
_MATH_RE = re.compile(
|
||||
rb"\\\[.*?\\\]|\$\$.*?\$\$|\\begin\{[^}]+\}.*?\\end\{[^}]+\}",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
def _segment_math(self, source: bytes, source_sha: str) -> Iterator[Segment]:
|
||||
"""Extract LaTeX display math environments."""
|
||||
for match in self._MATH_RE.finditer(source):
|
||||
start = match.start()
|
||||
end = match.end()
|
||||
yield Segment(
|
||||
span=SourceSpan(
|
||||
byte_start=start,
|
||||
byte_end=end,
|
||||
source_sha256=source_sha,
|
||||
region="math_env",
|
||||
),
|
||||
kind=SegmentKind.MATH,
|
||||
text=match.group(0).decode("utf-8", errors="replace"),
|
||||
)
|
||||
311
core_ingest/types.py
Normal file
311
core_ingest/types.py
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
"""
|
||||
Canonical type definitions for the core_ingest governance layer.
|
||||
|
||||
All dataclasses are frozen and immutable. Packets are proposed, validated,
|
||||
and exported — never modified in place.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum, auto
|
||||
from typing import Any
|
||||
|
||||
from core_ingest.pressure import make_pressure_id, make_semantic_key
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Enumerations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class Modality(str, Enum):
|
||||
"""Source medium of the incoming signal."""
|
||||
TEXT = "text"
|
||||
CODE = "code"
|
||||
MATH = "math"
|
||||
SCRIPTURE = "scripture" # Hebrew / Koine Greek canonical texts
|
||||
VISION = "vision"
|
||||
AUDIO = "audio"
|
||||
MOTOR = "motor"
|
||||
|
||||
|
||||
class DeterminismClass(str, Enum):
|
||||
"""
|
||||
Reliability class of the proposing instrument.
|
||||
|
||||
D0 Fully deterministic: pinned inputs, pinned code, identical output
|
||||
on every run. Auto-accept eligible.
|
||||
D1 Deterministic with a pinned external model artifact (e.g. a frozen
|
||||
embedding table at a fixed SHA). Auto-accept eligible.
|
||||
D2 Nondeterministic but replay-captured (output was logged and the log
|
||||
is being replayed). Requires review.
|
||||
D3 External unpinned model or API (live LLM call, external service).
|
||||
Requires review.
|
||||
D4 Human / operator proposal. Requires review.
|
||||
"""
|
||||
D0 = "D0"
|
||||
D1 = "D1"
|
||||
D2 = "D2"
|
||||
D3 = "D3"
|
||||
D4 = "D4"
|
||||
|
||||
@property
|
||||
def auto_accept_eligible(self) -> bool:
|
||||
return self in (DeterminismClass.D0, DeterminismClass.D1)
|
||||
|
||||
|
||||
class ReviewLevel(str, Enum):
|
||||
AUTO_REJECT = "auto_reject"
|
||||
AUTO_ACCEPT_ELIGIBLE = "auto_accept_eligible"
|
||||
OPERATOR_REVIEW_REQUIRED = "operator_review_required"
|
||||
ARCHITECT_REVIEW_REQUIRED = "architect_review_required"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Provenance
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SourceSpan:
|
||||
"""
|
||||
Exact location of a candidate within its source document.
|
||||
|
||||
byte_start / byte_end — byte offsets in the source bytestring
|
||||
page — page number (1-indexed), None for non-paginated
|
||||
region — structural region label (e.g. "heading", "body",
|
||||
"verse:GEN.1.1", "code_block", "math_env")
|
||||
source_sha256 — SHA-256 hex digest of the full source document
|
||||
at ingest time, for provenance anchoring
|
||||
"""
|
||||
byte_start: int
|
||||
byte_end: int
|
||||
source_sha256: str
|
||||
page: int | None = None
|
||||
region: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.byte_end <= self.byte_start:
|
||||
raise ValueError(
|
||||
f"SourceSpan byte_end ({self.byte_end}) must be > "
|
||||
f"byte_start ({self.byte_start})"
|
||||
)
|
||||
if len(self.source_sha256) != 64:
|
||||
raise ValueError(
|
||||
"source_sha256 must be a 64-character hex SHA-256 digest"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FrontendTrace:
|
||||
"""
|
||||
Identity and determinism class of the instrument that proposed a packet.
|
||||
|
||||
instrument_id — stable, unique identifier for the proposing instrument
|
||||
(e.g. "StructuralSegmenter/prose/v1",
|
||||
"StructuralSegmenter/scripture-he/v1")
|
||||
determinism — DeterminismClass of this instrument
|
||||
version — semantic version string of the instrument
|
||||
"""
|
||||
instrument_id: str
|
||||
determinism: DeterminismClass
|
||||
version: str
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Candidate packet
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CandidateGeometricPressure:
|
||||
"""
|
||||
A single proposed evidence packet at the input-to-pressure boundary.
|
||||
|
||||
Every piece of incoming information — text, code, scripture, math —
|
||||
is lifted into this envelope before any validation or gate interaction.
|
||||
|
||||
Fields
|
||||
------
|
||||
kind — claim type label (free string, e.g. "assertion",
|
||||
"definition", "verse", "theorem")
|
||||
modality — source medium (Modality enum)
|
||||
provenance — tuple of SourceSpan records (at least one required)
|
||||
frontend — identity + determinism class of the proposing instrument
|
||||
review_level — governance disposition for this packet
|
||||
confidence — probability in [0.0, 1.0]: how confident the instrument is
|
||||
uncertainty — probability in [0.0, 1.0]: explicit epistemic uncertainty
|
||||
lemma — canonical surface form of the primary term (for semantic
|
||||
key computation; empty string if not applicable)
|
||||
subject — SVO subject (empty string if not applicable)
|
||||
verb — SVO verb (empty string if not applicable)
|
||||
object_ — SVO object (empty string if not applicable)
|
||||
payload_json — structured claim content as a JSON string, normalized to
|
||||
canonical (sorted-keys, no-whitespace) form on construction
|
||||
|
||||
Computed in __post_init__
|
||||
-------------------------
|
||||
pressure_id — SHA-256 over the full canonical packet (structural identity)
|
||||
semantic_key — SHA-256 over semantic fields only (claim-level identity;
|
||||
two packets with the same semantic_key assert the same claim
|
||||
regardless of provenance or instrument)
|
||||
|
||||
Invariants (enforced at construction time)
|
||||
------------------------------------------
|
||||
- A D2–D4 frontend is forbidden from claiming AUTO_ACCEPT_ELIGIBLE.
|
||||
- confidence and uncertainty must be in [0.0, 1.0].
|
||||
- provenance must be non-empty.
|
||||
- payload_json must be valid JSON.
|
||||
"""
|
||||
kind: str
|
||||
modality: Modality
|
||||
provenance: tuple[SourceSpan, ...]
|
||||
frontend: FrontendTrace
|
||||
review_level: ReviewLevel
|
||||
confidence: float
|
||||
uncertainty: float
|
||||
lemma: str = ""
|
||||
subject: str = ""
|
||||
verb: str = ""
|
||||
object_: str = ""
|
||||
payload_json: str = "{}"
|
||||
|
||||
# Computed — set by __post_init__, declared as class-level defaults
|
||||
# to satisfy the frozen dataclass protocol.
|
||||
pressure_id: str = field(default="", init=False, compare=False, hash=False)
|
||||
semantic_key: str = field(default="", init=False, compare=False, hash=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# Confidence bounds
|
||||
if not (0.0 <= self.confidence <= 1.0):
|
||||
raise ValueError(f"confidence must be in [0.0, 1.0], got {self.confidence}")
|
||||
if not (0.0 <= self.uncertainty <= 1.0):
|
||||
raise ValueError(f"uncertainty must be in [0.0, 1.0], got {self.uncertainty}")
|
||||
|
||||
# Provenance non-empty
|
||||
if not self.provenance:
|
||||
raise ValueError("provenance must contain at least one SourceSpan")
|
||||
|
||||
# payload_json must be valid JSON; normalize to canonical form
|
||||
try:
|
||||
parsed = json.loads(self.payload_json)
|
||||
canonical = json.dumps(parsed, sort_keys=True, separators=(",", ":"))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"payload_json is not valid JSON: {exc}") from exc
|
||||
# Bypass frozen to set the normalized payload and computed fields
|
||||
object.__setattr__(self, "payload_json", canonical)
|
||||
|
||||
# Governance invariant: D2-D4 cannot claim AUTO_ACCEPT_ELIGIBLE
|
||||
if (
|
||||
not self.frontend.determinism.auto_accept_eligible
|
||||
and self.review_level == ReviewLevel.AUTO_ACCEPT_ELIGIBLE
|
||||
):
|
||||
raise ValueError(
|
||||
f"A {self.frontend.determinism} frontend cannot claim "
|
||||
"AUTO_ACCEPT_ELIGIBLE. Assign OPERATOR_REVIEW_REQUIRED or "
|
||||
"ARCHITECT_REVIEW_REQUIRED instead."
|
||||
)
|
||||
|
||||
# Compute content-addressed identifiers
|
||||
pid = make_pressure_id(
|
||||
kind=self.kind,
|
||||
modality=self.modality.value,
|
||||
provenance=[(s.byte_start, s.byte_end, s.source_sha256) for s in self.provenance],
|
||||
frontend_id=self.frontend.instrument_id,
|
||||
determinism=self.frontend.determinism.value,
|
||||
review_level=self.review_level.value,
|
||||
confidence=self.confidence,
|
||||
uncertainty=self.uncertainty,
|
||||
lemma=self.lemma,
|
||||
subject=self.subject,
|
||||
verb=self.verb,
|
||||
object_=self.object_,
|
||||
payload_json=self.payload_json,
|
||||
)
|
||||
sk = make_semantic_key(
|
||||
kind=self.kind,
|
||||
modality=self.modality.value,
|
||||
lemma=self.lemma,
|
||||
subject=self.subject,
|
||||
verb=self.verb,
|
||||
object_=self.object_,
|
||||
payload_json=self.payload_json,
|
||||
)
|
||||
object.__setattr__(self, "pressure_id", pid)
|
||||
object.__setattr__(self, "semantic_key", sk)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Validation outputs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class GateDisposition(str, Enum):
|
||||
ACCEPTED = "accepted"
|
||||
REJECTED_PROVENANCE = "rejected_provenance"
|
||||
REJECTED_SEMANTIC = "rejected_semantic"
|
||||
REJECTED_GOVERNANCE = "rejected_governance"
|
||||
REVIEW_REQUIRED = "review_required"
|
||||
OVERRIDE_ACCEPTED = "override_accepted" # ReviewDecision authorized
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ValidationResult:
|
||||
"""
|
||||
Per-packet disposition from the IngestCompiler.
|
||||
|
||||
The original packet is referenced by pressure_id, never mutated.
|
||||
warnings may contain 'semantic_convergence:<n>_prior_sources' when
|
||||
multiple independent packets share the same semantic_key.
|
||||
"""
|
||||
pressure_id: str
|
||||
semantic_key: str
|
||||
disposition: GateDisposition
|
||||
gate_failed: str | None = None # "provenance", "semantic", "governance"
|
||||
failure_reason: str | None = None
|
||||
warnings: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ValidationReport:
|
||||
"""
|
||||
Full output of one IngestCompiler.compile() call.
|
||||
|
||||
results — one ValidationResult per input packet, in input order
|
||||
accepted_ids — pressure_ids of packets that passed all three gates
|
||||
rejected_ids — pressure_ids of rejected packets
|
||||
review_ids — pressure_ids that require human/operator review
|
||||
"""
|
||||
results: tuple[ValidationResult, ...]
|
||||
accepted_ids: frozenset[str]
|
||||
rejected_ids: frozenset[str]
|
||||
review_ids: frozenset[str]
|
||||
|
||||
@property
|
||||
def acceptance_rate(self) -> float:
|
||||
total = len(self.results)
|
||||
return len(self.accepted_ids) / total if total else 0.0
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LearningArtifact:
|
||||
"""
|
||||
A governance-cleared packet exported to the train/ layer.
|
||||
|
||||
Carries the original immutable packet plus the validation result that
|
||||
authorized its export. The train/ layer must not modify either field.
|
||||
"""
|
||||
packet: CandidateGeometricPressure
|
||||
result: ValidationResult
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ReviewDecision:
|
||||
"""
|
||||
An operator or architect decision to authorize a packet that would
|
||||
otherwise be blocked by a review_required disposition.
|
||||
|
||||
Does not mutate the original packet. The IngestCompiler checks the
|
||||
authorized_ids set before applying the GovernanceGate rejection.
|
||||
"""
|
||||
authorized_ids: frozenset[str] # pressure_ids authorized for acceptance
|
||||
authorized_by: str # operator or architect identifier
|
||||
reason: str
|
||||
342
tests/test_core_ingest.py
Normal file
342
tests/test_core_ingest.py
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
"""
|
||||
Tests for core_ingest/ governance layer.
|
||||
|
||||
Covers:
|
||||
- CandidateGeometricPressure construction and invariants
|
||||
- content-addressing (pressure_id, semantic_key)
|
||||
- IngestCompiler three-gate pipeline
|
||||
- structural deduplication and convergent-evidence detection
|
||||
- ReviewDecision override path
|
||||
- StructuralSegmenter (prose, scripture, code, math)
|
||||
- SegmentManifold registration and lookup
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import pytest
|
||||
|
||||
from core_ingest.types import (
|
||||
CandidateGeometricPressure,
|
||||
DeterminismClass,
|
||||
FrontendTrace,
|
||||
GateDisposition,
|
||||
LearningArtifact,
|
||||
Modality,
|
||||
ReviewDecision,
|
||||
ReviewLevel,
|
||||
SourceSpan,
|
||||
)
|
||||
from core_ingest.compiler import IngestCompiler
|
||||
from core_ingest.segmenter import SegmentKind, StructuralSegmenter
|
||||
from core_ingest.manifold import SegmentManifold
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _sha256(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
SOURCE_BYTES = b"In the beginning God created the heavens and the earth."
|
||||
SOURCE_SHA = _sha256(SOURCE_BYTES)
|
||||
|
||||
|
||||
def make_span(
|
||||
start: int = 0,
|
||||
end: int = 10,
|
||||
sha: str = SOURCE_SHA,
|
||||
region: str | None = "body",
|
||||
) -> SourceSpan:
|
||||
return SourceSpan(
|
||||
byte_start=start,
|
||||
byte_end=end,
|
||||
source_sha256=sha,
|
||||
region=region,
|
||||
)
|
||||
|
||||
|
||||
def make_frontend(
|
||||
det: DeterminismClass = DeterminismClass.D0,
|
||||
iid: str = "StructuralSegmenter/prose/v1",
|
||||
) -> FrontendTrace:
|
||||
return FrontendTrace(
|
||||
instrument_id=iid,
|
||||
determinism=det,
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
|
||||
def make_packet(
|
||||
review_level: ReviewLevel = ReviewLevel.AUTO_ACCEPT_ELIGIBLE,
|
||||
det: DeterminismClass = DeterminismClass.D0,
|
||||
lemma: str = "beginning",
|
||||
payload: dict | None = None,
|
||||
kind: str = "assertion",
|
||||
modality: Modality = Modality.TEXT,
|
||||
span_start: int = 0,
|
||||
span_end: int = 10,
|
||||
) -> CandidateGeometricPressure:
|
||||
return CandidateGeometricPressure(
|
||||
kind=kind,
|
||||
modality=modality,
|
||||
provenance=(make_span(span_start, span_end),),
|
||||
frontend=make_frontend(det),
|
||||
review_level=review_level,
|
||||
confidence=0.95,
|
||||
uncertainty=0.05,
|
||||
lemma=lemma,
|
||||
payload_json=json.dumps(payload or {"text": "In the beginning"}),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CandidateGeometricPressure
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestCandidatePacket:
|
||||
|
||||
def test_construction_d0(self):
|
||||
p = make_packet()
|
||||
assert p.pressure_id != ""
|
||||
assert p.semantic_key != ""
|
||||
assert len(p.pressure_id) == 64
|
||||
assert len(p.semantic_key) == 64
|
||||
|
||||
def test_payload_normalized(self):
|
||||
# Unsorted keys should be canonicalized
|
||||
p = make_packet(payload={"z": 1, "a": 2})
|
||||
parsed = json.loads(p.payload_json)
|
||||
assert list(parsed.keys()) == sorted(parsed.keys())
|
||||
|
||||
def test_same_claim_same_semantic_key(self):
|
||||
# Two packets with same semantic fields but different provenance
|
||||
p1 = make_packet(span_start=0, span_end=10)
|
||||
p2 = make_packet(span_start=20, span_end=30)
|
||||
assert p1.semantic_key == p2.semantic_key
|
||||
assert p1.pressure_id != p2.pressure_id
|
||||
|
||||
def test_governance_invariant_d3_cannot_claim_auto_accept(self):
|
||||
with pytest.raises(ValueError, match="AUTO_ACCEPT_ELIGIBLE"):
|
||||
make_packet(
|
||||
det=DeterminismClass.D3,
|
||||
review_level=ReviewLevel.AUTO_ACCEPT_ELIGIBLE,
|
||||
)
|
||||
|
||||
def test_confidence_out_of_range(self):
|
||||
with pytest.raises(ValueError, match="confidence"):
|
||||
CandidateGeometricPressure(
|
||||
kind="assertion",
|
||||
modality=Modality.TEXT,
|
||||
provenance=(make_span(),),
|
||||
frontend=make_frontend(),
|
||||
review_level=ReviewLevel.AUTO_ACCEPT_ELIGIBLE,
|
||||
confidence=1.5,
|
||||
uncertainty=0.0,
|
||||
lemma="test",
|
||||
payload_json='{"text": "test"}',
|
||||
)
|
||||
|
||||
def test_empty_provenance_rejected(self):
|
||||
with pytest.raises(ValueError, match="provenance"):
|
||||
CandidateGeometricPressure(
|
||||
kind="assertion",
|
||||
modality=Modality.TEXT,
|
||||
provenance=(),
|
||||
frontend=make_frontend(),
|
||||
review_level=ReviewLevel.AUTO_ACCEPT_ELIGIBLE,
|
||||
confidence=0.9,
|
||||
uncertainty=0.1,
|
||||
lemma="test",
|
||||
payload_json='{"text": "test"}',
|
||||
)
|
||||
|
||||
def test_invalid_payload_json_rejected(self):
|
||||
with pytest.raises(ValueError, match="payload_json"):
|
||||
CandidateGeometricPressure(
|
||||
kind="assertion",
|
||||
modality=Modality.TEXT,
|
||||
provenance=(make_span(),),
|
||||
frontend=make_frontend(),
|
||||
review_level=ReviewLevel.AUTO_ACCEPT_ELIGIBLE,
|
||||
confidence=0.9,
|
||||
uncertainty=0.1,
|
||||
lemma="test",
|
||||
payload_json="not valid json{",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# IngestCompiler
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestIngestCompiler:
|
||||
|
||||
def setup_method(self):
|
||||
self.compiler = IngestCompiler()
|
||||
|
||||
def test_d0_packet_accepted(self):
|
||||
p = make_packet()
|
||||
report, artifacts = self.compiler.compile([p])
|
||||
assert p.pressure_id in report.accepted_ids
|
||||
assert len(artifacts) == 1
|
||||
assert isinstance(artifacts[0], LearningArtifact)
|
||||
|
||||
def test_d3_operator_review_required(self):
|
||||
p = make_packet(
|
||||
det=DeterminismClass.D3,
|
||||
review_level=ReviewLevel.OPERATOR_REVIEW_REQUIRED,
|
||||
)
|
||||
report, artifacts = self.compiler.compile([p])
|
||||
assert p.pressure_id in report.review_ids
|
||||
assert len(artifacts) == 0
|
||||
|
||||
def test_auto_reject_always_rejected(self):
|
||||
p = make_packet(
|
||||
det=DeterminismClass.D0,
|
||||
review_level=ReviewLevel.AUTO_REJECT,
|
||||
)
|
||||
report, artifacts = self.compiler.compile([p])
|
||||
assert p.pressure_id in report.rejected_ids
|
||||
|
||||
def test_structural_deduplication(self):
|
||||
p = make_packet()
|
||||
report, artifacts = self.compiler.compile([p, p])
|
||||
# First accepted, second rejected as duplicate
|
||||
assert len(report.accepted_ids) == 1
|
||||
assert len(report.rejected_ids) == 1
|
||||
|
||||
def test_convergent_evidence_warning(self):
|
||||
# Two packets same semantic claim, different provenance
|
||||
p1 = make_packet(span_start=0, span_end=10)
|
||||
p2 = make_packet(span_start=20, span_end=30)
|
||||
assert p1.semantic_key == p2.semantic_key
|
||||
report, _ = self.compiler.compile([p1, p2])
|
||||
# Second packet should carry a convergence warning
|
||||
result_p2 = next(r for r in report.results if r.pressure_id == p2.pressure_id)
|
||||
assert any("semantic_convergence" in w for w in result_p2.warnings)
|
||||
|
||||
def test_review_decision_override(self):
|
||||
p = make_packet(
|
||||
det=DeterminismClass.D4,
|
||||
review_level=ReviewLevel.ARCHITECT_REVIEW_REQUIRED,
|
||||
)
|
||||
decision = ReviewDecision(
|
||||
authorized_ids=frozenset({p.pressure_id}),
|
||||
authorized_by="joshua.shay",
|
||||
reason="Manually reviewed and approved",
|
||||
)
|
||||
report, artifacts = self.compiler.compile([p], review_decision=decision)
|
||||
assert p.pressure_id in report.accepted_ids
|
||||
assert len(artifacts) == 1
|
||||
|
||||
def test_acceptance_rate(self):
|
||||
p1 = make_packet(span_start=0, span_end=10)
|
||||
p2 = make_packet(
|
||||
det=DeterminismClass.D4,
|
||||
review_level=ReviewLevel.OPERATOR_REVIEW_REQUIRED,
|
||||
span_start=20, span_end=30,
|
||||
)
|
||||
report, _ = self.compiler.compile([p1, p2])
|
||||
assert report.acceptance_rate == 0.5
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# StructuralSegmenter
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestStructuralSegmenter:
|
||||
|
||||
def setup_method(self):
|
||||
self.seg = StructuralSegmenter()
|
||||
|
||||
def test_prose_segments_non_empty(self):
|
||||
source = b"# Heading\n\nFirst paragraph.\n\nSecond paragraph."
|
||||
segments = self.seg.segment(source, modality_hint="prose")
|
||||
assert len(segments) >= 2
|
||||
|
||||
def test_prose_heading_detected(self):
|
||||
source = b"# In the Beginning\n\nGod created the heavens."
|
||||
segments = self.seg.segment(source, modality_hint="prose")
|
||||
kinds = [s.kind for s in segments]
|
||||
assert SegmentKind.HEADING in kinds
|
||||
|
||||
def test_scripture_verse_detected(self):
|
||||
source = b"Gen 1:1 In the beginning God created.\nGen 1:2 The earth was formless."
|
||||
segments = self.seg.segment(source, modality_hint="scripture")
|
||||
assert len(segments) >= 1
|
||||
for seg in segments:
|
||||
assert seg.kind == SegmentKind.VERSE
|
||||
assert "verse:" in seg.span.region
|
||||
|
||||
def test_code_block_extracted(self):
|
||||
source = b"Some prose.\n\n```python\nprint('logos')\n```\n\nMore prose."
|
||||
segments = self.seg.segment(source, modality_hint="code")
|
||||
assert len(segments) == 1
|
||||
assert segments[0].kind == SegmentKind.CODE
|
||||
|
||||
def test_math_env_extracted(self):
|
||||
source = rb"Let \[E = mc^2\] be the energy equation."
|
||||
segments = self.seg.segment(source, modality_hint="math")
|
||||
assert len(segments) == 1
|
||||
assert segments[0].kind == SegmentKind.MATH
|
||||
|
||||
def test_span_byte_offsets_valid(self):
|
||||
source = b"# Title\n\nBody text here."
|
||||
for seg in self.seg.segment(source, modality_hint="prose"):
|
||||
assert seg.span.byte_start >= 0
|
||||
assert seg.span.byte_end > seg.span.byte_start
|
||||
assert seg.span.byte_end <= len(source)
|
||||
|
||||
def test_source_sha256_consistent(self):
|
||||
source = b"Consistent source."
|
||||
expected_sha = _sha256(source)
|
||||
for seg in self.seg.segment(source, modality_hint="prose"):
|
||||
assert seg.span.source_sha256 == expected_sha
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SegmentManifold
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestSegmentManifold:
|
||||
|
||||
def test_register_and_lookup(self):
|
||||
manifold = SegmentManifold()
|
||||
p = make_packet()
|
||||
manifold.register([p])
|
||||
entries = manifold.lookup(p.semantic_key)
|
||||
assert len(entries) == 1
|
||||
assert entries[0].pressure_id == p.pressure_id
|
||||
|
||||
def test_spans_for_returns_all_spans(self):
|
||||
manifold = SegmentManifold()
|
||||
p1 = make_packet(span_start=0, span_end=10)
|
||||
p2 = make_packet(span_start=20, span_end=30)
|
||||
# Same semantic_key — different structural identity
|
||||
assert p1.semantic_key == p2.semantic_key
|
||||
manifold.register([p1, p2])
|
||||
spans = manifold.spans_for(p1.semantic_key)
|
||||
assert len(spans) == 2
|
||||
|
||||
def test_len(self):
|
||||
manifold = SegmentManifold()
|
||||
p1 = make_packet(span_start=0, span_end=10)
|
||||
p2 = make_packet(span_start=20, span_end=30, lemma="earth")
|
||||
manifold.register([p1, p2])
|
||||
# p1 and p2 have different semantic keys (different lemma)
|
||||
assert len(manifold) == 2
|
||||
|
||||
def test_contains(self):
|
||||
manifold = SegmentManifold()
|
||||
p = make_packet()
|
||||
assert p.semantic_key not in manifold
|
||||
manifold.register([p])
|
||||
assert p.semantic_key in manifold
|
||||
|
||||
def test_lookup_missing_key(self):
|
||||
manifold = SegmentManifold()
|
||||
assert manifold.lookup("nonexistent_key") == []
|
||||
Loading…
Reference in a new issue