Merge pull request #130 from living-ip/codex/leo-atomic-claim-text-20260713
Some checks are pending
CI / lint-and-test (push) Waiting to run
Some checks are pending
CI / lint-and-test (push) Waiting to run
Preserve atomic claim text in source proposals
This commit is contained in:
commit
1a9c08c1a0
6 changed files with 31 additions and 3 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue