Preserve atomic claim text in source proposals

This commit is contained in:
twentyOne2x 2026-07-13 22:13:19 +02:00
parent 8f70e39bdd
commit ee3ee1d998
6 changed files with 31 additions and 3 deletions

View file

@ -149,7 +149,8 @@ new candidates with confidence at or below `0.75`, has the model select
densely numbered non-empty source fragments, resolves those IDs to exact source substrings, records
duplicate judgments, and validates a v2 manifest through the source compiler.
Unknown line IDs are rejected rather than fuzzily matched. It writes private
files only. A separate
files only. The extracted atomic proposition is the proposed claim text; exact
source wording remains separately hash-bound as quote and evidence. A separate
`teleo-kb propose-source` call is required to create a `pending_review` row;
neither command applies canonical rows.

View file

@ -149,7 +149,9 @@ This performs a canonical claim retrieval before model extraction, allows at
most three single-source candidates, caps confidence at `0.75`, binds every
claim/evidence/duplicate judgment to a dense numbered source fragment that is resolved
deterministically to an exact substring, and writes private mode-`0600`
outputs. It performs no database write. If status is
outputs. The concise extracted proposition becomes the candidate claim text;
the exact source wording remains its hash-bound quote and evidence. It performs
no database write. If status is
`no_novel_claims`, inspect the duplicate judgments and do not stage an empty
packet.

View file

@ -390,6 +390,7 @@ def build_parent_proposal(
{
"claim_key": claim["claim_key"],
"type": claim["type"],
"headline": claim["text"],
"text": claim["text"],
"body": claim["quote"],
"confidence": claim["confidence"],
@ -471,6 +472,8 @@ def _validate_compiled_child(
raise CompilerError("strict child claim count differs from the extraction manifest")
if len({row.get("id") for row in claims}) != len(claims):
raise CompilerError("strict child contains duplicate claim ids")
if [row.get("text") for row in claims] != [row["text"] for row in manifest["claims"]]:
raise CompilerError("strict child claim text differs from the extracted atomic propositions")
if len({row.get("id") for row in sources}) != len(sources):
raise CompilerError("strict child contains duplicate source ids")

View file

@ -31,6 +31,7 @@ RECEIPT_SCHEMA = "livingip.kbSourcePreparationReceipt.v1"
MODEL_OUTPUT_SCHEMA = "livingip.kbSourceModelExtraction.v2"
RESOLVED_OUTPUT_SCHEMA = "livingip.kbSourceResolvedExtraction.v1"
DEFAULT_MODEL = "anthropic/claude-sonnet-4.5"
EXTRACTOR_VERSION = "2"
DEFAULT_OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
DEFAULT_KEY_FILE = Path("/opt/teleo-eval/secrets/openrouter-key")
TEXT_SUFFIXES = {".csv", ".htm", ".html", ".json", ".md", ".rst", ".txt", ".xml"}
@ -373,7 +374,7 @@ def build_manifest(
"schema": compiler.MANIFEST_SCHEMA_V2,
"artifact_sha256": artifact_sha256,
"extracted_text_sha256": text_sha256,
"extractor": {"name": f"openrouter:{args.model}", "version": "1"},
"extractor": {"name": f"openrouter:{args.model}", "version": EXTRACTOR_VERSION},
"source": {
"identity": args.identity,
"source_key": args.source_key,

View file

@ -108,10 +108,14 @@ def test_compiles_deterministic_hash_bound_pending_review_bundle_without_db_writ
assert apply_payload["contract_version"] == 2
assert apply_payload["source_proposal_id"] == parent["id"]
assert len(apply_payload["claims"]) == 1
assert apply_payload["claims"][0]["text"] == _manifest()["claims"][0]["text"]
assert apply_payload["claims"][0]["text"] != CLAIM_QUOTE
assert len(apply_payload["sources"]) == 2
assert len(apply_payload["evidence"]) == 2
assert first["hashes"]["artifact_sha256"] in {row["hash"] for row in apply_payload["sources"]}
assert first["compiler_contract"]["source_from_chat_labels"] is False
assert parent["payload"]["claim_candidates"][0]["headline"] == _manifest()["claims"][0]["text"]
assert parent["payload"]["claim_candidates"][0]["body"] == CLAIM_QUOTE
without_bundle_hash = copy.deepcopy(first)
bundle_hash = without_bundle_hash["hashes"].pop("bundle_content_sha256")
@ -156,6 +160,22 @@ def test_v2_manifest_carries_canonical_dedupe_candidates_into_review_packet(tmp_
assert staged_assessment == assessment
def test_rejects_normalized_child_that_replaces_atomic_claim_text_with_quote(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
original = compiler.stage.prepare_normalized_child
def replace_claim_text(parent: dict) -> dict:
child = original(parent)
child["payload"]["apply_payload"]["claims"][0]["text"] = CLAIM_QUOTE
return child
monkeypatch.setattr(compiler.stage, "prepare_normalized_child", replace_claim_text)
with pytest.raises(compiler.CompilerError, match="claim text differs from the extracted atomic propositions"):
_compile(tmp_path)
def test_v2_manifest_rejects_unretrieved_or_unbound_duplicate_judgments(tmp_path: Path) -> None:
manifest = _manifest()
manifest["schema"] = compiler.MANIFEST_SCHEMA_V2

View file

@ -119,6 +119,7 @@ def test_prepare_builds_private_compiler_validated_manifest_without_db_write(
output_dir = Path(receipt["outputs"]["output_dir"])
manifest = json.loads(Path(receipt["outputs"]["manifest"]).read_text(encoding="utf-8"))
assert manifest["schema"] == preparer.compiler.MANIFEST_SCHEMA_V2
assert manifest["extractor"]["version"] == preparer.EXTRACTOR_VERSION == "2"
assert manifest["dedupe"]["duplicates"][0]["claim_id"] == CANDIDATE_ID
assert manifest["claims"][0]["quote"] == CLAIM_QUOTE
assert manifest["dedupe"]["duplicates"][0]["source_quote"] == DUPLICATE_QUOTE