1535 lines
70 KiB
Python
1535 lines
70 KiB
Python
#!/usr/bin/env python3
|
|
"""Ingest source-only fixtures into review staging in disposable Postgres.
|
|
|
|
The canary parses a raw document or repo-native Telegram JSONL transcript,
|
|
replays a versioned deterministic extraction sidecar, persists exact source
|
|
locations, normalizes the candidates, and stages one ``pending_review``
|
|
proposal. Representative canonical ``public.*`` rows are fingerprinted before
|
|
and after. No review, apply, network access, production target, or durable
|
|
Docker volume is used.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import uuid
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
REPO_ROOT = HERE.parent
|
|
sys.path.insert(0, str(HERE))
|
|
|
|
import apply_proposal as ap # noqa: E402
|
|
import stage_normalized_proposal as normalized_stage # noqa: E402
|
|
|
|
SCHEMA = "livingip.workingLeoLocalIngestionProposalCanary.v2"
|
|
SUITE_SCHEMA = "livingip.workingLeoLocalIngestionProposalSuite.v2"
|
|
FIXTURE_SCHEMA = "livingip.workingLeoSourceIngestionScenario.v2"
|
|
DEFAULT_DOCUMENT_FIXTURE = REPO_ROOT / "fixtures" / "working-leo" / "document-ingestion-v2.scenario.json"
|
|
DEFAULT_TELEGRAM_FIXTURE = REPO_ROOT / "fixtures" / "working-leo" / "telegram-transcript-ingestion-v1.scenario.json"
|
|
DEFAULT_FIXTURE = DEFAULT_DOCUMENT_FIXTURE
|
|
DEFAULT_FIXTURES = (DEFAULT_DOCUMENT_FIXTURE, DEFAULT_TELEGRAM_FIXTURE)
|
|
CONTAINER_LABEL = "livingip.canary=working-leo-local-ingestion"
|
|
CANONICAL_TABLES = ("claims", "sources", "claim_evidence", "claim_edges")
|
|
SEED_CLAIM_A = "00000000-0000-4000-8000-000000000101"
|
|
SEED_CLAIM_B = "00000000-0000-4000-8000-000000000102"
|
|
SEED_SOURCE = "00000000-0000-4000-8000-000000000201"
|
|
|
|
|
|
class CanaryError(RuntimeError):
|
|
"""Raised when a fixture or disposable runtime fails closed."""
|
|
|
|
|
|
def sha256_bytes(value: bytes) -> str:
|
|
return hashlib.sha256(value).hexdigest()
|
|
|
|
|
|
def canonical_json_bytes(value: Any) -> bytes:
|
|
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()
|
|
|
|
|
|
def canonical_sha256(value: Any) -> str:
|
|
return sha256_bytes(canonical_json_bytes(value))
|
|
|
|
|
|
def normalized_segment_manifest(segments: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
"""Return the complete normalized provenance identity for replay hashing."""
|
|
return [
|
|
{
|
|
"id": segment["id"],
|
|
"locator": segment["locator"],
|
|
"json_pointer": segment["json_pointer"],
|
|
"content_sha256": segment["content_sha256"],
|
|
"text_sha256": segment["text_sha256"],
|
|
}
|
|
for segment in segments
|
|
]
|
|
|
|
|
|
def replay_identity_payload(
|
|
*,
|
|
artifact_sha256: str,
|
|
artifact_format: str,
|
|
source_identity: str,
|
|
source_locator: str,
|
|
normalized_segments_sha256: str,
|
|
extractor: dict[str, str],
|
|
extraction_spec_sha256: str,
|
|
) -> dict[str, Any]:
|
|
"""Build the explicit source-to-extraction identity used by every row ID."""
|
|
return {
|
|
"artifact_sha256": artifact_sha256,
|
|
"artifact_format": artifact_format,
|
|
"source_identity": source_identity,
|
|
"source_locator": source_locator,
|
|
"normalized_segments_sha256": normalized_segments_sha256,
|
|
"extractor": extractor,
|
|
"extraction_spec_sha256": extraction_spec_sha256,
|
|
}
|
|
|
|
|
|
def canonical_snapshot_complete(snapshot: dict[str, Any]) -> bool:
|
|
return set(snapshot) == set(CANONICAL_TABLES) and all(
|
|
isinstance(snapshot.get(table), list) and bool(snapshot[table]) for table in CANONICAL_TABLES
|
|
)
|
|
|
|
|
|
def stable_uuid(seed: str, label: str) -> str:
|
|
return str(uuid.uuid5(uuid.NAMESPACE_URL, f"livingip:working-leo-ingestion:{seed}:{label}"))
|
|
|
|
|
|
def _require_object(value: Any, label: str) -> dict[str, Any]:
|
|
if not isinstance(value, dict):
|
|
raise CanaryError(f"{label} must be an object")
|
|
return value
|
|
|
|
|
|
def _require_list(value: Any, label: str) -> list[Any]:
|
|
if not isinstance(value, list):
|
|
raise CanaryError(f"{label} must be a list")
|
|
return value
|
|
|
|
|
|
def _require_text(value: Any, label: str) -> str:
|
|
if not isinstance(value, str) or not value.strip():
|
|
raise CanaryError(f"{label} must be a non-empty string")
|
|
return value.strip()
|
|
|
|
|
|
def _safe_source_key(value: str) -> str:
|
|
normalized = re.sub(r"[^a-z0-9._:-]+", "_", value.lower()).strip("_")
|
|
if not normalized:
|
|
raise CanaryError(f"could not derive a stable source key from {value!r}")
|
|
return normalized[:128]
|
|
|
|
|
|
def _load_plain_text_segments(raw: bytes, source: dict[str, Any]) -> list[dict[str, Any]]:
|
|
try:
|
|
text = raw.decode("utf-8", errors="strict")
|
|
except UnicodeDecodeError as exc:
|
|
raise CanaryError(f"plain_text artifact must be strict UTF-8: {exc}") from exc
|
|
locator = _require_text(source.get("locator"), "source.locator")
|
|
segments: list[dict[str, Any]] = []
|
|
cursor = 0
|
|
for part in re.split(r"\n[ \t]*\n", text):
|
|
part_start = text.find(part, cursor)
|
|
cursor = part_start + len(part)
|
|
body = part.strip()
|
|
if not body:
|
|
continue
|
|
start = part_start + part.find(body)
|
|
index = len(segments) + 1
|
|
canonical_record = {"paragraph": index, "text": body}
|
|
segments.append(
|
|
{
|
|
"id": f"paragraph-{index}",
|
|
"body": body,
|
|
"locator": f"{locator}#paragraph-{index}",
|
|
"json_pointer": None,
|
|
"content_sha256": canonical_sha256(canonical_record),
|
|
"text_sha256": sha256_bytes(body.encode()),
|
|
"metadata": {"paragraph": index, "char_start": start, "char_end": start + len(body)},
|
|
}
|
|
)
|
|
if not segments:
|
|
raise CanaryError("plain_text artifact contains no non-empty paragraphs")
|
|
return segments
|
|
|
|
|
|
def _load_telegram_jsonl_segments(raw: bytes) -> list[dict[str, Any]]:
|
|
try:
|
|
text = raw.decode("utf-8", errors="strict")
|
|
except UnicodeDecodeError as exc:
|
|
raise CanaryError(f"telegram_jsonl artifact must be strict UTF-8: {exc}") from exc
|
|
segments: list[dict[str, Any]] = []
|
|
seen: set[tuple[int, int]] = set()
|
|
for line_number, line in enumerate(text.splitlines(), start=1):
|
|
if not line.strip():
|
|
continue
|
|
try:
|
|
record = json.loads(line)
|
|
except json.JSONDecodeError as exc:
|
|
raise CanaryError(f"telegram JSONL line {line_number} is invalid JSON: {exc}") from exc
|
|
record = _require_object(record, f"telegram JSONL line {line_number}")
|
|
chat_id = record.get("chat_id")
|
|
message_id = record.get("message_id")
|
|
message = record.get("message")
|
|
if isinstance(chat_id, bool) or not isinstance(chat_id, int):
|
|
raise CanaryError(f"telegram JSONL line {line_number}.chat_id must be an integer")
|
|
if isinstance(message_id, bool) or not isinstance(message_id, int):
|
|
raise CanaryError(f"telegram JSONL line {line_number}.message_id must be an integer")
|
|
if not isinstance(message, str) or not message.strip():
|
|
raise CanaryError(f"telegram JSONL line {line_number}.message must be non-empty")
|
|
key = (chat_id, message_id)
|
|
if key in seen:
|
|
raise CanaryError(f"telegram JSONL repeats chat/message identity {key}")
|
|
seen.add(key)
|
|
body = message.strip()
|
|
segments.append(
|
|
{
|
|
"id": f"message-{chat_id}-{message_id}",
|
|
"body": body,
|
|
"locator": f"telegram://chat/{chat_id}/message/{message_id}",
|
|
"json_pointer": f"jsonl://line/{line_number}/message",
|
|
"content_sha256": canonical_sha256(record),
|
|
"text_sha256": sha256_bytes(body.encode()),
|
|
"metadata": {
|
|
"line_number": line_number,
|
|
"ts": record.get("ts"),
|
|
"chat_id": chat_id,
|
|
"chat_title": record.get("chat_title"),
|
|
"message_id": message_id,
|
|
"type": record.get("type"),
|
|
"username": record.get("username"),
|
|
"display_name": record.get("display_name"),
|
|
"user_id": record.get("user_id"),
|
|
"reply_to": record.get("reply_to"),
|
|
"synthetic": record.get("synthetic") is True,
|
|
},
|
|
}
|
|
)
|
|
if not segments:
|
|
raise CanaryError("telegram_jsonl artifact contains no messages")
|
|
return segments
|
|
|
|
|
|
def _load_segments(raw: bytes, artifact_format: str, source: dict[str, Any]) -> list[dict[str, Any]]:
|
|
if artifact_format == "plain_text":
|
|
return _load_plain_text_segments(raw, source)
|
|
if artifact_format == "telegram_jsonl":
|
|
return _load_telegram_jsonl_segments(raw)
|
|
raise CanaryError("artifact.format must be plain_text or telegram_jsonl")
|
|
|
|
|
|
def load_fixture(path: Path) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
scenario_path = path.resolve()
|
|
try:
|
|
scenario_raw = scenario_path.read_bytes()
|
|
scenario = json.loads(scenario_raw)
|
|
except (OSError, json.JSONDecodeError) as exc:
|
|
raise CanaryError(f"scenario is not readable JSON: {exc}") from exc
|
|
if not isinstance(scenario, dict) or scenario.get("schema") != FIXTURE_SCHEMA:
|
|
raise CanaryError(f"scenario schema must be {FIXTURE_SCHEMA}")
|
|
artifact = _require_object(scenario.get("artifact"), "artifact")
|
|
source = _require_object(scenario.get("source"), "source")
|
|
extractor = _require_object(scenario.get("extractor"), "extractor")
|
|
extraction = _require_object(scenario.get("extraction"), "extraction")
|
|
artifact_rel = Path(_require_text(artifact.get("path"), "artifact.path"))
|
|
if artifact_rel.is_absolute():
|
|
raise CanaryError("artifact.path must be relative to the scenario")
|
|
artifact_path = (scenario_path.parent / artifact_rel).resolve()
|
|
try:
|
|
artifact_path.relative_to(scenario_path.parent.resolve())
|
|
except ValueError as exc:
|
|
raise CanaryError("artifact.path must remain inside the scenario directory") from exc
|
|
try:
|
|
raw = artifact_path.read_bytes()
|
|
except OSError as exc:
|
|
raise CanaryError(f"source artifact is unreadable: {exc}") from exc
|
|
if not raw:
|
|
raise CanaryError("source artifact must not be empty")
|
|
artifact_format = _require_text(artifact.get("format"), "artifact.format")
|
|
extractor_name = _require_text(extractor.get("name"), "extractor.name")
|
|
extractor_version = _require_text(extractor.get("version"), "extractor.version")
|
|
required_source = ("identity", "source_key", "source_type", "title", "locator", "metadata")
|
|
missing_source = [key for key in required_source if source.get(key) in (None, "")]
|
|
if missing_source:
|
|
raise CanaryError(f"source is missing: {', '.join(missing_source)}")
|
|
if source["source_type"] not in ap.SOURCE_TYPES:
|
|
raise CanaryError(f"source.source_type must be one of {sorted(ap.SOURCE_TYPES)}")
|
|
if not isinstance(source.get("metadata"), dict):
|
|
raise CanaryError("source.metadata must be an object")
|
|
source_identity = _require_text(source.get("identity"), "source.identity")
|
|
source_locator = _require_text(source.get("locator"), "source.locator")
|
|
source = {**source, "identity": source_identity, "locator": source_locator}
|
|
segments = _load_segments(raw, artifact_format, source)
|
|
segment_by_id = {segment["id"]: segment for segment in segments}
|
|
|
|
claims = _require_list(extraction.get("claims"), "extraction.claims")
|
|
duplicates = _require_list(extraction.get("duplicates"), "extraction.duplicates")
|
|
conflicts = _require_list(extraction.get("conflicts"), "extraction.conflicts")
|
|
if not claims:
|
|
raise CanaryError("extraction.claims must contain at least one review candidate")
|
|
claim_keys: set[str] = set()
|
|
normalized_claims: list[dict[str, Any]] = []
|
|
evidence_count = 0
|
|
for claim_index, raw_claim in enumerate(claims):
|
|
claim = _require_object(raw_claim, f"extraction.claims[{claim_index}]")
|
|
claim_key = _require_text(claim.get("claim_key"), f"extraction.claims[{claim_index}].claim_key")
|
|
if claim_key in claim_keys:
|
|
raise CanaryError(f"duplicate claim_key {claim_key}")
|
|
claim_keys.add(claim_key)
|
|
claim_type = _require_text(claim.get("type"), f"extraction.claims[{claim_index}].type")
|
|
if claim_type not in ap.CLAIM_TYPES:
|
|
raise CanaryError(f"claim {claim_key} has unsupported type {claim_type}")
|
|
confidence = claim.get("confidence")
|
|
if isinstance(confidence, bool) or not isinstance(confidence, (int, float)) or not 0 <= confidence <= 1:
|
|
raise CanaryError(f"claim {claim_key} confidence must be between 0 and 1")
|
|
evidence_rows = _require_list(claim.get("evidence"), f"claim {claim_key}.evidence")
|
|
if not evidence_rows:
|
|
raise CanaryError(f"claim {claim_key} requires evidence")
|
|
normalized_evidence: list[dict[str, Any]] = []
|
|
for evidence_index, raw_evidence in enumerate(evidence_rows):
|
|
evidence = _require_object(raw_evidence, f"claim {claim_key}.evidence[{evidence_index}]")
|
|
segment_id = _require_text(evidence.get("segment_id"), f"claim {claim_key} evidence segment_id")
|
|
segment = segment_by_id.get(segment_id)
|
|
if segment is None:
|
|
raise CanaryError(f"claim {claim_key} evidence references unknown segment {segment_id}")
|
|
excerpt = _require_text(evidence.get("excerpt"), f"claim {claim_key} evidence excerpt")
|
|
if excerpt not in segment["body"]:
|
|
raise CanaryError(f"claim {claim_key} evidence excerpt is not an exact segment substring")
|
|
role = _require_text(evidence.get("role"), f"claim {claim_key} evidence role")
|
|
if role not in ap.EVIDENCE_ROLES:
|
|
raise CanaryError(f"claim {claim_key} evidence has unsupported role {role}")
|
|
normalized_evidence.append(
|
|
{
|
|
**evidence,
|
|
"segment_id": segment_id,
|
|
"excerpt": excerpt,
|
|
"role": role,
|
|
"source_locator": segment["locator"],
|
|
"source_json_pointer": segment["json_pointer"],
|
|
"source_content_sha256": segment["content_sha256"],
|
|
"source_text_sha256": segment["text_sha256"],
|
|
}
|
|
)
|
|
evidence_count += 1
|
|
body = _require_text(claim.get("body"), f"claim {claim_key}.body")
|
|
if not any(body in segment_by_id[row["segment_id"]]["body"] for row in normalized_evidence):
|
|
raise CanaryError(f"claim {claim_key} body must be an exact substring of its evidence segments")
|
|
edges = _require_list(claim.get("edges", []), f"claim {claim_key}.edges")
|
|
normalized_claims.append(
|
|
{
|
|
**claim,
|
|
"claim_key": claim_key,
|
|
"type": claim_type,
|
|
"confidence": confidence,
|
|
"text": _require_text(claim.get("text"), f"claim {claim_key}.text"),
|
|
"body": body,
|
|
"metadata": _require_object(claim.get("metadata", {}), f"claim {claim_key}.metadata"),
|
|
"evidence": normalized_evidence,
|
|
"edges": edges,
|
|
}
|
|
)
|
|
|
|
normalized_duplicates: list[dict[str, Any]] = []
|
|
for duplicate_index, raw_duplicate in enumerate(duplicates):
|
|
duplicate = _require_object(raw_duplicate, f"extraction.duplicates[{duplicate_index}]")
|
|
segment_id = _require_text(duplicate.get("segment_id"), f"duplicate {duplicate_index}.segment_id")
|
|
target = _require_text(
|
|
duplicate.get("duplicate_of_claim_key"), f"duplicate {duplicate_index}.duplicate_of_claim_key"
|
|
)
|
|
if target not in claim_keys:
|
|
raise CanaryError(f"duplicate {duplicate_index} references unknown claim {target}")
|
|
segment = segment_by_id.get(segment_id)
|
|
if segment is None:
|
|
raise CanaryError(f"duplicate {duplicate_index} references unknown segment {segment_id}")
|
|
excerpt = _require_text(duplicate.get("excerpt"), f"duplicate {duplicate_index}.excerpt")
|
|
if excerpt not in segment["body"]:
|
|
raise CanaryError(f"duplicate {duplicate_index} excerpt is not an exact segment substring")
|
|
normalized_duplicates.append(
|
|
{
|
|
**duplicate,
|
|
"segment_id": segment_id,
|
|
"duplicate_of_claim_key": target,
|
|
"excerpt": excerpt,
|
|
"source_locator": segment["locator"],
|
|
"source_json_pointer": segment["json_pointer"],
|
|
"source_content_sha256": segment["content_sha256"],
|
|
"source_text_sha256": segment["text_sha256"],
|
|
}
|
|
)
|
|
|
|
normalized_conflicts: list[dict[str, Any]] = []
|
|
for conflict_index, raw_conflict in enumerate(conflicts):
|
|
conflict = _require_object(raw_conflict, f"extraction.conflicts[{conflict_index}]")
|
|
from_key = _require_text(conflict.get("from_claim_key"), f"conflict {conflict_index}.from_claim_key")
|
|
to_key = _require_text(conflict.get("to_claim_key"), f"conflict {conflict_index}.to_claim_key")
|
|
relationship = _require_text(conflict.get("relationship"), f"conflict {conflict_index}.relationship")
|
|
if from_key not in claim_keys or to_key not in claim_keys:
|
|
raise CanaryError(f"conflict {conflict_index} must reference produced claim keys")
|
|
if relationship != "contradicts":
|
|
raise CanaryError("fixture conflicts must use relationship=contradicts")
|
|
matching_edge = any(
|
|
claim["claim_key"] == from_key
|
|
and any(edge.get("edge_type") == relationship and edge.get("target") == to_key for edge in claim["edges"])
|
|
for claim in normalized_claims
|
|
)
|
|
if not matching_edge:
|
|
raise CanaryError(f"conflict {from_key}->{to_key} has no matching candidate edge")
|
|
normalized_conflicts.append({**conflict, "from_claim_key": from_key, "to_claim_key": to_key})
|
|
|
|
artifact_sha256 = sha256_bytes(raw)
|
|
segment_manifest = normalized_segment_manifest(segments)
|
|
source_content_sha256 = canonical_sha256(segment_manifest)
|
|
extraction_spec_sha256 = canonical_sha256(extraction)
|
|
extractor_identity = {"name": extractor_name, "version": extractor_version}
|
|
replay_components = replay_identity_payload(
|
|
artifact_sha256=artifact_sha256,
|
|
artifact_format=artifact_format,
|
|
source_identity=source_identity,
|
|
source_locator=source_locator,
|
|
normalized_segments_sha256=source_content_sha256,
|
|
extractor=extractor_identity,
|
|
extraction_spec_sha256=extraction_spec_sha256,
|
|
)
|
|
replay_identity_sha256 = canonical_sha256(replay_components)
|
|
fixture = {
|
|
**scenario,
|
|
"artifact": {**artifact, "resolved_path": str(artifact_path)},
|
|
"source": source,
|
|
"extractor": extractor_identity,
|
|
"segments": segments,
|
|
"extraction": {
|
|
"claims": normalized_claims,
|
|
"duplicates": normalized_duplicates,
|
|
"conflicts": normalized_conflicts,
|
|
},
|
|
}
|
|
receipt = {
|
|
"scenario_path": str(scenario_path),
|
|
"scenario_sha256": sha256_bytes(scenario_raw),
|
|
"artifact_path": str(artifact_path),
|
|
"artifact_format": artifact_format,
|
|
"artifact_sha256": artifact_sha256,
|
|
"source_content_sha256": source_content_sha256,
|
|
"source_identity": source_identity,
|
|
"source_locator": source_locator,
|
|
"extractor": extractor_identity,
|
|
"extraction_spec_sha256": extraction_spec_sha256,
|
|
"replay_identity_components": replay_components,
|
|
"replay_identity_sha256": replay_identity_sha256,
|
|
"counts": {
|
|
"source_segments": len(segments),
|
|
"claim_candidates": len(normalized_claims),
|
|
"evidence_candidates": evidence_count,
|
|
"duplicate_judgments": len(normalized_duplicates),
|
|
"conflict_relationships": len(normalized_conflicts),
|
|
},
|
|
}
|
|
return fixture, receipt
|
|
|
|
|
|
def build_row_ids(input_receipt: dict[str, Any], fixture: dict[str, Any]) -> dict[str, Any]:
|
|
replay_seed = input_receipt["replay_identity_sha256"]
|
|
claim_ids = {
|
|
claim["claim_key"]: stable_uuid(replay_seed, f"claim-extraction:{claim['claim_key']}")
|
|
for claim in fixture["extraction"]["claims"]
|
|
}
|
|
evidence_ids: dict[str, str] = {}
|
|
for claim in fixture["extraction"]["claims"]:
|
|
for index, _evidence in enumerate(claim["evidence"]):
|
|
key = f"{claim['claim_key']}:{index}"
|
|
evidence_ids[key] = stable_uuid(replay_seed, f"evidence-extraction:{key}")
|
|
duplicate_ids = {
|
|
str(index): stable_uuid(replay_seed, f"duplicate-assessment:{index}")
|
|
for index, _duplicate in enumerate(fixture["extraction"]["duplicates"])
|
|
}
|
|
conflict_ids = {
|
|
str(index): stable_uuid(replay_seed, f"conflict-assessment:{index}")
|
|
for index, _conflict in enumerate(fixture["extraction"]["conflicts"])
|
|
}
|
|
return {
|
|
"source_capture_id": stable_uuid(replay_seed, "source-capture"),
|
|
"claim_extraction_ids": claim_ids,
|
|
"evidence_extraction_ids": evidence_ids,
|
|
"duplicate_assessment_ids": duplicate_ids,
|
|
"conflict_assessment_ids": conflict_ids,
|
|
"parent_proposal_id": stable_uuid(replay_seed, "rich-parent-proposal"),
|
|
}
|
|
|
|
|
|
def _segment_source_key(source_key: str, segment_id: str) -> str:
|
|
return _safe_source_key(f"{source_key}__{segment_id}")
|
|
|
|
|
|
def build_parent_packet(
|
|
fixture: dict[str, Any], input_receipt: dict[str, Any], row_ids: dict[str, Any]
|
|
) -> dict[str, Any]:
|
|
source = fixture["source"]
|
|
claims = fixture["extraction"]["claims"]
|
|
segment_by_id = {segment["id"]: segment for segment in fixture["segments"]}
|
|
referenced_segment_ids: list[str] = []
|
|
for claim in claims:
|
|
for evidence in claim["evidence"]:
|
|
if evidence["segment_id"] not in referenced_segment_ids:
|
|
referenced_segment_ids.append(evidence["segment_id"])
|
|
for duplicate in fixture["extraction"]["duplicates"]:
|
|
if duplicate["segment_id"] not in referenced_segment_ids:
|
|
referenced_segment_ids.append(duplicate["segment_id"])
|
|
|
|
source_candidates = []
|
|
for segment_id in referenced_segment_ids:
|
|
segment = segment_by_id[segment_id]
|
|
segment_source_key = _segment_source_key(source["source_key"], segment_id)
|
|
provenance = {
|
|
"artifact_sha256": input_receipt["artifact_sha256"],
|
|
"segment_id": segment_id,
|
|
"locator": segment["locator"],
|
|
"json_pointer": segment["json_pointer"],
|
|
"content_sha256": segment["content_sha256"],
|
|
"text_sha256": segment["text_sha256"],
|
|
}
|
|
source_candidates.append(
|
|
{
|
|
"source_key": segment_source_key,
|
|
"source_type": "dm" if input_receipt["artifact_format"] == "telegram_jsonl" else source["source_type"],
|
|
"title": f"{source['title']} - {segment_id}",
|
|
"storage_path": segment["locator"],
|
|
"url": None,
|
|
"source_quality": json.dumps(provenance, sort_keys=True, separators=(",", ":")),
|
|
"usage_limits": "Review-only exact excerpt; no canonical apply is authorized.",
|
|
"key_excerpt": segment["body"],
|
|
"content_sha256": segment["content_sha256"],
|
|
}
|
|
)
|
|
|
|
claim_candidates = []
|
|
for claim in claims:
|
|
claim_candidates.append(
|
|
{
|
|
"claim_key": claim["claim_key"],
|
|
"type": claim["type"],
|
|
"confidence": claim["confidence"],
|
|
"text": claim["text"],
|
|
"body": claim["body"],
|
|
"evidence_tier": "source_artifact_exact_location",
|
|
"needs_research": claim["metadata"].get("needs_research", False),
|
|
"evidence": [
|
|
{
|
|
"source_key": _segment_source_key(source["source_key"], evidence["segment_id"]),
|
|
"role": evidence["role"],
|
|
}
|
|
for evidence in claim["evidence"]
|
|
],
|
|
"edges": claim["edges"],
|
|
}
|
|
)
|
|
|
|
ingestion_manifest = {
|
|
"scenario_schema": fixture["schema"],
|
|
"scenario_sha256": input_receipt["scenario_sha256"],
|
|
"artifact_format": input_receipt["artifact_format"],
|
|
"artifact_sha256": input_receipt["artifact_sha256"],
|
|
"source_content_sha256": input_receipt["source_content_sha256"],
|
|
"source_identity": input_receipt["source_identity"],
|
|
"source_locator": input_receipt["source_locator"],
|
|
"extractor": input_receipt["extractor"],
|
|
"extraction_spec_sha256": input_receipt["extraction_spec_sha256"],
|
|
"replay_identity_components": input_receipt["replay_identity_components"],
|
|
"replay_identity_sha256": input_receipt["replay_identity_sha256"],
|
|
"segments": normalized_segment_manifest(fixture["segments"]),
|
|
"row_ids": row_ids,
|
|
"candidate_counts": input_receipt["counts"],
|
|
}
|
|
return {
|
|
"id": row_ids["parent_proposal_id"],
|
|
"proposal_type": "attach_evidence",
|
|
"status": "pending_review",
|
|
"proposed_by_handle": "leo",
|
|
"proposed_by_agent_id": None,
|
|
"channel": "local_ingestion_canary",
|
|
"source_ref": (
|
|
f"local-ingestion:{input_receipt['artifact_sha256']}:"
|
|
f"replay={input_receipt['replay_identity_sha256']}:capture={row_ids['source_capture_id']}"
|
|
),
|
|
"rationale": "Stage source-linked candidate claims for review without canonical apply.",
|
|
"payload": {
|
|
"ingestion_manifest": ingestion_manifest,
|
|
"claim_candidates": claim_candidates,
|
|
"source_candidates": source_candidates,
|
|
"dedupe_conflict_assessment": {
|
|
"database_search_query": " ".join(claim["claim_key"].replace("_", " ") for claim in claims),
|
|
"duplicates": fixture["extraction"]["duplicates"],
|
|
"conflicts": fixture["extraction"]["conflicts"],
|
|
"review_required": True,
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def _independent_planned_uuid(parent_proposal_id: str, kind: str, key: str) -> str:
|
|
"""Reproduce the public deterministic-ID contract without calling the planner."""
|
|
return str(uuid.uuid5(uuid.NAMESPACE_URL, f"teleo:kb-rich-contract:{parent_proposal_id}:{kind}:{key}"))
|
|
|
|
|
|
def expected_planned_graph_identity(fixture: dict[str, Any], row_ids: dict[str, Any]) -> dict[str, Any]:
|
|
"""Derive planned row IDs and FK identities directly from normalized source candidates."""
|
|
parent_id = row_ids["parent_proposal_id"]
|
|
claims = fixture["extraction"]["claims"]
|
|
claim_ids = {
|
|
claim["claim_key"]: _independent_planned_uuid(parent_id, "claim", claim["claim_key"]) for claim in claims
|
|
}
|
|
referenced_segment_ids: list[str] = []
|
|
for claim in claims:
|
|
for evidence in claim["evidence"]:
|
|
if evidence["segment_id"] not in referenced_segment_ids:
|
|
referenced_segment_ids.append(evidence["segment_id"])
|
|
for duplicate in fixture["extraction"]["duplicates"]:
|
|
if duplicate["segment_id"] not in referenced_segment_ids:
|
|
referenced_segment_ids.append(duplicate["segment_id"])
|
|
|
|
segment_source_ids = {
|
|
segment_id: _independent_planned_uuid(
|
|
parent_id,
|
|
"source",
|
|
_segment_source_key(fixture["source"]["source_key"], segment_id),
|
|
)
|
|
for segment_id in referenced_segment_ids
|
|
}
|
|
body_source_ids = {
|
|
claim["claim_key"]: _independent_planned_uuid(parent_id, "claim-body-source", claim["claim_key"])
|
|
for claim in claims
|
|
}
|
|
|
|
evidence_rows: list[dict[str, Any]] = []
|
|
seen_evidence: set[tuple[str, str, str]] = set()
|
|
|
|
def append_evidence(claim_id: str, source_id: str, role: str) -> None:
|
|
key = (claim_id, source_id, role)
|
|
if key in seen_evidence:
|
|
return
|
|
seen_evidence.add(key)
|
|
evidence_rows.append(
|
|
{
|
|
"claim_id": claim_id,
|
|
"source_id": source_id,
|
|
"role": role,
|
|
"weight": None,
|
|
"created_by": None,
|
|
}
|
|
)
|
|
|
|
for claim in claims:
|
|
claim_key = claim["claim_key"]
|
|
claim_id = claim_ids[claim_key]
|
|
append_evidence(claim_id, body_source_ids[claim_key], "illustrates")
|
|
for evidence in claim["evidence"]:
|
|
append_evidence(claim_id, segment_source_ids[evidence["segment_id"]], evidence["role"])
|
|
|
|
edge_rows: list[dict[str, Any]] = []
|
|
seen_edges: set[tuple[str, str, str]] = set()
|
|
for claim in claims:
|
|
from_claim = claim_ids[claim["claim_key"]]
|
|
for edge in claim["edges"]:
|
|
target_key = _require_text(edge.get("target"), f"claim {claim['claim_key']} edge target")
|
|
to_claim = claim_ids.get(target_key)
|
|
if to_claim is None:
|
|
raise CanaryError(f"claim {claim['claim_key']} edge references unknown claim {target_key}")
|
|
edge_type = _require_text(edge.get("edge_type"), f"claim {claim['claim_key']} edge type")
|
|
identity = (from_claim, to_claim, edge_type)
|
|
if identity in seen_edges:
|
|
continue
|
|
seen_edges.add(identity)
|
|
edge_rows.append(
|
|
{
|
|
"from_claim": from_claim,
|
|
"to_claim": to_claim,
|
|
"edge_type": edge_type,
|
|
"weight": edge.get("weight"),
|
|
"created_by": None,
|
|
}
|
|
)
|
|
|
|
return {
|
|
"planned_claim_ids": [claim_ids[claim["claim_key"]] for claim in claims],
|
|
"planned_source_ids": [
|
|
*(segment_source_ids[segment_id] for segment_id in referenced_segment_ids),
|
|
*(body_source_ids[claim["claim_key"]] for claim in claims),
|
|
],
|
|
"planned_evidence_rows": evidence_rows,
|
|
"planned_edge_rows": edge_rows,
|
|
}
|
|
|
|
|
|
def build_schema_sql() -> str:
|
|
return rf"""\set ON_ERROR_STOP on
|
|
create schema kb_canary;
|
|
create schema kb_stage;
|
|
create table public.claims (
|
|
id uuid primary key, type text not null, text text not null, status text not null,
|
|
confidence double precision, tags text[] not null default '{{}}'
|
|
);
|
|
create table public.sources (
|
|
id uuid primary key, source_type text not null, url text, storage_path text,
|
|
excerpt text, hash text not null
|
|
);
|
|
create table public.claim_evidence (
|
|
claim_id uuid not null, source_id uuid not null, role text not null, weight double precision,
|
|
primary key (claim_id, source_id, role)
|
|
);
|
|
create table public.claim_edges (
|
|
from_claim uuid not null, to_claim uuid not null, edge_type text not null, weight double precision,
|
|
primary key (from_claim, to_claim, edge_type)
|
|
);
|
|
insert into public.claims(id, type, text, status, confidence, tags) values
|
|
('{SEED_CLAIM_A}', 'structural', 'A staged proposal is non-canonical until guarded apply succeeds.', 'open', 0.99, array['seed']),
|
|
('{SEED_CLAIM_B}', 'meta', 'Review and canonical application are separate state transitions.', 'open', 0.98, array['seed']);
|
|
insert into public.sources(id, source_type, storage_path, excerpt, hash) values
|
|
('{SEED_SOURCE}', 'observation', 'fixture://canonical/seed', 'Representative canonical seed.',
|
|
'{"a" * 64}');
|
|
insert into public.claim_evidence(claim_id, source_id, role, weight) values
|
|
('{SEED_CLAIM_A}', '{SEED_SOURCE}', 'grounds', 1.0);
|
|
insert into public.claim_edges(from_claim, to_claim, edge_type, weight) values
|
|
('{SEED_CLAIM_A}', '{SEED_CLAIM_B}', 'supports', 1.0);
|
|
create table kb_canary.source_captures (
|
|
id uuid primary key,
|
|
source_identity text not null unique,
|
|
source_type text not null,
|
|
title text not null,
|
|
locator text not null,
|
|
artifact_path text not null,
|
|
artifact_sha256 text not null check (artifact_sha256 ~ '^[0-9a-f]{{64}}$'),
|
|
content_sha256 text not null check (content_sha256 ~ '^[0-9a-f]{{64}}$'),
|
|
replay_identity_sha256 text not null check (replay_identity_sha256 ~ '^[0-9a-f]{{64}}$'),
|
|
metadata jsonb not null
|
|
);
|
|
create table kb_canary.claim_extractions (
|
|
id uuid primary key,
|
|
source_capture_id uuid not null references kb_canary.source_captures(id),
|
|
claim_key text not null unique,
|
|
body text not null,
|
|
metadata jsonb not null
|
|
);
|
|
create table kb_canary.evidence_extractions (
|
|
id uuid primary key,
|
|
claim_extraction_id uuid not null references kb_canary.claim_extractions(id),
|
|
source_capture_id uuid not null references kb_canary.source_captures(id),
|
|
source_segment_id text not null,
|
|
source_locator text not null,
|
|
source_json_pointer text,
|
|
role text not null,
|
|
body text not null,
|
|
body_sha256 text not null,
|
|
source_content_sha256 text not null,
|
|
metadata jsonb not null
|
|
);
|
|
create table kb_canary.duplicate_assessments (
|
|
id uuid primary key,
|
|
source_capture_id uuid not null references kb_canary.source_captures(id),
|
|
source_segment_id text not null,
|
|
source_locator text not null,
|
|
duplicate_of_claim_key text not null,
|
|
excerpt text not null,
|
|
excerpt_sha256 text not null,
|
|
reason text not null,
|
|
metadata jsonb not null
|
|
);
|
|
create table kb_canary.conflict_assessments (
|
|
id uuid primary key,
|
|
source_capture_id uuid not null references kb_canary.source_captures(id),
|
|
from_claim_key text not null,
|
|
to_claim_key text not null,
|
|
relationship text not null,
|
|
metadata jsonb not null
|
|
);
|
|
create table kb_stage.kb_proposals (
|
|
id uuid primary key,
|
|
proposal_type text not null,
|
|
status text not null,
|
|
proposed_by_handle text,
|
|
proposed_by_agent_id uuid,
|
|
channel text not null,
|
|
source_ref text not null,
|
|
rationale text not null,
|
|
payload jsonb not null,
|
|
reviewed_by_handle text,
|
|
reviewed_by_agent_id uuid,
|
|
reviewed_at timestamptz,
|
|
review_note text,
|
|
applied_by_handle text,
|
|
applied_by_agent_id uuid,
|
|
applied_at timestamptz
|
|
);
|
|
"""
|
|
|
|
|
|
def build_canonical_snapshot_sql() -> str:
|
|
return r"""\set ON_ERROR_STOP on
|
|
begin transaction read only;
|
|
select jsonb_build_object(
|
|
'claims', coalesce((select jsonb_agg(to_jsonb(t) order by t.id) from public.claims t), '[]'::jsonb),
|
|
'sources', coalesce((select jsonb_agg(to_jsonb(t) order by t.id) from public.sources t), '[]'::jsonb),
|
|
'claim_evidence', coalesce((select jsonb_agg(to_jsonb(t) order by t.claim_id, t.source_id, t.role)
|
|
from public.claim_evidence t), '[]'::jsonb),
|
|
'claim_edges', coalesce((select jsonb_agg(to_jsonb(t) order by t.from_claim, t.to_claim, t.edge_type)
|
|
from public.claim_edges t), '[]'::jsonb)
|
|
)::text;
|
|
rollback;
|
|
"""
|
|
|
|
|
|
def expected_persisted_rows(
|
|
fixture: dict[str, Any], input_receipt: dict[str, Any], row_ids: dict[str, Any]
|
|
) -> dict[str, list[dict[str, Any]]]:
|
|
"""Project the complete relational identity expected from one fixture."""
|
|
source = fixture["source"]
|
|
expected: dict[str, list[dict[str, Any]]] = {
|
|
"source_captures": [
|
|
{
|
|
"id": row_ids["source_capture_id"],
|
|
"source_identity": source["identity"],
|
|
"source_type": source["source_type"],
|
|
"title": source["title"],
|
|
"locator": source["locator"],
|
|
"artifact_path": input_receipt["artifact_path"],
|
|
"artifact_sha256": input_receipt["artifact_sha256"],
|
|
"content_sha256": input_receipt["source_content_sha256"],
|
|
"replay_identity_sha256": input_receipt["replay_identity_sha256"],
|
|
"metadata": {**source["metadata"], "extractor": input_receipt["extractor"]},
|
|
}
|
|
],
|
|
"claim_extractions": [],
|
|
"evidence_extractions": [],
|
|
"duplicate_assessments": [],
|
|
"conflict_assessments": [],
|
|
}
|
|
segment_by_id = {segment["id"]: segment for segment in fixture["segments"]}
|
|
for claim in fixture["extraction"]["claims"]:
|
|
claim_id = row_ids["claim_extraction_ids"][claim["claim_key"]]
|
|
expected["claim_extractions"].append(
|
|
{
|
|
"id": claim_id,
|
|
"source_capture_id": row_ids["source_capture_id"],
|
|
"claim_key": claim["claim_key"],
|
|
"body": claim["body"],
|
|
"metadata": {
|
|
**claim["metadata"],
|
|
"claim_type": claim["type"],
|
|
"confidence": claim["confidence"],
|
|
"text": claim["text"],
|
|
"extractor": input_receipt["extractor"],
|
|
"replay_identity_sha256": input_receipt["replay_identity_sha256"],
|
|
},
|
|
}
|
|
)
|
|
for index, evidence in enumerate(claim["evidence"]):
|
|
evidence_key = f"{claim['claim_key']}:{index}"
|
|
segment = segment_by_id[evidence["segment_id"]]
|
|
expected["evidence_extractions"].append(
|
|
{
|
|
"id": row_ids["evidence_extraction_ids"][evidence_key],
|
|
"claim_extraction_id": claim_id,
|
|
"source_capture_id": row_ids["source_capture_id"],
|
|
"source_segment_id": segment["id"],
|
|
"source_locator": segment["locator"],
|
|
"source_json_pointer": segment["json_pointer"],
|
|
"role": evidence["role"],
|
|
"body": evidence["excerpt"],
|
|
"body_sha256": sha256_bytes(evidence["excerpt"].encode()),
|
|
"source_content_sha256": segment["content_sha256"],
|
|
"metadata": {
|
|
**_require_object(evidence.get("metadata", {}), f"evidence {evidence_key}.metadata"),
|
|
"source_text_sha256": segment["text_sha256"],
|
|
},
|
|
}
|
|
)
|
|
for index, duplicate in enumerate(fixture["extraction"]["duplicates"]):
|
|
expected["duplicate_assessments"].append(
|
|
{
|
|
"id": row_ids["duplicate_assessment_ids"][str(index)],
|
|
"source_capture_id": row_ids["source_capture_id"],
|
|
"source_segment_id": duplicate["segment_id"],
|
|
"source_locator": duplicate["source_locator"],
|
|
"duplicate_of_claim_key": duplicate["duplicate_of_claim_key"],
|
|
"excerpt": duplicate["excerpt"],
|
|
"excerpt_sha256": sha256_bytes(duplicate["excerpt"].encode()),
|
|
"reason": duplicate["reason"],
|
|
"metadata": {
|
|
"json_pointer": duplicate["source_json_pointer"],
|
|
"source_content_sha256": duplicate["source_content_sha256"],
|
|
"source_text_sha256": duplicate["source_text_sha256"],
|
|
},
|
|
}
|
|
)
|
|
for index, conflict in enumerate(fixture["extraction"]["conflicts"]):
|
|
expected["conflict_assessments"].append(
|
|
{
|
|
"id": row_ids["conflict_assessment_ids"][str(index)],
|
|
"source_capture_id": row_ids["source_capture_id"],
|
|
"from_claim_key": conflict["from_claim_key"],
|
|
"to_claim_key": conflict["to_claim_key"],
|
|
"relationship": conflict["relationship"],
|
|
"metadata": {
|
|
key: value
|
|
for key, value in conflict.items()
|
|
if key not in {"from_claim_key", "to_claim_key", "relationship"}
|
|
},
|
|
}
|
|
)
|
|
return expected
|
|
|
|
|
|
def rows_exact(actual: Any, expected: list[dict[str, Any]]) -> bool:
|
|
"""Compare complete row identities independent of database result order."""
|
|
return (
|
|
isinstance(actual, list)
|
|
and all(isinstance(row, dict) for row in actual)
|
|
and sorted(actual, key=canonical_json_bytes) == sorted(expected, key=canonical_json_bytes)
|
|
)
|
|
|
|
|
|
def build_capture_sql(fixture: dict[str, Any], input_receipt: dict[str, Any], row_ids: dict[str, Any]) -> str:
|
|
source = fixture["source"]
|
|
q = ap.sql_literal
|
|
statements = [
|
|
r"\set ON_ERROR_STOP on",
|
|
"begin;",
|
|
"insert into kb_canary.source_captures "
|
|
"(id, source_identity, source_type, title, locator, artifact_path, artifact_sha256, content_sha256, "
|
|
"replay_identity_sha256, metadata) values "
|
|
f"({q(row_ids['source_capture_id'])}::uuid, {q(source['identity'])}, {q(source['source_type'])}, "
|
|
f"{q(source['title'])}, {q(source['locator'])}, {q(input_receipt['artifact_path'])}, "
|
|
f"{q(input_receipt['artifact_sha256'])}, {q(input_receipt['source_content_sha256'])}, "
|
|
f"{q(input_receipt['replay_identity_sha256'])}, "
|
|
f"{q(json.dumps({**source['metadata'], 'extractor': input_receipt['extractor']}, sort_keys=True))}::jsonb);",
|
|
]
|
|
segment_by_id = {segment["id"]: segment for segment in fixture["segments"]}
|
|
for claim in fixture["extraction"]["claims"]:
|
|
claim_id = row_ids["claim_extraction_ids"][claim["claim_key"]]
|
|
claim_metadata = {
|
|
**claim["metadata"],
|
|
"claim_type": claim["type"],
|
|
"confidence": claim["confidence"],
|
|
"text": claim["text"],
|
|
"extractor": input_receipt["extractor"],
|
|
"replay_identity_sha256": input_receipt["replay_identity_sha256"],
|
|
}
|
|
statements.append(
|
|
"insert into kb_canary.claim_extractions "
|
|
"(id, source_capture_id, claim_key, body, metadata) values "
|
|
f"({q(claim_id)}::uuid, {q(row_ids['source_capture_id'])}::uuid, {q(claim['claim_key'])}, "
|
|
f"{q(claim['body'])}, {q(json.dumps(claim_metadata, sort_keys=True))}::jsonb);"
|
|
)
|
|
for index, evidence in enumerate(claim["evidence"]):
|
|
evidence_key = f"{claim['claim_key']}:{index}"
|
|
segment = segment_by_id[evidence["segment_id"]]
|
|
evidence_metadata = {
|
|
**_require_object(evidence.get("metadata", {}), f"evidence {evidence_key}.metadata"),
|
|
"source_text_sha256": segment["text_sha256"],
|
|
}
|
|
statements.append(
|
|
"insert into kb_canary.evidence_extractions "
|
|
"(id, claim_extraction_id, source_capture_id, source_segment_id, source_locator, "
|
|
"source_json_pointer, role, body, body_sha256, source_content_sha256, metadata) values "
|
|
f"({q(row_ids['evidence_extraction_ids'][evidence_key])}::uuid, {q(claim_id)}::uuid, "
|
|
f"{q(row_ids['source_capture_id'])}::uuid, {q(segment['id'])}, {q(segment['locator'])}, "
|
|
f"{q(segment['json_pointer'])}, {q(evidence['role'])}, {q(evidence['excerpt'])}, "
|
|
f"{q(sha256_bytes(evidence['excerpt'].encode()))}, {q(segment['content_sha256'])}, "
|
|
f"{q(json.dumps(evidence_metadata, sort_keys=True))}::jsonb);"
|
|
)
|
|
for index, duplicate in enumerate(fixture["extraction"]["duplicates"]):
|
|
statements.append(
|
|
"insert into kb_canary.duplicate_assessments "
|
|
"(id, source_capture_id, source_segment_id, source_locator, duplicate_of_claim_key, excerpt, "
|
|
"excerpt_sha256, reason, metadata) values "
|
|
f"({q(row_ids['duplicate_assessment_ids'][str(index)])}::uuid, "
|
|
f"{q(row_ids['source_capture_id'])}::uuid, {q(duplicate['segment_id'])}, "
|
|
f"{q(duplicate['source_locator'])}, {q(duplicate['duplicate_of_claim_key'])}, "
|
|
f"{q(duplicate['excerpt'])}, {q(sha256_bytes(duplicate['excerpt'].encode()))}, "
|
|
f"{q(duplicate['reason'])}, {q(json.dumps({'json_pointer': duplicate['source_json_pointer'], 'source_content_sha256': duplicate['source_content_sha256'], 'source_text_sha256': duplicate['source_text_sha256']}, sort_keys=True))}::jsonb);"
|
|
)
|
|
for index, conflict in enumerate(fixture["extraction"]["conflicts"]):
|
|
statements.append(
|
|
"insert into kb_canary.conflict_assessments "
|
|
"(id, source_capture_id, from_claim_key, to_claim_key, relationship, metadata) values "
|
|
f"({q(row_ids['conflict_assessment_ids'][str(index)])}::uuid, "
|
|
f"{q(row_ids['source_capture_id'])}::uuid, {q(conflict['from_claim_key'])}, "
|
|
f"{q(conflict['to_claim_key'])}, {q(conflict['relationship'])}, "
|
|
f"{q(json.dumps({key: value for key, value in conflict.items() if key not in {'from_claim_key', 'to_claim_key', 'relationship'}}, sort_keys=True))}::jsonb);"
|
|
)
|
|
statements.append("commit;")
|
|
return "\n".join(statements) + "\n"
|
|
|
|
|
|
def build_readback_sql(proposal_id: str) -> str:
|
|
q = ap.sql_literal
|
|
return rf"""\set ON_ERROR_STOP on
|
|
begin transaction read only;
|
|
select jsonb_build_object(
|
|
'source_captures', coalesce((select jsonb_agg(to_jsonb(t) order by t.id) from kb_canary.source_captures t), '[]'::jsonb),
|
|
'claim_extractions', coalesce((select jsonb_agg(to_jsonb(t) order by t.claim_key) from kb_canary.claim_extractions t), '[]'::jsonb),
|
|
'evidence_extractions', coalesce((select jsonb_agg(to_jsonb(t) order by t.id) from kb_canary.evidence_extractions t), '[]'::jsonb),
|
|
'duplicate_assessments', coalesce((select jsonb_agg(to_jsonb(t) order by t.id) from kb_canary.duplicate_assessments t), '[]'::jsonb),
|
|
'conflict_assessments', coalesce((select jsonb_agg(to_jsonb(t) order by t.id) from kb_canary.conflict_assessments t), '[]'::jsonb),
|
|
'staged_proposal', (select to_jsonb(p) from kb_stage.kb_proposals p where id = {q(proposal_id)}::uuid),
|
|
'counts', jsonb_build_object(
|
|
'source_captures', (select count(*) from kb_canary.source_captures),
|
|
'claim_extractions', (select count(*) from kb_canary.claim_extractions),
|
|
'evidence_extractions', (select count(*) from kb_canary.evidence_extractions),
|
|
'duplicate_assessments', (select count(*) from kb_canary.duplicate_assessments),
|
|
'conflict_assessments', (select count(*) from kb_canary.conflict_assessments),
|
|
'staged_proposals', (select count(*) from kb_stage.kb_proposals)
|
|
)
|
|
)::text;
|
|
rollback;
|
|
"""
|
|
|
|
|
|
class DockerPostgres:
|
|
def __init__(self, name: str) -> None:
|
|
self.name = name
|
|
self.docker = shutil.which("docker")
|
|
if not self.docker:
|
|
raise CanaryError("docker executable is unavailable")
|
|
self.started = False
|
|
self.volume_mounts: list[dict[str, Any]] = []
|
|
|
|
def _run(
|
|
self, args: list[str], *, input_text: str | None = None, check: bool = True
|
|
) -> subprocess.CompletedProcess[str]:
|
|
result = subprocess.run([self.docker, *args], input=input_text, text=True, capture_output=True, check=False)
|
|
if check and result.returncode != 0:
|
|
detail = (result.stderr or result.stdout).strip()
|
|
raise CanaryError(f"docker {' '.join(args[:3])} failed ({result.returncode}): {detail}")
|
|
return result
|
|
|
|
def start(self) -> dict[str, Any]:
|
|
result = self._run(
|
|
[
|
|
"run",
|
|
"--rm",
|
|
"-d",
|
|
"--name",
|
|
self.name,
|
|
"--network",
|
|
"none",
|
|
"--label",
|
|
CONTAINER_LABEL,
|
|
"--tmpfs",
|
|
"/var/lib/postgresql/data:rw,nosuid,size=256m",
|
|
"-e",
|
|
"POSTGRES_HOST_AUTH_METHOD=trust",
|
|
"-e",
|
|
"POSTGRES_DB=teleo",
|
|
"postgres:16-alpine",
|
|
]
|
|
)
|
|
self.started = True
|
|
container_id = result.stdout.strip()
|
|
deadline = time.monotonic() + 30
|
|
ready_streak = 0
|
|
while time.monotonic() < deadline:
|
|
ready = self._run(["exec", self.name, "pg_isready", "-U", "postgres", "-d", "teleo"], check=False)
|
|
if ready.returncode == 0:
|
|
ready_streak += 1
|
|
if ready_streak >= 2:
|
|
break
|
|
else:
|
|
ready_streak = 0
|
|
time.sleep(0.25)
|
|
else:
|
|
raise CanaryError("disposable Postgres did not become ready within 30 seconds")
|
|
inspect = json.loads(self._run(["inspect", self.name]).stdout)[0]
|
|
if inspect["HostConfig"]["NetworkMode"] != "none":
|
|
raise CanaryError("disposable Postgres must use network mode none")
|
|
if inspect["Config"]["Labels"].get("livingip.canary") != "working-leo-local-ingestion":
|
|
raise CanaryError("disposable Postgres is missing its canary label")
|
|
self.volume_mounts = [
|
|
{"type": mount.get("Type"), "name": mount.get("Name"), "destination": mount.get("Destination")}
|
|
for mount in inspect.get("Mounts", [])
|
|
if mount.get("Type") == "volume"
|
|
]
|
|
if self.volume_mounts:
|
|
raise CanaryError("disposable Postgres unexpectedly created a Docker volume")
|
|
return {
|
|
"container_id": container_id,
|
|
"name": self.name,
|
|
"image": inspect["Config"]["Image"],
|
|
"network_mode": inspect["HostConfig"]["NetworkMode"],
|
|
"tmpfs_data_dir": "/var/lib/postgresql/data" in inspect["HostConfig"].get("Tmpfs", {}),
|
|
"docker_volume_mounts": self.volume_mounts,
|
|
"canary_label": inspect["Config"]["Labels"].get("livingip.canary"),
|
|
}
|
|
|
|
def psql(self, sql: str) -> str:
|
|
result = self._run(
|
|
["exec", "-i", self.name, "psql", "-U", "postgres", "-d", "teleo", "-Atq"],
|
|
input_text=sql,
|
|
)
|
|
return result.stdout.strip()
|
|
|
|
def psql_json(self, sql: str) -> dict[str, Any]:
|
|
output = self.psql(sql)
|
|
lines = [line for line in output.splitlines() if line.strip()]
|
|
if not lines:
|
|
raise CanaryError("Postgres readback returned no JSON")
|
|
try:
|
|
value = json.loads(lines[-1])
|
|
except json.JSONDecodeError as exc:
|
|
raise CanaryError(f"Postgres readback was not JSON: {lines[-1]}") from exc
|
|
if not isinstance(value, dict):
|
|
raise CanaryError("Postgres readback must be a JSON object")
|
|
return value
|
|
|
|
def cleanup(self) -> dict[str, Any]:
|
|
remove = self._run(["rm", "-f", self.name], check=False) if self.started else None
|
|
inspect = self._run(["inspect", self.name], check=False)
|
|
return {
|
|
"remove_attempted": self.started,
|
|
"remove_returncode": remove.returncode if remove else None,
|
|
"container_absent": inspect.returncode != 0,
|
|
"docker_volume_mounts_observed": self.volume_mounts,
|
|
"anonymous_or_named_volume_created": bool(self.volume_mounts),
|
|
}
|
|
|
|
|
|
def proposal_readback_parts(
|
|
readback: dict[str, Any],
|
|
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], dict[str, Any], dict[str, Any]]:
|
|
"""Extract typed proposal layers so every verifier path fails closed consistently."""
|
|
proposal_value = readback.get("staged_proposal")
|
|
proposal = proposal_value if isinstance(proposal_value, dict) else {}
|
|
payload_value = proposal.get("payload")
|
|
payload = payload_value if isinstance(payload_value, dict) else {}
|
|
apply_value = payload.get("apply_payload")
|
|
apply_payload = apply_value if isinstance(apply_value, dict) else {}
|
|
manifest_value = apply_payload.get("normalization_manifest")
|
|
manifest = manifest_value if isinstance(manifest_value, dict) else {}
|
|
ingestion_value = manifest.get("ingestion_manifest")
|
|
ingestion_manifest = ingestion_value if isinstance(ingestion_value, dict) else {}
|
|
assessment_value = manifest.get("dedupe_conflict_assessment")
|
|
assessment = assessment_value if isinstance(assessment_value, dict) else {}
|
|
return proposal, apply_payload, manifest, ingestion_manifest, assessment
|
|
|
|
|
|
def proposal_links(readback: dict[str, Any]) -> dict[str, Any]:
|
|
"""Project observed proposal row identities and links without judging them."""
|
|
proposal, apply_payload, _manifest, ingestion_manifest, _assessment = proposal_readback_parts(readback)
|
|
claim_rows = apply_payload.get("claims") if isinstance(apply_payload.get("claims"), list) else []
|
|
source_rows = apply_payload.get("sources") if isinstance(apply_payload.get("sources"), list) else []
|
|
evidence_rows = apply_payload.get("evidence") if isinstance(apply_payload.get("evidence"), list) else []
|
|
edge_rows = apply_payload.get("edges") if isinstance(apply_payload.get("edges"), list) else []
|
|
planned_evidence_rows = [
|
|
{
|
|
"claim_id": row.get("claim_id"),
|
|
"source_id": row.get("source_id"),
|
|
"role": row.get("role"),
|
|
"weight": row.get("weight"),
|
|
"created_by": row.get("created_by"),
|
|
}
|
|
for row in evidence_rows
|
|
if isinstance(row, dict)
|
|
]
|
|
planned_edge_rows = [
|
|
{
|
|
"from_claim": row.get("from_claim"),
|
|
"to_claim": row.get("to_claim"),
|
|
"edge_type": row.get("edge_type"),
|
|
"weight": row.get("weight"),
|
|
"created_by": row.get("created_by"),
|
|
}
|
|
for row in edge_rows
|
|
if isinstance(row, dict)
|
|
]
|
|
return {
|
|
"proposal_id": proposal.get("id"),
|
|
"proposal_status": proposal.get("status"),
|
|
"proposal_source_ref": proposal.get("source_ref"),
|
|
"planned_claim_ids": [row.get("id") for row in claim_rows if isinstance(row, dict)],
|
|
"planned_source_ids": [row.get("id") for row in source_rows if isinstance(row, dict)],
|
|
"planned_evidence_keys": [
|
|
{key: row[key] for key in ("claim_id", "source_id", "role")} for row in planned_evidence_rows
|
|
],
|
|
"planned_edge_keys": [
|
|
{key: row[key] for key in ("from_claim", "to_claim", "edge_type")} for row in planned_edge_rows
|
|
],
|
|
"planned_evidence_rows": planned_evidence_rows,
|
|
"planned_edge_rows": planned_edge_rows,
|
|
"planned_counts": {
|
|
"claims": len(claim_rows),
|
|
"sources": len(source_rows),
|
|
"evidence": len(evidence_rows),
|
|
"edges": len(edge_rows),
|
|
},
|
|
"manifest_artifact_sha256": ingestion_manifest.get("artifact_sha256"),
|
|
"manifest_replay_identity_sha256": ingestion_manifest.get("replay_identity_sha256"),
|
|
"manifest_row_ids": ingestion_manifest.get("row_ids"),
|
|
}
|
|
|
|
|
|
def proposal_link_proof(readback: dict[str, Any], input_receipt: dict[str, Any]) -> dict[str, Any]:
|
|
"""Attach explicit comparisons to the pure observed link projection for receipts."""
|
|
links = proposal_links(readback)
|
|
return {
|
|
**links,
|
|
"input_hash_in_manifest": links["manifest_artifact_sha256"] == input_receipt["artifact_sha256"],
|
|
"replay_identity_in_manifest": (
|
|
links["manifest_replay_identity_sha256"] == input_receipt["replay_identity_sha256"]
|
|
),
|
|
}
|
|
|
|
|
|
def evaluate_checks(
|
|
fixture: dict[str, Any],
|
|
input_receipt: dict[str, Any],
|
|
row_ids: dict[str, Any],
|
|
readback: dict[str, Any],
|
|
canonical_before: dict[str, Any],
|
|
canonical_after: dict[str, Any],
|
|
) -> dict[str, bool]:
|
|
source_value = readback.get("source_captures")
|
|
claim_value = readback.get("claim_extractions")
|
|
evidence_value = readback.get("evidence_extractions")
|
|
duplicate_value = readback.get("duplicate_assessments")
|
|
conflict_value = readback.get("conflict_assessments")
|
|
source_rows = source_value if isinstance(source_value, list) else []
|
|
claim_rows = claim_value if isinstance(claim_value, list) else []
|
|
evidence_rows = evidence_value if isinstance(evidence_value, list) else []
|
|
duplicate_rows = duplicate_value if isinstance(duplicate_value, list) else []
|
|
conflict_rows = conflict_value if isinstance(conflict_value, list) else []
|
|
proposal, apply_payload, _manifest, ingestion_manifest, assessment = proposal_readback_parts(readback)
|
|
counts = input_receipt["counts"]
|
|
links = proposal_links(readback)
|
|
deterministic_row_ids = build_row_ids(input_receipt, fixture)
|
|
expected_rows = expected_persisted_rows(fixture, input_receipt, deterministic_row_ids)
|
|
expected_parent = build_parent_packet(fixture, input_receipt, deterministic_row_ids)
|
|
expected_child = normalized_stage.prepare_normalized_child(expected_parent)
|
|
expected_apply_payload = expected_child["payload"]["apply_payload"]
|
|
persisted_proposal_envelope_fields = (
|
|
"id",
|
|
"proposal_type",
|
|
"status",
|
|
"proposed_by_handle",
|
|
"proposed_by_agent_id",
|
|
"channel",
|
|
"source_ref",
|
|
"rationale",
|
|
"reviewed_by_handle",
|
|
"reviewed_by_agent_id",
|
|
"reviewed_at",
|
|
"review_note",
|
|
"applied_by_handle",
|
|
"applied_by_agent_id",
|
|
"applied_at",
|
|
)
|
|
expected_proposal_envelope = {key: expected_child.get(key) for key in persisted_proposal_envelope_fields}
|
|
actual_proposal_envelope = {key: proposal.get(key) for key in persisted_proposal_envelope_fields}
|
|
expected_db_counts = {
|
|
"source_captures": 1,
|
|
"claim_extractions": counts["claim_candidates"],
|
|
"evidence_extractions": counts["evidence_candidates"],
|
|
"duplicate_assessments": counts["duplicate_judgments"],
|
|
"conflict_assessments": counts["conflict_relationships"],
|
|
"staged_proposals": 1,
|
|
}
|
|
source_rows_exact = rows_exact(source_rows, expected_rows["source_captures"])
|
|
claim_rows_exact = rows_exact(claim_rows, expected_rows["claim_extractions"])
|
|
evidence_rows_exact = rows_exact(evidence_rows, expected_rows["evidence_extractions"])
|
|
duplicate_rows_exact = rows_exact(duplicate_rows, expected_rows["duplicate_assessments"])
|
|
conflict_rows_exact = rows_exact(conflict_rows, expected_rows["conflict_assessments"])
|
|
proposal_envelope_exact = actual_proposal_envelope == expected_proposal_envelope
|
|
planned_rows_exact = all(
|
|
apply_payload.get(collection) == expected_apply_payload.get(collection)
|
|
for collection in ("claims", "sources", "evidence", "edges")
|
|
)
|
|
graph_collections = {"claims", "sources", "evidence", "edges"}
|
|
apply_auxiliary_exact = {key: value for key, value in apply_payload.items() if key not in graph_collections} == {
|
|
key: value for key, value in expected_apply_payload.items() if key not in graph_collections
|
|
}
|
|
expected_graph_identity = expected_planned_graph_identity(fixture, deterministic_row_ids)
|
|
actual_graph_identity = {
|
|
key: links[key]
|
|
for key in ("planned_claim_ids", "planned_source_ids", "planned_evidence_rows", "planned_edge_rows")
|
|
}
|
|
independent_graph_identity_exact = actual_graph_identity == expected_graph_identity
|
|
pending_state = normalized_stage.bound._state_semantics(proposal, "pending_review")
|
|
source_row = source_rows[0] if len(source_rows) == 1 and isinstance(source_rows[0], dict) else {}
|
|
claim_rows_are_objects = all(isinstance(row, dict) for row in claim_rows)
|
|
return {
|
|
"source_only_artifact_hash_persisted": (
|
|
len(source_rows) == 1 and source_row.get("artifact_sha256") == input_receipt["artifact_sha256"]
|
|
),
|
|
"source_capture_identity_exact": source_rows_exact,
|
|
"deterministic_row_ids_recomputed_exact": row_ids == deterministic_row_ids,
|
|
"replay_identity_persisted": (
|
|
len(source_rows) == 1
|
|
and source_row.get("replay_identity_sha256") == input_receipt["replay_identity_sha256"]
|
|
),
|
|
"extractor_name_and_version_persisted": (
|
|
ingestion_manifest.get("extractor") == input_receipt["extractor"]
|
|
and claim_rows_are_objects
|
|
and all(
|
|
isinstance(row.get("metadata"), dict) and row["metadata"].get("extractor") == input_receipt["extractor"]
|
|
for row in claim_rows
|
|
)
|
|
),
|
|
"all_claim_candidates_persisted_once": (
|
|
claim_rows_are_objects
|
|
and {row.get("claim_key") for row in claim_rows}
|
|
== {claim["claim_key"] for claim in fixture["extraction"]["claims"]}
|
|
and len(claim_rows) == counts["claim_candidates"]
|
|
),
|
|
"claim_extraction_identities_and_fks_exact": claim_rows_exact,
|
|
"all_evidence_has_exact_source_location": (
|
|
len(evidence_rows) == counts["evidence_candidates"] and evidence_rows_exact
|
|
),
|
|
"evidence_extraction_identities_and_fks_exact": evidence_rows_exact,
|
|
"duplicate_judgments_persisted": (
|
|
len(duplicate_rows) == counts["duplicate_judgments"]
|
|
and duplicate_rows_exact
|
|
and assessment.get("duplicates") == fixture["extraction"]["duplicates"]
|
|
),
|
|
"conflicts_and_relationships_persisted": (
|
|
len(conflict_rows) == counts["conflict_relationships"]
|
|
and conflict_rows_exact
|
|
and assessment.get("conflicts") == fixture["extraction"]["conflicts"]
|
|
and links["planned_edge_rows"] == expected_graph_identity["planned_edge_rows"]
|
|
),
|
|
"planned_graph_matches_independent_source_oracle": independent_graph_identity_exact,
|
|
"planned_proposal_identities_and_linkages_exact": (
|
|
proposal_envelope_exact
|
|
and planned_rows_exact
|
|
and apply_auxiliary_exact
|
|
and independent_graph_identity_exact
|
|
and links["manifest_row_ids"] == deterministic_row_ids
|
|
and links["proposal_id"] == expected_child["id"]
|
|
and links["proposal_status"] == expected_child["status"]
|
|
and links["proposal_source_ref"] == expected_child["source_ref"]
|
|
),
|
|
"database_candidate_counts_exact": readback.get("counts") == expected_db_counts,
|
|
"proposal_is_pending_review": pending_state["status_matches"],
|
|
"proposal_has_replayable_ingestion_manifest": (
|
|
links["manifest_artifact_sha256"] == input_receipt["artifact_sha256"]
|
|
and links["manifest_replay_identity_sha256"] == input_receipt["replay_identity_sha256"]
|
|
and ingestion_manifest == expected_apply_payload["normalization_manifest"]["ingestion_manifest"]
|
|
),
|
|
"no_review_or_apply_metadata": (
|
|
pending_state["review_fields_match"]
|
|
and pending_state["apply_fields_match"]
|
|
and proposal.get("review_note") is None
|
|
),
|
|
"canonical_snapshot_nonempty": (
|
|
canonical_snapshot_complete(canonical_before) and canonical_snapshot_complete(canonical_after)
|
|
),
|
|
"canonical_fingerprint_unchanged": (
|
|
canonical_snapshot_complete(canonical_before)
|
|
and canonical_snapshot_complete(canonical_after)
|
|
and canonical_sha256(canonical_before) == canonical_sha256(canonical_after)
|
|
),
|
|
}
|
|
|
|
|
|
def run_canary(fixture_path: Path, output: Path | None = None) -> dict[str, Any]:
|
|
receipt: dict[str, Any] = {
|
|
"schema": SCHEMA,
|
|
"status": "fail",
|
|
"required_tier": "T2_runtime_to_review_queue_staging",
|
|
"input": {"scenario_path": str(fixture_path.resolve())},
|
|
"row_ids": {},
|
|
"production_mutation_attempted": False,
|
|
"canonical_apply_attempted": False,
|
|
}
|
|
postgres: DockerPostgres | None = None
|
|
try:
|
|
fixture, input_receipt = load_fixture(fixture_path)
|
|
row_ids = build_row_ids(input_receipt, fixture)
|
|
parent = build_parent_packet(fixture, input_receipt, row_ids)
|
|
child = normalized_stage.prepare_normalized_child(parent)
|
|
receipt["input"] = input_receipt
|
|
receipt["row_ids"] = row_ids
|
|
postgres = DockerPostgres(f"working-leo-ingest-{uuid.uuid4().hex[:12]}")
|
|
receipt["environment"] = postgres.start()
|
|
postgres.psql(build_schema_sql())
|
|
canonical_before = postgres.psql_json(build_canonical_snapshot_sql())
|
|
postgres.psql(build_capture_sql(fixture, input_receipt, row_ids))
|
|
stage_result = postgres.psql_json(normalized_stage.build_stage_sql(child))
|
|
readback = postgres.psql_json(build_readback_sql(child["id"]))
|
|
canonical_after = postgres.psql_json(build_canonical_snapshot_sql())
|
|
canonical_unchanged = (
|
|
canonical_snapshot_complete(canonical_before)
|
|
and canonical_snapshot_complete(canonical_after)
|
|
and canonical_sha256(canonical_before) == canonical_sha256(canonical_after)
|
|
)
|
|
checks = evaluate_checks(fixture, input_receipt, row_ids, readback, canonical_before, canonical_after)
|
|
receipt.update(
|
|
{
|
|
"stage_command_result": stage_result,
|
|
"rows": readback,
|
|
"candidate_counts": input_receipt["counts"],
|
|
"row_link_fields_proven": proposal_link_proof(readback, input_receipt),
|
|
"canonical": {
|
|
"fingerprint_before": canonical_sha256(canonical_before),
|
|
"fingerprint_after": canonical_sha256(canonical_after),
|
|
"table_counts": {key: len(canonical_before[key]) for key in CANONICAL_TABLES},
|
|
"unchanged": canonical_unchanged,
|
|
},
|
|
"checks": checks,
|
|
"status": "pass" if all(checks.values()) else "fail",
|
|
}
|
|
)
|
|
except (
|
|
CanaryError,
|
|
OSError,
|
|
ValueError,
|
|
KeyError,
|
|
TypeError,
|
|
AttributeError,
|
|
IndexError,
|
|
normalized_stage.bound.CheckpointError,
|
|
) as exc:
|
|
receipt["error"] = {"type": type(exc).__name__, "message": str(exc)}
|
|
finally:
|
|
if postgres is None:
|
|
receipt["cleanup"] = {
|
|
"remove_attempted": False,
|
|
"remove_returncode": None,
|
|
"container_absent": True,
|
|
"docker_volume_mounts_observed": [],
|
|
"anonymous_or_named_volume_created": False,
|
|
"runtime_not_started": True,
|
|
"network_mode_none": True,
|
|
}
|
|
else:
|
|
receipt["cleanup"] = postgres.cleanup()
|
|
receipt["cleanup"]["runtime_not_started"] = not postgres.started
|
|
receipt["cleanup"]["network_mode_none"] = receipt.get("environment", {}).get("network_mode") == "none"
|
|
receipt["cleanup"]["no_production_target_referenced"] = receipt["cleanup"]["network_mode_none"]
|
|
if not all((receipt["cleanup"]["container_absent"], receipt["cleanup"]["network_mode_none"])):
|
|
receipt["status"] = "fail"
|
|
if receipt["status"] == "pass":
|
|
receipt["strongest_claim"] = (
|
|
"One source-only artifact was parsed into exact-location candidate claims, duplicate/conflict "
|
|
"assessments, and a replay-bound pending_review proposal in disposable networkless Postgres; "
|
|
"representative canonical row contents remained fingerprint-identical."
|
|
)
|
|
else:
|
|
receipt["strongest_claim"] = (
|
|
"No source-to-review-staging capability claim is made because acceptance failed closed."
|
|
)
|
|
receipt["residual_gap"] = (
|
|
"This proves the deterministic local fixture extractor and review queue only; it does not prove "
|
|
"arbitrary generative extraction, approval/apply, hosted runtime, or production mutation."
|
|
)
|
|
if output:
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
output.write_text(json.dumps(receipt, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
return receipt
|
|
|
|
|
|
def receipt_proves_canonical_invariance(receipt: dict[str, Any]) -> bool:
|
|
"""Recompute the suite-level canonical verdict from receipt evidence."""
|
|
canonical = receipt.get("canonical")
|
|
checks = receipt.get("checks")
|
|
if not isinstance(canonical, dict) or not isinstance(checks, dict):
|
|
return False
|
|
before = canonical.get("fingerprint_before")
|
|
after = canonical.get("fingerprint_after")
|
|
return (
|
|
isinstance(before, str)
|
|
and isinstance(after, str)
|
|
and re.fullmatch(r"[0-9a-f]{64}", before) is not None
|
|
and re.fullmatch(r"[0-9a-f]{64}", after) is not None
|
|
and before == after
|
|
and canonical.get("unchanged") is True
|
|
and checks.get("canonical_fingerprint_unchanged") is True
|
|
)
|
|
|
|
|
|
def run_suite(fixture_paths: list[Path], output: Path | None = None) -> dict[str, Any]:
|
|
receipts = [run_canary(path.resolve()) for path in fixture_paths]
|
|
all_supplied_pass = bool(receipts) and all(item.get("status") == "pass" for item in receipts)
|
|
document_fixture_passed = any(
|
|
item.get("status") == "pass" and item.get("input", {}).get("artifact_format") == "plain_text"
|
|
for item in receipts
|
|
)
|
|
social_conversation_fixture_passed = any(
|
|
item.get("status") == "pass" and item.get("input", {}).get("artifact_format") == "telegram_jsonl"
|
|
for item in receipts
|
|
)
|
|
canonical_fingerprint_invariant_all = bool(receipts) and all(
|
|
receipt_proves_canonical_invariance(item) for item in receipts
|
|
)
|
|
required_shape_coverage = document_fixture_passed and social_conversation_fixture_passed
|
|
full_suite_passed = all_supplied_pass and required_shape_coverage and canonical_fingerprint_invariant_all
|
|
if not all_supplied_pass or not canonical_fingerprint_invariant_all:
|
|
status = "fail"
|
|
elif full_suite_passed:
|
|
status = "pass"
|
|
else:
|
|
status = "partial"
|
|
missing_required_coverage = []
|
|
if not document_fixture_passed:
|
|
missing_required_coverage.append("document")
|
|
if not social_conversation_fixture_passed:
|
|
missing_required_coverage.append("social_conversation")
|
|
if not canonical_fingerprint_invariant_all:
|
|
missing_required_coverage.append("canonical_fingerprint_invariance")
|
|
suite = {
|
|
"schema": SUITE_SCHEMA,
|
|
"status": status,
|
|
"required_tier": "T2_runtime_to_review_queue_staging",
|
|
"current_tier": (
|
|
"T2_runtime_to_review_queue_staging"
|
|
if full_suite_passed
|
|
else "T2_runtime_single_fixture"
|
|
if status == "partial"
|
|
else "runtime_verification_failed"
|
|
),
|
|
"fixture_count": len(receipts),
|
|
"all_supplied_fixtures_passed": all_supplied_pass,
|
|
"document_fixture_passed": document_fixture_passed,
|
|
"social_conversation_fixture_passed": social_conversation_fixture_passed,
|
|
"canonical_fingerprint_invariant_all": canonical_fingerprint_invariant_all,
|
|
"required_shape_coverage_met": required_shape_coverage,
|
|
"coverage_status": "full" if required_shape_coverage else "partial",
|
|
"missing_required_coverage": missing_required_coverage,
|
|
"full_suite_passed": full_suite_passed,
|
|
"receipts": receipts,
|
|
}
|
|
if output:
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
output.write_text(json.dumps(suite, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
return suite
|
|
|
|
|
|
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"--fixture",
|
|
type=Path,
|
|
action="append",
|
|
help="repeatable scenario path; defaults to the document and Telegram scenarios",
|
|
)
|
|
parser.add_argument("--output", type=Path)
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = parse_args(argv)
|
|
fixtures = args.fixture or list(DEFAULT_FIXTURES)
|
|
receipt = run_suite([path.resolve() for path in fixtures], args.output.resolve() if args.output else None)
|
|
print(json.dumps(receipt, sort_keys=True, separators=(",", ":")))
|
|
return 0 if receipt["status"] == "pass" else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|