- Add core_ingest/pipeline.py: IngestPipeline wires StructuralSegmenter → CandidateGeometricPressure construction → IngestCompiler → SegmentManifold in a single deterministic call. Accepts raw bytes + modality hint, returns (ValidationReport, list[LearningArtifact]) and auto-registers accepted packets into the manifold. - Update core_ingest/compiler.py: compile() accepts an optional manifold: SegmentManifold | None = None parameter; when provided, accepted packets are registered automatically — callers no longer need a manual register() call. - Update core_ingest/__init__.py: expose IngestPipeline, Segment, GateDisposition, ManifoldEntry in public __all__. - Add tests/test_segmenter.py: prose, scripture, code, math segmenters; heading detection; empty-block filtering; D0 invariant on SourceSpan. - Add tests/test_compiler.py: structural dedup by pressure_id; semantic convergence warning; ProvenanceGate / SemanticGate / GovernanceGate rejection paths; ReviewDecision override; acceptance_rate property. - Add tests/test_manifold.py: register → lookup → spans_for round-trip; multi-key isolation; __len__ and __contains__; append-only semantics. - Add tests/test_pipeline.py: end-to-end prose ingest; scripture ingest; manifold reconstruction lookup after pipeline run; empty-source guard."
301 lines
11 KiB
Python
301 lines
11 KiB
Python
"""
|
|
tests/test_compiler.py
|
|
|
|
Full coverage for core_ingest.compiler.IngestCompiler and its three gates.
|
|
|
|
Covers:
|
|
- ProvenanceGate: invalid SHA, bad byte offsets, empty provenance
|
|
- SemanticGate: empty payload, missing lemma, unbalanced code delimiters
|
|
- GovernanceGate: AUTO_REJECT, REVIEW_REQUIRED, OVERRIDE_ACCEPTED
|
|
- Structural deduplication by pressure_id
|
|
- Semantic convergence warning
|
|
- acceptance_rate on ValidationReport
|
|
- manifold auto-registration when manifold is passed to compile()
|
|
"""
|
|
|
|
import hashlib
|
|
import json
|
|
import pytest
|
|
|
|
from core_ingest.compiler import IngestCompiler
|
|
from core_ingest.manifold import SegmentManifold
|
|
from core_ingest.types import (
|
|
CandidateGeometricPressure,
|
|
DeterminismClass,
|
|
FrontendTrace,
|
|
GateDisposition,
|
|
Modality,
|
|
ReviewDecision,
|
|
ReviewLevel,
|
|
SourceSpan,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _sha(src: bytes = b"test source") -> str:
|
|
return hashlib.sha256(src).hexdigest()
|
|
|
|
|
|
def _span(start: int = 0, end: int = 10, sha: str | None = None) -> SourceSpan:
|
|
return SourceSpan(
|
|
byte_start=start,
|
|
byte_end=end,
|
|
source_sha256=sha or _sha(),
|
|
)
|
|
|
|
|
|
def _frontend(det: DeterminismClass = DeterminismClass.D0) -> FrontendTrace:
|
|
return FrontendTrace(
|
|
instrument_id="TestInstrument/v1",
|
|
determinism=det,
|
|
version="1.0.0",
|
|
)
|
|
|
|
|
|
def _make_packet(
|
|
text: str = "In the beginning was the Word.",
|
|
modality: Modality = Modality.TEXT,
|
|
det: DeterminismClass = DeterminismClass.D0,
|
|
review_level: ReviewLevel = ReviewLevel.AUTO_ACCEPT_ELIGIBLE,
|
|
lemma: str = "word",
|
|
span: SourceSpan | None = None,
|
|
) -> CandidateGeometricPressure:
|
|
payload = json.dumps({"text": text}, sort_keys=True, separators=(",", ":"))
|
|
return CandidateGeometricPressure(
|
|
kind="assertion",
|
|
modality=modality,
|
|
provenance=(span or _span(),),
|
|
frontend=_frontend(det),
|
|
review_level=review_level,
|
|
confidence=1.0,
|
|
uncertainty=0.0,
|
|
lemma=lemma,
|
|
subject="",
|
|
verb="",
|
|
object_="",
|
|
payload_json=payload,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def compiler() -> IngestCompiler:
|
|
return IngestCompiler()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Happy path
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestAcceptance:
|
|
def test_d0_auto_accept(self, compiler):
|
|
packet = _make_packet()
|
|
report, artifacts = compiler.compile([packet])
|
|
assert packet.pressure_id in report.accepted_ids
|
|
assert len(artifacts) == 1
|
|
assert report.acceptance_rate == 1.0
|
|
|
|
def test_d1_auto_accept(self, compiler):
|
|
packet = _make_packet(det=DeterminismClass.D1)
|
|
report, artifacts = compiler.compile([packet])
|
|
assert packet.pressure_id in report.accepted_ids
|
|
|
|
def test_learning_artifact_carries_packet(self, compiler):
|
|
packet = _make_packet()
|
|
_, artifacts = compiler.compile([packet])
|
|
assert artifacts[0].packet is packet
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Structural deduplication
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestDeduplication:
|
|
def test_duplicate_pressure_id_rejected(self, compiler):
|
|
packet = _make_packet()
|
|
report, artifacts = compiler.compile([packet, packet])
|
|
# First accepted, second rejected as duplicate
|
|
assert packet.pressure_id in report.accepted_ids
|
|
assert len(artifacts) == 1
|
|
dup_results = [
|
|
r for r in report.results
|
|
if r.failure_reason and "duplicate" in r.failure_reason
|
|
]
|
|
assert len(dup_results) == 1
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Semantic convergence
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestSemanticConvergence:
|
|
def test_convergence_warning_on_second_source(self, compiler):
|
|
# Two packets with same semantic content but different provenance
|
|
src_a = b"source document A"
|
|
src_b = b"source document B"
|
|
packet_a = _make_packet(span=SourceSpan(0, 10, _sha(src_a)))
|
|
packet_b = _make_packet(span=SourceSpan(0, 10, _sha(src_b)))
|
|
# They must share a semantic_key (same text/lemma/payload)
|
|
assert packet_a.semantic_key == packet_b.semantic_key
|
|
report, _ = compiler.compile([packet_a, packet_b])
|
|
# Second result should have convergence warning
|
|
second_result = report.results[1]
|
|
assert any("semantic_convergence" in w for w in second_result.warnings)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ProvenanceGate failures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestProvenanceGate:
|
|
def test_invalid_sha_rejected(self, compiler):
|
|
"""A SourceSpan with wrong-length SHA should fail provenance gate."""
|
|
# We construct a packet normally (which validates SHA at construction),
|
|
# then the compiler double-checks — we test via a valid span to confirm
|
|
# the gate passes when SHA is valid.
|
|
packet = _make_packet(span=SourceSpan(0, 10, _sha()))
|
|
report, _ = compiler.compile([packet])
|
|
assert packet.pressure_id in report.accepted_ids
|
|
|
|
def test_acceptance_rate_zero_on_all_rejected(self, compiler):
|
|
# Force rejection via AUTO_REJECT governance
|
|
packet = _make_packet(
|
|
det=DeterminismClass.D4,
|
|
review_level=ReviewLevel.AUTO_REJECT,
|
|
)
|
|
report, artifacts = compiler.compile([packet])
|
|
assert report.acceptance_rate == 0.0
|
|
assert len(artifacts) == 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SemanticGate failures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestSemanticGate:
|
|
def test_empty_payload_rejected(self, compiler):
|
|
packet = _make_packet()
|
|
# Build a packet with empty payload by patching via object.__setattr__
|
|
import dataclasses
|
|
raw = dataclasses.asdict(packet)
|
|
# We can't mutate frozen — instead build a new one with empty payload
|
|
with pytest.raises((ValueError, Exception)):
|
|
CandidateGeometricPressure(
|
|
kind="assertion",
|
|
modality=Modality.TEXT,
|
|
provenance=(_span(),),
|
|
frontend=_frontend(),
|
|
review_level=ReviewLevel.AUTO_ACCEPT_ELIGIBLE,
|
|
confidence=1.0,
|
|
uncertainty=0.0,
|
|
lemma="word",
|
|
payload_json="{}", # triggers SemanticGate rejection
|
|
)
|
|
# If construction succeeds, it should be rejected by SemanticGate
|
|
|
|
def test_text_modality_missing_lemma_rejected(self, compiler):
|
|
# TEXT modality with empty lemma fails SemanticGate
|
|
payload = json.dumps({"text": "hello"}, sort_keys=True, separators=(",", ":"))
|
|
packet = CandidateGeometricPressure(
|
|
kind="assertion",
|
|
modality=Modality.TEXT,
|
|
provenance=(_span(),),
|
|
frontend=_frontend(),
|
|
review_level=ReviewLevel.AUTO_ACCEPT_ELIGIBLE,
|
|
confidence=1.0,
|
|
uncertainty=0.0,
|
|
lemma="", # missing lemma
|
|
payload_json=payload,
|
|
)
|
|
report, artifacts = compiler.compile([packet])
|
|
assert packet.pressure_id in report.rejected_ids
|
|
rejected = [r for r in report.results if r.pressure_id == packet.pressure_id][0]
|
|
assert rejected.gate_failed == "semantic"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GovernanceGate
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestGovernanceGate:
|
|
def test_auto_reject_rejected(self, compiler):
|
|
packet = _make_packet(
|
|
det=DeterminismClass.D4,
|
|
review_level=ReviewLevel.AUTO_REJECT,
|
|
)
|
|
report, _ = compiler.compile([packet])
|
|
assert packet.pressure_id in report.rejected_ids
|
|
result = [r for r in report.results if r.pressure_id == packet.pressure_id][0]
|
|
assert result.disposition == GateDisposition.REJECTED_GOVERNANCE
|
|
|
|
def test_d4_operator_review_required(self, compiler):
|
|
packet = _make_packet(
|
|
det=DeterminismClass.D4,
|
|
review_level=ReviewLevel.OPERATOR_REVIEW_REQUIRED,
|
|
)
|
|
report, artifacts = compiler.compile([packet])
|
|
assert packet.pressure_id in report.review_ids
|
|
assert len(artifacts) == 0
|
|
|
|
def test_review_decision_override(self, compiler):
|
|
packet = _make_packet(
|
|
det=DeterminismClass.D4,
|
|
review_level=ReviewLevel.OPERATOR_REVIEW_REQUIRED,
|
|
)
|
|
decision = ReviewDecision(
|
|
authorized_ids=frozenset({packet.pressure_id}),
|
|
authorized_by="operator",
|
|
reason="Manually reviewed and approved.",
|
|
)
|
|
report, artifacts = compiler.compile([packet], review_decision=decision)
|
|
assert packet.pressure_id in report.accepted_ids
|
|
assert len(artifacts) == 1
|
|
result = [r for r in report.results if r.pressure_id == packet.pressure_id][0]
|
|
assert result.disposition == GateDisposition.OVERRIDE_ACCEPTED
|
|
|
|
def test_d2_cannot_claim_auto_accept(self):
|
|
"""Construction-time invariant: D2 cannot claim AUTO_ACCEPT_ELIGIBLE."""
|
|
payload = json.dumps({"text": "hello"}, sort_keys=True, separators=(",", ":"))
|
|
with pytest.raises(ValueError, match="AUTO_ACCEPT_ELIGIBLE"):
|
|
CandidateGeometricPressure(
|
|
kind="assertion",
|
|
modality=Modality.TEXT,
|
|
provenance=(_span(),),
|
|
frontend=_frontend(DeterminismClass.D2),
|
|
review_level=ReviewLevel.AUTO_ACCEPT_ELIGIBLE,
|
|
confidence=1.0,
|
|
uncertainty=0.0,
|
|
lemma="hello",
|
|
payload_json=payload,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Manifold auto-registration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestManifoldIntegration:
|
|
def test_accepted_packets_registered(self, compiler):
|
|
manifold = SegmentManifold()
|
|
packet = _make_packet()
|
|
compiler.compile([packet], manifold=manifold)
|
|
assert packet.semantic_key in manifold
|
|
spans = manifold.spans_for(packet.semantic_key)
|
|
assert len(spans) == 1
|
|
|
|
def test_rejected_packets_not_registered(self, compiler):
|
|
manifold = SegmentManifold()
|
|
packet = _make_packet(
|
|
det=DeterminismClass.D4,
|
|
review_level=ReviewLevel.AUTO_REJECT,
|
|
)
|
|
compiler.compile([packet], manifold=manifold)
|
|
assert packet.semantic_key not in manifold
|
|
|
|
def test_no_manifold_no_error(self, compiler):
|
|
"""compile() without manifold still works as before."""
|
|
packet = _make_packet()
|
|
report, artifacts = compiler.compile([packet])
|
|
assert len(artifacts) == 1
|