From 0153bd9d35a92205d0e68a7fe1dff1cf21ba7e77 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 13:37:38 +0530 Subject: [PATCH 01/39] Add verify_citations v1 case inventory Signed-off-by: docushell-admin --- ...t_milestone_d_verify_citations_contract.py | 72 +++++++++++++++ docs/milestone-d-verify-citations-contract.md | 4 + examples/verify/README.md | 5 + .../verify/verify_citations_v1_contract.json | 92 +++++++++++++++++++ 4 files changed, 173 insertions(+) create mode 100644 examples/verify/verify_citations_v1_contract.json diff --git a/.github/scripts/test_milestone_d_verify_citations_contract.py b/.github/scripts/test_milestone_d_verify_citations_contract.py index cb9667d..9573229 100644 --- a/.github/scripts/test_milestone_d_verify_citations_contract.py +++ b/.github/scripts/test_milestone_d_verify_citations_contract.py @@ -17,6 +17,7 @@ from __future__ import annotations +import json import re import unittest from pathlib import Path @@ -26,6 +27,8 @@ ROOT = Path(__file__).resolve().parents[2] CONTRACT = ROOT / "docs/milestone-d-verify-citations-contract.md" +VERIFY_CASES = ROOT / "examples/verify/cases.json" +CONTRACT_INVENTORY = ROOT / "examples/verify/verify_citations_v1_contract.json" ROADMAP = ROOT / "docs/roadmap.md" EXECUTION_STATUS = ROOT / "docs/execution-status.md" SCHEMAS_README = ROOT / "schemas/README.md" @@ -39,6 +42,10 @@ def normalized_contract_text() -> str: return re.sub(r"\s+", " ", contract_text()) +def load_json(path: Path): + return json.loads(path.read_text(encoding="utf-8")) + + class MilestoneDVerifyCitationsContractTests(unittest.TestCase): def test_target_is_declared_phony(self) -> None: text = makefile_text() @@ -114,6 +121,7 @@ def test_contract_names_fixture_backed_validation(self) -> None: self.assertIn("`schemas/examples/citations.example.json`", text) self.assertIn("`schemas/examples/verification-report.example.json`", text) + self.assertIn("`examples/verify/verify_citations_v1_contract.json`", text) self.assertIn("echoes the example claims in input order", text) self.assertIn("`all_evidence_grounded` is true only under the invariant", text) self.assertIn( @@ -121,6 +129,70 @@ def test_contract_names_fixture_backed_validation(self) -> None: text, ) + def test_contract_inventory_matches_executable_case_inventory(self) -> None: + cases = load_json(VERIFY_CASES) + inventory = load_json(CONTRACT_INVENTORY) + + self.assertEqual(inventory["schema_version"], 1) + self.assertEqual(inventory["contract"], "verify_citations.v1") + self.assertEqual(inventory["status"], "source-only-pre-alpha") + self.assertEqual(inventory["carrier"], "ethos verify") + + report_case_names = {case["name"] for case in cases["report_cases"]} + inventory_report_names = {case["name"] for case in inventory["report_cases"]} + self.assertEqual(inventory_report_names, report_case_names) + + usage_case_names = {case["name"] for case in cases["usage_error_cases"]} + self.assertEqual(set(inventory["usage_error_cases"]), usage_case_names) + + summary_case_names = {case["name"] for case in cases["summary_cases"]} + self.assertEqual(set(inventory["summary_cases"]), summary_case_names) + + def test_contract_inventory_matches_report_goldens(self) -> None: + cases = load_json(VERIFY_CASES) + inventory = load_json(CONTRACT_INVENTORY) + case_by_name = {case["name"]: case for case in cases["report_cases"]} + + allowed_categories = { + "grounded", + "grounded-with-capability-warning", + "unsupported-non-v1", + "diagnostic-non-grounded", + "stale-fingerprint", + "capability-blocked", + } + + for contract_case in inventory["report_cases"]: + self.assertIn(contract_case["category"], allowed_categories) + report = load_json(ROOT / case_by_name[contract_case["name"]]["golden"]) + statuses = [check["status"] for check in report["checks"]] + reasons = [ + check["reason"] + for check in report["checks"] + if check.get("reason") is not None + ] + + self.assertEqual( + contract_case["all_evidence_grounded"], + report["all_evidence_grounded"], + contract_case["name"], + ) + self.assertEqual(contract_case["statuses"], statuses, contract_case["name"]) + self.assertEqual(contract_case["reasons"], reasons, contract_case["name"]) + + def test_contract_inventory_keeps_blockers_explicit(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + blockers = " ".join(inventory["explicit_blockers"]) + + for expected in [ + "new verify_citations CLI alias", + "Python Node MCP or hosted API surfaces", + "crop API implementation", + "sandbox subprocess backend expansion", + "semantic or arithmetic verification", + ]: + self.assertIn(expected, blockers) + if __name__ == "__main__": unittest.main() diff --git a/docs/milestone-d-verify-citations-contract.md b/docs/milestone-d-verify-citations-contract.md index d269946..5c54e92 100644 --- a/docs/milestone-d-verify-citations-contract.md +++ b/docs/milestone-d-verify-citations-contract.md @@ -25,6 +25,10 @@ The example pair `schemas/examples/citations.example.json` and contract. Schema validation now checks that the example report echoes the example claims in input order and keeps the grounded gate coherent. +`examples/verify/verify_citations_v1_contract.json` classifies the existing executable verifier +cases as the current v1 contract inventory. The guard checks that this inventory stays aligned with +`examples/verify/cases.json` and each referenced report golden. + Focused validation command: - `make milestone-d-verify-citations-contract PYTHON=/bin/python` diff --git a/examples/verify/README.md b/examples/verify/README.md index 923839c..2bbd787 100644 --- a/examples/verify/README.md +++ b/examples/verify/README.md @@ -7,6 +7,11 @@ listed fixture path is missing, if a report golden is not covered by the invento real OpenDataLoader fixture manifest hashes drift, or if this README stops naming an inventory case. +`verify_citations_v1_contract.json` classifies the same executable cases for the current +Milestone D source-only pre-alpha `verify_citations` v1 contract. The focused +`make milestone-d-verify-citations-contract` target checks that this contract inventory stays +aligned with `cases.json` and the report goldens. + ## Verify-Alpha Case Inventory Report cases: diff --git a/examples/verify/verify_citations_v1_contract.json b/examples/verify/verify_citations_v1_contract.json new file mode 100644 index 0000000..5aa7038 --- /dev/null +++ b/examples/verify/verify_citations_v1_contract.json @@ -0,0 +1,92 @@ +{ + "schema_version": 1, + "contract": "verify_citations.v1", + "status": "source-only-pre-alpha", + "carrier": "ethos verify", + "report_cases": [ + { + "name": "native-grounded", + "category": "grounded", + "all_evidence_grounded": true, + "statuses": ["grounded", "grounded", "grounded"], + "reasons": [] + }, + { + "name": "opendataloader-grounded", + "category": "grounded-with-capability-warning", + "all_evidence_grounded": true, + "statuses": ["grounded", "grounded", "grounded"], + "reasons": [] + }, + { + "name": "native-split-quote", + "category": "grounded", + "all_evidence_grounded": true, + "statuses": ["grounded"], + "reasons": [] + }, + { + "name": "native-non-v1-claims", + "category": "unsupported-non-v1", + "all_evidence_grounded": false, + "statuses": ["grounded", "unsupported_claim_kind", "unsupported_claim_kind"], + "reasons": ["unsupported_claim_kind", "unsupported_claim_kind"] + }, + { + "name": "native-ungrounded", + "category": "diagnostic-non-grounded", + "all_evidence_grounded": false, + "statuses": ["mismatch", "not_found"], + "reasons": ["text_mismatch", "element_not_found"] + }, + { + "name": "opendataloader-not-found", + "category": "diagnostic-non-grounded", + "all_evidence_grounded": false, + "statuses": ["not_found"], + "reasons": ["element_not_found"] + }, + { + "name": "native-stale", + "category": "stale-fingerprint", + "all_evidence_grounded": false, + "statuses": ["stale"], + "reasons": ["stale_fingerprint"] + }, + { + "name": "opendataloader-capability-limited", + "category": "capability-blocked", + "all_evidence_grounded": false, + "statuses": ["capability_blocked"], + "reasons": ["missing_table_capability"] + }, + { + "name": "real-opendataloader-grounded", + "category": "grounded-with-capability-warning", + "all_evidence_grounded": true, + "statuses": ["grounded", "grounded", "grounded"], + "reasons": [] + }, + { + "name": "real-opendataloader-ungrounded", + "category": "diagnostic-non-grounded", + "all_evidence_grounded": false, + "statuses": ["mismatch"], + "reasons": ["text_mismatch"] + } + ], + "usage_error_cases": [ + "invalid-table-cell-citation", + "invalid-bbox-citation", + "opendataloader-malformed-bbox-input", + "opendataloader-unknown-page-input" + ], + "summary_cases": ["native-ungrounded-summary"], + "explicit_blockers": [ + "new verify_citations CLI alias", + "Python Node MCP or hosted API surfaces", + "crop API implementation", + "sandbox subprocess backend expansion", + "semantic or arithmetic verification" + ] +} From de560e70dad399dd2d68ab7be63c62bc69700fa6 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 13:44:29 +0530 Subject: [PATCH 02/39] Schema validate verify_citations contract inventory Signed-off-by: docushell-admin --- ...t_milestone_d_verify_citations_contract.py | 1 + docs/milestone-d-verify-citations-contract.md | 5 +- examples/verify/README.md | 4 +- .../verify/verify_citations_v1_contract.json | 1 + schemas/README.md | 5 +- ...thos-verify-citations-contract.schema.json | 116 ++++++++++++++++++ schemas/validate_examples.py | 3 + 7 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 schemas/ethos-verify-citations-contract.schema.json diff --git a/.github/scripts/test_milestone_d_verify_citations_contract.py b/.github/scripts/test_milestone_d_verify_citations_contract.py index 9573229..92125e0 100644 --- a/.github/scripts/test_milestone_d_verify_citations_contract.py +++ b/.github/scripts/test_milestone_d_verify_citations_contract.py @@ -187,6 +187,7 @@ def test_contract_inventory_keeps_blockers_explicit(self) -> None: for expected in [ "new verify_citations CLI alias", "Python Node MCP or hosted API surfaces", + "broad foreign-adapter hardening beyond existing fixtures", "crop API implementation", "sandbox subprocess backend expansion", "semantic or arithmetic verification", diff --git a/docs/milestone-d-verify-citations-contract.md b/docs/milestone-d-verify-citations-contract.md index 5c54e92..03764ae 100644 --- a/docs/milestone-d-verify-citations-contract.md +++ b/docs/milestone-d-verify-citations-contract.md @@ -26,8 +26,9 @@ contract. Schema validation now checks that the example report echoes the exampl order and keeps the grounded gate coherent. `examples/verify/verify_citations_v1_contract.json` classifies the existing executable verifier -cases as the current v1 contract inventory. The guard checks that this inventory stays aligned with -`examples/verify/cases.json` and each referenced report golden. +cases as the current v1 contract inventory. Schema validation checks the inventory shape and +vocabulary; the guard checks that this inventory stays aligned with `examples/verify/cases.json` +and each referenced report golden. Focused validation command: diff --git a/examples/verify/README.md b/examples/verify/README.md index 2bbd787..bb93562 100644 --- a/examples/verify/README.md +++ b/examples/verify/README.md @@ -9,8 +9,8 @@ case. `verify_citations_v1_contract.json` classifies the same executable cases for the current Milestone D source-only pre-alpha `verify_citations` v1 contract. The focused -`make milestone-d-verify-citations-contract` target checks that this contract inventory stays -aligned with `cases.json` and the report goldens. +`make milestone-d-verify-citations-contract` target schema-validates this contract inventory and +checks that it stays aligned with `cases.json` and the report goldens. ## Verify-Alpha Case Inventory diff --git a/examples/verify/verify_citations_v1_contract.json b/examples/verify/verify_citations_v1_contract.json index 5aa7038..9aee1fd 100644 --- a/examples/verify/verify_citations_v1_contract.json +++ b/examples/verify/verify_citations_v1_contract.json @@ -85,6 +85,7 @@ "explicit_blockers": [ "new verify_citations CLI alias", "Python Node MCP or hosted API surfaces", + "broad foreign-adapter hardening beyond existing fixtures", "crop API implementation", "sandbox subprocess backend expansion", "semantic or arithmetic verification" diff --git a/schemas/README.md b/schemas/README.md index 925c525..e824d24 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -13,6 +13,7 @@ bumps and downstream sign-off; output-changing heuristics are semver events (PRD | `ethos-verification-report.schema.json` | `verification_report.json` | | `ethos-verification-config.schema.json` | verification config (its c14n hash stamps reports) | | `ethos-crop-descriptor.schema.json` | crop descriptor JSON emitted by `ethos verify --crop-dir` | +| `ethos-verify-citations-contract.schema.json` | Milestone D `verify_citations` v1 source-only contract inventory | | `ethos-deterministic-profile.schema.json` | `profiles/ethos-deterministic-v*.json` checker | Conventions: snake_case keys; `additionalProperties: false` everywhere except the @@ -32,7 +33,9 @@ security-report / verification-report examples). Milestone D `verify_citations` v1 contract work is tracked in `docs/milestone-d-verify-citations-contract.md`. In this source-only pre-alpha slice, `verify_citations` names the citation-input to verification-report contract currently carried by -`ethos verify`; it does not add a new command or binding surface. +`ethos verify`; it does not add a new command or binding surface. The contract inventory at +`examples/verify/verify_citations_v1_contract.json` is schema-validated here; its alignment with +the executable case inventory and report goldens is checked by the Milestone D repository guard. Derived artifacts not schema'd here: `document.md` / `document.txt` (deterministic exports specified by the exporter config, Milestone B) and `debug.html` (Milestone C). diff --git a/schemas/ethos-verify-citations-contract.schema.json b/schemas/ethos-verify-citations-contract.schema.json new file mode 100644 index 0000000..83be6e3 --- /dev/null +++ b/schemas/ethos-verify-citations-contract.schema.json @@ -0,0 +1,116 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:ethos:schema:verify-citations-contract:1", + "title": "Ethos verify_citations v1 contract inventory", + "description": "Source-only pre-alpha Milestone D inventory for the current verify_citations v1 contract carried by `ethos verify`. This schema validates inventory shape and vocabulary; executable case/golden alignment stays in the repository guard.", + "type": "object", + "required": [ + "schema_version", + "contract", + "status", + "carrier", + "report_cases", + "usage_error_cases", + "summary_cases", + "explicit_blockers" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "const": 1 }, + "contract": { "const": "verify_citations.v1" }, + "status": { "const": "source-only-pre-alpha" }, + "carrier": { "const": "ethos verify" }, + "report_cases": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "name", + "category", + "all_evidence_grounded", + "statuses", + "reasons" + ], + "additionalProperties": false, + "properties": { + "name": { "$ref": "#/$defs/case_name" }, + "category": { + "enum": [ + "grounded", + "grounded-with-capability-warning", + "unsupported-non-v1", + "diagnostic-non-grounded", + "stale-fingerprint", + "capability-blocked" + ] + }, + "all_evidence_grounded": { "type": "boolean" }, + "statuses": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/check_status" } + }, + "reasons": { + "type": "array", + "items": { "$ref": "#/$defs/check_reason" } + } + } + } + }, + "usage_error_cases": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/case_name" }, + "uniqueItems": true + }, + "summary_cases": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/case_name" }, + "uniqueItems": true + }, + "explicit_blockers": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + } + }, + "$defs": { + "case_name": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, + "check_status": { + "enum": [ + "grounded", + "not_found", + "mismatch", + "stale", + "unsupported_claim_kind", + "capability_blocked", + "error" + ] + }, + "check_reason": { + "enum": [ + "missing_locator", + "missing_required_text", + "unsupported_claim_kind", + "stale_fingerprint", + "missing_source_fingerprint", + "missing_citation_fingerprint", + "missing_span_capability", + "missing_table_capability", + "unknown_coordinate_origin", + "element_not_found", + "span_not_found", + "page_not_found", + "bbox_not_found", + "missing_page_for_bbox", + "missing_table_cell_locator", + "table_not_found", + "table_cell_not_found", + "text_mismatch" + ] + } + } +} diff --git a/schemas/validate_examples.py b/schemas/validate_examples.py index 3a6f85c..2a29381 100644 --- a/schemas/validate_examples.py +++ b/schemas/validate_examples.py @@ -80,6 +80,9 @@ ]), ("ethos-verification-config.schema.json", [EXAMPLES / "verification-config.example.json"]), ("ethos-crop-descriptor.schema.json", [EXAMPLES / "crop-descriptor.example.json"]), + ("ethos-verify-citations-contract.schema.json", [ + ROOT / "examples" / "verify" / "verify_citations_v1_contract.json", + ]), ("ethos-deterministic-profile.schema.json", [ROOT / "profiles" / "ethos-deterministic-v1.json"]), ] From f3205013775faf888fe8225501d46f276c4c56cc Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 14:04:38 +0530 Subject: [PATCH 03/39] Harden verify_citations inventory guard Signed-off-by: docushell-admin --- ...t_milestone_d_verify_citations_contract.py | 91 +++++++++++++++++-- 1 file changed, 85 insertions(+), 6 deletions(-) diff --git a/.github/scripts/test_milestone_d_verify_citations_contract.py b/.github/scripts/test_milestone_d_verify_citations_contract.py index 92125e0..0671bf1 100644 --- a/.github/scripts/test_milestone_d_verify_citations_contract.py +++ b/.github/scripts/test_milestone_d_verify_citations_contract.py @@ -46,6 +46,42 @@ def load_json(path: Path): return json.loads(path.read_text(encoding="utf-8")) +def case_names(items: list[dict]) -> list[str]: + return [item["name"] for item in items] + + +def assert_unique(testcase: unittest.TestCase, values: list[str], label: str) -> None: + testcase.assertEqual( + len(values), + len(set(values)), + f"{label} contains duplicate names: {values}", + ) + + +def citation_claims(citations) -> list[dict]: + if isinstance(citations, list): + return citations + return citations["claims"] + + +def derived_category(report: dict) -> str: + statuses = [check["status"] for check in report["checks"]] + warnings = set(report["warnings"]) + capability_limits = report["capability_limits"] + + if report["fingerprint_stale"] or "stale" in statuses: + return "stale-fingerprint" + if report["unsupported_claim_kinds"] or "unsupported_claim_kind" in statuses: + return "unsupported-non-v1" + if "capability_blocked" in statuses: + return "capability-blocked" + if report["all_evidence_grounded"]: + if "capability_limited" in warnings or capability_limits: + return "grounded-with-capability-warning" + return "grounded" + return "diagnostic-non-grounded" + + class MilestoneDVerifyCitationsContractTests(unittest.TestCase): def test_target_is_declared_phony(self) -> None: text = makefile_text() @@ -138,15 +174,19 @@ def test_contract_inventory_matches_executable_case_inventory(self) -> None: self.assertEqual(inventory["status"], "source-only-pre-alpha") self.assertEqual(inventory["carrier"], "ethos verify") - report_case_names = {case["name"] for case in cases["report_cases"]} - inventory_report_names = {case["name"] for case in inventory["report_cases"]} + report_case_names = case_names(cases["report_cases"]) + inventory_report_names = case_names(inventory["report_cases"]) + assert_unique(self, report_case_names, "cases.json report_cases") + assert_unique(self, inventory_report_names, "contract inventory report_cases") self.assertEqual(inventory_report_names, report_case_names) - usage_case_names = {case["name"] for case in cases["usage_error_cases"]} - self.assertEqual(set(inventory["usage_error_cases"]), usage_case_names) + usage_case_names = case_names(cases["usage_error_cases"]) + assert_unique(self, usage_case_names, "cases.json usage_error_cases") + self.assertEqual(inventory["usage_error_cases"], usage_case_names) - summary_case_names = {case["name"] for case in cases["summary_cases"]} - self.assertEqual(set(inventory["summary_cases"]), summary_case_names) + summary_case_names = case_names(cases["summary_cases"]) + assert_unique(self, summary_case_names, "cases.json summary_cases") + self.assertEqual(inventory["summary_cases"], summary_case_names) def test_contract_inventory_matches_report_goldens(self) -> None: cases = load_json(VERIFY_CASES) @@ -179,6 +219,45 @@ def test_contract_inventory_matches_report_goldens(self) -> None: ) self.assertEqual(contract_case["statuses"], statuses, contract_case["name"]) self.assertEqual(contract_case["reasons"], reasons, contract_case["name"]) + self.assertEqual( + contract_case["category"], + derived_category(report), + contract_case["name"], + ) + + def test_report_goldens_echo_citation_inputs_in_order(self) -> None: + cases = load_json(VERIFY_CASES) + + for case in cases["report_cases"]: + citations = load_json(ROOT / case["citations"]) + report = load_json(ROOT / case["golden"]) + claims = citation_claims(citations) + + self.assertEqual(len(report["checks"]), len(claims), case["name"]) + for index, (check, claim) in enumerate(zip(report["checks"], claims), 1): + self.assertEqual(check["id"], f"v{index:04}", case["name"]) + self.assertEqual(check["claim"], claim, case["name"]) + + def test_report_goldens_keep_current_v1_literal_checks_non_semantic(self) -> None: + cases = load_json(VERIFY_CASES) + + for case in cases["report_cases"]: + report = load_json(ROOT / case["golden"]) + checks = report["checks"] + self.assertGreater(len(checks), 0, case["name"]) + for check in checks: + self.assertFalse(check["semantic_unverified"], case["name"]) + expected_gate = ( + all(check["status"] == "grounded" for check in checks) + and not any(check["semantic_unverified"] for check in checks) + and report["unsupported_claim_kinds"] == [] + and report["fingerprint_stale"] is False + ) + self.assertEqual( + report["all_evidence_grounded"], + expected_gate, + case["name"], + ) def test_contract_inventory_keeps_blockers_explicit(self) -> None: inventory = load_json(CONTRACT_INVENTORY) From 879a4ee01bbbc3d1be72c49c4a0f1b8b3b0c220a Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 14:10:25 +0530 Subject: [PATCH 04/39] Guard verify_citations schema vocabularies Signed-off-by: docushell-admin --- ...t_milestone_d_verify_citations_contract.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.github/scripts/test_milestone_d_verify_citations_contract.py b/.github/scripts/test_milestone_d_verify_citations_contract.py index 0671bf1..bccf471 100644 --- a/.github/scripts/test_milestone_d_verify_citations_contract.py +++ b/.github/scripts/test_milestone_d_verify_citations_contract.py @@ -29,6 +29,8 @@ CONTRACT = ROOT / "docs/milestone-d-verify-citations-contract.md" VERIFY_CASES = ROOT / "examples/verify/cases.json" CONTRACT_INVENTORY = ROOT / "examples/verify/verify_citations_v1_contract.json" +CONTRACT_INVENTORY_SCHEMA = ROOT / "schemas/ethos-verify-citations-contract.schema.json" +VERIFICATION_REPORT_SCHEMA = ROOT / "schemas/ethos-verification-report.schema.json" ROADMAP = ROOT / "docs/roadmap.md" EXECUTION_STATUS = ROOT / "docs/execution-status.md" SCHEMAS_README = ROOT / "schemas/README.md" @@ -165,6 +167,29 @@ def test_contract_names_fixture_backed_validation(self) -> None: text, ) + def test_contract_inventory_schema_check_enums_match_report_schema(self) -> None: + inventory_schema = load_json(CONTRACT_INVENTORY_SCHEMA) + report_schema = load_json(VERIFICATION_REPORT_SCHEMA) + + self.assertEqual( + { + "check_status": sorted( + inventory_schema["$defs"]["check_status"]["enum"] + ), + "check_reason": sorted( + inventory_schema["$defs"]["check_reason"]["enum"] + ), + }, + { + "check_status": sorted( + report_schema["properties"]["checks"]["items"]["properties"][ + "status" + ]["enum"] + ), + "check_reason": sorted(report_schema["$defs"]["check_reason"]["enum"]), + }, + ) + def test_contract_inventory_matches_executable_case_inventory(self) -> None: cases = load_json(VERIFY_CASES) inventory = load_json(CONTRACT_INVENTORY) From ab224a3710117ae8a7294194e88c83dd7668d61a Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 14:19:35 +0530 Subject: [PATCH 05/39] Tighten verify_citations blocker guard Signed-off-by: docushell-admin --- ...t_milestone_d_verify_citations_contract.py | 59 ++++++++++++------- .../verify/verify_citations_v1_contract.json | 6 +- 2 files changed, 42 insertions(+), 23 deletions(-) diff --git a/.github/scripts/test_milestone_d_verify_citations_contract.py b/.github/scripts/test_milestone_d_verify_citations_contract.py index bccf471..97f0c04 100644 --- a/.github/scripts/test_milestone_d_verify_citations_contract.py +++ b/.github/scripts/test_milestone_d_verify_citations_contract.py @@ -34,6 +34,14 @@ ROADMAP = ROOT / "docs/roadmap.md" EXECUTION_STATUS = ROOT / "docs/execution-status.md" SCHEMAS_README = ROOT / "schemas/README.md" +EXPECTED_EXPLICIT_BLOCKERS = [ + "a new `verify_citations` CLI alias", + "Python, Node, MCP, or hosted API surfaces", + "broad foreign-adapter hardening beyond existing fixtures", + "crop API implementation", + "sandbox/subprocess backend expansion", + "semantic or arithmetic verification", +] def contract_text() -> str: @@ -48,6 +56,21 @@ def load_json(path: Path): return json.loads(path.read_text(encoding="utf-8")) +def contract_explicit_blockers() -> list[str]: + match = re.search( + r"## Explicit Blockers For This Slice\n\n" + r"This first D slice does not add:\n\n" + r"(?P(?:- .+\n)+)", + contract_text(), + ) + if match is None: + raise AssertionError("missing explicit blocker list") + return [ + line.removeprefix("- ").rstrip(";.") + for line in match.group("bullets").strip().splitlines() + ] + + def case_names(items: list[dict]) -> list[str]: return [item["name"] for item in items] @@ -216,19 +239,19 @@ def test_contract_inventory_matches_executable_case_inventory(self) -> None: def test_contract_inventory_matches_report_goldens(self) -> None: cases = load_json(VERIFY_CASES) inventory = load_json(CONTRACT_INVENTORY) + inventory_schema = load_json(CONTRACT_INVENTORY_SCHEMA) case_by_name = {case["name"]: case for case in cases["report_cases"]} - - allowed_categories = { - "grounded", - "grounded-with-capability-warning", - "unsupported-non-v1", - "diagnostic-non-grounded", - "stale-fingerprint", - "capability-blocked", - } + allowed_categories = set( + inventory_schema["properties"]["report_cases"]["items"]["properties"][ + "category" + ]["enum"] + ) + seen_categories = set() + derived_categories = set() for contract_case in inventory["report_cases"]: self.assertIn(contract_case["category"], allowed_categories) + seen_categories.add(contract_case["category"]) report = load_json(ROOT / case_by_name[contract_case["name"]]["golden"]) statuses = [check["status"] for check in report["checks"]] reasons = [ @@ -244,11 +267,15 @@ def test_contract_inventory_matches_report_goldens(self) -> None: ) self.assertEqual(contract_case["statuses"], statuses, contract_case["name"]) self.assertEqual(contract_case["reasons"], reasons, contract_case["name"]) + category = derived_category(report) + derived_categories.add(category) self.assertEqual( contract_case["category"], - derived_category(report), + category, contract_case["name"], ) + self.assertEqual(allowed_categories, seen_categories) + self.assertEqual(allowed_categories, derived_categories) def test_report_goldens_echo_citation_inputs_in_order(self) -> None: cases = load_json(VERIFY_CASES) @@ -286,17 +313,9 @@ def test_report_goldens_keep_current_v1_literal_checks_non_semantic(self) -> Non def test_contract_inventory_keeps_blockers_explicit(self) -> None: inventory = load_json(CONTRACT_INVENTORY) - blockers = " ".join(inventory["explicit_blockers"]) - for expected in [ - "new verify_citations CLI alias", - "Python Node MCP or hosted API surfaces", - "broad foreign-adapter hardening beyond existing fixtures", - "crop API implementation", - "sandbox subprocess backend expansion", - "semantic or arithmetic verification", - ]: - self.assertIn(expected, blockers) + self.assertEqual(EXPECTED_EXPLICIT_BLOCKERS, contract_explicit_blockers()) + self.assertEqual(EXPECTED_EXPLICIT_BLOCKERS, inventory["explicit_blockers"]) if __name__ == "__main__": diff --git a/examples/verify/verify_citations_v1_contract.json b/examples/verify/verify_citations_v1_contract.json index 9aee1fd..9365602 100644 --- a/examples/verify/verify_citations_v1_contract.json +++ b/examples/verify/verify_citations_v1_contract.json @@ -83,11 +83,11 @@ ], "summary_cases": ["native-ungrounded-summary"], "explicit_blockers": [ - "new verify_citations CLI alias", - "Python Node MCP or hosted API surfaces", + "a new `verify_citations` CLI alias", + "Python, Node, MCP, or hosted API surfaces", "broad foreign-adapter hardening beyond existing fixtures", "crop API implementation", - "sandbox subprocess backend expansion", + "sandbox/subprocess backend expansion", "semantic or arithmetic verification" ] } From 397fec5f2c2e8619daa5071b8b706501a4b6de02 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 14:24:48 +0530 Subject: [PATCH 06/39] Guard verify_citations config hash Signed-off-by: docushell-admin --- ...t_milestone_d_verify_citations_contract.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/scripts/test_milestone_d_verify_citations_contract.py b/.github/scripts/test_milestone_d_verify_citations_contract.py index 97f0c04..9cf2967 100644 --- a/.github/scripts/test_milestone_d_verify_citations_contract.py +++ b/.github/scripts/test_milestone_d_verify_citations_contract.py @@ -17,6 +17,7 @@ from __future__ import annotations +import hashlib import json import re import unittest @@ -30,6 +31,7 @@ VERIFY_CASES = ROOT / "examples/verify/cases.json" CONTRACT_INVENTORY = ROOT / "examples/verify/verify_citations_v1_contract.json" CONTRACT_INVENTORY_SCHEMA = ROOT / "schemas/ethos-verify-citations-contract.schema.json" +VERIFICATION_CONFIG_EXAMPLE = ROOT / "schemas/examples/verification-config.example.json" VERIFICATION_REPORT_SCHEMA = ROOT / "schemas/ethos-verification-report.schema.json" ROADMAP = ROOT / "docs/roadmap.md" EXECUTION_STATUS = ROOT / "docs/execution-status.md" @@ -56,6 +58,16 @@ def load_json(path: Path): return json.loads(path.read_text(encoding="utf-8")) +def sha256_c14n(value: dict) -> str: + encoded = json.dumps( + value, + separators=(",", ":"), + sort_keys=True, + ensure_ascii=False, + ).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + def contract_explicit_blockers() -> list[str]: match = re.search( r"## Explicit Blockers For This Slice\n\n" @@ -311,6 +323,18 @@ def test_report_goldens_keep_current_v1_literal_checks_non_semantic(self) -> Non case["name"], ) + def test_report_goldens_use_default_verification_config_hash(self) -> None: + cases = load_json(VERIFY_CASES) + expected_hash = sha256_c14n(load_json(VERIFICATION_CONFIG_EXAMPLE)) + + for case in cases["report_cases"]: + report = load_json(ROOT / case["golden"]) + self.assertEqual( + expected_hash, + report["verification_config_sha256"], + case["name"], + ) + def test_contract_inventory_keeps_blockers_explicit(self) -> None: inventory = load_json(CONTRACT_INVENTORY) From 50a1fb524a938a778b247728df0aac8fb1bb37aa Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 14:31:23 +0530 Subject: [PATCH 07/39] Guard verify_citations CLI alias boundary Signed-off-by: docushell-admin --- crates/ethos-cli/tests/verify.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/ethos-cli/tests/verify.rs b/crates/ethos-cli/tests/verify.rs index 83c9647..a91696d 100644 --- a/crates/ethos-cli/tests/verify.rs +++ b/crates/ethos-cli/tests/verify.rs @@ -676,6 +676,21 @@ fn fail_on_ungrounded_keeps_invalid_input_on_usage_exit_code() { .contains("citations file must contain at least one claim")); } +#[test] +fn verify_citations_contract_is_not_a_cli_alias() { + for subcommand in ["verify-citations", "verify_citations"] { + let output = run_ethos(&[subcommand]); + + assert_eq!(output.status.code(), Some(2), "case {subcommand}"); + assert_eq!(output.stdout, b"", "case {subcommand}"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("unrecognized subcommand") && stderr.contains(subcommand), + "case {subcommand} stderr:\n{stderr}" + ); + } +} + #[test] fn malformed_native_document_is_usage_error() { let root = repo_root(); From 9b0f4a40c99289cfe5945a2a4cc58cf3ba32b3c7 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 14:36:49 +0530 Subject: [PATCH 08/39] Guard verify_citations case lane names Signed-off-by: docushell-admin --- ...t_milestone_d_verify_citations_contract.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/.github/scripts/test_milestone_d_verify_citations_contract.py b/.github/scripts/test_milestone_d_verify_citations_contract.py index 9cf2967..8949a3c 100644 --- a/.github/scripts/test_milestone_d_verify_citations_contract.py +++ b/.github/scripts/test_milestone_d_verify_citations_contract.py @@ -95,6 +95,20 @@ def assert_unique(testcase: unittest.TestCase, values: list[str], label: str) -> ) +def assert_disjoint_case_groups( + testcase: unittest.TestCase, groups: dict[str, list[str]], label: str +) -> None: + seen: dict[str, str] = {} + duplicates: list[str] = [] + for group_name, names in groups.items(): + for name in names: + if name in seen: + duplicates.append(f"{name} appears in both {seen[name]} and {group_name}") + else: + seen[name] = group_name + testcase.assertEqual([], duplicates, f"{label} case names overlap across groups") + + def citation_claims(citations) -> list[dict]: if isinstance(citations, list): return citations @@ -248,6 +262,29 @@ def test_contract_inventory_matches_executable_case_inventory(self) -> None: assert_unique(self, summary_case_names, "cases.json summary_cases") self.assertEqual(inventory["summary_cases"], summary_case_names) + def test_contract_inventory_case_names_are_disjoint_across_lanes(self) -> None: + cases = load_json(VERIFY_CASES) + inventory = load_json(CONTRACT_INVENTORY) + + assert_disjoint_case_groups( + self, + { + "cases.json report_cases": case_names(cases["report_cases"]), + "cases.json usage_error_cases": case_names(cases["usage_error_cases"]), + "cases.json summary_cases": case_names(cases["summary_cases"]), + }, + "examples/verify/cases.json", + ) + assert_disjoint_case_groups( + self, + { + "contract inventory report_cases": case_names(inventory["report_cases"]), + "contract inventory usage_error_cases": inventory["usage_error_cases"], + "contract inventory summary_cases": inventory["summary_cases"], + }, + "verify_citations v1 contract inventory", + ) + def test_contract_inventory_matches_report_goldens(self) -> None: cases = load_json(VERIFY_CASES) inventory = load_json(CONTRACT_INVENTORY) From a33e7ea423533d22f9b41f6e98c40a3d39e4ceb0 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 14:39:58 +0530 Subject: [PATCH 09/39] Guard explicit verify config hash Signed-off-by: docushell-admin --- crates/ethos-cli/tests/verify.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/ethos-cli/tests/verify.rs b/crates/ethos-cli/tests/verify.rs index a91696d..c5f67ed 100644 --- a/crates/ethos-cli/tests/verify.rs +++ b/crates/ethos-cli/tests/verify.rs @@ -2587,7 +2587,13 @@ fn case_insensitive_config_allows_literal_case_difference() { "--config", config.to_str().unwrap(), ]); + let expected_config_hash = + ethos_core::c14n::sha256_hex(&json_file(&config)).expect("config hash computes"); + assert_eq!( + report["verification_config_sha256"].as_str().unwrap(), + expected_config_hash + ); assert_eq!(report["checks"][0]["status"], "grounded"); assert_eq!( report["checks"][0]["match_method"], From 4a9b5e8cc76bf0e63281650fd6c795727d11c841 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 14:50:13 +0530 Subject: [PATCH 10/39] Add crop_element v1 contract guard Signed-off-by: docushell-admin --- .github/scripts/test_execution_status.py | 2 + .../test_milestone_d_crop_element_contract.py | 243 ++++++++++++++++++ Makefile | 10 +- docs/execution-status.md | 3 +- docs/milestone-d-crop-element-contract.md | 79 ++++++ docs/roadmap.md | 7 +- examples/crop/crop_element_v1_contract.json | 23 ++ schemas/README.md | 9 + .../ethos-crop-element-contract.schema.json | 54 ++++ schemas/validate_examples.py | 3 + 10 files changed, 430 insertions(+), 3 deletions(-) create mode 100644 .github/scripts/test_milestone_d_crop_element_contract.py create mode 100644 docs/milestone-d-crop-element-contract.md create mode 100644 examples/crop/crop_element_v1_contract.json create mode 100644 schemas/ethos-crop-element-contract.schema.json diff --git a/.github/scripts/test_execution_status.py b/.github/scripts/test_execution_status.py index a2b5110..b7eb014 100644 --- a/.github/scripts/test_execution_status.py +++ b/.github/scripts/test_execution_status.py @@ -39,6 +39,7 @@ def test_status_is_scoped_to_internal_continuation(self) -> None: text, ) self.assertIn("docs/milestone-d-verify-citations-contract.md", text) + self.assertIn("docs/milestone-d-crop-element-contract.md", text) self.assertNotIn("Status: Pre-alpha / Milestone B entry.", text) def test_internal_check_command_is_documented(self) -> None: @@ -47,6 +48,7 @@ def test_internal_check_command_is_documented(self) -> None: self.assertIn("make milestone-b-internal-checks", text) self.assertIn("make milestone-c-internal-checks", text) self.assertIn("make milestone-d-verify-citations-contract", text) + self.assertIn("make milestone-d-crop-element-contract", text) self.assertIn("CI has a static guard for that target's command wiring", text) def test_public_posture_boundary_remains_explicit(self) -> None: diff --git a/.github/scripts/test_milestone_d_crop_element_contract.py b/.github/scripts/test_milestone_d_crop_element_contract.py new file mode 100644 index 0000000..e268c8f --- /dev/null +++ b/.github/scripts/test_milestone_d_crop_element_contract.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +# +# Copyright 2026 The Ethos maintainers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import annotations + +import json +import re +import unittest +from pathlib import Path + +from jsonschema import Draft202012Validator +from makefile_guard import makefile_text, target_block + + +ROOT = Path(__file__).resolve().parents[2] +CONTRACT = ROOT / "docs/milestone-d-crop-element-contract.md" +CONTRACT_INVENTORY = ROOT / "examples/crop/crop_element_v1_contract.json" +CONTRACT_INVENTORY_SCHEMA = ROOT / "schemas/ethos-crop-element-contract.schema.json" +CROP_DESCRIPTOR_SCHEMA = ROOT / "schemas/ethos-crop-descriptor.schema.json" +ROADMAP = ROOT / "docs/roadmap.md" +EXECUTION_STATUS = ROOT / "docs/execution-status.md" +SCHEMAS_README = ROOT / "schemas/README.md" +CLI_MAIN = ROOT / "crates/ethos-cli/src/main.rs" +VERIFY_TESTS = ROOT / "crates/ethos-cli/tests/verify.rs" +EXPECTED_EXPLICIT_BLOCKERS = [ + "a first-class `crop_element` CLI command or binding surface", + "Python, Node, MCP, or hosted crop API surfaces", + "sandbox/subprocess backend expansion", + "rendered-crop backend changes", + "foreign-adapter crop coordinate hardening", + "cross-platform rendered-crop byte identity claims", +] + + +def load_json(path: Path): + return json.loads(path.read_text(encoding="utf-8")) + + +def contract_text() -> str: + return CONTRACT.read_text(encoding="utf-8") + + +def normalized_contract_text() -> str: + return re.sub(r"\s+", " ", contract_text()) + + +def contract_explicit_blockers() -> list[str]: + match = re.search( + r"## Explicit Blockers For This Slice\n\n" + r"This first `crop_element` slice does not add:\n\n" + r"(?P(?:- .+\n)+)", + contract_text(), + ) + if match is None: + raise AssertionError("missing crop_element explicit blocker list") + return [ + line.removeprefix("- ").rstrip(";.") + for line in match.group("bullets").strip().splitlines() + ] + + +def elements_by_id(document: dict) -> dict[str, dict]: + return { + element["id"]: element + for element in document["payload"]["elements"] + if "id" in element + } + + +class MilestoneDCropElementContractTests(unittest.TestCase): + def test_target_is_declared_phony(self) -> None: + text = makefile_text() + + self.assertIn(".PHONY:", text) + self.assertIn("milestone-d-crop-element-contract", text) + + def test_target_composes_contract_gates(self) -> None: + block = target_block("milestone-d-crop-element-contract") + + required = [ + "cargo test --locked -p ethos-cli --test verify " + "native_verify_crop_dir_writes_deterministic_crop_descriptors", + "$(PYTHON) schemas/validate_examples.py", + "$(PYTHON) .github/scripts/test_execution_status.py", + "$(PYTHON) .github/scripts/test_roadmap_status.py", + "$(PYTHON) .github/scripts/test_milestone_d_crop_element_contract.py", + "git diff --check", + ] + for command in required: + self.assertIn(command, block) + + def test_target_stays_contract_scoped(self) -> None: + block = target_block("milestone-d-crop-element-contract") + + for out_of_scope in [ + "verify-rendered-crops", + "compare-rendered-crops", + "layout-evaluator-alpha", + "python-surface-test", + "release-", + "third-party-license-manifest", + "npm", + "mcp", + ]: + self.assertNotIn(out_of_scope, block) + + def test_contract_is_linked_from_status_docs(self) -> None: + for path in [ROADMAP, EXECUTION_STATUS, SCHEMAS_README]: + text = path.read_text(encoding="utf-8") + self.assertIn("milestone-d-crop-element-contract.md", text, path) + + def test_contract_defines_existing_carrier_not_new_surface(self) -> None: + text = normalized_contract_text() + + self.assertIn("source-only pre-alpha contract work", text) + self.assertIn("does not create a first-class CLI command", text) + self.assertIn( + "The current executable crop carrier remains `ethos verify --crop-dir` " + "and optional `--crop-source-pdf`", + text, + ) + self.assertIn( + "`crop_element` names the future first-class contract between a parsed Ethos " + "document, an explicit element locator, a crop descriptor, and an optional rendered " + "artifact", + text, + ) + + def test_contract_pins_descriptor_artifact_boundary(self) -> None: + text = normalized_contract_text() + + for required in [ + "`schemas/ethos-crop-descriptor.schema.json`", + "`examples/crop/crop_element_v1_contract.json`", + "`schemas/examples/crop-descriptor.example.json`", + "document fingerprint", + "element id", + "page id", + "bbox", + "crop descriptor filename", + "optional rendered PNG metadata and source PDF fingerprint", + "`make milestone-d-crop-element-contract PYTHON=/bin/python`", + ]: + self.assertIn(required, text) + + def test_contract_keeps_boundaries_explicit(self) -> None: + text = normalized_contract_text() + + self.assertIn("missing elements, missing pages, missing bboxes, malformed bboxes", text) + self.assertIn("source fingerprint mismatch fail closed", text) + self.assertIn("does not infer missing geometry", text) + self.assertIn("Cross-platform rendered crop byte identity is not part", text) + self.assertEqual(EXPECTED_EXPLICIT_BLOCKERS, contract_explicit_blockers()) + + def test_contract_inventory_schema_validates_inventory(self) -> None: + schema = load_json(CONTRACT_INVENTORY_SCHEMA) + inventory = load_json(CONTRACT_INVENTORY) + Draft202012Validator.check_schema(schema) + errors = sorted( + Draft202012Validator(schema).iter_errors(inventory), + key=lambda error: list(error.absolute_path), + ) + self.assertEqual([], errors) + + def test_contract_inventory_binds_element_to_descriptor_example(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + + self.assertEqual(inventory["schema_version"], 1) + self.assertEqual(inventory["contract"], "crop_element.v1") + self.assertEqual(inventory["status"], "source-only-pre-alpha") + self.assertEqual(inventory["carrier"], "ethos verify --crop-dir") + self.assertEqual(EXPECTED_EXPLICIT_BLOCKERS, inventory["explicit_blockers"]) + + case_names = [case["name"] for case in inventory["cases"]] + self.assertEqual(len(case_names), len(set(case_names))) + + for case in inventory["cases"]: + document_path = ROOT / case["document"] + descriptor_path = ROOT / case["descriptor"] + self.assertTrue(document_path.is_file(), case["name"]) + self.assertTrue(descriptor_path.is_file(), case["name"]) + + document = load_json(document_path) + descriptor = load_json(descriptor_path) + element = elements_by_id(document)[case["element_id"]] + + self.assertEqual(descriptor["artifact_type"], "ethos.crop_descriptor.v1") + self.assertEqual(descriptor["document_fingerprint"], document["fingerprint"]) + self.assertEqual(descriptor["page"], element["page"]) + self.assertEqual(descriptor["bbox"], element["bbox"]) + self.assertEqual(descriptor["rendering_status"], case["rendering_status"]) + self.assertEqual(descriptor["check_ids"], ["v0001"]) + + def test_crop_descriptor_example_validates_against_descriptor_schema(self) -> None: + schema = load_json(CROP_DESCRIPTOR_SCHEMA) + descriptor = load_json(ROOT / "schemas/examples/crop-descriptor.example.json") + errors = sorted( + Draft202012Validator(schema).iter_errors(descriptor), + key=lambda error: list(error.absolute_path), + ) + self.assertEqual([], errors) + + def test_current_cli_surface_has_no_first_class_crop_element_command(self) -> None: + source = CLI_MAIN.read_text(encoding="utf-8") + match = re.search( + r"#\[derive\(Subcommand\)\]\s*enum Command \{\n(?P.*?)\n\}", + source, + flags=re.S, + ) + self.assertIsNotNone(match, "missing top-level CLI Command enum") + + command_enum = match.group("body") + for alias in ["crop-element", "crop_element", "CropElement"]: + self.assertNotIn(alias, command_enum) + + def test_existing_verify_tests_cover_current_crop_carrier(self) -> None: + text = VERIFY_TESTS.read_text(encoding="utf-8") + + for test_name in [ + "native_verify_crop_dir_writes_deterministic_crop_descriptors", + "crop_source_pdf_requires_crop_dir", + "crop_source_pdf_rejects_source_fingerprint_mismatch", + "crop_source_pdf_writes_rendered_crop_artifacts_when_pdfium_is_configured", + ]: + self.assertIn(f"fn {test_name}()", text) + + +if __name__ == "__main__": + unittest.main() diff --git a/Makefile b/Makefile index 5501667..a11ab37 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ COMPARE_RENDERED_CROPS_LEFT ?= $(VERIFY_RENDERED_CROPS_OUT)/run1 COMPARE_RENDERED_CROPS_RIGHT ?= $(VERIFY_RENDERED_CROPS_OUT)/run2 LAYOUT_EVALUATOR_OUT ?= $(ROOT)/target/layout-evaluator-alpha -.PHONY: verify-alpha verify-alpha-tree rag-chunk-alpha security-report-alpha milestone-d-verify-citations-contract verify-rendered-crops compare-rendered-crops layout-evaluator-alpha python-surface-test milestone-b-internal-checks milestone-c-internal-checks release-hygiene release-advisory third-party-license-manifest release-notice-draft +.PHONY: verify-alpha verify-alpha-tree rag-chunk-alpha security-report-alpha milestone-d-verify-citations-contract milestone-d-crop-element-contract verify-rendered-crops compare-rendered-crops layout-evaluator-alpha python-surface-test milestone-b-internal-checks milestone-c-internal-checks release-hygiene release-advisory third-party-license-manifest release-notice-draft $(ETHOS_BIN): cargo build --locked -p ethos-cli @@ -55,6 +55,14 @@ milestone-d-verify-citations-contract: $(PYTHON) .github/scripts/test_milestone_d_verify_citations_contract.py git diff --check +milestone-d-crop-element-contract: + cargo test --locked -p ethos-cli --test verify native_verify_crop_dir_writes_deterministic_crop_descriptors + $(PYTHON) schemas/validate_examples.py + $(PYTHON) .github/scripts/test_execution_status.py + $(PYTHON) .github/scripts/test_roadmap_status.py + $(PYTHON) .github/scripts/test_milestone_d_crop_element_contract.py + git diff --check + verify-rendered-crops: $(ETHOS_BIN) $(PYTHON) examples/verify/check_rendered_crops.py --repo-root $(ROOT) --ethos-bin $(ETHOS_BIN) --out-dir $(VERIFY_RENDERED_CROPS_OUT) git diff --check diff --git a/docs/execution-status.md b/docs/execution-status.md index 0d6a405..bd7d1b0 100644 --- a/docs/execution-status.md +++ b/docs/execution-status.md @@ -2,7 +2,7 @@ Date: 2026-06-18 Owner: product / decider -Status: Pre-alpha / internal transition from Milestone C artifact-validation closeout to Milestone D source-only contract work. Week 0 governance is accepted, WS-ENGINE Phase 1 has a real narrow PDFium path, WS-VERIFY-ALPHA has real deterministic evidence checks over native Ethos JSON and pinned OpenDataLoader output, WS-HARNESS has fail-closed readiness scaffolding, the Gate Zero corpus/hardware manifest and direct competitor lock are frozen/signed, ADR-0005 records an accepted `PROCEED` decision for internal Milestone B continuation, ADR-0006 closes package identifier/trademark validation, ADR-0007 locks the product direction, and the public-source preflight is green for a source-only pre-alpha GitHub push. Milestone C has a source-tree internal artifact-validation closeout for the RAG chunk and security-report trust-loop checks. Current Milestone D work begins with the narrow `verify_citations` v1 contract in `docs/milestone-d-verify-citations-contract.md`, carried by the existing `ethos verify` path and fixture-backed validation. Public benchmark reports, releases, packages, production positioning, and all performance/quality/footprint claims remain blocked. The controlled-run handoff remains `docs/gate-zero-evidence-runbook.md`; the accepted decision record is `docs/decisions/ADR-0005-gate-zero-decision.md`. +Status: Pre-alpha / internal transition from Milestone C artifact-validation closeout to Milestone D source-only contract work. Week 0 governance is accepted, WS-ENGINE Phase 1 has a real narrow PDFium path, WS-VERIFY-ALPHA has real deterministic evidence checks over native Ethos JSON and pinned OpenDataLoader output, WS-HARNESS has fail-closed readiness scaffolding, the Gate Zero corpus/hardware manifest and direct competitor lock are frozen/signed, ADR-0005 records an accepted `PROCEED` decision for internal Milestone B continuation, ADR-0006 closes package identifier/trademark validation, ADR-0007 locks the product direction, and the public-source preflight is green for a source-only pre-alpha GitHub push. Milestone C has a source-tree internal artifact-validation closeout for the RAG chunk and security-report trust-loop checks. Current Milestone D work began with the narrow `verify_citations` v1 contract in `docs/milestone-d-verify-citations-contract.md`, carried by the existing `ethos verify` path and fixture-backed validation. The next D contract-prep slice is the `crop_element` v1 contract in `docs/milestone-d-crop-element-contract.md`, carried by the existing `ethos verify --crop-dir` descriptor path and fixture-backed validation without adding a first-class crop surface. Public benchmark reports, releases, packages, production positioning, and all performance/quality/footprint claims remain blocked. The controlled-run handoff remains `docs/gate-zero-evidence-runbook.md`; the accepted decision record is `docs/decisions/ADR-0005-gate-zero-decision.md`. ## Current Reality @@ -27,6 +27,7 @@ The committed implementation now includes: - `ethos security report` has a source-only pre-alpha artifact check over the committed document example. The current internal checks cover deterministic report output, report/source identity grounding, security-warning lane and message diagnostics, locator grounding, inventory/report parity, summary drift, warning id uniqueness, deterministic warning numbering, and explicit rejection of unsupported current source-warning references. - `make milestone-c-internal-checks` composes the current internal Milestone C artifact-validation path across RAG chunk and security-report gates; CI/static guard scripts fail closed if that command wiring or the dated closeout record drifts. - Milestone D source-only pre-alpha contract work has started with `docs/milestone-d-verify-citations-contract.md`. It defines `verify_citations` v1 as the citation-input, verification-config, grounding-source, and verification-report contract currently carried by `ethos verify`; schema/example validation checks that the minimal citation example and verification-report example stay coherent. Focused validation is `make milestone-d-verify-citations-contract PYTHON=/bin/python`. +- Milestone D `crop_element` v1 contract prep has started with `docs/milestone-d-crop-element-contract.md`. It defines `crop_element` as the future element-to-crop-descriptor contract currently represented by the existing `ethos verify --crop-dir` carrier; schema/example validation checks the inventory at `examples/crop/crop_element_v1_contract.json`, and the repository guard checks that it stays coherent with the document and crop-descriptor examples. Focused validation is `make milestone-d-crop-element-contract PYTHON=/bin/python`. Still absent or not claimable: public benchmark reports, public competitor-comparison claims, public speed/quality/footprint claims, OCR/image-only support, real table extraction, mature list/heading/layout semantics beyond current fixture-backed alpha paths, semantic/arithmetic verification beyond deterministic evidence lookup, Phase 2 project-maintained PDFium builds, release packaging, and claim-audit approval for any public result wording. diff --git a/docs/milestone-d-crop-element-contract.md b/docs/milestone-d-crop-element-contract.md new file mode 100644 index 0000000..c2cd639 --- /dev/null +++ b/docs/milestone-d-crop-element-contract.md @@ -0,0 +1,79 @@ +# Milestone D `crop_element` v1 Contract + +Status: source-only pre-alpha contract work for internal Milestone D continuation. + +This note defines the narrow `crop_element` contract-prep slice for Milestone D. It does not +create a first-class CLI command, Python binding, Node binding, MCP server method, or hosted +surface. The current executable crop carrier remains `ethos verify --crop-dir` and optional +`--crop-source-pdf`; `crop_element` names the future first-class contract between a parsed Ethos +document, an explicit element locator, a crop descriptor, and an optional rendered artifact. + +## Contract Surface + +`crop_element` v1 will consume: + +- a canonical Ethos document JSON grounding source with explicit element ids, page ids, integer + bboxes, and a document fingerprint; +- an `element_id` locator that resolves to one element with one page and one bbox; +- optional source PDF bytes whose fingerprint matches the document source fingerprint when + rendered output is requested. + +It emits a crop descriptor governed by `schemas/ethos-crop-descriptor.schema.json`. When rendering +is requested and a matching source PDF is available, the descriptor also binds the rendered PNG +filename, byte hash, dimensions, and source PDF fingerprint. + +The current source-tree fixture for this contract boundary is +`examples/crop/crop_element_v1_contract.json`. That inventory binds +`schemas/examples/document.example.json` element `e000002` to +`schemas/examples/crop-descriptor.example.json`. + +Focused validation command: + +- `make milestone-d-crop-element-contract PYTHON=/bin/python` + +The target runs current verifier crop coverage, schema/example validation, status/roadmap guards, +this contract guard, and diff hygiene. It intentionally stays narrower than implementation work for +a first-class crop surface. + +## Supported v1 Boundaries + +The v1 contract boundary is native, explicit, and source-bound: + +- crop locators resolve through one `element_id`; +- the resolved element must carry one page id and one integer bbox; +- descriptor `crop_ref` values remain opaque artifact filenames; +- descriptor JSON remains the canonical crop audit artifact; +- rendered PNG output is optional and must be bound to a matching source PDF fingerprint; +- missing elements, missing pages, missing bboxes, malformed bboxes, and source fingerprint + mismatch fail closed. + +The contract does not infer missing geometry, synthesize evidence, or reinterpret foreign adapter +coordinates. Cross-platform rendered crop byte identity is not part of this contract boundary. + +## Relationship To Existing Verifier Artifacts + +`ethos verify --crop-dir` can already emit crop descriptors for grounded evidence checks. That path +is evidence-artifact plumbing, not a first-class crop API. The future `crop_element` surface must +preserve the same audit bindings before it can replace or wrap the current carrier: + +- document fingerprint; +- element id; +- page id; +- bbox; +- crop descriptor filename; +- request identity; +- optional rendered PNG metadata and source PDF fingerprint. + +## Explicit Blockers For This Slice + +This first `crop_element` slice does not add: + +- a first-class `crop_element` CLI command or binding surface; +- Python, Node, MCP, or hosted crop API surfaces; +- sandbox/subprocess backend expansion; +- rendered-crop backend changes; +- foreign-adapter crop coordinate hardening; +- cross-platform rendered-crop byte identity claims. + +Public-facing language remains limited to source-only pre-alpha internal continuation, evidence +grounding, diagnostics, fixture-backed validation, and explicit blockers. diff --git a/docs/roadmap.md b/docs/roadmap.md index 7cca555..c23cf02 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -21,6 +21,11 @@ Milestone D source-only pre-alpha contract work has started with the narrow current executable carrier remains `ethos verify`; the first D slice is a contract and fixture-backed validation boundary, not a new public command, binding, crop API, sandbox backend, Node beta, or MCP experimental scope. +The next D contract-prep slice defines the source-only +[`crop_element` v1 contract](milestone-d-crop-element-contract.md) over the +existing `ethos verify --crop-dir` crop descriptor carrier; it does not add a +first-class crop command, binding surface, sandbox backend, Node beta, or MCP +experimental scope. | Milestone | Window | Contents | Gate | | --- | --- | --- | --- | @@ -28,7 +33,7 @@ binding, crop API, sandbox backend, Node beta, or MCP experimental scope. | A | weeks 1-8 | Contracts (5 schemas, c14n, deterministic profile), trust-boundary artifacts (`GroundingSource`, verification schemas, OpenDataLoader adapter stub, `ethos verify` CLI stub), PDFium Phase 1 spike, harness + competitor adapters, CLI skeleton | **Gate Zero**: ADR-0005 is accepted as `PROCEED` for internal Milestone B continuation. This is not public benchmark, release, package, production, or claim approval. | | B | weeks 9-14 | **`ethos verify` alpha first**: native Ethos JSON + synthetic and pinned real OpenDataLoader verification demos, stale fingerprint checks, capability-limited reports, deterministic evidence matching including split-quote coverage, explicit unsupported non-v1 claim reporting, adapter structure diagnostics; then reading order, blocks, headings, lists, Markdown/text exporters, Python wheel scaffold, quality dashboard, Windows x64 nightly determinism | [13-B exit checklist](milestone-b-exit-checklist.md) | | C | weeks 15-22 | Simple/bordered tables; RAG chunker + citations; non-text region coordinates; security report + default-chunk exclusion; debug overlay; internal benchmark snapshot | Current artifact-validation checkpoint recorded in [Milestone C closeout validation](validation/milestone-c-closeout-validation-2026-06-18.md); broader debug/crop/table follow-ups remain explicit | -| D | weeks 23-30 | [`verify_citations` v1](milestone-d-verify-citations-contract.md); crop API; sandbox/subprocess backend; Node beta and MCP experimental only if staffed or accepted by release-scope ADR | 13-D exit | +| D | weeks 23-30 | [`verify_citations` v1](milestone-d-verify-citations-contract.md); [`crop_element` v1 contract prep](milestone-d-crop-element-contract.md); crop API; sandbox/subprocess backend; Node beta and MCP experimental only if staffed or accepted by release-scope ADR | 13-D exit | | E | weeks 31-40 | Public benchmark report (reproducible, labeled tiers); PDFium Phase 2 project-maintained builds; stable CLI/Python docs; proof-of-trust demos; **Public Beta** | Release 1 claim audit + public-beta checkpoint | | F / Release 2 | post-E | Complex tables, formula/LaTeX, chart classification, optional enrichment modules (never base) | Scoped after E from beta fixtures | diff --git a/examples/crop/crop_element_v1_contract.json b/examples/crop/crop_element_v1_contract.json new file mode 100644 index 0000000..394a9e9 --- /dev/null +++ b/examples/crop/crop_element_v1_contract.json @@ -0,0 +1,23 @@ +{ + "schema_version": 1, + "contract": "crop_element.v1", + "status": "source-only-pre-alpha", + "carrier": "ethos verify --crop-dir", + "cases": [ + { + "name": "document-example-e000002", + "document": "schemas/examples/document.example.json", + "element_id": "e000002", + "descriptor": "schemas/examples/crop-descriptor.example.json", + "rendering_status": "descriptor_only" + } + ], + "explicit_blockers": [ + "a first-class `crop_element` CLI command or binding surface", + "Python, Node, MCP, or hosted crop API surfaces", + "sandbox/subprocess backend expansion", + "rendered-crop backend changes", + "foreign-adapter crop coordinate hardening", + "cross-platform rendered-crop byte identity claims" + ] +} diff --git a/schemas/README.md b/schemas/README.md index e824d24..3d009b6 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -14,6 +14,7 @@ bumps and downstream sign-off; output-changing heuristics are semver events (PRD | `ethos-verification-config.schema.json` | verification config (its c14n hash stamps reports) | | `ethos-crop-descriptor.schema.json` | crop descriptor JSON emitted by `ethos verify --crop-dir` | | `ethos-verify-citations-contract.schema.json` | Milestone D `verify_citations` v1 source-only contract inventory | +| `ethos-crop-element-contract.schema.json` | Milestone D `crop_element` v1 source-only contract inventory | | `ethos-deterministic-profile.schema.json` | `profiles/ethos-deterministic-v*.json` checker | Conventions: snake_case keys; `additionalProperties: false` everywhere except the @@ -37,5 +38,13 @@ Milestone D `verify_citations` v1 contract work is tracked in `examples/verify/verify_citations_v1_contract.json` is schema-validated here; its alignment with the executable case inventory and report goldens is checked by the Milestone D repository guard. +Milestone D `crop_element` v1 contract work is tracked in +`docs/milestone-d-crop-element-contract.md`. In this source-only pre-alpha slice, +`crop_element` names the future element-to-crop-descriptor contract currently represented by the +existing `ethos verify --crop-dir` carrier; it does not add a first-class command or binding +surface. The contract inventory at `examples/crop/crop_element_v1_contract.json` is +schema-validated here; its alignment with the document and crop-descriptor examples is checked by +the Milestone D repository guard. + Derived artifacts not schema'd here: `document.md` / `document.txt` (deterministic exports specified by the exporter config, Milestone B) and `debug.html` (Milestone C). diff --git a/schemas/ethos-crop-element-contract.schema.json b/schemas/ethos-crop-element-contract.schema.json new file mode 100644 index 0000000..18b36a9 --- /dev/null +++ b/schemas/ethos-crop-element-contract.schema.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:ethos:schema:crop-element-contract:1", + "title": "Ethos crop_element v1 contract inventory", + "description": "Source-only pre-alpha Milestone D inventory for the future crop_element v1 contract. This validates inventory shape and vocabulary; executable behavior remains on the existing verifier crop carrier until a first-class surface is explicitly added.", + "type": "object", + "required": [ + "schema_version", + "contract", + "status", + "carrier", + "cases", + "explicit_blockers" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "const": 1 }, + "contract": { "const": "crop_element.v1" }, + "status": { "const": "source-only-pre-alpha" }, + "carrier": { "const": "ethos verify --crop-dir" }, + "cases": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "name", + "document", + "element_id", + "descriptor", + "rendering_status" + ], + "additionalProperties": false, + "properties": { + "name": { "$ref": "#/$defs/case_name" }, + "document": { "$ref": "#/$defs/repo_path" }, + "element_id": { "type": "string", "pattern": "^e[0-9]{6}$" }, + "descriptor": { "$ref": "#/$defs/repo_path" }, + "rendering_status": { "enum": ["descriptor_only", "rendered"] } + } + } + }, + "explicit_blockers": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + } + }, + "$defs": { + "case_name": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, + "repo_path": { "type": "string", "minLength": 1, "pattern": "^[A-Za-z0-9_./-]+$" } + } +} diff --git a/schemas/validate_examples.py b/schemas/validate_examples.py index 2a29381..6a968f0 100644 --- a/schemas/validate_examples.py +++ b/schemas/validate_examples.py @@ -83,6 +83,9 @@ ("ethos-verify-citations-contract.schema.json", [ ROOT / "examples" / "verify" / "verify_citations_v1_contract.json", ]), + ("ethos-crop-element-contract.schema.json", [ + ROOT / "examples" / "crop" / "crop_element_v1_contract.json", + ]), ("ethos-deterministic-profile.schema.json", [ROOT / "profiles" / "ethos-deterministic-v1.json"]), ] From 42ce062b971780d113a5a918de9167bf2d330b63 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 14:53:07 +0530 Subject: [PATCH 11/39] Guard default verify citations crop refs Signed-off-by: docushell-admin --- .../test_milestone_d_verify_citations_contract.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/scripts/test_milestone_d_verify_citations_contract.py b/.github/scripts/test_milestone_d_verify_citations_contract.py index 8949a3c..7b8d3ae 100644 --- a/.github/scripts/test_milestone_d_verify_citations_contract.py +++ b/.github/scripts/test_milestone_d_verify_citations_contract.py @@ -372,6 +372,15 @@ def test_report_goldens_use_default_verification_config_hash(self) -> None: case["name"], ) + def test_default_contract_goldens_do_not_emit_crop_refs(self) -> None: + cases = load_json(VERIFY_CASES) + + for case in cases["report_cases"]: + report = load_json(ROOT / case["golden"]) + for check in report["checks"]: + evidence = check.get("evidence") or {} + self.assertNotIn("crop_ref", evidence, case["name"]) + def test_contract_inventory_keeps_blockers_explicit(self) -> None: inventory = load_json(CONTRACT_INVENTORY) From e15d1f6999778bbe78d78728921365c692865eb6 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 14:59:54 +0530 Subject: [PATCH 12/39] Add sandbox_subprocess v1 contract guard Signed-off-by: docushell-admin --- .github/scripts/test_execution_status.py | 2 + ...milestone_d_sandbox_subprocess_contract.py | 192 ++++++++++++++++++ Makefile | 10 +- docs/execution-status.md | 3 +- ...milestone-d-sandbox-subprocess-contract.md | 57 ++++++ docs/roadmap.md | 6 +- .../sandbox_subprocess_v1_contract.json | 53 +++++ schemas/README.md | 9 + ...os-sandbox-subprocess-contract.schema.json | 61 ++++++ schemas/validate_examples.py | 3 + 10 files changed, 393 insertions(+), 3 deletions(-) create mode 100644 .github/scripts/test_milestone_d_sandbox_subprocess_contract.py create mode 100644 docs/milestone-d-sandbox-subprocess-contract.md create mode 100644 examples/sandbox/sandbox_subprocess_v1_contract.json create mode 100644 schemas/ethos-sandbox-subprocess-contract.schema.json diff --git a/.github/scripts/test_execution_status.py b/.github/scripts/test_execution_status.py index b7eb014..d35d09f 100644 --- a/.github/scripts/test_execution_status.py +++ b/.github/scripts/test_execution_status.py @@ -40,6 +40,7 @@ def test_status_is_scoped_to_internal_continuation(self) -> None: ) self.assertIn("docs/milestone-d-verify-citations-contract.md", text) self.assertIn("docs/milestone-d-crop-element-contract.md", text) + self.assertIn("docs/milestone-d-sandbox-subprocess-contract.md", text) self.assertNotIn("Status: Pre-alpha / Milestone B entry.", text) def test_internal_check_command_is_documented(self) -> None: @@ -49,6 +50,7 @@ def test_internal_check_command_is_documented(self) -> None: self.assertIn("make milestone-c-internal-checks", text) self.assertIn("make milestone-d-verify-citations-contract", text) self.assertIn("make milestone-d-crop-element-contract", text) + self.assertIn("make milestone-d-sandbox-subprocess-contract", text) self.assertIn("CI has a static guard for that target's command wiring", text) def test_public_posture_boundary_remains_explicit(self) -> None: diff --git a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py new file mode 100644 index 0000000..29c2974 --- /dev/null +++ b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +# +# Copyright 2026 The Ethos maintainers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import annotations + +import json +import re +import unittest +from pathlib import Path + +from jsonschema import Draft202012Validator +from makefile_guard import makefile_text, target_block + + +ROOT = Path(__file__).resolve().parents[2] +CONTRACT = ROOT / "docs/milestone-d-sandbox-subprocess-contract.md" +CONTRACT_INVENTORY = ROOT / "examples/sandbox/sandbox_subprocess_v1_contract.json" +CONTRACT_INVENTORY_SCHEMA = ROOT / "schemas/ethos-sandbox-subprocess-contract.schema.json" +ROADMAP = ROOT / "docs/roadmap.md" +EXECUTION_STATUS = ROOT / "docs/execution-status.md" +SCHEMAS_README = ROOT / "schemas/README.md" +PDF_PARSE_TESTS = ROOT / "crates/ethos-cli/tests/pdf_parse.rs" +EXPECTED_EXPLICIT_BLOCKERS = [ + "hardened OS sandbox rules", + "network-denying runtime proof", + "file-descriptor or child-process enforcement", + "arbitrary filesystem allowlist enforcement", + "a new public command or binding surface", + "Python, Node, MCP, or hosted sandbox surfaces", + "crop or verification API changes", +] +EXPECTED_BOUNDARIES = [ + "max_parse_ms_timeout", + "memory_limit_error", + "stable_error_envelope", + "diagnostics_gated_stderr", +] + + +def load_json(path: Path): + return json.loads(path.read_text(encoding="utf-8")) + + +def contract_text() -> str: + return CONTRACT.read_text(encoding="utf-8") + + +def normalized_contract_text() -> str: + return re.sub(r"\s+", " ", contract_text()) + + +def contract_explicit_blockers() -> list[str]: + match = re.search( + r"## Explicit Blockers For This Slice\n\n" + r"This first `sandbox_subprocess` slice does not add:\n\n" + r"(?P(?:- .+\n)+)", + contract_text(), + ) + if match is None: + raise AssertionError("missing sandbox_subprocess explicit blocker list") + return [ + line.removeprefix("- ").rstrip(";.") + for line in match.group("bullets").strip().splitlines() + ] + + +class MilestoneDSandboxSubprocessContractTests(unittest.TestCase): + def test_target_is_declared_phony(self) -> None: + text = makefile_text() + + self.assertIn(".PHONY:", text) + self.assertIn("milestone-d-sandbox-subprocess-contract", text) + + def test_target_composes_contract_gates(self) -> None: + block = target_block("milestone-d-sandbox-subprocess-contract") + + required = [ + "cargo test --locked -p ethos-cli --test pdf_parse worker", + "$(PYTHON) schemas/validate_examples.py", + "$(PYTHON) .github/scripts/test_execution_status.py", + "$(PYTHON) .github/scripts/test_roadmap_status.py", + "$(PYTHON) .github/scripts/test_milestone_d_sandbox_subprocess_contract.py", + "git diff --check", + ] + for command in required: + self.assertIn(command, block) + + def test_target_stays_contract_scoped(self) -> None: + block = target_block("milestone-d-sandbox-subprocess-contract") + + for out_of_scope in [ + "verify-rendered-crops", + "compare-rendered-crops", + "layout-evaluator-alpha", + "python-surface-test", + "milestone-c-internal-checks", + "milestone-d-crop-element-contract", + "npm", + "mcp", + ]: + self.assertNotIn(out_of_scope, block) + + def test_contract_is_linked_from_status_docs(self) -> None: + for path in [ROADMAP, EXECUTION_STATUS, SCHEMAS_README]: + text = path.read_text(encoding="utf-8") + self.assertIn("milestone-d-sandbox-subprocess-contract.md", text, path) + + def test_contract_defines_existing_carrier_not_new_surface(self) -> None: + text = normalized_contract_text() + + self.assertIn("source-only pre-alpha contract work", text) + self.assertIn("does not add a hardened OS sandbox", text) + self.assertIn( + "The current executable carrier remains the PDF worker process behind " + "`ethos doc parse` and `ethos fingerprint`", + text, + ) + self.assertIn( + "`sandbox_subprocess` names the future backend contract between hostile PDF input, " + "bounded worker execution, normalized document output, and stable error envelopes", + text, + ) + + def test_contract_pins_fail_closed_boundaries(self) -> None: + text = normalized_contract_text() + + for required in [ + "`parse_timeout` error code", + "`memory_limit_exceeded` error code", + "stable worker error envelopes are relayed", + "non-envelope worker stderr is hidden by default", + "explicit diagnostics", + "stdout remains empty on worker failures", + "`make milestone-d-sandbox-subprocess-contract PYTHON=/bin/python`", + ]: + self.assertIn(required, text) + self.assertEqual(EXPECTED_EXPLICIT_BLOCKERS, contract_explicit_blockers()) + + def test_contract_inventory_schema_validates_inventory(self) -> None: + schema = load_json(CONTRACT_INVENTORY_SCHEMA) + inventory = load_json(CONTRACT_INVENTORY) + Draft202012Validator.check_schema(schema) + errors = sorted( + Draft202012Validator(schema).iter_errors(inventory), + key=lambda error: list(error.absolute_path), + ) + self.assertEqual([], errors) + + def test_contract_inventory_matches_existing_worker_tests(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + test_source = PDF_PARSE_TESTS.read_text(encoding="utf-8") + + self.assertEqual(inventory["schema_version"], 1) + self.assertEqual(inventory["contract"], "sandbox_subprocess.v1") + self.assertEqual(inventory["status"], "source-only-pre-alpha") + self.assertEqual(inventory["carrier"], "pdfium worker process") + self.assertEqual(EXPECTED_EXPLICIT_BLOCKERS, inventory["explicit_blockers"]) + + case_names = [case["name"] for case in inventory["cases"]] + self.assertEqual(len(case_names), len(set(case_names))) + self.assertEqual( + sorted(EXPECTED_BOUNDARIES), + sorted({case["boundary"] for case in inventory["cases"]}), + ) + for case in inventory["cases"]: + self.assertIn(f"fn {case['test_filter']}()", test_source, case["name"]) + + def test_inventory_keeps_current_command_surfaces_narrow(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + + self.assertEqual( + {"ethos doc parse", "ethos fingerprint"}, + {case["command_surface"] for case in inventory["cases"]}, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/Makefile b/Makefile index a11ab37..3546d5c 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ COMPARE_RENDERED_CROPS_LEFT ?= $(VERIFY_RENDERED_CROPS_OUT)/run1 COMPARE_RENDERED_CROPS_RIGHT ?= $(VERIFY_RENDERED_CROPS_OUT)/run2 LAYOUT_EVALUATOR_OUT ?= $(ROOT)/target/layout-evaluator-alpha -.PHONY: verify-alpha verify-alpha-tree rag-chunk-alpha security-report-alpha milestone-d-verify-citations-contract milestone-d-crop-element-contract verify-rendered-crops compare-rendered-crops layout-evaluator-alpha python-surface-test milestone-b-internal-checks milestone-c-internal-checks release-hygiene release-advisory third-party-license-manifest release-notice-draft +.PHONY: verify-alpha verify-alpha-tree rag-chunk-alpha security-report-alpha milestone-d-verify-citations-contract milestone-d-crop-element-contract milestone-d-sandbox-subprocess-contract verify-rendered-crops compare-rendered-crops layout-evaluator-alpha python-surface-test milestone-b-internal-checks milestone-c-internal-checks release-hygiene release-advisory third-party-license-manifest release-notice-draft $(ETHOS_BIN): cargo build --locked -p ethos-cli @@ -63,6 +63,14 @@ milestone-d-crop-element-contract: $(PYTHON) .github/scripts/test_milestone_d_crop_element_contract.py git diff --check +milestone-d-sandbox-subprocess-contract: + cargo test --locked -p ethos-cli --test pdf_parse worker + $(PYTHON) schemas/validate_examples.py + $(PYTHON) .github/scripts/test_execution_status.py + $(PYTHON) .github/scripts/test_roadmap_status.py + $(PYTHON) .github/scripts/test_milestone_d_sandbox_subprocess_contract.py + git diff --check + verify-rendered-crops: $(ETHOS_BIN) $(PYTHON) examples/verify/check_rendered_crops.py --repo-root $(ROOT) --ethos-bin $(ETHOS_BIN) --out-dir $(VERIFY_RENDERED_CROPS_OUT) git diff --check diff --git a/docs/execution-status.md b/docs/execution-status.md index bd7d1b0..242c660 100644 --- a/docs/execution-status.md +++ b/docs/execution-status.md @@ -2,7 +2,7 @@ Date: 2026-06-18 Owner: product / decider -Status: Pre-alpha / internal transition from Milestone C artifact-validation closeout to Milestone D source-only contract work. Week 0 governance is accepted, WS-ENGINE Phase 1 has a real narrow PDFium path, WS-VERIFY-ALPHA has real deterministic evidence checks over native Ethos JSON and pinned OpenDataLoader output, WS-HARNESS has fail-closed readiness scaffolding, the Gate Zero corpus/hardware manifest and direct competitor lock are frozen/signed, ADR-0005 records an accepted `PROCEED` decision for internal Milestone B continuation, ADR-0006 closes package identifier/trademark validation, ADR-0007 locks the product direction, and the public-source preflight is green for a source-only pre-alpha GitHub push. Milestone C has a source-tree internal artifact-validation closeout for the RAG chunk and security-report trust-loop checks. Current Milestone D work began with the narrow `verify_citations` v1 contract in `docs/milestone-d-verify-citations-contract.md`, carried by the existing `ethos verify` path and fixture-backed validation. The next D contract-prep slice is the `crop_element` v1 contract in `docs/milestone-d-crop-element-contract.md`, carried by the existing `ethos verify --crop-dir` descriptor path and fixture-backed validation without adding a first-class crop surface. Public benchmark reports, releases, packages, production positioning, and all performance/quality/footprint claims remain blocked. The controlled-run handoff remains `docs/gate-zero-evidence-runbook.md`; the accepted decision record is `docs/decisions/ADR-0005-gate-zero-decision.md`. +Status: Pre-alpha / internal transition from Milestone C artifact-validation closeout to Milestone D source-only contract work. Week 0 governance is accepted, WS-ENGINE Phase 1 has a real narrow PDFium path, WS-VERIFY-ALPHA has real deterministic evidence checks over native Ethos JSON and pinned OpenDataLoader output, WS-HARNESS has fail-closed readiness scaffolding, the Gate Zero corpus/hardware manifest and direct competitor lock are frozen/signed, ADR-0005 records an accepted `PROCEED` decision for internal Milestone B continuation, ADR-0006 closes package identifier/trademark validation, ADR-0007 locks the product direction, and the public-source preflight is green for a source-only pre-alpha GitHub push. Milestone C has a source-tree internal artifact-validation closeout for the RAG chunk and security-report trust-loop checks. Current Milestone D work began with the narrow `verify_citations` v1 contract in `docs/milestone-d-verify-citations-contract.md`, carried by the existing `ethos verify` path and fixture-backed validation. The next D contract-prep slice is the `crop_element` v1 contract in `docs/milestone-d-crop-element-contract.md`, carried by the existing `ethos verify --crop-dir` descriptor path and fixture-backed validation without adding a first-class crop surface. The `sandbox_subprocess` v1 contract in `docs/milestone-d-sandbox-subprocess-contract.md` now classifies existing PDF worker-process timeout, memory-limit, stable-error, and diagnostics-gated stderr behavior without adding hardened sandbox rules. Public benchmark reports, releases, packages, production positioning, and all performance/quality/footprint claims remain blocked. The controlled-run handoff remains `docs/gate-zero-evidence-runbook.md`; the accepted decision record is `docs/decisions/ADR-0005-gate-zero-decision.md`. ## Current Reality @@ -28,6 +28,7 @@ The committed implementation now includes: - `make milestone-c-internal-checks` composes the current internal Milestone C artifact-validation path across RAG chunk and security-report gates; CI/static guard scripts fail closed if that command wiring or the dated closeout record drifts. - Milestone D source-only pre-alpha contract work has started with `docs/milestone-d-verify-citations-contract.md`. It defines `verify_citations` v1 as the citation-input, verification-config, grounding-source, and verification-report contract currently carried by `ethos verify`; schema/example validation checks that the minimal citation example and verification-report example stay coherent. Focused validation is `make milestone-d-verify-citations-contract PYTHON=/bin/python`. - Milestone D `crop_element` v1 contract prep has started with `docs/milestone-d-crop-element-contract.md`. It defines `crop_element` as the future element-to-crop-descriptor contract currently represented by the existing `ethos verify --crop-dir` carrier; schema/example validation checks the inventory at `examples/crop/crop_element_v1_contract.json`, and the repository guard checks that it stays coherent with the document and crop-descriptor examples. Focused validation is `make milestone-d-crop-element-contract PYTHON=/bin/python`. +- Milestone D `sandbox_subprocess` v1 contract prep has started with `docs/milestone-d-sandbox-subprocess-contract.md`. It defines `sandbox_subprocess` as the future worker-boundary contract currently represented by the existing PDF worker process behind `ethos doc parse` and `ethos fingerprint`; schema/example validation checks the inventory at `examples/sandbox/sandbox_subprocess_v1_contract.json`, and the repository guard checks that it stays coherent with the worker test slice. Focused validation is `make milestone-d-sandbox-subprocess-contract PYTHON=/bin/python`. Still absent or not claimable: public benchmark reports, public competitor-comparison claims, public speed/quality/footprint claims, OCR/image-only support, real table extraction, mature list/heading/layout semantics beyond current fixture-backed alpha paths, semantic/arithmetic verification beyond deterministic evidence lookup, Phase 2 project-maintained PDFium builds, release packaging, and claim-audit approval for any public result wording. diff --git a/docs/milestone-d-sandbox-subprocess-contract.md b/docs/milestone-d-sandbox-subprocess-contract.md new file mode 100644 index 0000000..5e4c31b --- /dev/null +++ b/docs/milestone-d-sandbox-subprocess-contract.md @@ -0,0 +1,57 @@ +# Milestone D `sandbox_subprocess` v1 Contract + +Status: source-only pre-alpha contract work for internal Milestone D continuation. + +This note defines the narrow `sandbox_subprocess` contract-prep slice for Milestone D. It does not +add a hardened OS sandbox, new public command, binding, Node surface, MCP surface, or hosted +surface. The current executable carrier remains the PDF worker process behind `ethos doc parse` +and `ethos fingerprint`; `sandbox_subprocess` names the future backend contract between hostile +PDF input, bounded worker execution, normalized document output, and stable error envelopes. + +## Contract Surface + +`sandbox_subprocess` v1 will consume: + +- PDF input already accepted by the current parse/fingerprint paths; +- parse configuration, including page selection and `max_parse_ms`; +- a worker boundary that returns either normalized graph data or a stable error envelope; +- optional diagnostics that may expose worker stderr only when explicitly requested. + +The current source-tree inventory for this contract boundary is +`examples/sandbox/sandbox_subprocess_v1_contract.json`. It classifies existing worker tests that +exercise timeout handling, memory-limit error reporting, stable error relay, and diagnostics-gated +stderr behavior. + +Focused validation command: + +- `make milestone-d-sandbox-subprocess-contract PYTHON=/bin/python` + +The target runs the current worker-boundary test slice, schema/example validation, +status/roadmap guards, this contract guard, and diff hygiene. It intentionally stays narrower than +backend hardening work. + +## Supported v1 Boundaries + +The v1 contract boundary is fail-closed and error-envelope-first: + +- `max_parse_ms` timeout exits through the stable `parse_timeout` error code; +- worker memory-limit failures exit through the stable `memory_limit_exceeded` error code; +- stable worker error envelopes are relayed without converting them to generic failures; +- non-envelope worker stderr is hidden by default; +- non-envelope worker stderr is exposed only under explicit diagnostics; +- stdout remains empty on worker failures covered by this contract inventory. + +## Explicit Blockers For This Slice + +This first `sandbox_subprocess` slice does not add: + +- hardened OS sandbox rules; +- network-denying runtime proof; +- file-descriptor or child-process enforcement; +- arbitrary filesystem allowlist enforcement; +- a new public command or binding surface; +- Python, Node, MCP, or hosted sandbox surfaces; +- crop or verification API changes. + +Public-facing language remains limited to source-only pre-alpha internal continuation, evidence +grounding, diagnostics, fixture-backed validation, and explicit blockers. diff --git a/docs/roadmap.md b/docs/roadmap.md index c23cf02..f0b7c70 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -26,6 +26,10 @@ The next D contract-prep slice defines the source-only existing `ethos verify --crop-dir` crop descriptor carrier; it does not add a first-class crop command, binding surface, sandbox backend, Node beta, or MCP experimental scope. +The source-only [`sandbox_subprocess` v1 contract](milestone-d-sandbox-subprocess-contract.md) +classifies the existing PDF worker-process boundary behind `ethos doc parse` and +`ethos fingerprint`; it does not add hardened sandbox rules or a new command or +binding surface. | Milestone | Window | Contents | Gate | | --- | --- | --- | --- | @@ -33,7 +37,7 @@ experimental scope. | A | weeks 1-8 | Contracts (5 schemas, c14n, deterministic profile), trust-boundary artifacts (`GroundingSource`, verification schemas, OpenDataLoader adapter stub, `ethos verify` CLI stub), PDFium Phase 1 spike, harness + competitor adapters, CLI skeleton | **Gate Zero**: ADR-0005 is accepted as `PROCEED` for internal Milestone B continuation. This is not public benchmark, release, package, production, or claim approval. | | B | weeks 9-14 | **`ethos verify` alpha first**: native Ethos JSON + synthetic and pinned real OpenDataLoader verification demos, stale fingerprint checks, capability-limited reports, deterministic evidence matching including split-quote coverage, explicit unsupported non-v1 claim reporting, adapter structure diagnostics; then reading order, blocks, headings, lists, Markdown/text exporters, Python wheel scaffold, quality dashboard, Windows x64 nightly determinism | [13-B exit checklist](milestone-b-exit-checklist.md) | | C | weeks 15-22 | Simple/bordered tables; RAG chunker + citations; non-text region coordinates; security report + default-chunk exclusion; debug overlay; internal benchmark snapshot | Current artifact-validation checkpoint recorded in [Milestone C closeout validation](validation/milestone-c-closeout-validation-2026-06-18.md); broader debug/crop/table follow-ups remain explicit | -| D | weeks 23-30 | [`verify_citations` v1](milestone-d-verify-citations-contract.md); [`crop_element` v1 contract prep](milestone-d-crop-element-contract.md); crop API; sandbox/subprocess backend; Node beta and MCP experimental only if staffed or accepted by release-scope ADR | 13-D exit | +| D | weeks 23-30 | [`verify_citations` v1](milestone-d-verify-citations-contract.md); [`crop_element` v1 contract prep](milestone-d-crop-element-contract.md); [`sandbox_subprocess` v1 contract prep](milestone-d-sandbox-subprocess-contract.md); crop API; sandbox/subprocess backend; Node beta and MCP experimental only if staffed or accepted by release-scope ADR | 13-D exit | | E | weeks 31-40 | Public benchmark report (reproducible, labeled tiers); PDFium Phase 2 project-maintained builds; stable CLI/Python docs; proof-of-trust demos; **Public Beta** | Release 1 claim audit + public-beta checkpoint | | F / Release 2 | post-E | Complex tables, formula/LaTeX, chart classification, optional enrichment modules (never base) | Scoped after E from beta fixtures | diff --git a/examples/sandbox/sandbox_subprocess_v1_contract.json b/examples/sandbox/sandbox_subprocess_v1_contract.json new file mode 100644 index 0000000..928d363 --- /dev/null +++ b/examples/sandbox/sandbox_subprocess_v1_contract.json @@ -0,0 +1,53 @@ +{ + "schema_version": 1, + "contract": "sandbox_subprocess.v1", + "status": "source-only-pre-alpha", + "carrier": "pdfium worker process", + "cases": [ + { + "name": "doc-parse-timeout", + "command_surface": "ethos doc parse", + "test_filter": "doc_parse_timeout_kills_pdfium_worker", + "boundary": "max_parse_ms_timeout" + }, + { + "name": "fingerprint-timeout", + "command_surface": "ethos fingerprint", + "test_filter": "pdf_fingerprint_timeout_kills_pdfium_worker", + "boundary": "max_parse_ms_timeout" + }, + { + "name": "doc-parse-memory-limit", + "command_surface": "ethos doc parse", + "test_filter": "doc_parse_memory_limit_worker_failure_is_stable", + "boundary": "memory_limit_error" + }, + { + "name": "doc-parse-stable-error-envelope", + "command_surface": "ethos doc parse", + "test_filter": "doc_parse_relays_worker_stable_error_envelope", + "boundary": "stable_error_envelope" + }, + { + "name": "doc-parse-stderr-hidden-by-default", + "command_surface": "ethos doc parse", + "test_filter": "doc_parse_non_envelope_worker_failure_stays_canonical_without_diagnostics", + "boundary": "diagnostics_gated_stderr" + }, + { + "name": "doc-parse-diagnostics-gated-stderr", + "command_surface": "ethos doc parse", + "test_filter": "doc_parse_non_envelope_worker_failure_includes_stderr_with_diagnostics", + "boundary": "diagnostics_gated_stderr" + } + ], + "explicit_blockers": [ + "hardened OS sandbox rules", + "network-denying runtime proof", + "file-descriptor or child-process enforcement", + "arbitrary filesystem allowlist enforcement", + "a new public command or binding surface", + "Python, Node, MCP, or hosted sandbox surfaces", + "crop or verification API changes" + ] +} diff --git a/schemas/README.md b/schemas/README.md index 3d009b6..a0b06fd 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -15,6 +15,7 @@ bumps and downstream sign-off; output-changing heuristics are semver events (PRD | `ethos-crop-descriptor.schema.json` | crop descriptor JSON emitted by `ethos verify --crop-dir` | | `ethos-verify-citations-contract.schema.json` | Milestone D `verify_citations` v1 source-only contract inventory | | `ethos-crop-element-contract.schema.json` | Milestone D `crop_element` v1 source-only contract inventory | +| `ethos-sandbox-subprocess-contract.schema.json` | Milestone D `sandbox_subprocess` v1 source-only contract inventory | | `ethos-deterministic-profile.schema.json` | `profiles/ethos-deterministic-v*.json` checker | Conventions: snake_case keys; `additionalProperties: false` everywhere except the @@ -46,5 +47,13 @@ surface. The contract inventory at `examples/crop/crop_element_v1_contract.json` schema-validated here; its alignment with the document and crop-descriptor examples is checked by the Milestone D repository guard. +Milestone D `sandbox_subprocess` v1 contract work is tracked in +`docs/milestone-d-sandbox-subprocess-contract.md`. In this source-only pre-alpha slice, +`sandbox_subprocess` names the future worker-boundary contract currently represented by the +existing PDF worker process behind `ethos doc parse` and `ethos fingerprint`; it does not add a +hardened sandbox or a new command/binding surface. The contract inventory at +`examples/sandbox/sandbox_subprocess_v1_contract.json` is schema-validated here; its alignment with +the executable worker tests is checked by the Milestone D repository guard. + Derived artifacts not schema'd here: `document.md` / `document.txt` (deterministic exports specified by the exporter config, Milestone B) and `debug.html` (Milestone C). diff --git a/schemas/ethos-sandbox-subprocess-contract.schema.json b/schemas/ethos-sandbox-subprocess-contract.schema.json new file mode 100644 index 0000000..9a3860d --- /dev/null +++ b/schemas/ethos-sandbox-subprocess-contract.schema.json @@ -0,0 +1,61 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:ethos:schema:sandbox-subprocess-contract:1", + "title": "Ethos sandbox_subprocess v1 contract inventory", + "description": "Source-only pre-alpha Milestone D inventory for the future sandbox_subprocess v1 backend contract. This validates inventory shape and vocabulary; executable behavior remains on the existing PDF worker-process carrier until sandbox hardening is explicitly added.", + "type": "object", + "required": [ + "schema_version", + "contract", + "status", + "carrier", + "cases", + "explicit_blockers" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "const": 1 }, + "contract": { "const": "sandbox_subprocess.v1" }, + "status": { "const": "source-only-pre-alpha" }, + "carrier": { "const": "pdfium worker process" }, + "cases": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "name", + "command_surface", + "test_filter", + "boundary" + ], + "additionalProperties": false, + "properties": { + "name": { "$ref": "#/$defs/case_name" }, + "command_surface": { + "enum": ["ethos doc parse", "ethos fingerprint"] + }, + "test_filter": { "$ref": "#/$defs/rust_test_filter" }, + "boundary": { + "enum": [ + "max_parse_ms_timeout", + "memory_limit_error", + "stable_error_envelope", + "diagnostics_gated_stderr" + ] + } + } + } + }, + "explicit_blockers": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + } + }, + "$defs": { + "case_name": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, + "rust_test_filter": { "type": "string", "pattern": "^[a-z0-9_]+$" } + } +} diff --git a/schemas/validate_examples.py b/schemas/validate_examples.py index 6a968f0..5acbff4 100644 --- a/schemas/validate_examples.py +++ b/schemas/validate_examples.py @@ -86,6 +86,9 @@ ("ethos-crop-element-contract.schema.json", [ ROOT / "examples" / "crop" / "crop_element_v1_contract.json", ]), + ("ethos-sandbox-subprocess-contract.schema.json", [ + ROOT / "examples" / "sandbox" / "sandbox_subprocess_v1_contract.json", + ]), ("ethos-deterministic-profile.schema.json", [ROOT / "profiles" / "ethos-deterministic-v1.json"]), ] From a8470386d35fd8fd096a53db6ef76d46a71abbb6 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 15:03:22 +0530 Subject: [PATCH 13/39] Guard crop descriptor evidence binding Signed-off-by: docushell-admin --- .../test_milestone_d_crop_element_contract.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/.github/scripts/test_milestone_d_crop_element_contract.py b/.github/scripts/test_milestone_d_crop_element_contract.py index e268c8f..d94d408 100644 --- a/.github/scripts/test_milestone_d_crop_element_contract.py +++ b/.github/scripts/test_milestone_d_crop_element_contract.py @@ -17,6 +17,7 @@ from __future__ import annotations +import hashlib import json import re import unittest @@ -31,6 +32,7 @@ CONTRACT_INVENTORY = ROOT / "examples/crop/crop_element_v1_contract.json" CONTRACT_INVENTORY_SCHEMA = ROOT / "schemas/ethos-crop-element-contract.schema.json" CROP_DESCRIPTOR_SCHEMA = ROOT / "schemas/ethos-crop-descriptor.schema.json" +VERIFICATION_REPORT_EXAMPLE = ROOT / "schemas/examples/verification-report.example.json" ROADMAP = ROOT / "docs/roadmap.md" EXECUTION_STATUS = ROOT / "docs/execution-status.md" SCHEMAS_README = ROOT / "schemas/README.md" @@ -81,6 +83,14 @@ def elements_by_id(document: dict) -> dict[str, dict]: } +def checks_by_id(report: dict) -> dict[str, dict]: + return {check["id"]: check for check in report["checks"]} + + +def sha256_text(text: str) -> str: + return hashlib.sha256(text.encode("utf-8")).hexdigest() + + class MilestoneDCropElementContractTests(unittest.TestCase): def test_target_is_declared_phony(self) -> None: text = makefile_text() @@ -205,6 +215,29 @@ def test_contract_inventory_binds_element_to_descriptor_example(self) -> None: self.assertEqual(descriptor["rendering_status"], case["rendering_status"]) self.assertEqual(descriptor["check_ids"], ["v0001"]) + def test_contract_inventory_binds_descriptor_to_verification_evidence(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + report = load_json(VERIFICATION_REPORT_EXAMPLE) + report_checks = checks_by_id(report) + + for case in inventory["cases"]: + descriptor = load_json(ROOT / case["descriptor"]) + + self.assertEqual(report["document_fingerprint"], descriptor["document_fingerprint"]) + for check_id in descriptor["check_ids"]: + check = report_checks[check_id] + evidence = check["evidence"] + citation = check["claim"]["citation"] + + self.assertEqual(citation["element_id"], case["element_id"], case["name"]) + self.assertEqual(evidence["page"], descriptor["page"], case["name"]) + self.assertEqual(evidence["bbox"], descriptor["bbox"], case["name"]) + self.assertEqual( + descriptor["text_sha256"], + sha256_text(evidence["text"]), + case["name"], + ) + def test_crop_descriptor_example_validates_against_descriptor_schema(self) -> None: schema = load_json(CROP_DESCRIPTOR_SCHEMA) descriptor = load_json(ROOT / "schemas/examples/crop-descriptor.example.json") From d5625428132e2aa4eddeba201be6399ba090af88 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 15:09:14 +0530 Subject: [PATCH 14/39] Add Milestone D internal contracts target Signed-off-by: docushell-admin --- .github/scripts/test_execution_status.py | 1 + .../test_milestone_d_internal_contracts.py | 69 +++++++++++++++++++ Makefile | 9 ++- docs/execution-status.md | 1 + 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 .github/scripts/test_milestone_d_internal_contracts.py diff --git a/.github/scripts/test_execution_status.py b/.github/scripts/test_execution_status.py index d35d09f..375bf77 100644 --- a/.github/scripts/test_execution_status.py +++ b/.github/scripts/test_execution_status.py @@ -51,6 +51,7 @@ def test_internal_check_command_is_documented(self) -> None: self.assertIn("make milestone-d-verify-citations-contract", text) self.assertIn("make milestone-d-crop-element-contract", text) self.assertIn("make milestone-d-sandbox-subprocess-contract", text) + self.assertIn("make milestone-d-internal-contracts", text) self.assertIn("CI has a static guard for that target's command wiring", text) def test_public_posture_boundary_remains_explicit(self) -> None: diff --git a/.github/scripts/test_milestone_d_internal_contracts.py b/.github/scripts/test_milestone_d_internal_contracts.py new file mode 100644 index 0000000..c4ecdb7 --- /dev/null +++ b/.github/scripts/test_milestone_d_internal_contracts.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# +# Copyright 2026 The Ethos maintainers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import annotations + +import unittest + +from makefile_guard import makefile_text, target_block + + +CONTRACT_TARGETS = [ + "milestone-d-verify-citations-contract", + "milestone-d-crop-element-contract", + "milestone-d-sandbox-subprocess-contract", +] + + +class MilestoneDInternalContractsTests(unittest.TestCase): + def test_target_is_declared_phony(self) -> None: + text = makefile_text() + + self.assertIn(".PHONY:", text) + self.assertIn("milestone-d-internal-contracts", text) + + def test_target_composes_current_d_contract_targets(self) -> None: + block = target_block("milestone-d-internal-contracts") + + for target in CONTRACT_TARGETS: + self.assertIn(f"$(MAKE) {target} PYTHON=$(PYTHON)", block) + self.assertIn("$(PYTHON) .github/scripts/test_milestone_d_internal_contracts.py", block) + self.assertIn("git diff --check", block) + + def test_target_stays_internal_contract_scoped(self) -> None: + block = target_block("milestone-d-internal-contracts") + + for out_of_scope in [ + "verify-alpha", + "rag-chunk-alpha", + "security-report-alpha", + "verify-rendered-crops", + "compare-rendered-crops", + "layout-evaluator-alpha", + "python-surface-test", + "milestone-b-internal-checks", + "milestone-c-internal-checks", + "release-", + "third-party-license-manifest", + "npm", + "mcp", + ]: + self.assertNotIn(out_of_scope, block) + + +if __name__ == "__main__": + unittest.main() diff --git a/Makefile b/Makefile index 3546d5c..dd9c2a6 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ COMPARE_RENDERED_CROPS_LEFT ?= $(VERIFY_RENDERED_CROPS_OUT)/run1 COMPARE_RENDERED_CROPS_RIGHT ?= $(VERIFY_RENDERED_CROPS_OUT)/run2 LAYOUT_EVALUATOR_OUT ?= $(ROOT)/target/layout-evaluator-alpha -.PHONY: verify-alpha verify-alpha-tree rag-chunk-alpha security-report-alpha milestone-d-verify-citations-contract milestone-d-crop-element-contract milestone-d-sandbox-subprocess-contract verify-rendered-crops compare-rendered-crops layout-evaluator-alpha python-surface-test milestone-b-internal-checks milestone-c-internal-checks release-hygiene release-advisory third-party-license-manifest release-notice-draft +.PHONY: verify-alpha verify-alpha-tree rag-chunk-alpha security-report-alpha milestone-d-verify-citations-contract milestone-d-crop-element-contract milestone-d-sandbox-subprocess-contract milestone-d-internal-contracts verify-rendered-crops compare-rendered-crops layout-evaluator-alpha python-surface-test milestone-b-internal-checks milestone-c-internal-checks release-hygiene release-advisory third-party-license-manifest release-notice-draft $(ETHOS_BIN): cargo build --locked -p ethos-cli @@ -71,6 +71,13 @@ milestone-d-sandbox-subprocess-contract: $(PYTHON) .github/scripts/test_milestone_d_sandbox_subprocess_contract.py git diff --check +milestone-d-internal-contracts: + $(MAKE) milestone-d-verify-citations-contract PYTHON=$(PYTHON) + $(MAKE) milestone-d-crop-element-contract PYTHON=$(PYTHON) + $(MAKE) milestone-d-sandbox-subprocess-contract PYTHON=$(PYTHON) + $(PYTHON) .github/scripts/test_milestone_d_internal_contracts.py + git diff --check + verify-rendered-crops: $(ETHOS_BIN) $(PYTHON) examples/verify/check_rendered_crops.py --repo-root $(ROOT) --ethos-bin $(ETHOS_BIN) --out-dir $(VERIFY_RENDERED_CROPS_OUT) git diff --check diff --git a/docs/execution-status.md b/docs/execution-status.md index 242c660..129180e 100644 --- a/docs/execution-status.md +++ b/docs/execution-status.md @@ -29,6 +29,7 @@ The committed implementation now includes: - Milestone D source-only pre-alpha contract work has started with `docs/milestone-d-verify-citations-contract.md`. It defines `verify_citations` v1 as the citation-input, verification-config, grounding-source, and verification-report contract currently carried by `ethos verify`; schema/example validation checks that the minimal citation example and verification-report example stay coherent. Focused validation is `make milestone-d-verify-citations-contract PYTHON=/bin/python`. - Milestone D `crop_element` v1 contract prep has started with `docs/milestone-d-crop-element-contract.md`. It defines `crop_element` as the future element-to-crop-descriptor contract currently represented by the existing `ethos verify --crop-dir` carrier; schema/example validation checks the inventory at `examples/crop/crop_element_v1_contract.json`, and the repository guard checks that it stays coherent with the document and crop-descriptor examples. Focused validation is `make milestone-d-crop-element-contract PYTHON=/bin/python`. - Milestone D `sandbox_subprocess` v1 contract prep has started with `docs/milestone-d-sandbox-subprocess-contract.md`. It defines `sandbox_subprocess` as the future worker-boundary contract currently represented by the existing PDF worker process behind `ethos doc parse` and `ethos fingerprint`; schema/example validation checks the inventory at `examples/sandbox/sandbox_subprocess_v1_contract.json`, and the repository guard checks that it stays coherent with the worker test slice. Focused validation is `make milestone-d-sandbox-subprocess-contract PYTHON=/bin/python`. +- `make milestone-d-internal-contracts PYTHON=/bin/python` composes the current Milestone D source-only contract gates and has a static guard for that target's command wiring. Still absent or not claimable: public benchmark reports, public competitor-comparison claims, public speed/quality/footprint claims, OCR/image-only support, real table extraction, mature list/heading/layout semantics beyond current fixture-backed alpha paths, semantic/arithmetic verification beyond deterministic evidence lookup, Phase 2 project-maintained PDFium builds, release packaging, and claim-audit approval for any public result wording. From 2917c38a0d4f885df3def66544019f6855eb7098 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 15:15:27 +0530 Subject: [PATCH 15/39] Add crop element request contract guard Signed-off-by: docushell-admin --- .../test_milestone_d_crop_element_contract.py | 125 +++++++++++++++++- docs/execution-status.md | 2 +- docs/milestone-d-crop-element-contract.md | 12 +- examples/crop/crop_element_v1_contract.json | 1 + schemas/README.md | 8 +- .../ethos-crop-element-contract.schema.json | 2 + .../ethos-crop-element-request.schema.json | 58 ++++++++ .../crop-element-request.example.json | 7 + schemas/validate_examples.py | 3 + 9 files changed, 208 insertions(+), 10 deletions(-) create mode 100644 schemas/ethos-crop-element-request.schema.json create mode 100644 schemas/examples/crop-element-request.example.json diff --git a/.github/scripts/test_milestone_d_crop_element_contract.py b/.github/scripts/test_milestone_d_crop_element_contract.py index d94d408..89729a3 100644 --- a/.github/scripts/test_milestone_d_crop_element_contract.py +++ b/.github/scripts/test_milestone_d_crop_element_contract.py @@ -21,6 +21,7 @@ import json import re import unittest +from copy import deepcopy from pathlib import Path from jsonschema import Draft202012Validator @@ -31,6 +32,8 @@ CONTRACT = ROOT / "docs/milestone-d-crop-element-contract.md" CONTRACT_INVENTORY = ROOT / "examples/crop/crop_element_v1_contract.json" CONTRACT_INVENTORY_SCHEMA = ROOT / "schemas/ethos-crop-element-contract.schema.json" +CROP_ELEMENT_REQUEST_SCHEMA = ROOT / "schemas/ethos-crop-element-request.schema.json" +CROP_ELEMENT_REQUEST_EXAMPLE = ROOT / "schemas/examples/crop-element-request.example.json" CROP_DESCRIPTOR_SCHEMA = ROOT / "schemas/ethos-crop-descriptor.schema.json" VERIFICATION_REPORT_EXAMPLE = ROOT / "schemas/examples/verification-report.example.json" ROADMAP = ROOT / "docs/roadmap.md" @@ -91,6 +94,51 @@ def sha256_text(text: str) -> str: return hashlib.sha256(text.encode("utf-8")).hexdigest() +def schema_errors(schema: dict, instance: dict) -> list: + return sorted( + Draft202012Validator(schema).iter_errors(instance), + key=lambda error: list(error.absolute_path), + ) + + +def request_case_diagnostics( + request: dict, + document: dict, + descriptor: dict, + case: dict, +) -> list[str]: + diagnostics: list[str] = [] + + if request["document_fingerprint"] != document["fingerprint"]: + diagnostics.append("request document_fingerprint does not match document fingerprint") + if descriptor["document_fingerprint"] != request["document_fingerprint"]: + diagnostics.append("descriptor document_fingerprint does not match request") + if request["element_id"] != case["element_id"]: + diagnostics.append("request element_id does not match contract inventory case") + + element = elements_by_id(document).get(request["element_id"]) + if element is None: + diagnostics.append("request element_id does not resolve in document") + return diagnostics + + if "page" not in element: + diagnostics.append("resolved element is missing page") + elif descriptor["page"] != element["page"]: + diagnostics.append("descriptor page does not match resolved element") + + if "bbox" not in element: + diagnostics.append("resolved element is missing bbox") + elif descriptor["bbox"] != element["bbox"]: + diagnostics.append("descriptor bbox does not match resolved element") + + if request["rendering"] != case["rendering_status"]: + diagnostics.append("request rendering does not match contract inventory case") + if descriptor["rendering_status"] != request["rendering"]: + diagnostics.append("descriptor rendering_status does not match request") + + return diagnostics + + class MilestoneDCropElementContractTests(unittest.TestCase): def test_target_is_declared_phony(self) -> None: text = makefile_text() @@ -154,8 +202,10 @@ def test_contract_pins_descriptor_artifact_boundary(self) -> None: text = normalized_contract_text() for required in [ + "`schemas/ethos-crop-element-request.schema.json`", "`schemas/ethos-crop-descriptor.schema.json`", "`examples/crop/crop_element_v1_contract.json`", + "`schemas/examples/crop-element-request.example.json`", "`schemas/examples/crop-descriptor.example.json`", "document fingerprint", "element id", @@ -186,8 +236,32 @@ def test_contract_inventory_schema_validates_inventory(self) -> None: ) self.assertEqual([], errors) + def test_crop_element_request_example_validates_against_request_schema(self) -> None: + schema = load_json(CROP_ELEMENT_REQUEST_SCHEMA) + request = load_json(CROP_ELEMENT_REQUEST_EXAMPLE) + + Draft202012Validator.check_schema(schema) + self.assertEqual([], schema_errors(schema, request)) + + rendered_request = dict( + request, + rendering="rendered", + source_pdf_fingerprint="sha256:" + "1" * 64, + ) + self.assertEqual([], schema_errors(schema, rendered_request)) + + rendered_without_source = dict(request, rendering="rendered") + self.assertNotEqual([], schema_errors(schema, rendered_without_source)) + + descriptor_only_with_source = dict( + request, + source_pdf_fingerprint="sha256:" + "1" * 64, + ) + self.assertNotEqual([], schema_errors(schema, descriptor_only_with_source)) + def test_contract_inventory_binds_element_to_descriptor_example(self) -> None: inventory = load_json(CONTRACT_INVENTORY) + request_schema = load_json(CROP_ELEMENT_REQUEST_SCHEMA) self.assertEqual(inventory["schema_version"], 1) self.assertEqual(inventory["contract"], "crop_element.v1") @@ -200,20 +274,67 @@ def test_contract_inventory_binds_element_to_descriptor_example(self) -> None: for case in inventory["cases"]: document_path = ROOT / case["document"] + request_path = ROOT / case["request"] descriptor_path = ROOT / case["descriptor"] self.assertTrue(document_path.is_file(), case["name"]) + self.assertTrue(request_path.is_file(), case["name"]) self.assertTrue(descriptor_path.is_file(), case["name"]) document = load_json(document_path) + request = load_json(request_path) descriptor = load_json(descriptor_path) element = elements_by_id(document)[case["element_id"]] + self.assertEqual([], schema_errors(request_schema, request), case["name"]) + self.assertEqual(request["document_fingerprint"], document["fingerprint"]) + self.assertEqual(request["element_id"], case["element_id"]) + self.assertEqual(request["rendering"], case["rendering_status"]) self.assertEqual(descriptor["artifact_type"], "ethos.crop_descriptor.v1") - self.assertEqual(descriptor["document_fingerprint"], document["fingerprint"]) + self.assertEqual(descriptor["document_fingerprint"], request["document_fingerprint"]) self.assertEqual(descriptor["page"], element["page"]) self.assertEqual(descriptor["bbox"], element["bbox"]) - self.assertEqual(descriptor["rendering_status"], case["rendering_status"]) + self.assertEqual(descriptor["rendering_status"], request["rendering"]) self.assertEqual(descriptor["check_ids"], ["v0001"]) + self.assertEqual([], request_case_diagnostics(request, document, descriptor, case)) + + def test_request_binding_guard_fails_closed_on_stale_or_unresolved_inputs(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + case = inventory["cases"][0] + request = load_json(ROOT / case["request"]) + document = load_json(ROOT / case["document"]) + descriptor = load_json(ROOT / case["descriptor"]) + + unknown_element_request = dict(request, element_id="e999999") + self.assertIn( + "request element_id does not resolve in document", + request_case_diagnostics(unknown_element_request, document, descriptor, case), + ) + + stale_request = dict(request, document_fingerprint="sha256:" + "0" * 64) + self.assertIn( + "request document_fingerprint does not match document fingerprint", + request_case_diagnostics(stale_request, document, descriptor, case), + ) + + mismatched_element_request = dict(request, element_id="e000001") + self.assertIn( + "request element_id does not match contract inventory case", + request_case_diagnostics(mismatched_element_request, document, descriptor, case), + ) + + document_without_page = deepcopy(document) + del elements_by_id(document_without_page)[request["element_id"]]["page"] + self.assertIn( + "resolved element is missing page", + request_case_diagnostics(request, document_without_page, descriptor, case), + ) + + document_without_bbox = deepcopy(document) + del elements_by_id(document_without_bbox)[request["element_id"]]["bbox"] + self.assertIn( + "resolved element is missing bbox", + request_case_diagnostics(request, document_without_bbox, descriptor, case), + ) def test_contract_inventory_binds_descriptor_to_verification_evidence(self) -> None: inventory = load_json(CONTRACT_INVENTORY) diff --git a/docs/execution-status.md b/docs/execution-status.md index 129180e..fd01f76 100644 --- a/docs/execution-status.md +++ b/docs/execution-status.md @@ -27,7 +27,7 @@ The committed implementation now includes: - `ethos security report` has a source-only pre-alpha artifact check over the committed document example. The current internal checks cover deterministic report output, report/source identity grounding, security-warning lane and message diagnostics, locator grounding, inventory/report parity, summary drift, warning id uniqueness, deterministic warning numbering, and explicit rejection of unsupported current source-warning references. - `make milestone-c-internal-checks` composes the current internal Milestone C artifact-validation path across RAG chunk and security-report gates; CI/static guard scripts fail closed if that command wiring or the dated closeout record drifts. - Milestone D source-only pre-alpha contract work has started with `docs/milestone-d-verify-citations-contract.md`. It defines `verify_citations` v1 as the citation-input, verification-config, grounding-source, and verification-report contract currently carried by `ethos verify`; schema/example validation checks that the minimal citation example and verification-report example stay coherent. Focused validation is `make milestone-d-verify-citations-contract PYTHON=/bin/python`. -- Milestone D `crop_element` v1 contract prep has started with `docs/milestone-d-crop-element-contract.md`. It defines `crop_element` as the future element-to-crop-descriptor contract currently represented by the existing `ethos verify --crop-dir` carrier; schema/example validation checks the inventory at `examples/crop/crop_element_v1_contract.json`, and the repository guard checks that it stays coherent with the document and crop-descriptor examples. Focused validation is `make milestone-d-crop-element-contract PYTHON=/bin/python`. +- Milestone D `crop_element` v1 contract prep has started with `docs/milestone-d-crop-element-contract.md`. It defines `crop_element` as the future element-to-crop-descriptor contract currently represented by the existing `ethos verify --crop-dir` carrier; schema/example validation checks the request envelope at `schemas/examples/crop-element-request.example.json` and inventory at `examples/crop/crop_element_v1_contract.json`, and the repository guard checks that they stay coherent with the document and crop-descriptor examples. Focused validation is `make milestone-d-crop-element-contract PYTHON=/bin/python`. - Milestone D `sandbox_subprocess` v1 contract prep has started with `docs/milestone-d-sandbox-subprocess-contract.md`. It defines `sandbox_subprocess` as the future worker-boundary contract currently represented by the existing PDF worker process behind `ethos doc parse` and `ethos fingerprint`; schema/example validation checks the inventory at `examples/sandbox/sandbox_subprocess_v1_contract.json`, and the repository guard checks that it stays coherent with the worker test slice. Focused validation is `make milestone-d-sandbox-subprocess-contract PYTHON=/bin/python`. - `make milestone-d-internal-contracts PYTHON=/bin/python` composes the current Milestone D source-only contract gates and has a static guard for that target's command wiring. diff --git a/docs/milestone-d-crop-element-contract.md b/docs/milestone-d-crop-element-contract.md index c2cd639..bcc397e 100644 --- a/docs/milestone-d-crop-element-contract.md +++ b/docs/milestone-d-crop-element-contract.md @@ -12,9 +12,12 @@ document, an explicit element locator, a crop descriptor, and an optional render `crop_element` v1 will consume: +- a `schemas/ethos-crop-element-request.schema.json` request envelope carrying the document + fingerprint, `element_id`, and requested rendering mode; - a canonical Ethos document JSON grounding source with explicit element ids, page ids, integer - bboxes, and a document fingerprint; -- an `element_id` locator that resolves to one element with one page and one bbox; + bboxes, and a matching document fingerprint; +- an `element_id` locator that resolves through that request to one element with one page and one + bbox; - optional source PDF bytes whose fingerprint matches the document source fingerprint when rendered output is requested. @@ -24,7 +27,8 @@ filename, byte hash, dimensions, and source PDF fingerprint. The current source-tree fixture for this contract boundary is `examples/crop/crop_element_v1_contract.json`. That inventory binds -`schemas/examples/document.example.json` element `e000002` to +`schemas/examples/crop-element-request.example.json`, +`schemas/examples/document.example.json` element `e000002`, and `schemas/examples/crop-descriptor.example.json`. Focused validation command: @@ -61,7 +65,7 @@ preserve the same audit bindings before it can replace or wrap the current carri - page id; - bbox; - crop descriptor filename; -- request identity; +- request envelope identity; - optional rendered PNG metadata and source PDF fingerprint. ## Explicit Blockers For This Slice diff --git a/examples/crop/crop_element_v1_contract.json b/examples/crop/crop_element_v1_contract.json index 394a9e9..992a85e 100644 --- a/examples/crop/crop_element_v1_contract.json +++ b/examples/crop/crop_element_v1_contract.json @@ -8,6 +8,7 @@ "name": "document-example-e000002", "document": "schemas/examples/document.example.json", "element_id": "e000002", + "request": "schemas/examples/crop-element-request.example.json", "descriptor": "schemas/examples/crop-descriptor.example.json", "rendering_status": "descriptor_only" } diff --git a/schemas/README.md b/schemas/README.md index a0b06fd..d7edd65 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -13,6 +13,7 @@ bumps and downstream sign-off; output-changing heuristics are semver events (PRD | `ethos-verification-report.schema.json` | `verification_report.json` | | `ethos-verification-config.schema.json` | verification config (its c14n hash stamps reports) | | `ethos-crop-descriptor.schema.json` | crop descriptor JSON emitted by `ethos verify --crop-dir` | +| `ethos-crop-element-request.schema.json` | source-only request envelope for Milestone D `crop_element` v1 contract work | | `ethos-verify-citations-contract.schema.json` | Milestone D `verify_citations` v1 source-only contract inventory | | `ethos-crop-element-contract.schema.json` | Milestone D `crop_element` v1 source-only contract inventory | | `ethos-sandbox-subprocess-contract.schema.json` | Milestone D `sandbox_subprocess` v1 source-only contract inventory | @@ -43,9 +44,10 @@ Milestone D `crop_element` v1 contract work is tracked in `docs/milestone-d-crop-element-contract.md`. In this source-only pre-alpha slice, `crop_element` names the future element-to-crop-descriptor contract currently represented by the existing `ethos verify --crop-dir` carrier; it does not add a first-class command or binding -surface. The contract inventory at `examples/crop/crop_element_v1_contract.json` is -schema-validated here; its alignment with the document and crop-descriptor examples is checked by -the Milestone D repository guard. +surface. The request envelope example at `schemas/examples/crop-element-request.example.json` and +contract inventory at `examples/crop/crop_element_v1_contract.json` are schema-validated here; +their alignment with the document and crop-descriptor examples is checked by the Milestone D +repository guard. Milestone D `sandbox_subprocess` v1 contract work is tracked in `docs/milestone-d-sandbox-subprocess-contract.md`. In this source-only pre-alpha slice, diff --git a/schemas/ethos-crop-element-contract.schema.json b/schemas/ethos-crop-element-contract.schema.json index 18b36a9..4a4e7c7 100644 --- a/schemas/ethos-crop-element-contract.schema.json +++ b/schemas/ethos-crop-element-contract.schema.json @@ -27,6 +27,7 @@ "name", "document", "element_id", + "request", "descriptor", "rendering_status" ], @@ -35,6 +36,7 @@ "name": { "$ref": "#/$defs/case_name" }, "document": { "$ref": "#/$defs/repo_path" }, "element_id": { "type": "string", "pattern": "^e[0-9]{6}$" }, + "request": { "$ref": "#/$defs/repo_path" }, "descriptor": { "$ref": "#/$defs/repo_path" }, "rendering_status": { "enum": ["descriptor_only", "rendered"] } } diff --git a/schemas/ethos-crop-element-request.schema.json b/schemas/ethos-crop-element-request.schema.json new file mode 100644 index 0000000..8043e5a --- /dev/null +++ b/schemas/ethos-crop-element-request.schema.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:ethos:schema:crop-element-request:1", + "title": "Ethos crop_element v1 request", + "description": "Source-only pre-alpha request envelope for the future crop_element v1 contract. The current executable carrier remains ethos verify --crop-dir until a first-class surface is explicitly added.", + "type": "object", + "required": [ + "artifact_type", + "schema_version", + "document_fingerprint", + "element_id", + "rendering" + ], + "additionalProperties": false, + "properties": { + "artifact_type": { "const": "ethos.crop_element_request.v1" }, + "schema_version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" }, + "document_fingerprint": { "$ref": "#/$defs/fingerprint" }, + "element_id": { + "type": "string", + "pattern": "^e[0-9]{6}$", + "description": "Element id in the canonical Ethos document graph." + }, + "rendering": { + "enum": ["descriptor_only", "rendered"], + "description": "descriptor_only requests only a crop descriptor; rendered requests a descriptor plus PNG artifact from matching source PDF bytes." + }, + "source_pdf_fingerprint": { + "$ref": "#/$defs/fingerprint", + "description": "Fingerprint of the source PDF bytes supplied by the caller for rendered output." + } + }, + "allOf": [ + { + "if": { + "properties": { "rendering": { "const": "rendered" } }, + "required": ["rendering"] + }, + "then": { + "required": ["source_pdf_fingerprint"] + } + }, + { + "if": { + "properties": { "rendering": { "const": "descriptor_only" } }, + "required": ["rendering"] + }, + "then": { + "not": { + "required": ["source_pdf_fingerprint"] + } + } + } + ], + "$defs": { + "fingerprint": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" } + } +} diff --git a/schemas/examples/crop-element-request.example.json b/schemas/examples/crop-element-request.example.json new file mode 100644 index 0000000..51f5e73 --- /dev/null +++ b/schemas/examples/crop-element-request.example.json @@ -0,0 +1,7 @@ +{ + "artifact_type": "ethos.crop_element_request.v1", + "schema_version": "1.0.0", + "document_fingerprint": "sha256:b5d30710d0c25cc38d8dec924ecaf57ae4f81276dd5dc14d75cb3b5b6bde62d3", + "element_id": "e000002", + "rendering": "descriptor_only" +} diff --git a/schemas/validate_examples.py b/schemas/validate_examples.py index 5acbff4..c4df606 100644 --- a/schemas/validate_examples.py +++ b/schemas/validate_examples.py @@ -80,6 +80,9 @@ ]), ("ethos-verification-config.schema.json", [EXAMPLES / "verification-config.example.json"]), ("ethos-crop-descriptor.schema.json", [EXAMPLES / "crop-descriptor.example.json"]), + ("ethos-crop-element-request.schema.json", [ + EXAMPLES / "crop-element-request.example.json", + ]), ("ethos-verify-citations-contract.schema.json", [ ROOT / "examples" / "verify" / "verify_citations_v1_contract.json", ]), From 7059238f1886ce8afdde3b9734a3c842430a21b3 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 15:19:27 +0530 Subject: [PATCH 16/39] Guard crop descriptor logical identity refs Signed-off-by: docushell-admin --- .../test_milestone_d_crop_element_contract.py | 88 +++++++++++++++++++ docs/milestone-d-crop-element-contract.md | 4 +- schemas/ethos-crop-descriptor.schema.json | 2 +- 3 files changed, 92 insertions(+), 2 deletions(-) diff --git a/.github/scripts/test_milestone_d_crop_element_contract.py b/.github/scripts/test_milestone_d_crop_element_contract.py index 89729a3..5350f30 100644 --- a/.github/scripts/test_milestone_d_crop_element_contract.py +++ b/.github/scripts/test_milestone_d_crop_element_contract.py @@ -94,6 +94,46 @@ def sha256_text(text: str) -> str: return hashlib.sha256(text.encode("utf-8")).hexdigest() +def sha256_c14n(value: dict) -> str: + encoded = json.dumps( + value, + separators=(",", ":"), + sort_keys=True, + ensure_ascii=False, + ).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + +def logical_crop_ref(document_fingerprint: str, check_id: str, page: str) -> str: + return "crop-{}.json".format( + sha256_c14n( + { + "check_id": check_id, + "document_fingerprint": document_fingerprint, + "page": page, + "version": "ethos.logical_crop_ref.v1", + } + ) + ) + + +def crop_ref_drift_diagnostics(descriptor: dict) -> list[str]: + diagnostics: list[str] = [] + check_ids = descriptor.get("check_ids", []) + if len(check_ids) != 1: + diagnostics.append("descriptor must bind exactly one logical check id") + return diagnostics + + expected = logical_crop_ref( + descriptor["document_fingerprint"], + check_ids[0], + descriptor["page"], + ) + if descriptor["crop_ref"] != expected: + diagnostics.append("descriptor crop_ref does not match logical identity tuple") + return diagnostics + + def schema_errors(schema: dict, instance: dict) -> list: return sorted( Draft202012Validator(schema).iter_errors(instance), @@ -211,7 +251,10 @@ def test_contract_pins_descriptor_artifact_boundary(self) -> None: "element id", "page id", "bbox", + "check id", "crop descriptor filename", + "logical evidence identity", + "`ethos.logical_crop_ref.v1`", "optional rendered PNG metadata and source PDF fingerprint", "`make milestone-d-crop-element-contract PYTHON=/bin/python`", ]: @@ -295,6 +338,7 @@ def test_contract_inventory_binds_element_to_descriptor_example(self) -> None: self.assertEqual(descriptor["bbox"], element["bbox"]) self.assertEqual(descriptor["rendering_status"], request["rendering"]) self.assertEqual(descriptor["check_ids"], ["v0001"]) + self.assertEqual([], crop_ref_drift_diagnostics(descriptor), case["name"]) self.assertEqual([], request_case_diagnostics(request, document, descriptor, case)) def test_request_binding_guard_fails_closed_on_stale_or_unresolved_inputs(self) -> None: @@ -353,12 +397,56 @@ def test_contract_inventory_binds_descriptor_to_verification_evidence(self) -> N self.assertEqual(citation["element_id"], case["element_id"], case["name"]) self.assertEqual(evidence["page"], descriptor["page"], case["name"]) self.assertEqual(evidence["bbox"], descriptor["bbox"], case["name"]) + self.assertEqual( + logical_crop_ref(report["document_fingerprint"], check_id, descriptor["page"]), + descriptor["crop_ref"], + case["name"], + ) self.assertEqual( descriptor["text_sha256"], sha256_text(evidence["text"]), case["name"], ) + def test_crop_descriptor_crop_ref_fails_closed_on_logical_identity_drift(self) -> None: + descriptor = load_json(ROOT / "schemas/examples/crop-descriptor.example.json") + + self.assertEqual( + "crop-17e98204468b3c83e92fabe1ce7749ff4c1c1eaf919c327e6234ab29b50b2677.json", + logical_crop_ref(descriptor["document_fingerprint"], "v0001", descriptor["page"]), + ) + self.assertEqual( + [], + crop_ref_drift_diagnostics(descriptor), + ) + + stale_crop_ref = dict(descriptor, crop_ref="crop-" + "0" * 64 + ".json") + self.assertIn( + "descriptor crop_ref does not match logical identity tuple", + crop_ref_drift_diagnostics(stale_crop_ref), + ) + + stale_page = dict(descriptor, page="p0002") + self.assertIn( + "descriptor crop_ref does not match logical identity tuple", + crop_ref_drift_diagnostics(stale_page), + ) + + ambiguous_checks = dict(descriptor, check_ids=["v0001", "v0002"]) + self.assertIn( + "descriptor must bind exactly one logical check id", + crop_ref_drift_diagnostics(ambiguous_checks), + ) + + self.assertNotEqual( + logical_crop_ref("sha256:" + "0" * 64, "v0001", descriptor["page"]), + descriptor["crop_ref"], + ) + self.assertNotEqual( + logical_crop_ref(descriptor["document_fingerprint"], "v9999", descriptor["page"]), + descriptor["crop_ref"], + ) + def test_crop_descriptor_example_validates_against_descriptor_schema(self) -> None: schema = load_json(CROP_DESCRIPTOR_SCHEMA) descriptor = load_json(ROOT / "schemas/examples/crop-descriptor.example.json") diff --git a/docs/milestone-d-crop-element-contract.md b/docs/milestone-d-crop-element-contract.md index bcc397e..4887eb4 100644 --- a/docs/milestone-d-crop-element-contract.md +++ b/docs/milestone-d-crop-element-contract.md @@ -45,7 +45,9 @@ The v1 contract boundary is native, explicit, and source-bound: - crop locators resolve through one `element_id`; - the resolved element must carry one page id and one integer bbox; -- descriptor `crop_ref` values remain opaque artifact filenames; +- descriptor `crop_ref` values remain opaque artifact filenames for callers; +- source-tree fixture validation binds descriptor filenames to the logical evidence identity tuple + of `document_fingerprint`, check id, page id, and `ethos.logical_crop_ref.v1`; - descriptor JSON remains the canonical crop audit artifact; - rendered PNG output is optional and must be bound to a matching source PDF fingerprint; - missing elements, missing pages, missing bboxes, malformed bboxes, and source fingerprint diff --git a/schemas/ethos-crop-descriptor.schema.json b/schemas/ethos-crop-descriptor.schema.json index b0bc5e1..8057e51 100644 --- a/schemas/ethos-crop-descriptor.schema.json +++ b/schemas/ethos-crop-descriptor.schema.json @@ -21,7 +21,7 @@ "crop_ref": { "type": "string", "pattern": "^crop-[0-9a-f]{64}\\.json$", - "description": "Stable descriptor filename referenced from verification_report checks[].evidence.crop_ref. Native Ethos CLI crop refs are logical evidence references derived from document fingerprint, check id, and page, not from raw bbox." + "description": "Stable descriptor filename referenced from verification_report checks[].evidence.crop_ref. Native Ethos CLI crop refs are logical evidence references derived from document fingerprint, check id, page, and the ethos.logical_crop_ref.v1 version marker, not from raw bbox." }, "document_fingerprint": { "$ref": "#/$defs/fingerprint" }, "page": { "type": "string" }, From 296e3461a237b63195efe18493353f431b8d0143 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 15:26:27 +0530 Subject: [PATCH 17/39] Add sandbox subprocess request contract guard Signed-off-by: docushell-admin --- ...milestone_d_sandbox_subprocess_contract.py | 151 ++++++++++++++++++ docs/execution-status.md | 2 +- ...milestone-d-sandbox-subprocess-contract.md | 9 +- .../sandbox_subprocess_v1_contract.json | 6 + schemas/README.md | 8 +- ...os-sandbox-subprocess-contract.schema.json | 3 + ...hos-sandbox-subprocess-request.schema.json | 99 ++++++++++++ ...doc-parse-diagnostics-request.example.json | 15 ++ ...-subprocess-doc-parse-request.example.json | 15 ++ ...ess-doc-parse-timeout-request.example.json | 15 ++ ...s-fingerprint-timeout-request.example.json | 14 ++ schemas/validate_examples.py | 6 + 12 files changed, 337 insertions(+), 6 deletions(-) create mode 100644 schemas/ethos-sandbox-subprocess-request.schema.json create mode 100644 schemas/examples/sandbox-subprocess-doc-parse-diagnostics-request.example.json create mode 100644 schemas/examples/sandbox-subprocess-doc-parse-request.example.json create mode 100644 schemas/examples/sandbox-subprocess-doc-parse-timeout-request.example.json create mode 100644 schemas/examples/sandbox-subprocess-fingerprint-timeout-request.example.json diff --git a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py index 29c2974..985c42f 100644 --- a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py +++ b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py @@ -30,6 +30,7 @@ CONTRACT = ROOT / "docs/milestone-d-sandbox-subprocess-contract.md" CONTRACT_INVENTORY = ROOT / "examples/sandbox/sandbox_subprocess_v1_contract.json" CONTRACT_INVENTORY_SCHEMA = ROOT / "schemas/ethos-sandbox-subprocess-contract.schema.json" +SANDBOX_REQUEST_SCHEMA = ROOT / "schemas/ethos-sandbox-subprocess-request.schema.json" ROADMAP = ROOT / "docs/roadmap.md" EXECUTION_STATUS = ROOT / "docs/execution-status.md" SCHEMAS_README = ROOT / "schemas/README.md" @@ -78,6 +79,74 @@ def contract_explicit_blockers() -> list[str]: ] +def schema_errors(schema: dict, instance: dict) -> list: + return sorted( + Draft202012Validator(schema).iter_errors(instance), + key=lambda error: list(error.absolute_path), + ) + + +def expected_operation(command_surface: str) -> str: + return { + "ethos doc parse": "doc_parse", + "ethos fingerprint": "fingerprint", + }[command_surface] + + +def expected_request_policy(case: dict) -> tuple[bool, str, int]: + if case["name"] == "doc-parse-diagnostics-gated-stderr": + return True, "stable_error_envelope_with_worker_stderr", 120_000 + if case["boundary"] == "max_parse_ms_timeout": + return False, "stable_error_envelope", 25 + return False, "stable_error_envelope", 120_000 + + +def request_case_diagnostics(request: dict, case: dict) -> list[str]: + diagnostics: list[str] = [] + want_diagnostics, want_stderr_policy, want_max_parse_ms = expected_request_policy(case) + + if request["operation"] != expected_operation(case["command_surface"]): + diagnostics.append("request operation does not match command surface") + if request["diagnostics"] != want_diagnostics: + diagnostics.append("request diagnostics does not match inventory case") + if request["stderr_policy"] != want_stderr_policy: + diagnostics.append("request stderr policy does not match diagnostics mode") + if request["stdout_on_failure"] != "empty": + diagnostics.append("request failure stdout policy is not empty") + if request["limits"]["max_parse_ms"] != want_max_parse_ms: + diagnostics.append("request max_parse_ms does not match inventory case") + + if request["operation"] == "doc_parse" and request.get("page_selection") != "all": + diagnostics.append("doc_parse request must bind explicit page selection") + if request["operation"] == "fingerprint" and "page_selection" in request: + diagnostics.append("fingerprint request must not carry page selection") + + return diagnostics + + +def rust_test_body(source: str, test_name: str) -> str: + match = re.search( + rf"(?:#\[[^\n]+\]\n)*\s*#\[test\]\s*fn {re.escape(test_name)}\(\) \{{", + source, + ) + if match is None: + raise AssertionError(f"missing Rust test body for {test_name}") + + depth = 1 + index = match.end() + while index < len(source): + char = source[index] + if char == "{": + depth += 1 + elif char == "}": + depth -= 1 + if depth == 0: + return source[match.end():index] + index += 1 + + raise AssertionError(f"unterminated Rust test body for {test_name}") + + class MilestoneDSandboxSubprocessContractTests(unittest.TestCase): def test_target_is_declared_phony(self) -> None: text = makefile_text() @@ -141,9 +210,12 @@ def test_contract_pins_fail_closed_boundaries(self) -> None: for required in [ "`parse_timeout` error code", "`memory_limit_exceeded` error code", + "`schemas/ethos-sandbox-subprocess-request.schema.json`", + "`schemas/examples/sandbox-subprocess-*.example.json`", "stable worker error envelopes are relayed", "non-envelope worker stderr is hidden by default", "explicit diagnostics", + "request envelopes bind each failure case", "stdout remains empty on worker failures", "`make milestone-d-sandbox-subprocess-contract PYTHON=/bin/python`", ]: @@ -160,8 +232,37 @@ def test_contract_inventory_schema_validates_inventory(self) -> None: ) self.assertEqual([], errors) + def test_request_examples_validate_against_request_schema(self) -> None: + schema = load_json(SANDBOX_REQUEST_SCHEMA) + inventory = load_json(CONTRACT_INVENTORY) + Draft202012Validator.check_schema(schema) + + request_paths = sorted({case["request"] for case in inventory["cases"]}) + self.assertGreaterEqual(len(request_paths), 3) + + for request_path in request_paths: + request = load_json(ROOT / request_path) + self.assertEqual([], schema_errors(schema, request), request_path) + + doc_parse_request = load_json(ROOT / "schemas/examples/sandbox-subprocess-doc-parse-request.example.json") + doc_parse_without_pages = dict(doc_parse_request) + del doc_parse_without_pages["page_selection"] + self.assertNotEqual([], schema_errors(schema, doc_parse_without_pages)) + + fingerprint_request = load_json(ROOT / "schemas/examples/sandbox-subprocess-fingerprint-timeout-request.example.json") + fingerprint_with_pages = dict(fingerprint_request, page_selection="all") + self.assertNotEqual([], schema_errors(schema, fingerprint_with_pages)) + + diagnostics_request = load_json(ROOT / "schemas/examples/sandbox-subprocess-doc-parse-diagnostics-request.example.json") + diagnostics_without_stderr = dict( + diagnostics_request, + stderr_policy="stable_error_envelope", + ) + self.assertNotEqual([], schema_errors(schema, diagnostics_without_stderr)) + def test_contract_inventory_matches_existing_worker_tests(self) -> None: inventory = load_json(CONTRACT_INVENTORY) + request_schema = load_json(SANDBOX_REQUEST_SCHEMA) test_source = PDF_PARSE_TESTS.read_text(encoding="utf-8") self.assertEqual(inventory["schema_version"], 1) @@ -177,8 +278,58 @@ def test_contract_inventory_matches_existing_worker_tests(self) -> None: sorted({case["boundary"] for case in inventory["cases"]}), ) for case in inventory["cases"]: + request_path = ROOT / case["request"] + self.assertTrue(request_path.is_file(), case["name"]) + request = load_json(request_path) + self.assertEqual([], schema_errors(request_schema, request), case["name"]) + self.assertEqual([], request_case_diagnostics(request, case), case["name"]) self.assertIn(f"fn {case['test_filter']}()", test_source, case["name"]) + def test_request_binding_guard_fails_closed_on_policy_drift(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + case = next( + case + for case in inventory["cases"] + if case["name"] == "doc-parse-diagnostics-gated-stderr" + ) + request = load_json(ROOT / case["request"]) + + wrong_operation = dict(request, operation="fingerprint") + self.assertIn( + "request operation does not match command surface", + request_case_diagnostics(wrong_operation, case), + ) + + diagnostics_disabled = dict(request, diagnostics=False) + self.assertIn( + "request diagnostics does not match inventory case", + request_case_diagnostics(diagnostics_disabled, case), + ) + + stdout_not_empty = dict(request, stdout_on_failure="not-empty") + self.assertIn( + "request failure stdout policy is not empty", + request_case_diagnostics(stdout_not_empty, case), + ) + + wrong_timeout = dict(request, limits={"max_parse_ms": 25}) + self.assertIn( + "request max_parse_ms does not match inventory case", + request_case_diagnostics(wrong_timeout, case), + ) + + def test_inventory_worker_failure_tests_keep_stdout_empty(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + test_source = PDF_PARSE_TESTS.read_text(encoding="utf-8") + + for case in inventory["cases"]: + body = rust_test_body(test_source, case["test_filter"]) + self.assertIn( + "assert!(output.stdout.is_empty());", + body, + case["name"], + ) + def test_inventory_keeps_current_command_surfaces_narrow(self) -> None: inventory = load_json(CONTRACT_INVENTORY) diff --git a/docs/execution-status.md b/docs/execution-status.md index fd01f76..285697e 100644 --- a/docs/execution-status.md +++ b/docs/execution-status.md @@ -28,7 +28,7 @@ The committed implementation now includes: - `make milestone-c-internal-checks` composes the current internal Milestone C artifact-validation path across RAG chunk and security-report gates; CI/static guard scripts fail closed if that command wiring or the dated closeout record drifts. - Milestone D source-only pre-alpha contract work has started with `docs/milestone-d-verify-citations-contract.md`. It defines `verify_citations` v1 as the citation-input, verification-config, grounding-source, and verification-report contract currently carried by `ethos verify`; schema/example validation checks that the minimal citation example and verification-report example stay coherent. Focused validation is `make milestone-d-verify-citations-contract PYTHON=/bin/python`. - Milestone D `crop_element` v1 contract prep has started with `docs/milestone-d-crop-element-contract.md`. It defines `crop_element` as the future element-to-crop-descriptor contract currently represented by the existing `ethos verify --crop-dir` carrier; schema/example validation checks the request envelope at `schemas/examples/crop-element-request.example.json` and inventory at `examples/crop/crop_element_v1_contract.json`, and the repository guard checks that they stay coherent with the document and crop-descriptor examples. Focused validation is `make milestone-d-crop-element-contract PYTHON=/bin/python`. -- Milestone D `sandbox_subprocess` v1 contract prep has started with `docs/milestone-d-sandbox-subprocess-contract.md`. It defines `sandbox_subprocess` as the future worker-boundary contract currently represented by the existing PDF worker process behind `ethos doc parse` and `ethos fingerprint`; schema/example validation checks the inventory at `examples/sandbox/sandbox_subprocess_v1_contract.json`, and the repository guard checks that it stays coherent with the worker test slice. Focused validation is `make milestone-d-sandbox-subprocess-contract PYTHON=/bin/python`. +- Milestone D `sandbox_subprocess` v1 contract prep has started with `docs/milestone-d-sandbox-subprocess-contract.md`. It defines `sandbox_subprocess` as the future worker-boundary contract currently represented by the existing PDF worker process behind `ethos doc parse` and `ethos fingerprint`; schema/example validation checks the request envelopes under `schemas/examples/sandbox-subprocess-*.example.json` and inventory at `examples/sandbox/sandbox_subprocess_v1_contract.json`, and the repository guard checks that they stay coherent with the worker test slice. Focused validation is `make milestone-d-sandbox-subprocess-contract PYTHON=/bin/python`. - `make milestone-d-internal-contracts PYTHON=/bin/python` composes the current Milestone D source-only contract gates and has a static guard for that target's command wiring. Still absent or not claimable: public benchmark reports, public competitor-comparison claims, public speed/quality/footprint claims, OCR/image-only support, real table extraction, mature list/heading/layout semantics beyond current fixture-backed alpha paths, semantic/arithmetic verification beyond deterministic evidence lookup, Phase 2 project-maintained PDFium builds, release packaging, and claim-audit approval for any public result wording. diff --git a/docs/milestone-d-sandbox-subprocess-contract.md b/docs/milestone-d-sandbox-subprocess-contract.md index 5e4c31b..c646965 100644 --- a/docs/milestone-d-sandbox-subprocess-contract.md +++ b/docs/milestone-d-sandbox-subprocess-contract.md @@ -12,15 +12,18 @@ PDF input, bounded worker execution, normalized document output, and stable erro `sandbox_subprocess` v1 will consume: +- a `schemas/ethos-sandbox-subprocess-request.schema.json` request envelope carrying operation, + PDF-byte input kind, limits, diagnostics intent, and failure-output policy; - PDF input already accepted by the current parse/fingerprint paths; -- parse configuration, including page selection and `max_parse_ms`; +- parse configuration, including page selection for document parsing and `max_parse_ms`; - a worker boundary that returns either normalized graph data or a stable error envelope; - optional diagnostics that may expose worker stderr only when explicitly requested. The current source-tree inventory for this contract boundary is `examples/sandbox/sandbox_subprocess_v1_contract.json`. It classifies existing worker tests that exercise timeout handling, memory-limit error reporting, stable error relay, and diagnostics-gated -stderr behavior. +stderr behavior. Each inventory case binds to a request envelope under +`schemas/examples/sandbox-subprocess-*.example.json`. Focused validation command: @@ -39,6 +42,8 @@ The v1 contract boundary is fail-closed and error-envelope-first: - stable worker error envelopes are relayed without converting them to generic failures; - non-envelope worker stderr is hidden by default; - non-envelope worker stderr is exposed only under explicit diagnostics; +- request envelopes bind each failure case to the intended operation, timeout limit, diagnostics + mode, and failure-output policy; - stdout remains empty on worker failures covered by this contract inventory. ## Explicit Blockers For This Slice diff --git a/examples/sandbox/sandbox_subprocess_v1_contract.json b/examples/sandbox/sandbox_subprocess_v1_contract.json index 928d363..238a64b 100644 --- a/examples/sandbox/sandbox_subprocess_v1_contract.json +++ b/examples/sandbox/sandbox_subprocess_v1_contract.json @@ -7,36 +7,42 @@ { "name": "doc-parse-timeout", "command_surface": "ethos doc parse", + "request": "schemas/examples/sandbox-subprocess-doc-parse-timeout-request.example.json", "test_filter": "doc_parse_timeout_kills_pdfium_worker", "boundary": "max_parse_ms_timeout" }, { "name": "fingerprint-timeout", "command_surface": "ethos fingerprint", + "request": "schemas/examples/sandbox-subprocess-fingerprint-timeout-request.example.json", "test_filter": "pdf_fingerprint_timeout_kills_pdfium_worker", "boundary": "max_parse_ms_timeout" }, { "name": "doc-parse-memory-limit", "command_surface": "ethos doc parse", + "request": "schemas/examples/sandbox-subprocess-doc-parse-request.example.json", "test_filter": "doc_parse_memory_limit_worker_failure_is_stable", "boundary": "memory_limit_error" }, { "name": "doc-parse-stable-error-envelope", "command_surface": "ethos doc parse", + "request": "schemas/examples/sandbox-subprocess-doc-parse-request.example.json", "test_filter": "doc_parse_relays_worker_stable_error_envelope", "boundary": "stable_error_envelope" }, { "name": "doc-parse-stderr-hidden-by-default", "command_surface": "ethos doc parse", + "request": "schemas/examples/sandbox-subprocess-doc-parse-request.example.json", "test_filter": "doc_parse_non_envelope_worker_failure_stays_canonical_without_diagnostics", "boundary": "diagnostics_gated_stderr" }, { "name": "doc-parse-diagnostics-gated-stderr", "command_surface": "ethos doc parse", + "request": "schemas/examples/sandbox-subprocess-doc-parse-diagnostics-request.example.json", "test_filter": "doc_parse_non_envelope_worker_failure_includes_stderr_with_diagnostics", "boundary": "diagnostics_gated_stderr" } diff --git a/schemas/README.md b/schemas/README.md index d7edd65..00471dc 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -16,6 +16,7 @@ bumps and downstream sign-off; output-changing heuristics are semver events (PRD | `ethos-crop-element-request.schema.json` | source-only request envelope for Milestone D `crop_element` v1 contract work | | `ethos-verify-citations-contract.schema.json` | Milestone D `verify_citations` v1 source-only contract inventory | | `ethos-crop-element-contract.schema.json` | Milestone D `crop_element` v1 source-only contract inventory | +| `ethos-sandbox-subprocess-request.schema.json` | source-only request envelope for Milestone D `sandbox_subprocess` v1 contract work | | `ethos-sandbox-subprocess-contract.schema.json` | Milestone D `sandbox_subprocess` v1 source-only contract inventory | | `ethos-deterministic-profile.schema.json` | `profiles/ethos-deterministic-v*.json` checker | @@ -53,9 +54,10 @@ Milestone D `sandbox_subprocess` v1 contract work is tracked in `docs/milestone-d-sandbox-subprocess-contract.md`. In this source-only pre-alpha slice, `sandbox_subprocess` names the future worker-boundary contract currently represented by the existing PDF worker process behind `ethos doc parse` and `ethos fingerprint`; it does not add a -hardened sandbox or a new command/binding surface. The contract inventory at -`examples/sandbox/sandbox_subprocess_v1_contract.json` is schema-validated here; its alignment with -the executable worker tests is checked by the Milestone D repository guard. +hardened sandbox or a new command/binding surface. The request envelope examples under +`schemas/examples/sandbox-subprocess-*.example.json` and the contract inventory at +`examples/sandbox/sandbox_subprocess_v1_contract.json` are schema-validated here; their alignment +with the executable worker tests is checked by the Milestone D repository guard. Derived artifacts not schema'd here: `document.md` / `document.txt` (deterministic exports specified by the exporter config, Milestone B) and `debug.html` (Milestone C). diff --git a/schemas/ethos-sandbox-subprocess-contract.schema.json b/schemas/ethos-sandbox-subprocess-contract.schema.json index 9a3860d..527f9de 100644 --- a/schemas/ethos-sandbox-subprocess-contract.schema.json +++ b/schemas/ethos-sandbox-subprocess-contract.schema.json @@ -26,6 +26,7 @@ "required": [ "name", "command_surface", + "request", "test_filter", "boundary" ], @@ -35,6 +36,7 @@ "command_surface": { "enum": ["ethos doc parse", "ethos fingerprint"] }, + "request": { "$ref": "#/$defs/repo_path" }, "test_filter": { "$ref": "#/$defs/rust_test_filter" }, "boundary": { "enum": [ @@ -56,6 +58,7 @@ }, "$defs": { "case_name": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, + "repo_path": { "type": "string", "minLength": 1, "pattern": "^[A-Za-z0-9_./-]+$" }, "rust_test_filter": { "type": "string", "pattern": "^[a-z0-9_]+$" } } } diff --git a/schemas/ethos-sandbox-subprocess-request.schema.json b/schemas/ethos-sandbox-subprocess-request.schema.json new file mode 100644 index 0000000..3a8f7cf --- /dev/null +++ b/schemas/ethos-sandbox-subprocess-request.schema.json @@ -0,0 +1,99 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:ethos:schema:sandbox-subprocess-request:1", + "title": "Ethos sandbox_subprocess v1 request", + "description": "Source-only pre-alpha request envelope for the future sandbox_subprocess v1 contract. The current executable carrier remains the existing PDF worker process until sandbox hardening is explicitly added.", + "type": "object", + "required": [ + "artifact_type", + "schema_version", + "operation", + "input", + "limits", + "diagnostics", + "stdout_on_failure", + "stderr_policy" + ], + "additionalProperties": false, + "properties": { + "artifact_type": { "const": "ethos.sandbox_subprocess_request.v1" }, + "schema_version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" }, + "operation": { "enum": ["doc_parse", "fingerprint"] }, + "input": { + "type": "object", + "required": ["kind"], + "additionalProperties": false, + "properties": { + "kind": { "const": "caller_supplied_pdf_bytes" } + } + }, + "page_selection": { + "oneOf": [ + { "const": "all" }, + { "type": "string", "pattern": "^[0-9]+(?:-[0-9]+)?(?:,[0-9]+(?:-[0-9]+)?)*$" } + ] + }, + "limits": { + "type": "object", + "required": ["max_parse_ms"], + "additionalProperties": false, + "properties": { + "max_parse_ms": { "type": "integer", "minimum": 1 } + } + }, + "diagnostics": { "type": "boolean" }, + "stdout_on_failure": { "const": "empty" }, + "stderr_policy": { + "enum": [ + "stable_error_envelope", + "stable_error_envelope_with_worker_stderr" + ] + } + }, + "allOf": [ + { + "if": { + "properties": { "operation": { "const": "doc_parse" } }, + "required": ["operation"] + }, + "then": { + "required": ["page_selection"] + } + }, + { + "if": { + "properties": { "operation": { "const": "fingerprint" } }, + "required": ["operation"] + }, + "then": { + "not": { "required": ["page_selection"] }, + "properties": { + "diagnostics": { "const": false }, + "stderr_policy": { "const": "stable_error_envelope" } + } + } + }, + { + "if": { + "properties": { "diagnostics": { "const": true } }, + "required": ["diagnostics"] + }, + "then": { + "properties": { + "stderr_policy": { "const": "stable_error_envelope_with_worker_stderr" } + } + } + }, + { + "if": { + "properties": { "diagnostics": { "const": false } }, + "required": ["diagnostics"] + }, + "then": { + "properties": { + "stderr_policy": { "const": "stable_error_envelope" } + } + } + } + ] +} diff --git a/schemas/examples/sandbox-subprocess-doc-parse-diagnostics-request.example.json b/schemas/examples/sandbox-subprocess-doc-parse-diagnostics-request.example.json new file mode 100644 index 0000000..29a31dd --- /dev/null +++ b/schemas/examples/sandbox-subprocess-doc-parse-diagnostics-request.example.json @@ -0,0 +1,15 @@ +{ + "artifact_type": "ethos.sandbox_subprocess_request.v1", + "schema_version": "1.0.0", + "operation": "doc_parse", + "input": { + "kind": "caller_supplied_pdf_bytes" + }, + "page_selection": "all", + "limits": { + "max_parse_ms": 120000 + }, + "diagnostics": true, + "stdout_on_failure": "empty", + "stderr_policy": "stable_error_envelope_with_worker_stderr" +} diff --git a/schemas/examples/sandbox-subprocess-doc-parse-request.example.json b/schemas/examples/sandbox-subprocess-doc-parse-request.example.json new file mode 100644 index 0000000..ac9bfba --- /dev/null +++ b/schemas/examples/sandbox-subprocess-doc-parse-request.example.json @@ -0,0 +1,15 @@ +{ + "artifact_type": "ethos.sandbox_subprocess_request.v1", + "schema_version": "1.0.0", + "operation": "doc_parse", + "input": { + "kind": "caller_supplied_pdf_bytes" + }, + "page_selection": "all", + "limits": { + "max_parse_ms": 120000 + }, + "diagnostics": false, + "stdout_on_failure": "empty", + "stderr_policy": "stable_error_envelope" +} diff --git a/schemas/examples/sandbox-subprocess-doc-parse-timeout-request.example.json b/schemas/examples/sandbox-subprocess-doc-parse-timeout-request.example.json new file mode 100644 index 0000000..fd222fa --- /dev/null +++ b/schemas/examples/sandbox-subprocess-doc-parse-timeout-request.example.json @@ -0,0 +1,15 @@ +{ + "artifact_type": "ethos.sandbox_subprocess_request.v1", + "schema_version": "1.0.0", + "operation": "doc_parse", + "input": { + "kind": "caller_supplied_pdf_bytes" + }, + "page_selection": "all", + "limits": { + "max_parse_ms": 25 + }, + "diagnostics": false, + "stdout_on_failure": "empty", + "stderr_policy": "stable_error_envelope" +} diff --git a/schemas/examples/sandbox-subprocess-fingerprint-timeout-request.example.json b/schemas/examples/sandbox-subprocess-fingerprint-timeout-request.example.json new file mode 100644 index 0000000..0d34640 --- /dev/null +++ b/schemas/examples/sandbox-subprocess-fingerprint-timeout-request.example.json @@ -0,0 +1,14 @@ +{ + "artifact_type": "ethos.sandbox_subprocess_request.v1", + "schema_version": "1.0.0", + "operation": "fingerprint", + "input": { + "kind": "caller_supplied_pdf_bytes" + }, + "limits": { + "max_parse_ms": 25 + }, + "diagnostics": false, + "stdout_on_failure": "empty", + "stderr_policy": "stable_error_envelope" +} diff --git a/schemas/validate_examples.py b/schemas/validate_examples.py index c4df606..0c171ad 100644 --- a/schemas/validate_examples.py +++ b/schemas/validate_examples.py @@ -89,6 +89,12 @@ ("ethos-crop-element-contract.schema.json", [ ROOT / "examples" / "crop" / "crop_element_v1_contract.json", ]), + ("ethos-sandbox-subprocess-request.schema.json", [ + EXAMPLES / "sandbox-subprocess-doc-parse-request.example.json", + EXAMPLES / "sandbox-subprocess-doc-parse-timeout-request.example.json", + EXAMPLES / "sandbox-subprocess-doc-parse-diagnostics-request.example.json", + EXAMPLES / "sandbox-subprocess-fingerprint-timeout-request.example.json", + ]), ("ethos-sandbox-subprocess-contract.schema.json", [ ROOT / "examples" / "sandbox" / "sandbox_subprocess_v1_contract.json", ]), From 2c15bb991c576ea8a9a17c5f9735473702cb9ebf Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 15:32:16 +0530 Subject: [PATCH 18/39] Guard crop element request identity Signed-off-by: docushell-admin --- .../test_milestone_d_crop_element_contract.py | 62 +++++++++++++++++++ docs/execution-status.md | 2 +- docs/milestone-d-crop-element-contract.md | 7 ++- schemas/README.md | 3 +- .../ethos-crop-element-request.schema.json | 6 ++ .../crop-element-request.example.json | 1 + 6 files changed, 77 insertions(+), 4 deletions(-) diff --git a/.github/scripts/test_milestone_d_crop_element_contract.py b/.github/scripts/test_milestone_d_crop_element_contract.py index 5350f30..4eb37d3 100644 --- a/.github/scripts/test_milestone_d_crop_element_contract.py +++ b/.github/scripts/test_milestone_d_crop_element_contract.py @@ -117,6 +117,26 @@ def logical_crop_ref(document_fingerprint: str, check_id: str, page: str) -> str ) +def logical_request_ref(request: dict) -> str: + identity = { + "document_fingerprint": request["document_fingerprint"], + "element_id": request["element_id"], + "rendering": request["rendering"], + "version": "ethos.crop_element_request_ref.v1", + } + if "source_pdf_fingerprint" in request: + identity["source_pdf_fingerprint"] = request["source_pdf_fingerprint"] + return "request-{}".format(sha256_c14n(identity)) + + +def request_ref_drift_diagnostics(request: dict) -> list[str]: + if "request_ref" not in request: + return ["request_ref is missing"] + if request["request_ref"] != logical_request_ref(request): + return ["request_ref does not match crop element request identity tuple"] + return [] + + def crop_ref_drift_diagnostics(descriptor: dict) -> list[str]: diagnostics: list[str] = [] check_ids = descriptor.get("check_ids", []) @@ -249,6 +269,9 @@ def test_contract_pins_descriptor_artifact_boundary(self) -> None: "`schemas/examples/crop-descriptor.example.json`", "document fingerprint", "element id", + "request_ref", + "crop element request identity", + "`ethos.crop_element_request_ref.v1`", "page id", "bbox", "check id", @@ -285,13 +308,24 @@ def test_crop_element_request_example_validates_against_request_schema(self) -> Draft202012Validator.check_schema(schema) self.assertEqual([], schema_errors(schema, request)) + self.assertEqual( + "request-489e91879dd347b3fce36cec50598144cfa96d0158c557665b8f35c6dc46ef85", + request["request_ref"], + ) + self.assertEqual([], request_ref_drift_diagnostics(request)) rendered_request = dict( request, rendering="rendered", source_pdf_fingerprint="sha256:" + "1" * 64, ) + rendered_request["request_ref"] = logical_request_ref(rendered_request) self.assertEqual([], schema_errors(schema, rendered_request)) + self.assertEqual([], request_ref_drift_diagnostics(rendered_request)) + + missing_request_ref = dict(request) + del missing_request_ref["request_ref"] + self.assertNotEqual([], schema_errors(schema, missing_request_ref)) rendered_without_source = dict(request, rendering="rendered") self.assertNotEqual([], schema_errors(schema, rendered_without_source)) @@ -302,6 +336,33 @@ def test_crop_element_request_example_validates_against_request_schema(self) -> ) self.assertNotEqual([], schema_errors(schema, descriptor_only_with_source)) + stale_request_ref = dict(request, request_ref="request-" + "0" * 64) + self.assertIn( + "request_ref does not match crop element request identity tuple", + request_ref_drift_diagnostics(stale_request_ref), + ) + + stale_request_element = dict(request, element_id="e000001") + self.assertIn( + "request_ref does not match crop element request identity tuple", + request_ref_drift_diagnostics(stale_request_element), + ) + + stale_request_fingerprint = dict(request, document_fingerprint="sha256:" + "0" * 64) + self.assertIn( + "request_ref does not match crop element request identity tuple", + request_ref_drift_diagnostics(stale_request_fingerprint), + ) + + rendered_stale_source = dict( + rendered_request, + source_pdf_fingerprint="sha256:" + "2" * 64, + ) + self.assertIn( + "request_ref does not match crop element request identity tuple", + request_ref_drift_diagnostics(rendered_stale_source), + ) + def test_contract_inventory_binds_element_to_descriptor_example(self) -> None: inventory = load_json(CONTRACT_INVENTORY) request_schema = load_json(CROP_ELEMENT_REQUEST_SCHEMA) @@ -329,6 +390,7 @@ def test_contract_inventory_binds_element_to_descriptor_example(self) -> None: element = elements_by_id(document)[case["element_id"]] self.assertEqual([], schema_errors(request_schema, request), case["name"]) + self.assertEqual([], request_ref_drift_diagnostics(request), case["name"]) self.assertEqual(request["document_fingerprint"], document["fingerprint"]) self.assertEqual(request["element_id"], case["element_id"]) self.assertEqual(request["rendering"], case["rendering_status"]) diff --git a/docs/execution-status.md b/docs/execution-status.md index 285697e..c9f953d 100644 --- a/docs/execution-status.md +++ b/docs/execution-status.md @@ -27,7 +27,7 @@ The committed implementation now includes: - `ethos security report` has a source-only pre-alpha artifact check over the committed document example. The current internal checks cover deterministic report output, report/source identity grounding, security-warning lane and message diagnostics, locator grounding, inventory/report parity, summary drift, warning id uniqueness, deterministic warning numbering, and explicit rejection of unsupported current source-warning references. - `make milestone-c-internal-checks` composes the current internal Milestone C artifact-validation path across RAG chunk and security-report gates; CI/static guard scripts fail closed if that command wiring or the dated closeout record drifts. - Milestone D source-only pre-alpha contract work has started with `docs/milestone-d-verify-citations-contract.md`. It defines `verify_citations` v1 as the citation-input, verification-config, grounding-source, and verification-report contract currently carried by `ethos verify`; schema/example validation checks that the minimal citation example and verification-report example stay coherent. Focused validation is `make milestone-d-verify-citations-contract PYTHON=/bin/python`. -- Milestone D `crop_element` v1 contract prep has started with `docs/milestone-d-crop-element-contract.md`. It defines `crop_element` as the future element-to-crop-descriptor contract currently represented by the existing `ethos verify --crop-dir` carrier; schema/example validation checks the request envelope at `schemas/examples/crop-element-request.example.json` and inventory at `examples/crop/crop_element_v1_contract.json`, and the repository guard checks that they stay coherent with the document and crop-descriptor examples. Focused validation is `make milestone-d-crop-element-contract PYTHON=/bin/python`. +- Milestone D `crop_element` v1 contract prep has started with `docs/milestone-d-crop-element-contract.md`. It defines `crop_element` as the future element-to-crop-descriptor contract currently represented by the existing `ethos verify --crop-dir` carrier; schema/example validation checks the request envelope at `schemas/examples/crop-element-request.example.json` and inventory at `examples/crop/crop_element_v1_contract.json`, and the repository guard checks that request identity, document, and crop-descriptor examples stay coherent. Focused validation is `make milestone-d-crop-element-contract PYTHON=/bin/python`. - Milestone D `sandbox_subprocess` v1 contract prep has started with `docs/milestone-d-sandbox-subprocess-contract.md`. It defines `sandbox_subprocess` as the future worker-boundary contract currently represented by the existing PDF worker process behind `ethos doc parse` and `ethos fingerprint`; schema/example validation checks the request envelopes under `schemas/examples/sandbox-subprocess-*.example.json` and inventory at `examples/sandbox/sandbox_subprocess_v1_contract.json`, and the repository guard checks that they stay coherent with the worker test slice. Focused validation is `make milestone-d-sandbox-subprocess-contract PYTHON=/bin/python`. - `make milestone-d-internal-contracts PYTHON=/bin/python` composes the current Milestone D source-only contract gates and has a static guard for that target's command wiring. diff --git a/docs/milestone-d-crop-element-contract.md b/docs/milestone-d-crop-element-contract.md index 4887eb4..be9527c 100644 --- a/docs/milestone-d-crop-element-contract.md +++ b/docs/milestone-d-crop-element-contract.md @@ -13,7 +13,7 @@ document, an explicit element locator, a crop descriptor, and an optional render `crop_element` v1 will consume: - a `schemas/ethos-crop-element-request.schema.json` request envelope carrying the document - fingerprint, `element_id`, and requested rendering mode; + fingerprint, `element_id`, requested rendering mode, and stable `request_ref`; - a canonical Ethos document JSON grounding source with explicit element ids, page ids, integer bboxes, and a matching document fingerprint; - an `element_id` locator that resolves through that request to one element with one page and one @@ -45,6 +45,9 @@ The v1 contract boundary is native, explicit, and source-bound: - crop locators resolve through one `element_id`; - the resolved element must carry one page id and one integer bbox; +- source-tree fixture validation binds crop element request identity to `document_fingerprint`, + `element_id`, rendering mode, optional source PDF fingerprint, and + `ethos.crop_element_request_ref.v1`; - descriptor `crop_ref` values remain opaque artifact filenames for callers; - source-tree fixture validation binds descriptor filenames to the logical evidence identity tuple of `document_fingerprint`, check id, page id, and `ethos.logical_crop_ref.v1`; @@ -67,7 +70,7 @@ preserve the same audit bindings before it can replace or wrap the current carri - page id; - bbox; - crop descriptor filename; -- request envelope identity; +- crop element request identity (`request_ref`); - optional rendered PNG metadata and source PDF fingerprint. ## Explicit Blockers For This Slice diff --git a/schemas/README.md b/schemas/README.md index 00471dc..b421ef5 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -48,7 +48,8 @@ existing `ethos verify --crop-dir` carrier; it does not add a first-class comman surface. The request envelope example at `schemas/examples/crop-element-request.example.json` and contract inventory at `examples/crop/crop_element_v1_contract.json` are schema-validated here; their alignment with the document and crop-descriptor examples is checked by the Milestone D -repository guard. +repository guard. The request envelope carries a c14n-derived `request_ref` identity guarded in +that same source-only contract check. Milestone D `sandbox_subprocess` v1 contract work is tracked in `docs/milestone-d-sandbox-subprocess-contract.md`. In this source-only pre-alpha slice, diff --git a/schemas/ethos-crop-element-request.schema.json b/schemas/ethos-crop-element-request.schema.json index 8043e5a..71b9b71 100644 --- a/schemas/ethos-crop-element-request.schema.json +++ b/schemas/ethos-crop-element-request.schema.json @@ -7,6 +7,7 @@ "required": [ "artifact_type", "schema_version", + "request_ref", "document_fingerprint", "element_id", "rendering" @@ -15,6 +16,11 @@ "properties": { "artifact_type": { "const": "ethos.crop_element_request.v1" }, "schema_version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" }, + "request_ref": { + "type": "string", + "pattern": "^request-[0-9a-f]{64}$", + "description": "Stable source-only request identity derived from document fingerprint, element id, rendering mode, optional source PDF fingerprint, and the ethos.crop_element_request_ref.v1 version marker." + }, "document_fingerprint": { "$ref": "#/$defs/fingerprint" }, "element_id": { "type": "string", diff --git a/schemas/examples/crop-element-request.example.json b/schemas/examples/crop-element-request.example.json index 51f5e73..c03c1fa 100644 --- a/schemas/examples/crop-element-request.example.json +++ b/schemas/examples/crop-element-request.example.json @@ -1,6 +1,7 @@ { "artifact_type": "ethos.crop_element_request.v1", "schema_version": "1.0.0", + "request_ref": "request-489e91879dd347b3fce36cec50598144cfa96d0158c557665b8f35c6dc46ef85", "document_fingerprint": "sha256:b5d30710d0c25cc38d8dec924ecaf57ae4f81276dd5dc14d75cb3b5b6bde62d3", "element_id": "e000002", "rendering": "descriptor_only" From a5cea28a7ca957709747ef54dcdac7c2c816b61d Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 15:36:27 +0530 Subject: [PATCH 19/39] Guard Milestone D contract registry Signed-off-by: docushell-admin --- .../test_milestone_d_internal_contracts.py | 82 +++++++++++++++++-- docs/execution-status.md | 2 +- 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/.github/scripts/test_milestone_d_internal_contracts.py b/.github/scripts/test_milestone_d_internal_contracts.py index c4ecdb7..9c49512 100644 --- a/.github/scripts/test_milestone_d_internal_contracts.py +++ b/.github/scripts/test_milestone_d_internal_contracts.py @@ -17,18 +17,51 @@ from __future__ import annotations +import json import unittest +from pathlib import Path from makefile_guard import makefile_text, target_block -CONTRACT_TARGETS = [ - "milestone-d-verify-citations-contract", - "milestone-d-crop-element-contract", - "milestone-d-sandbox-subprocess-contract", +ROOT = Path(__file__).resolve().parents[2] +EXECUTION_STATUS = ROOT / "docs/execution-status.md" +CONTRACT_REGISTRY = [ + { + "contract": "verify_citations.v1", + "carrier": "ethos verify", + "target": "milestone-d-verify-citations-contract", + "doc": "docs/milestone-d-verify-citations-contract.md", + "inventory": "examples/verify/verify_citations_v1_contract.json", + "schema": "schemas/ethos-verify-citations-contract.schema.json", + }, + { + "contract": "crop_element.v1", + "carrier": "ethos verify --crop-dir", + "target": "milestone-d-crop-element-contract", + "doc": "docs/milestone-d-crop-element-contract.md", + "inventory": "examples/crop/crop_element_v1_contract.json", + "schema": "schemas/ethos-crop-element-contract.schema.json", + }, + { + "contract": "sandbox_subprocess.v1", + "carrier": "pdfium worker process", + "target": "milestone-d-sandbox-subprocess-contract", + "doc": "docs/milestone-d-sandbox-subprocess-contract.md", + "inventory": "examples/sandbox/sandbox_subprocess_v1_contract.json", + "schema": "schemas/ethos-sandbox-subprocess-contract.schema.json", + }, ] +def load_json(path: str) -> dict: + return json.loads((ROOT / path).read_text(encoding="utf-8")) + + +def registered_targets() -> list[str]: + return [entry["target"] for entry in CONTRACT_REGISTRY] + + class MilestoneDInternalContractsTests(unittest.TestCase): def test_target_is_declared_phony(self) -> None: text = makefile_text() @@ -39,11 +72,50 @@ def test_target_is_declared_phony(self) -> None: def test_target_composes_current_d_contract_targets(self) -> None: block = target_block("milestone-d-internal-contracts") - for target in CONTRACT_TARGETS: + for target in registered_targets(): self.assertIn(f"$(MAKE) {target} PYTHON=$(PYTHON)", block) self.assertIn("$(PYTHON) .github/scripts/test_milestone_d_internal_contracts.py", block) self.assertIn("git diff --check", block) + def test_target_commands_match_registered_contracts(self) -> None: + block = target_block("milestone-d-internal-contracts") + commands = [line.strip() for line in block.splitlines() if line.strip()] + + self.assertEqual( + [f"$(MAKE) {target} PYTHON=$(PYTHON)" for target in registered_targets()] + + [ + "$(PYTHON) .github/scripts/test_milestone_d_internal_contracts.py", + "git diff --check", + ], + commands, + ) + + def test_contract_registry_matches_current_d_inventories(self) -> None: + contracts = [entry["contract"] for entry in CONTRACT_REGISTRY] + targets = registered_targets() + + self.assertEqual(len(contracts), len(set(contracts))) + self.assertEqual(len(targets), len(set(targets))) + + for entry in CONTRACT_REGISTRY: + inventory = load_json(entry["inventory"]) + self.assertEqual(entry["contract"], inventory["contract"]) + self.assertEqual("source-only-pre-alpha", inventory["status"]) + self.assertEqual(entry["carrier"], inventory["carrier"]) + self.assertTrue((ROOT / entry["schema"]).is_file(), entry["contract"]) + + def test_registry_references_are_consistent(self) -> None: + for entry in CONTRACT_REGISTRY: + doc = (ROOT / entry["doc"]).read_text(encoding="utf-8") + self.assertIn(Path(entry["inventory"]).name, doc, entry["contract"]) + self.assertIn(entry["target"], doc, entry["contract"]) + + def test_execution_status_names_registry_guard(self) -> None: + text = EXECUTION_STATUS.read_text(encoding="utf-8") + + self.assertIn("make milestone-d-internal-contracts", text) + self.assertIn("command wiring and contract registry", text) + def test_target_stays_internal_contract_scoped(self) -> None: block = target_block("milestone-d-internal-contracts") diff --git a/docs/execution-status.md b/docs/execution-status.md index c9f953d..e2041b3 100644 --- a/docs/execution-status.md +++ b/docs/execution-status.md @@ -29,7 +29,7 @@ The committed implementation now includes: - Milestone D source-only pre-alpha contract work has started with `docs/milestone-d-verify-citations-contract.md`. It defines `verify_citations` v1 as the citation-input, verification-config, grounding-source, and verification-report contract currently carried by `ethos verify`; schema/example validation checks that the minimal citation example and verification-report example stay coherent. Focused validation is `make milestone-d-verify-citations-contract PYTHON=/bin/python`. - Milestone D `crop_element` v1 contract prep has started with `docs/milestone-d-crop-element-contract.md`. It defines `crop_element` as the future element-to-crop-descriptor contract currently represented by the existing `ethos verify --crop-dir` carrier; schema/example validation checks the request envelope at `schemas/examples/crop-element-request.example.json` and inventory at `examples/crop/crop_element_v1_contract.json`, and the repository guard checks that request identity, document, and crop-descriptor examples stay coherent. Focused validation is `make milestone-d-crop-element-contract PYTHON=/bin/python`. - Milestone D `sandbox_subprocess` v1 contract prep has started with `docs/milestone-d-sandbox-subprocess-contract.md`. It defines `sandbox_subprocess` as the future worker-boundary contract currently represented by the existing PDF worker process behind `ethos doc parse` and `ethos fingerprint`; schema/example validation checks the request envelopes under `schemas/examples/sandbox-subprocess-*.example.json` and inventory at `examples/sandbox/sandbox_subprocess_v1_contract.json`, and the repository guard checks that they stay coherent with the worker test slice. Focused validation is `make milestone-d-sandbox-subprocess-contract PYTHON=/bin/python`. -- `make milestone-d-internal-contracts PYTHON=/bin/python` composes the current Milestone D source-only contract gates and has a static guard for that target's command wiring. +- `make milestone-d-internal-contracts PYTHON=/bin/python` composes the current Milestone D source-only contract gates and has a static guard for that target's command wiring and contract registry. Still absent or not claimable: public benchmark reports, public competitor-comparison claims, public speed/quality/footprint claims, OCR/image-only support, real table extraction, mature list/heading/layout semantics beyond current fixture-backed alpha paths, semantic/arithmetic verification beyond deterministic evidence lookup, Phase 2 project-maintained PDFium builds, release packaging, and claim-audit approval for any public result wording. From 6b1c1663c85b6af6b0e77ffb5ce9f07acf093758 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 15:40:05 +0530 Subject: [PATCH 20/39] Guard crop descriptor rendering metadata Signed-off-by: docushell-admin --- .../test_milestone_d_crop_element_contract.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/.github/scripts/test_milestone_d_crop_element_contract.py b/.github/scripts/test_milestone_d_crop_element_contract.py index 4eb37d3..7ecc9ca 100644 --- a/.github/scripts/test_milestone_d_crop_element_contract.py +++ b/.github/scripts/test_milestone_d_crop_element_contract.py @@ -518,6 +518,48 @@ def test_crop_descriptor_example_validates_against_descriptor_schema(self) -> No ) self.assertEqual([], errors) + def test_crop_descriptor_rendering_metadata_schema_boundaries(self) -> None: + schema = load_json(CROP_DESCRIPTOR_SCHEMA) + descriptor = load_json(ROOT / "schemas/examples/crop-descriptor.example.json") + + self.assertEqual("descriptor_only", descriptor["rendering_status"]) + self.assertEqual([], schema_errors(schema, descriptor)) + + conditional_fields = [ + "source_pdf_fingerprint", + "rendered_ref", + "rendered_format", + "rendered_sha256", + "rendered_width_px", + "rendered_height_px", + ] + + rendered_descriptor = dict( + descriptor, + rendering_status="rendered", + source_pdf_fingerprint="sha256:" + "1" * 64, + rendered_ref="crop-" + "2" * 64 + ".png", + rendered_format="png", + rendered_sha256="3" * 64, + rendered_width_px=10, + rendered_height_px=20, + ) + self.assertEqual([], schema_errors(schema, rendered_descriptor)) + + for field in conditional_fields: + missing_field = dict(rendered_descriptor) + del missing_field[field] + self.assertNotEqual([], schema_errors(schema, missing_field), field) + + for field in conditional_fields: + descriptor_only_with_rendered_metadata = dict(descriptor) + descriptor_only_with_rendered_metadata[field] = rendered_descriptor[field] + self.assertNotEqual( + [], + schema_errors(schema, descriptor_only_with_rendered_metadata), + field, + ) + def test_current_cli_surface_has_no_first_class_crop_element_command(self) -> None: source = CLI_MAIN.read_text(encoding="utf-8") match = re.search( From a66ae397cd27150036d8242637701563a1309aa8 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 15:45:56 +0530 Subject: [PATCH 21/39] Guard sandbox stderr diagnostics boundary Signed-off-by: docushell-admin --- .../test_milestone_d_sandbox_subprocess_contract.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py index 985c42f..3c0e3ab 100644 --- a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py +++ b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py @@ -260,6 +260,15 @@ def test_request_examples_validate_against_request_schema(self) -> None: ) self.assertNotEqual([], schema_errors(schema, diagnostics_without_stderr)) + doc_parse_with_stderr_without_diagnostics = dict( + doc_parse_request, + stderr_policy="stable_error_envelope_with_worker_stderr", + ) + self.assertNotEqual( + [], + schema_errors(schema, doc_parse_with_stderr_without_diagnostics), + ) + def test_contract_inventory_matches_existing_worker_tests(self) -> None: inventory = load_json(CONTRACT_INVENTORY) request_schema = load_json(SANDBOX_REQUEST_SCHEMA) From 5fa82a13cabe4aa0eff923018ac64c31d86bbd01 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 16:18:12 +0530 Subject: [PATCH 22/39] Add sandbox request identity refs Signed-off-by: docushell-admin --- ...milestone_d_sandbox_subprocess_contract.py | 119 ++++++++++++++++++ ...milestone-d-sandbox-subprocess-contract.md | 6 +- schemas/README.md | 4 +- ...hos-sandbox-subprocess-request.schema.json | 6 + ...doc-parse-diagnostics-request.example.json | 1 + ...-subprocess-doc-parse-request.example.json | 1 + ...ess-doc-parse-timeout-request.example.json | 1 + ...s-fingerprint-timeout-request.example.json | 1 + 8 files changed, 136 insertions(+), 3 deletions(-) diff --git a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py index 3c0e3ab..a006774 100644 --- a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py +++ b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py @@ -17,6 +17,7 @@ from __future__ import annotations +import hashlib import json import re import unittest @@ -56,6 +57,16 @@ def load_json(path: Path): return json.loads(path.read_text(encoding="utf-8")) +def sha256_c14n(value: dict) -> str: + encoded = json.dumps( + value, + separators=(",", ":"), + sort_keys=True, + ensure_ascii=False, + ).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + def contract_text() -> str: return CONTRACT.read_text(encoding="utf-8") @@ -101,6 +112,29 @@ def expected_request_policy(case: dict) -> tuple[bool, str, int]: return False, "stable_error_envelope", 120_000 +def logical_sandbox_request_ref(request: dict) -> str: + identity = { + "operation": request["operation"], + "input": request["input"], + "limits": request["limits"], + "diagnostics": request["diagnostics"], + "stdout_on_failure": request["stdout_on_failure"], + "stderr_policy": request["stderr_policy"], + "version": "ethos.sandbox_subprocess_request_ref.v1", + } + if "page_selection" in request: + identity["page_selection"] = request["page_selection"] + return "request-{}".format(sha256_c14n(identity)) + + +def request_ref_drift_diagnostics(request: dict) -> list[str]: + if "request_ref" not in request: + return ["request_ref is missing"] + if request["request_ref"] != logical_sandbox_request_ref(request): + return ["request_ref does not match sandbox subprocess request identity tuple"] + return [] + + def request_case_diagnostics(request: dict, case: dict) -> list[str]: diagnostics: list[str] = [] want_diagnostics, want_stderr_policy, want_max_parse_ms = expected_request_policy(case) @@ -212,6 +246,8 @@ def test_contract_pins_fail_closed_boundaries(self) -> None: "`memory_limit_exceeded` error code", "`schemas/ethos-sandbox-subprocess-request.schema.json`", "`schemas/examples/sandbox-subprocess-*.example.json`", + "`request_ref`", + "`ethos.sandbox_subprocess_request_ref.v1`", "stable worker error envelopes are relayed", "non-envelope worker stderr is hidden by default", "explicit diagnostics", @@ -245,6 +281,10 @@ def test_request_examples_validate_against_request_schema(self) -> None: self.assertEqual([], schema_errors(schema, request), request_path) doc_parse_request = load_json(ROOT / "schemas/examples/sandbox-subprocess-doc-parse-request.example.json") + doc_parse_without_request_ref = dict(doc_parse_request) + del doc_parse_without_request_ref["request_ref"] + self.assertNotEqual([], schema_errors(schema, doc_parse_without_request_ref)) + doc_parse_without_pages = dict(doc_parse_request) del doc_parse_without_pages["page_selection"] self.assertNotEqual([], schema_errors(schema, doc_parse_without_pages)) @@ -291,9 +331,88 @@ def test_contract_inventory_matches_existing_worker_tests(self) -> None: self.assertTrue(request_path.is_file(), case["name"]) request = load_json(request_path) self.assertEqual([], schema_errors(request_schema, request), case["name"]) + self.assertEqual([], request_ref_drift_diagnostics(request), case["name"]) self.assertEqual([], request_case_diagnostics(request, case), case["name"]) self.assertIn(f"fn {case['test_filter']}()", test_source, case["name"]) + def test_request_ref_fails_closed_on_identity_drift(self) -> None: + request = load_json(ROOT / "schemas/examples/sandbox-subprocess-doc-parse-request.example.json") + + expected_refs = { + "schemas/examples/sandbox-subprocess-doc-parse-request.example.json": + "request-5e6951ae8d44fcfa636bbf7cbd079414bcc6b9ca6cd1fd2037c10550014f94ac", + "schemas/examples/sandbox-subprocess-doc-parse-timeout-request.example.json": + "request-37ad10e9ab78491dfb4cb26268774f18d7d35b76cf226069d0ef16940f75350c", + "schemas/examples/sandbox-subprocess-doc-parse-diagnostics-request.example.json": + "request-590058131f2c0b21a8892f131802d0d52b3db08efabe8164e22f119b8d4c4e18", + "schemas/examples/sandbox-subprocess-fingerprint-timeout-request.example.json": + "request-da18cc9ac61abb6d4f11e20b72d1428b3e6a0d2c7036292f7dc5b2fb8c858ade", + } + for request_path, expected_ref in expected_refs.items(): + current = load_json(ROOT / request_path) + self.assertEqual(expected_ref, current["request_ref"], request_path) + self.assertEqual(expected_ref, logical_sandbox_request_ref(current), request_path) + + self.assertEqual( + "request-5e6951ae8d44fcfa636bbf7cbd079414bcc6b9ca6cd1fd2037c10550014f94ac", + logical_sandbox_request_ref(request), + ) + self.assertEqual([], request_ref_drift_diagnostics(request)) + + missing_request_ref = dict(request) + del missing_request_ref["request_ref"] + self.assertEqual( + ["request_ref is missing"], + request_ref_drift_diagnostics(missing_request_ref), + ) + + stale_request_ref = dict(request, request_ref="request-" + "0" * 64) + self.assertEqual( + ["request_ref does not match sandbox subprocess request identity tuple"], + request_ref_drift_diagnostics(stale_request_ref), + ) + + stale_limit = dict(request, limits={"max_parse_ms": 25}) + self.assertNotEqual(request["request_ref"], logical_sandbox_request_ref(stale_limit)) + self.assertEqual( + ["request_ref does not match sandbox subprocess request identity tuple"], + request_ref_drift_diagnostics(stale_limit), + ) + + stale_stderr_policy = dict( + request, + stderr_policy="stable_error_envelope_with_worker_stderr", + ) + self.assertNotEqual( + request["request_ref"], + logical_sandbox_request_ref(stale_stderr_policy), + ) + self.assertEqual( + ["request_ref does not match sandbox subprocess request identity tuple"], + request_ref_drift_diagnostics(stale_stderr_policy), + ) + + stale_page_selection = dict(request, page_selection="1") + self.assertNotEqual( + request["request_ref"], + logical_sandbox_request_ref(stale_page_selection), + ) + self.assertEqual( + ["request_ref does not match sandbox subprocess request identity tuple"], + request_ref_drift_diagnostics(stale_page_selection), + ) + + stale_operation = dict(request, operation="fingerprint") + del stale_operation["page_selection"] + self.assertNotEqual( + request["request_ref"], + logical_sandbox_request_ref(stale_operation), + ) + self.assertEqual( + ["request_ref does not match sandbox subprocess request identity tuple"], + request_ref_drift_diagnostics(stale_operation), + ) + def test_request_binding_guard_fails_closed_on_policy_drift(self) -> None: inventory = load_json(CONTRACT_INVENTORY) case = next( diff --git a/docs/milestone-d-sandbox-subprocess-contract.md b/docs/milestone-d-sandbox-subprocess-contract.md index c646965..1a8ed28 100644 --- a/docs/milestone-d-sandbox-subprocess-contract.md +++ b/docs/milestone-d-sandbox-subprocess-contract.md @@ -13,7 +13,8 @@ PDF input, bounded worker execution, normalized document output, and stable erro `sandbox_subprocess` v1 will consume: - a `schemas/ethos-sandbox-subprocess-request.schema.json` request envelope carrying operation, - PDF-byte input kind, limits, diagnostics intent, and failure-output policy; + PDF-byte input kind, limits, diagnostics intent, failure-output policy, and stable + `request_ref`; - PDF input already accepted by the current parse/fingerprint paths; - parse configuration, including page selection for document parsing and `max_parse_ms`; - a worker boundary that returns either normalized graph data or a stable error envelope; @@ -43,7 +44,8 @@ The v1 contract boundary is fail-closed and error-envelope-first: - non-envelope worker stderr is hidden by default; - non-envelope worker stderr is exposed only under explicit diagnostics; - request envelopes bind each failure case to the intended operation, timeout limit, diagnostics - mode, and failure-output policy; + mode, failure-output policy, and c14n-derived request identity + `ethos.sandbox_subprocess_request_ref.v1`; - stdout remains empty on worker failures covered by this contract inventory. ## Explicit Blockers For This Slice diff --git a/schemas/README.md b/schemas/README.md index b421ef5..f604e51 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -58,7 +58,9 @@ existing PDF worker process behind `ethos doc parse` and `ethos fingerprint`; it hardened sandbox or a new command/binding surface. The request envelope examples under `schemas/examples/sandbox-subprocess-*.example.json` and the contract inventory at `examples/sandbox/sandbox_subprocess_v1_contract.json` are schema-validated here; their alignment -with the executable worker tests is checked by the Milestone D repository guard. +with the executable worker tests is checked by the Milestone D repository guard. Each request +envelope carries a c14n-derived `request_ref` identity guarded in that same source-only contract +check. Derived artifacts not schema'd here: `document.md` / `document.txt` (deterministic exports specified by the exporter config, Milestone B) and `debug.html` (Milestone C). diff --git a/schemas/ethos-sandbox-subprocess-request.schema.json b/schemas/ethos-sandbox-subprocess-request.schema.json index 3a8f7cf..ad1238d 100644 --- a/schemas/ethos-sandbox-subprocess-request.schema.json +++ b/schemas/ethos-sandbox-subprocess-request.schema.json @@ -7,6 +7,7 @@ "required": [ "artifact_type", "schema_version", + "request_ref", "operation", "input", "limits", @@ -18,6 +19,11 @@ "properties": { "artifact_type": { "const": "ethos.sandbox_subprocess_request.v1" }, "schema_version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" }, + "request_ref": { + "type": "string", + "pattern": "^request-[0-9a-f]{64}$", + "description": "Stable source-only request identity derived from operation, input kind, page selection when present, limits, diagnostics intent, failure-output policy, and the ethos.sandbox_subprocess_request_ref.v1 version marker." + }, "operation": { "enum": ["doc_parse", "fingerprint"] }, "input": { "type": "object", diff --git a/schemas/examples/sandbox-subprocess-doc-parse-diagnostics-request.example.json b/schemas/examples/sandbox-subprocess-doc-parse-diagnostics-request.example.json index 29a31dd..17bf30c 100644 --- a/schemas/examples/sandbox-subprocess-doc-parse-diagnostics-request.example.json +++ b/schemas/examples/sandbox-subprocess-doc-parse-diagnostics-request.example.json @@ -1,6 +1,7 @@ { "artifact_type": "ethos.sandbox_subprocess_request.v1", "schema_version": "1.0.0", + "request_ref": "request-590058131f2c0b21a8892f131802d0d52b3db08efabe8164e22f119b8d4c4e18", "operation": "doc_parse", "input": { "kind": "caller_supplied_pdf_bytes" diff --git a/schemas/examples/sandbox-subprocess-doc-parse-request.example.json b/schemas/examples/sandbox-subprocess-doc-parse-request.example.json index ac9bfba..fc547d0 100644 --- a/schemas/examples/sandbox-subprocess-doc-parse-request.example.json +++ b/schemas/examples/sandbox-subprocess-doc-parse-request.example.json @@ -1,6 +1,7 @@ { "artifact_type": "ethos.sandbox_subprocess_request.v1", "schema_version": "1.0.0", + "request_ref": "request-5e6951ae8d44fcfa636bbf7cbd079414bcc6b9ca6cd1fd2037c10550014f94ac", "operation": "doc_parse", "input": { "kind": "caller_supplied_pdf_bytes" diff --git a/schemas/examples/sandbox-subprocess-doc-parse-timeout-request.example.json b/schemas/examples/sandbox-subprocess-doc-parse-timeout-request.example.json index fd222fa..a01213e 100644 --- a/schemas/examples/sandbox-subprocess-doc-parse-timeout-request.example.json +++ b/schemas/examples/sandbox-subprocess-doc-parse-timeout-request.example.json @@ -1,6 +1,7 @@ { "artifact_type": "ethos.sandbox_subprocess_request.v1", "schema_version": "1.0.0", + "request_ref": "request-37ad10e9ab78491dfb4cb26268774f18d7d35b76cf226069d0ef16940f75350c", "operation": "doc_parse", "input": { "kind": "caller_supplied_pdf_bytes" diff --git a/schemas/examples/sandbox-subprocess-fingerprint-timeout-request.example.json b/schemas/examples/sandbox-subprocess-fingerprint-timeout-request.example.json index 0d34640..d8def74 100644 --- a/schemas/examples/sandbox-subprocess-fingerprint-timeout-request.example.json +++ b/schemas/examples/sandbox-subprocess-fingerprint-timeout-request.example.json @@ -1,6 +1,7 @@ { "artifact_type": "ethos.sandbox_subprocess_request.v1", "schema_version": "1.0.0", + "request_ref": "request-da18cc9ac61abb6d4f11e20b72d1428b3e6a0d2c7036292f7dc5b2fb8c858ade", "operation": "fingerprint", "input": { "kind": "caller_supplied_pdf_bytes" From d41f4414b67396407319bafeeb1497dc01d31901 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 16:59:54 +0530 Subject: [PATCH 23/39] Guard sandbox contract case order Signed-off-by: docushell-admin --- ...est_milestone_d_sandbox_subprocess_contract.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py index a006774..fb7b87a 100644 --- a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py +++ b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py @@ -335,6 +335,21 @@ def test_contract_inventory_matches_existing_worker_tests(self) -> None: self.assertEqual([], request_case_diagnostics(request, case), case["name"]) self.assertIn(f"fn {case['test_filter']}()", test_source, case["name"]) + def test_contract_inventory_case_order_is_deterministic(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + + self.assertEqual( + [ + "doc-parse-timeout", + "fingerprint-timeout", + "doc-parse-memory-limit", + "doc-parse-stable-error-envelope", + "doc-parse-stderr-hidden-by-default", + "doc-parse-diagnostics-gated-stderr", + ], + [case["name"] for case in inventory["cases"]], + ) + def test_request_ref_fails_closed_on_identity_drift(self) -> None: request = load_json(ROOT / "schemas/examples/sandbox-subprocess-doc-parse-request.example.json") From a8ff7e76fc0722a611900f85a94fd2268a40aac7 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 17:12:20 +0530 Subject: [PATCH 24/39] Add capability downgrade contract guard Signed-off-by: docushell-admin --- .github/scripts/test_execution_status.py | 2 + ...lestone_d_capability_downgrade_contract.py | 292 ++++++++++++++++++ .../test_milestone_d_internal_contracts.py | 8 + Makefile | 11 + docs/execution-status.md | 1 + ...lestone-d-capability-downgrade-contract.md | 63 ++++ docs/roadmap.md | 5 + .../capability_downgrade_v1_contract.json | 116 +++++++ schemas/README.md | 9 + ...-capability-downgrade-contract.schema.json | 140 +++++++++ schemas/validate_examples.py | 3 + 11 files changed, 650 insertions(+) create mode 100644 .github/scripts/test_milestone_d_capability_downgrade_contract.py create mode 100644 docs/milestone-d-capability-downgrade-contract.md create mode 100644 examples/verify/capability_downgrade_v1_contract.json create mode 100644 schemas/ethos-capability-downgrade-contract.schema.json diff --git a/.github/scripts/test_execution_status.py b/.github/scripts/test_execution_status.py index 375bf77..bf20689 100644 --- a/.github/scripts/test_execution_status.py +++ b/.github/scripts/test_execution_status.py @@ -39,6 +39,7 @@ def test_status_is_scoped_to_internal_continuation(self) -> None: text, ) self.assertIn("docs/milestone-d-verify-citations-contract.md", text) + self.assertIn("docs/milestone-d-capability-downgrade-contract.md", text) self.assertIn("docs/milestone-d-crop-element-contract.md", text) self.assertIn("docs/milestone-d-sandbox-subprocess-contract.md", text) self.assertNotIn("Status: Pre-alpha / Milestone B entry.", text) @@ -49,6 +50,7 @@ def test_internal_check_command_is_documented(self) -> None: self.assertIn("make milestone-b-internal-checks", text) self.assertIn("make milestone-c-internal-checks", text) self.assertIn("make milestone-d-verify-citations-contract", text) + self.assertIn("make milestone-d-capability-downgrade-contract", text) self.assertIn("make milestone-d-crop-element-contract", text) self.assertIn("make milestone-d-sandbox-subprocess-contract", text) self.assertIn("make milestone-d-internal-contracts", text) diff --git a/.github/scripts/test_milestone_d_capability_downgrade_contract.py b/.github/scripts/test_milestone_d_capability_downgrade_contract.py new file mode 100644 index 0000000..73c16fe --- /dev/null +++ b/.github/scripts/test_milestone_d_capability_downgrade_contract.py @@ -0,0 +1,292 @@ +#!/usr/bin/env python3 +# +# Copyright 2026 The Ethos maintainers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import annotations + +import json +import re +import unittest +from pathlib import Path + +from jsonschema import Draft202012Validator +from makefile_guard import makefile_text, target_block + + +ROOT = Path(__file__).resolve().parents[2] +CONTRACT = ROOT / "docs/milestone-d-capability-downgrade-contract.md" +CONTRACT_INVENTORY = ROOT / "examples/verify/capability_downgrade_v1_contract.json" +CONTRACT_INVENTORY_SCHEMA = ROOT / "schemas/ethos-capability-downgrade-contract.schema.json" +VERIFICATION_REPORT_SCHEMA = ROOT / "schemas/ethos-verification-report.schema.json" +ROADMAP = ROOT / "docs/roadmap.md" +EXECUTION_STATUS = ROOT / "docs/execution-status.md" +SCHEMAS_README = ROOT / "schemas/README.md" +EXPECTED_EXPLICIT_BLOCKERS = [ + "a new public command or binding surface", + "Python, Node, MCP, or hosted capability surfaces", + "adapter hardening beyond committed fixtures", + "crop API implementation", + "sandbox backend expansion", + "semantic or arithmetic verification", +] + + +def load_json(path: Path): + return json.loads(path.read_text(encoding="utf-8")) + + +def contract_text() -> str: + return CONTRACT.read_text(encoding="utf-8") + + +def normalized_contract_text() -> str: + return re.sub(r"\s+", " ", contract_text()) + + +def contract_explicit_blockers() -> list[str]: + match = re.search( + r"## Explicit Blockers For This Slice\n\n" + r"This first `capability_downgrade` slice does not add:\n\n" + r"(?P(?:- .+\n)+)", + contract_text(), + ) + if match is None: + raise AssertionError("missing capability_downgrade explicit blocker list") + return [ + line.removeprefix("- ").rstrip(";.") + for line in match.group("bullets").strip().splitlines() + ] + + +def schema_errors(schema: dict, instance: dict) -> list: + return sorted( + Draft202012Validator(schema).iter_errors(instance), + key=lambda error: list(error.absolute_path), + ) + + +def blocked_checks(report: dict) -> list[dict]: + return [check for check in report["checks"] if check["status"] == "capability_blocked"] + + +class MilestoneDCapabilityDowngradeContractTests(unittest.TestCase): + def test_target_is_declared_phony(self) -> None: + text = makefile_text() + + self.assertIn(".PHONY:", text) + self.assertIn("milestone-d-capability-downgrade-contract", text) + + def test_target_composes_contract_gates(self) -> None: + block = target_block("milestone-d-capability-downgrade-contract") + + required = [ + "cargo test --locked -p ethos-verify capability", + "cargo test --locked -p ethos-cli --test verify capability", + "$(PYTHON) schemas/validate_examples.py", + "$(PYTHON) .github/scripts/test_execution_status.py", + "$(PYTHON) .github/scripts/test_roadmap_status.py", + "$(PYTHON) .github/scripts/test_milestone_d_capability_downgrade_contract.py", + "git diff --check", + ] + for command in required: + self.assertIn(command, block) + + def test_target_stays_contract_scoped(self) -> None: + block = target_block("milestone-d-capability-downgrade-contract") + + for out_of_scope in [ + "verify-alpha", + "rag-chunk-alpha", + "security-report-alpha", + "verify-rendered-crops", + "compare-rendered-crops", + "layout-evaluator-alpha", + "python-surface-test", + "milestone-b-internal-checks", + "milestone-c-internal-checks", + "npm", + "mcp", + ]: + self.assertNotIn(out_of_scope, block) + + def test_contract_is_linked_from_status_docs(self) -> None: + for path in [ROADMAP, EXECUTION_STATUS, SCHEMAS_README]: + text = path.read_text(encoding="utf-8") + self.assertIn("milestone-d-capability-downgrade-contract.md", text, path) + + def test_contract_defines_existing_carrier_not_new_surface(self) -> None: + text = normalized_contract_text() + + self.assertIn("source-only pre-alpha contract work", text) + self.assertIn("The current executable carrier remains `ethos verify`", text) + self.assertIn("does not add a new public command", text) + self.assertIn( + "`capability_downgrade` names the existing contract between " + "grounding-source capability declarations", + text, + ) + + def test_contract_pins_supported_boundaries(self) -> None: + text = normalized_contract_text() + + for required in [ + "`examples/verify/capability_downgrade_v1_contract.json`", + "`capability_limits`", + "`capability_limited`", + "`capability_blocked`", + "`missing_table_capability`", + "native Ethos grounding", + "OpenDataLoader-style grounding", + "`make milestone-d-capability-downgrade-contract PYTHON=/bin/python`", + ]: + self.assertIn(required, text) + self.assertEqual(EXPECTED_EXPLICIT_BLOCKERS, contract_explicit_blockers()) + + def test_contract_inventory_schema_validates_inventory(self) -> None: + schema = load_json(CONTRACT_INVENTORY_SCHEMA) + inventory = load_json(CONTRACT_INVENTORY) + + Draft202012Validator.check_schema(schema) + self.assertEqual([], schema_errors(schema, inventory)) + + def test_contract_inventory_schema_enums_match_report_schema(self) -> None: + inventory_schema = load_json(CONTRACT_INVENTORY_SCHEMA) + report_schema = load_json(VERIFICATION_REPORT_SCHEMA) + + self.assertEqual( + sorted(inventory_schema["$defs"]["capability_limit"]["enum"]), + sorted(report_schema["properties"]["capability_limits"]["items"]["enum"]), + ) + self.assertEqual( + sorted(inventory_schema["$defs"]["check_status"]["enum"]), + sorted( + report_schema["properties"]["checks"]["items"]["properties"]["status"]["enum"] + ), + ) + self.assertEqual( + sorted(inventory_schema["$defs"]["check_reason"]["enum"]), + sorted(report_schema["$defs"]["check_reason"]["enum"]), + ) + report_warnings = set(report_schema["$defs"]["warning_code"]["enum"]) + self.assertEqual(["capability_limited"], inventory_schema["$defs"]["report_warning"]["enum"]) + self.assertIn("capability_limited", report_warnings) + + def test_contract_inventory_case_order_is_deterministic(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + + self.assertEqual( + [ + "native-grounded", + "opendataloader-grounded", + "opendataloader-not-found", + "opendataloader-capability-limited", + "real-opendataloader-grounded", + "real-opendataloader-ungrounded", + ], + [case["name"] for case in inventory["report_cases"]], + ) + self.assertEqual( + { + "no-downgrade-control", + "report-only-downgrade", + "non-grounded-with-downgrade", + "check-blocked-downgrade", + }, + {case["category"] for case in inventory["report_cases"]}, + ) + + def test_contract_inventory_matches_report_goldens(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + + self.assertEqual(inventory["schema_version"], 1) + self.assertEqual(inventory["contract"], "capability_downgrade.v1") + self.assertEqual(inventory["status"], "source-only-pre-alpha") + self.assertEqual(inventory["carrier"], "ethos verify") + self.assertEqual(EXPECTED_EXPLICIT_BLOCKERS, inventory["explicit_blockers"]) + + case_names = [case["name"] for case in inventory["report_cases"]] + self.assertEqual(len(case_names), len(set(case_names))) + + for case in inventory["report_cases"]: + report = load_json(ROOT / case["golden"]) + blocked = blocked_checks(report) + + self.assertEqual( + case["all_evidence_grounded"], + report["all_evidence_grounded"], + case["name"], + ) + self.assertEqual( + case["expected_statuses"], + [check["status"] for check in report["checks"]], + case["name"], + ) + self.assertEqual( + case["expected_reasons"], + [ + check["reason"] + for check in report["checks"] + if check.get("reason") is not None + ], + case["name"], + ) + self.assertEqual( + case["expected_capability_limits"], + report["capability_limits"], + case["name"], + ) + self.assertEqual( + case["expected_report_warnings"], + report["warnings"], + case["name"], + ) + self.assertEqual( + case["expected_blocked_check_ids"], + [check["id"] for check in blocked], + case["name"], + ) + self.assertEqual( + case["expected_blocked_reasons"], + [check["reason"] for check in blocked], + case["name"], + ) + + def test_capability_warning_matches_structured_limits(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + + for case in inventory["report_cases"]: + report = load_json(ROOT / case["golden"]) + has_limits = bool(report["capability_limits"]) + + self.assertEqual( + has_limits, + "capability_limited" in report["warnings"], + case["name"], + ) + for check in blocked_checks(report): + self.assertIn("capability_limited", check["warnings"], case["name"]) + + def test_capability_blocked_checks_do_not_count_as_grounded(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + + for case in inventory["report_cases"]: + report = load_json(ROOT / case["golden"]) + if blocked_checks(report): + self.assertFalse(report["all_evidence_grounded"], case["name"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/scripts/test_milestone_d_internal_contracts.py b/.github/scripts/test_milestone_d_internal_contracts.py index 9c49512..783c972 100644 --- a/.github/scripts/test_milestone_d_internal_contracts.py +++ b/.github/scripts/test_milestone_d_internal_contracts.py @@ -35,6 +35,14 @@ "inventory": "examples/verify/verify_citations_v1_contract.json", "schema": "schemas/ethos-verify-citations-contract.schema.json", }, + { + "contract": "capability_downgrade.v1", + "carrier": "ethos verify", + "target": "milestone-d-capability-downgrade-contract", + "doc": "docs/milestone-d-capability-downgrade-contract.md", + "inventory": "examples/verify/capability_downgrade_v1_contract.json", + "schema": "schemas/ethos-capability-downgrade-contract.schema.json", + }, { "contract": "crop_element.v1", "carrier": "ethos verify --crop-dir", diff --git a/Makefile b/Makefile index dd9c2a6..7b3b3bf 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,7 @@ COMPARE_RENDERED_CROPS_RIGHT ?= $(VERIFY_RENDERED_CROPS_OUT)/run2 LAYOUT_EVALUATOR_OUT ?= $(ROOT)/target/layout-evaluator-alpha .PHONY: verify-alpha verify-alpha-tree rag-chunk-alpha security-report-alpha milestone-d-verify-citations-contract milestone-d-crop-element-contract milestone-d-sandbox-subprocess-contract milestone-d-internal-contracts verify-rendered-crops compare-rendered-crops layout-evaluator-alpha python-surface-test milestone-b-internal-checks milestone-c-internal-checks release-hygiene release-advisory third-party-license-manifest release-notice-draft +.PHONY: milestone-d-capability-downgrade-contract $(ETHOS_BIN): cargo build --locked -p ethos-cli @@ -71,8 +72,18 @@ milestone-d-sandbox-subprocess-contract: $(PYTHON) .github/scripts/test_milestone_d_sandbox_subprocess_contract.py git diff --check +milestone-d-capability-downgrade-contract: + cargo test --locked -p ethos-verify capability + cargo test --locked -p ethos-cli --test verify capability + $(PYTHON) schemas/validate_examples.py + $(PYTHON) .github/scripts/test_execution_status.py + $(PYTHON) .github/scripts/test_roadmap_status.py + $(PYTHON) .github/scripts/test_milestone_d_capability_downgrade_contract.py + git diff --check + milestone-d-internal-contracts: $(MAKE) milestone-d-verify-citations-contract PYTHON=$(PYTHON) + $(MAKE) milestone-d-capability-downgrade-contract PYTHON=$(PYTHON) $(MAKE) milestone-d-crop-element-contract PYTHON=$(PYTHON) $(MAKE) milestone-d-sandbox-subprocess-contract PYTHON=$(PYTHON) $(PYTHON) .github/scripts/test_milestone_d_internal_contracts.py diff --git a/docs/execution-status.md b/docs/execution-status.md index e2041b3..4edfa03 100644 --- a/docs/execution-status.md +++ b/docs/execution-status.md @@ -27,6 +27,7 @@ The committed implementation now includes: - `ethos security report` has a source-only pre-alpha artifact check over the committed document example. The current internal checks cover deterministic report output, report/source identity grounding, security-warning lane and message diagnostics, locator grounding, inventory/report parity, summary drift, warning id uniqueness, deterministic warning numbering, and explicit rejection of unsupported current source-warning references. - `make milestone-c-internal-checks` composes the current internal Milestone C artifact-validation path across RAG chunk and security-report gates; CI/static guard scripts fail closed if that command wiring or the dated closeout record drifts. - Milestone D source-only pre-alpha contract work has started with `docs/milestone-d-verify-citations-contract.md`. It defines `verify_citations` v1 as the citation-input, verification-config, grounding-source, and verification-report contract currently carried by `ethos verify`; schema/example validation checks that the minimal citation example and verification-report example stay coherent. Focused validation is `make milestone-d-verify-citations-contract PYTHON=/bin/python`. +- Milestone D `capability_downgrade` v1 contract prep has started with `docs/milestone-d-capability-downgrade-contract.md`. It defines `capability_downgrade` as the grounding-source capability declaration to verification-report downgrade contract currently carried by `ethos verify`; schema/example validation checks the inventory at `examples/verify/capability_downgrade_v1_contract.json`, and the repository guard checks that it stays coherent with report goldens. Focused validation is `make milestone-d-capability-downgrade-contract PYTHON=/bin/python`. - Milestone D `crop_element` v1 contract prep has started with `docs/milestone-d-crop-element-contract.md`. It defines `crop_element` as the future element-to-crop-descriptor contract currently represented by the existing `ethos verify --crop-dir` carrier; schema/example validation checks the request envelope at `schemas/examples/crop-element-request.example.json` and inventory at `examples/crop/crop_element_v1_contract.json`, and the repository guard checks that request identity, document, and crop-descriptor examples stay coherent. Focused validation is `make milestone-d-crop-element-contract PYTHON=/bin/python`. - Milestone D `sandbox_subprocess` v1 contract prep has started with `docs/milestone-d-sandbox-subprocess-contract.md`. It defines `sandbox_subprocess` as the future worker-boundary contract currently represented by the existing PDF worker process behind `ethos doc parse` and `ethos fingerprint`; schema/example validation checks the request envelopes under `schemas/examples/sandbox-subprocess-*.example.json` and inventory at `examples/sandbox/sandbox_subprocess_v1_contract.json`, and the repository guard checks that they stay coherent with the worker test slice. Focused validation is `make milestone-d-sandbox-subprocess-contract PYTHON=/bin/python`. - `make milestone-d-internal-contracts PYTHON=/bin/python` composes the current Milestone D source-only contract gates and has a static guard for that target's command wiring and contract registry. diff --git a/docs/milestone-d-capability-downgrade-contract.md b/docs/milestone-d-capability-downgrade-contract.md new file mode 100644 index 0000000..9e52482 --- /dev/null +++ b/docs/milestone-d-capability-downgrade-contract.md @@ -0,0 +1,63 @@ +# Milestone D `capability_downgrade` v1 Contract + +Status: source-only pre-alpha contract work for internal Milestone D continuation. + +This note defines the narrow `capability_downgrade` contract-prep slice for Milestone D. It does +not add a new public command, binding, Node surface, MCP surface, hosted surface, adapter +hardening, crop API, or sandbox backend. The current executable carrier remains `ethos verify`; +`capability_downgrade` names the existing contract between grounding-source capability +declarations, structured verification-report capability limits, report warnings, and +capability-blocked checks. + +## Contract Surface + +`capability_downgrade` v1 consumes: + +- capability declarations already exposed through `GroundingSource`; +- the active verification config, including claim kinds and evidence options; +- verification reports that surface missing capabilities through `capability_limits`; +- report-level `capability_limited` warnings when any structured capability limit is present; +- per-check `capability_blocked` status only when the missing capability blocks that specific + check. + +The current source-tree inventory for this contract boundary is +`examples/verify/capability_downgrade_v1_contract.json`. It binds existing native and +OpenDataLoader-style verification report goldens to expected capability limits, warnings, and +blocked-check reasons. + +Focused validation command: + +- `make milestone-d-capability-downgrade-contract PYTHON=/bin/python` + +The target runs the current capability-focused verification tests, schema/example validation, +status/roadmap guards, this contract guard, and diff hygiene. It intentionally stays narrower than +adapter or surface work. + +## Supported v1 Boundaries + +The v1 contract boundary is explicit and fail-closed: + +- native Ethos grounding with declared spans, offsets, tables, fingerprint, and coordinate origin + emits no capability downgrade; +- OpenDataLoader-style grounding without fingerprint, spans, character offsets, or coordinate + origin emits structured capability limits and a report-level `capability_limited` warning; +- table-cell checks become `capability_blocked` with `missing_table_capability` only when the + grounding source does not expose tables; +- evidence mismatches and not-found checks retain their specific diagnostics while still carrying + report-level capability downgrade warnings when the source is limited; +- `all_evidence_grounded` remains true only when every supported check is grounded and no stale, + unsupported, or blocked check is present. + +## Explicit Blockers For This Slice + +This first `capability_downgrade` slice does not add: + +- a new public command or binding surface; +- Python, Node, MCP, or hosted capability surfaces; +- adapter hardening beyond committed fixtures; +- crop API implementation; +- sandbox backend expansion; +- semantic or arithmetic verification. + +Public-facing language remains limited to source-only pre-alpha internal continuation, evidence +grounding, diagnostics, fixture-backed validation, and explicit blockers. diff --git a/docs/roadmap.md b/docs/roadmap.md index f0b7c70..20aceca 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -26,6 +26,11 @@ The next D contract-prep slice defines the source-only existing `ethos verify --crop-dir` crop descriptor carrier; it does not add a first-class crop command, binding surface, sandbox backend, Node beta, or MCP experimental scope. +The source-only +[`capability_downgrade` v1 contract](milestone-d-capability-downgrade-contract.md) +binds existing grounding-source capability declarations to verification-report +capability limits, warnings, and blocked-check diagnostics without adding a new +command or binding surface. The source-only [`sandbox_subprocess` v1 contract](milestone-d-sandbox-subprocess-contract.md) classifies the existing PDF worker-process boundary behind `ethos doc parse` and `ethos fingerprint`; it does not add hardened sandbox rules or a new command or diff --git a/examples/verify/capability_downgrade_v1_contract.json b/examples/verify/capability_downgrade_v1_contract.json new file mode 100644 index 0000000..892160a --- /dev/null +++ b/examples/verify/capability_downgrade_v1_contract.json @@ -0,0 +1,116 @@ +{ + "schema_version": 1, + "contract": "capability_downgrade.v1", + "status": "source-only-pre-alpha", + "carrier": "ethos verify", + "report_cases": [ + { + "name": "native-grounded", + "category": "no-downgrade-control", + "golden": "examples/verify/goldens/native_grounded_report.json", + "all_evidence_grounded": true, + "expected_statuses": ["grounded", "grounded", "grounded"], + "expected_reasons": [], + "expected_capability_limits": [], + "expected_report_warnings": [], + "expected_blocked_check_ids": [], + "expected_blocked_reasons": [] + }, + { + "name": "opendataloader-grounded", + "category": "report-only-downgrade", + "golden": "examples/verify/goldens/opendataloader_grounded_report.json", + "all_evidence_grounded": true, + "expected_statuses": ["grounded", "grounded", "grounded"], + "expected_reasons": [], + "expected_capability_limits": [ + "missing_fingerprint", + "missing_spans", + "missing_char_offsets", + "unknown_coordinate_origin" + ], + "expected_report_warnings": ["capability_limited"], + "expected_blocked_check_ids": [], + "expected_blocked_reasons": [] + }, + { + "name": "opendataloader-not-found", + "category": "non-grounded-with-downgrade", + "golden": "examples/verify/goldens/opendataloader_not_found_report.json", + "all_evidence_grounded": false, + "expected_statuses": ["not_found"], + "expected_reasons": ["element_not_found"], + "expected_capability_limits": [ + "missing_fingerprint", + "missing_spans", + "missing_char_offsets", + "unknown_coordinate_origin" + ], + "expected_report_warnings": ["capability_limited"], + "expected_blocked_check_ids": [], + "expected_blocked_reasons": [] + }, + { + "name": "opendataloader-capability-limited", + "category": "check-blocked-downgrade", + "golden": "examples/verify/goldens/opendataloader_capability_limited_report.json", + "all_evidence_grounded": false, + "expected_statuses": ["capability_blocked"], + "expected_reasons": ["missing_table_capability"], + "expected_capability_limits": [ + "missing_fingerprint", + "missing_spans", + "missing_char_offsets", + "missing_tables", + "unknown_coordinate_origin" + ], + "expected_report_warnings": ["capability_limited"], + "expected_blocked_check_ids": ["v0001"], + "expected_blocked_reasons": ["missing_table_capability"] + }, + { + "name": "real-opendataloader-grounded", + "category": "report-only-downgrade", + "golden": "fixtures/foreign/opendataloader/real/expected.verification_report.json", + "all_evidence_grounded": true, + "expected_statuses": ["grounded", "grounded", "grounded"], + "expected_reasons": [], + "expected_capability_limits": [ + "missing_fingerprint", + "missing_spans", + "missing_char_offsets", + "missing_tables", + "unknown_coordinate_origin" + ], + "expected_report_warnings": ["capability_limited"], + "expected_blocked_check_ids": [], + "expected_blocked_reasons": [] + }, + { + "name": "real-opendataloader-ungrounded", + "category": "non-grounded-with-downgrade", + "golden": "fixtures/foreign/opendataloader/real/expected.ungrounded.verification_report.json", + "all_evidence_grounded": false, + "expected_statuses": ["mismatch"], + "expected_reasons": ["text_mismatch"], + "expected_capability_limits": [ + "missing_fingerprint", + "missing_spans", + "missing_char_offsets", + "missing_tables", + "unknown_coordinate_origin" + ], + "expected_report_warnings": ["capability_limited"], + "expected_blocked_check_ids": [], + "expected_blocked_reasons": [] + } + ], + "explicit_blockers": [ + "a new public command or binding surface", + "Python, Node, MCP, or hosted capability surfaces", + "adapter hardening beyond committed fixtures", + "crop API implementation", + "sandbox backend expansion", + "semantic or arithmetic verification" + ] +} diff --git a/schemas/README.md b/schemas/README.md index f604e51..1076013 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -15,6 +15,7 @@ bumps and downstream sign-off; output-changing heuristics are semver events (PRD | `ethos-crop-descriptor.schema.json` | crop descriptor JSON emitted by `ethos verify --crop-dir` | | `ethos-crop-element-request.schema.json` | source-only request envelope for Milestone D `crop_element` v1 contract work | | `ethos-verify-citations-contract.schema.json` | Milestone D `verify_citations` v1 source-only contract inventory | +| `ethos-capability-downgrade-contract.schema.json` | Milestone D `capability_downgrade` v1 source-only contract inventory | | `ethos-crop-element-contract.schema.json` | Milestone D `crop_element` v1 source-only contract inventory | | `ethos-sandbox-subprocess-request.schema.json` | source-only request envelope for Milestone D `sandbox_subprocess` v1 contract work | | `ethos-sandbox-subprocess-contract.schema.json` | Milestone D `sandbox_subprocess` v1 source-only contract inventory | @@ -41,6 +42,14 @@ Milestone D `verify_citations` v1 contract work is tracked in `examples/verify/verify_citations_v1_contract.json` is schema-validated here; its alignment with the executable case inventory and report goldens is checked by the Milestone D repository guard. +Milestone D `capability_downgrade` v1 contract work is tracked in +`docs/milestone-d-capability-downgrade-contract.md`. In this source-only pre-alpha slice, +`capability_downgrade` names the grounding-source capability declaration to verification-report +downgrade contract currently carried by `ethos verify`; it does not add a new command or binding +surface. The contract inventory at `examples/verify/capability_downgrade_v1_contract.json` is +schema-validated here; its alignment with report goldens is checked by the Milestone D repository +guard. + Milestone D `crop_element` v1 contract work is tracked in `docs/milestone-d-crop-element-contract.md`. In this source-only pre-alpha slice, `crop_element` names the future element-to-crop-descriptor contract currently represented by the diff --git a/schemas/ethos-capability-downgrade-contract.schema.json b/schemas/ethos-capability-downgrade-contract.schema.json new file mode 100644 index 0000000..d70fccf --- /dev/null +++ b/schemas/ethos-capability-downgrade-contract.schema.json @@ -0,0 +1,140 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:ethos:schema:capability-downgrade-contract:1", + "title": "Ethos capability_downgrade v1 contract inventory", + "description": "Source-only pre-alpha Milestone D inventory for the current capability_downgrade v1 contract carried by ethos verify. This validates inventory shape and vocabulary; report-golden alignment stays in the repository guard.", + "type": "object", + "required": [ + "schema_version", + "contract", + "status", + "carrier", + "report_cases", + "explicit_blockers" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "const": 1 }, + "contract": { "const": "capability_downgrade.v1" }, + "status": { "const": "source-only-pre-alpha" }, + "carrier": { "const": "ethos verify" }, + "report_cases": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "name", + "category", + "golden", + "all_evidence_grounded", + "expected_statuses", + "expected_reasons", + "expected_capability_limits", + "expected_report_warnings", + "expected_blocked_check_ids", + "expected_blocked_reasons" + ], + "additionalProperties": false, + "properties": { + "name": { "$ref": "#/$defs/case_name" }, + "category": { + "enum": [ + "no-downgrade-control", + "report-only-downgrade", + "non-grounded-with-downgrade", + "check-blocked-downgrade" + ] + }, + "golden": { "$ref": "#/$defs/repo_path" }, + "all_evidence_grounded": { "type": "boolean" }, + "expected_statuses": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/check_status" } + }, + "expected_reasons": { + "type": "array", + "items": { "$ref": "#/$defs/check_reason" } + }, + "expected_capability_limits": { + "type": "array", + "items": { "$ref": "#/$defs/capability_limit" }, + "uniqueItems": true + }, + "expected_report_warnings": { + "type": "array", + "items": { "$ref": "#/$defs/report_warning" }, + "uniqueItems": true + }, + "expected_blocked_check_ids": { + "type": "array", + "items": { "$ref": "#/$defs/check_id" }, + "uniqueItems": true + }, + "expected_blocked_reasons": { + "type": "array", + "items": { "$ref": "#/$defs/check_reason" } + } + } + } + }, + "explicit_blockers": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + } + }, + "$defs": { + "case_name": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, + "repo_path": { "type": "string", "minLength": 1, "pattern": "^[A-Za-z0-9_./-]+$" }, + "check_id": { "type": "string", "pattern": "^v[0-9]{4}$" }, + "check_status": { + "enum": [ + "grounded", + "not_found", + "mismatch", + "stale", + "unsupported_claim_kind", + "capability_blocked", + "error" + ] + }, + "capability_limit": { + "enum": [ + "missing_fingerprint", + "missing_spans", + "missing_char_offsets", + "missing_tables", + "unknown_coordinate_origin", + "missing_crop_support" + ] + }, + "report_warning": { + "enum": ["capability_limited"] + }, + "check_reason": { + "enum": [ + "missing_locator", + "missing_required_text", + "unsupported_claim_kind", + "stale_fingerprint", + "missing_source_fingerprint", + "missing_citation_fingerprint", + "missing_span_capability", + "missing_table_capability", + "unknown_coordinate_origin", + "element_not_found", + "span_not_found", + "page_not_found", + "bbox_not_found", + "missing_page_for_bbox", + "missing_table_cell_locator", + "table_not_found", + "table_cell_not_found", + "text_mismatch" + ] + } + } +} diff --git a/schemas/validate_examples.py b/schemas/validate_examples.py index 0c171ad..8429e4b 100644 --- a/schemas/validate_examples.py +++ b/schemas/validate_examples.py @@ -86,6 +86,9 @@ ("ethos-verify-citations-contract.schema.json", [ ROOT / "examples" / "verify" / "verify_citations_v1_contract.json", ]), + ("ethos-capability-downgrade-contract.schema.json", [ + ROOT / "examples" / "verify" / "capability_downgrade_v1_contract.json", + ]), ("ethos-crop-element-contract.schema.json", [ ROOT / "examples" / "crop" / "crop_element_v1_contract.json", ]), From 8d4f6a291ba521e0e0f5137212c62c03965796db Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 18:36:19 +0530 Subject: [PATCH 25/39] Add OpenDataLoader adapter shape contract guard Signed-off-by: docushell-admin --- .github/scripts/test_execution_status.py | 2 + .../test_milestone_d_internal_contracts.py | 8 + ...d_opendataloader_adapter_shape_contract.py | 294 ++++++++++++++++++ Makefile | 11 + docs/execution-status.md | 1 + ...d-opendataloader-adapter-shape-contract.md | 62 ++++ docs/roadmap.md | 4 + ...ndataloader_adapter_shape_v1_contract.json | 203 ++++++++++++ schemas/README.md | 10 + ...aloader-adapter-shape-contract.schema.json | 155 +++++++++ schemas/validate_examples.py | 3 + 11 files changed, 753 insertions(+) create mode 100644 .github/scripts/test_milestone_d_opendataloader_adapter_shape_contract.py create mode 100644 docs/milestone-d-opendataloader-adapter-shape-contract.md create mode 100644 examples/verify/opendataloader_adapter_shape_v1_contract.json create mode 100644 schemas/ethos-opendataloader-adapter-shape-contract.schema.json diff --git a/.github/scripts/test_execution_status.py b/.github/scripts/test_execution_status.py index bf20689..894d498 100644 --- a/.github/scripts/test_execution_status.py +++ b/.github/scripts/test_execution_status.py @@ -40,6 +40,7 @@ def test_status_is_scoped_to_internal_continuation(self) -> None: ) self.assertIn("docs/milestone-d-verify-citations-contract.md", text) self.assertIn("docs/milestone-d-capability-downgrade-contract.md", text) + self.assertIn("docs/milestone-d-opendataloader-adapter-shape-contract.md", text) self.assertIn("docs/milestone-d-crop-element-contract.md", text) self.assertIn("docs/milestone-d-sandbox-subprocess-contract.md", text) self.assertNotIn("Status: Pre-alpha / Milestone B entry.", text) @@ -51,6 +52,7 @@ def test_internal_check_command_is_documented(self) -> None: self.assertIn("make milestone-c-internal-checks", text) self.assertIn("make milestone-d-verify-citations-contract", text) self.assertIn("make milestone-d-capability-downgrade-contract", text) + self.assertIn("make milestone-d-opendataloader-adapter-shape-contract", text) self.assertIn("make milestone-d-crop-element-contract", text) self.assertIn("make milestone-d-sandbox-subprocess-contract", text) self.assertIn("make milestone-d-internal-contracts", text) diff --git a/.github/scripts/test_milestone_d_internal_contracts.py b/.github/scripts/test_milestone_d_internal_contracts.py index 783c972..937c435 100644 --- a/.github/scripts/test_milestone_d_internal_contracts.py +++ b/.github/scripts/test_milestone_d_internal_contracts.py @@ -35,6 +35,14 @@ "inventory": "examples/verify/verify_citations_v1_contract.json", "schema": "schemas/ethos-verify-citations-contract.schema.json", }, + { + "contract": "opendataloader_adapter_shape.v1", + "carrier": "opendataloader-json adapter", + "target": "milestone-d-opendataloader-adapter-shape-contract", + "doc": "docs/milestone-d-opendataloader-adapter-shape-contract.md", + "inventory": "examples/verify/opendataloader_adapter_shape_v1_contract.json", + "schema": "schemas/ethos-opendataloader-adapter-shape-contract.schema.json", + }, { "contract": "capability_downgrade.v1", "carrier": "ethos verify", diff --git a/.github/scripts/test_milestone_d_opendataloader_adapter_shape_contract.py b/.github/scripts/test_milestone_d_opendataloader_adapter_shape_contract.py new file mode 100644 index 0000000..16dea65 --- /dev/null +++ b/.github/scripts/test_milestone_d_opendataloader_adapter_shape_contract.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python3 +# +# Copyright 2026 The Ethos maintainers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import annotations + +import json +import re +import unittest +from pathlib import Path + +from jsonschema import Draft202012Validator +from makefile_guard import makefile_text, target_block + + +ROOT = Path(__file__).resolve().parents[2] +CONTRACT = ROOT / "docs/milestone-d-opendataloader-adapter-shape-contract.md" +CONTRACT_INVENTORY = ROOT / "examples/verify/opendataloader_adapter_shape_v1_contract.json" +CONTRACT_INVENTORY_SCHEMA = ROOT / "schemas/ethos-opendataloader-adapter-shape-contract.schema.json" +ADAPTER_SOURCE = ROOT / "adapters/grounding/opendataloader-json/src/lib.rs" +ADAPTER_README = ROOT / "adapters/grounding/opendataloader-json/README.md" +CLI_VERIFY_TEST = ROOT / "crates/ethos-cli/tests/verify.rs" +VERIFY_CASES = ROOT / "examples/verify/cases.json" +VERIFICATION_REPORT_SCHEMA = ROOT / "schemas/ethos-verification-report.schema.json" +ROADMAP = ROOT / "docs/roadmap.md" +EXECUTION_STATUS = ROOT / "docs/execution-status.md" +SCHEMAS_README = ROOT / "schemas/README.md" +EXPECTED_EXPLICIT_BLOCKERS = [ + "a new command or binding surface", + "a new foreign parser adapter", + "adapter behavior beyond committed fixtures", + "coordinate-origin inference", + "fingerprint, span, char-offset, or crop support", + "claim-kind expansion or semantic verification", +] + + +def load_json(path: Path): + return json.loads(path.read_text(encoding="utf-8")) + + +def contract_text() -> str: + return CONTRACT.read_text(encoding="utf-8") + + +def normalized_contract_text() -> str: + return re.sub(r"\s+", " ", contract_text()) + + +def contract_explicit_blockers() -> list[str]: + match = re.search( + r"## Explicit Blockers For This Slice\n\n" + r"This first `opendataloader_adapter_shape` slice does not add:\n\n" + r"(?P(?:- .+\n)+)", + contract_text(), + ) + if match is None: + raise AssertionError("missing opendataloader_adapter_shape explicit blocker list") + return [ + line.removeprefix("- ").rstrip(";.") + for line in match.group("bullets").strip().splitlines() + ] + + +def schema_errors(schema: dict, instance: dict) -> list: + return sorted( + Draft202012Validator(schema).iter_errors(instance), + key=lambda error: list(error.absolute_path), + ) + + +def rust_test_names(path: Path) -> set[str]: + text = path.read_text(encoding="utf-8") + return set(re.findall(r"(?m)^\s*fn ([a-z][a-z0-9_]*)\(", text)) + + +def verify_cases_by_name(section: str) -> dict[str, dict]: + cases = load_json(VERIFY_CASES)[section] + return {case["name"]: case for case in cases} + + +class MilestoneDOpendataloaderAdapterShapeContractTests(unittest.TestCase): + def test_target_is_declared_phony(self) -> None: + text = makefile_text() + + self.assertIn(".PHONY:", text) + self.assertIn("milestone-d-opendataloader-adapter-shape-contract", text) + + def test_target_composes_contract_gates(self) -> None: + block = target_block("milestone-d-opendataloader-adapter-shape-contract") + + required = [ + "cargo test --locked -p ethos-grounding-opendataloader-json", + "cargo test --locked -p ethos-cli --test verify opendataloader", + "$(PYTHON) schemas/validate_examples.py", + "$(PYTHON) .github/scripts/test_execution_status.py", + "$(PYTHON) .github/scripts/test_roadmap_status.py", + "$(PYTHON) .github/scripts/test_milestone_d_opendataloader_adapter_shape_contract.py", + "git diff --check", + ] + self.assertEqual(required, [line.strip() for line in block.splitlines() if line.strip()]) + + def test_target_stays_contract_scoped(self) -> None: + block = target_block("milestone-d-opendataloader-adapter-shape-contract") + + for out_of_scope in [ + "verify-alpha", + "rag-chunk-alpha", + "security-report-alpha", + "verify-rendered-crops", + "compare-rendered-crops", + "layout-evaluator-alpha", + "python-surface-test", + "milestone-b-internal-checks", + "milestone-c-internal-checks", + "npm", + "mcp", + ]: + self.assertNotIn(out_of_scope, block) + + def test_contract_is_linked_from_status_docs(self) -> None: + for path in [ROADMAP, EXECUTION_STATUS, SCHEMAS_README]: + text = path.read_text(encoding="utf-8") + self.assertIn("milestone-d-opendataloader-adapter-shape-contract.md", text, path) + + def test_contract_defines_existing_carrier_not_new_surface(self) -> None: + text = normalized_contract_text() + + self.assertIn("source-only pre-alpha contract work", text) + self.assertIn("does not add a new public command", text) + self.assertIn( + "The current executable carrier remains the `ethos-grounding-opendataloader-json` " + "adapter crate and `ethos verify --grounding opendataloader-json`", + text, + ) + self.assertIn( + "`opendataloader_adapter_shape` names the current contract between " + "OpenDataLoader-style JSON shapes and the parser-neutral `GroundingSource` boundary", + text, + ) + + def test_contract_pins_supported_boundaries(self) -> None: + text = normalized_contract_text() + + for required in [ + "`examples/verify/opendataloader_adapter_shape_v1_contract.json`", + "`tool`, `pages`, `elements`, and optional `tables`", + "`kids`, `children`, `list items`, `list_items`, `rows`, and `cells`", + "centipoint bbox quantization", + "deterministic element ids", + "malformed bbox", + "unknown page references", + "`make milestone-d-opendataloader-adapter-shape-contract PYTHON=/bin/python`", + ]: + self.assertIn(required, text) + self.assertEqual(EXPECTED_EXPLICIT_BLOCKERS, contract_explicit_blockers()) + + def test_adapter_readme_still_documents_current_shape_boundary(self) -> None: + text = ADAPTER_README.read_text(encoding="utf-8") + + for required in [ + "documented synthetic subset", + "OpenDataLoader 2.4.7 output", + "Nested `kids`/`children`, `list items`/`list_items`, and", + "`rows[].cells` containers are traversed in document order", + "`coordinate_origin: unknown`", + ]: + self.assertIn(required, text) + + def test_contract_inventory_schema_validates_inventory(self) -> None: + schema = load_json(CONTRACT_INVENTORY_SCHEMA) + inventory = load_json(CONTRACT_INVENTORY) + + Draft202012Validator.check_schema(schema) + self.assertEqual([], schema_errors(schema, inventory)) + + def test_contract_inventory_schema_matches_report_grounding_shape(self) -> None: + inventory_schema = load_json(CONTRACT_INVENTORY_SCHEMA) + report_schema = load_json(VERIFICATION_REPORT_SCHEMA) + report_grounding = report_schema["properties"]["grounding"]["properties"] + + self.assertEqual( + sorted(inventory_schema["$defs"]["capabilities"]["properties"].keys()), + sorted(report_grounding["capabilities"]["properties"].keys()), + ) + self.assertEqual( + sorted(inventory_schema["$defs"]["capabilities"]["required"]), + sorted(report_grounding["capabilities"]["required"]), + ) + self.assertEqual( + ["top-left", "bottom-left", "unknown"], + report_grounding["capabilities"]["properties"]["coordinate_origin"]["enum"], + ) + self.assertEqual( + sorted(inventory_schema["$defs"]["parser"]["properties"].keys()), + sorted(report_grounding["parser"]["properties"].keys()), + ) + + def test_contract_inventory_case_order_is_deterministic(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + + self.assertEqual( + [ + "documented-subset", + "real-basic-tree", + "real-text-child-aliases", + "real-table-cells", + "real-structural-containers", + ], + [case["name"] for case in inventory["accepted_shapes"]], + ) + self.assertEqual( + [ + "documented-malformed-bbox", + "documented-unknown-page", + "real-malformed-child-containers", + "real-malformed-table-cells", + "unrecognized-root", + ], + [case["name"] for case in inventory["rejected_shapes"]], + ) + + def test_contract_inventory_binds_existing_tests_and_fixtures(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + adapter_tests = rust_test_names(ADAPTER_SOURCE) + cli_tests = rust_test_names(CLI_VERIFY_TEST) + + self.assertEqual(inventory["schema_version"], 1) + self.assertEqual(inventory["contract"], "opendataloader_adapter_shape.v1") + self.assertEqual(inventory["status"], "source-only-pre-alpha") + self.assertEqual(inventory["carrier"], "opendataloader-json adapter") + self.assertEqual(EXPECTED_EXPLICIT_BLOCKERS, inventory["explicit_blockers"]) + + for case in inventory["accepted_shapes"] + inventory["rejected_shapes"]: + if "source_fixture" in case: + self.assertTrue((ROOT / case["source_fixture"]).is_file(), case["name"]) + for test_name in case["adapter_tests"]: + self.assertIn(test_name, adapter_tests, case["name"]) + for test_name in case["cli_verify_tests"]: + self.assertIn(test_name, cli_tests, case["name"]) + + def test_accepted_report_cases_match_expected_grounding(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + report_cases = verify_cases_by_name("report_cases") + + for shape in inventory["accepted_shapes"]: + for case_name in shape["cli_verify_cases"]: + self.assertIn(case_name, report_cases, shape["name"]) + report_case = report_cases[case_name] + self.assertEqual("opendataloader-json", report_case.get("grounding")) + if "source_fixture" in shape: + self.assertEqual(shape["source_fixture"], report_case["input"]) + golden = load_json(ROOT / report_case["golden"]) + self.assertEqual(shape["expected_parser"], golden["grounding"]["parser"]) + self.assertEqual( + shape["expected_capabilities"], + golden["grounding"]["capabilities"], + ) + self.assertIn("capability_limited", golden["warnings"]) + self.assertFalse(golden["fingerprint_stale"]) + + def test_rejected_usage_error_cases_match_inventory(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + usage_cases = verify_cases_by_name("usage_error_cases") + adapter_source = ADAPTER_SOURCE.read_text(encoding="utf-8") + + for shape in inventory["rejected_shapes"]: + expected = shape["expected_error_contains"] + if "cli_usage_error_case" in shape: + case_name = shape["cli_usage_error_case"] + self.assertIn(case_name, usage_cases, shape["name"]) + usage_case = usage_cases[case_name] + if "source_fixture" in shape: + self.assertEqual(shape["source_fixture"], usage_case["input"]) + self.assertEqual(expected, usage_case["stderr_contains"]) + continue + self.assertIn(expected, adapter_source, shape["name"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/Makefile b/Makefile index 7b3b3bf..ca0a972 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ LAYOUT_EVALUATOR_OUT ?= $(ROOT)/target/layout-evaluator-alpha .PHONY: verify-alpha verify-alpha-tree rag-chunk-alpha security-report-alpha milestone-d-verify-citations-contract milestone-d-crop-element-contract milestone-d-sandbox-subprocess-contract milestone-d-internal-contracts verify-rendered-crops compare-rendered-crops layout-evaluator-alpha python-surface-test milestone-b-internal-checks milestone-c-internal-checks release-hygiene release-advisory third-party-license-manifest release-notice-draft .PHONY: milestone-d-capability-downgrade-contract +.PHONY: milestone-d-opendataloader-adapter-shape-contract $(ETHOS_BIN): cargo build --locked -p ethos-cli @@ -81,8 +82,18 @@ milestone-d-capability-downgrade-contract: $(PYTHON) .github/scripts/test_milestone_d_capability_downgrade_contract.py git diff --check +milestone-d-opendataloader-adapter-shape-contract: + cargo test --locked -p ethos-grounding-opendataloader-json + cargo test --locked -p ethos-cli --test verify opendataloader + $(PYTHON) schemas/validate_examples.py + $(PYTHON) .github/scripts/test_execution_status.py + $(PYTHON) .github/scripts/test_roadmap_status.py + $(PYTHON) .github/scripts/test_milestone_d_opendataloader_adapter_shape_contract.py + git diff --check + milestone-d-internal-contracts: $(MAKE) milestone-d-verify-citations-contract PYTHON=$(PYTHON) + $(MAKE) milestone-d-opendataloader-adapter-shape-contract PYTHON=$(PYTHON) $(MAKE) milestone-d-capability-downgrade-contract PYTHON=$(PYTHON) $(MAKE) milestone-d-crop-element-contract PYTHON=$(PYTHON) $(MAKE) milestone-d-sandbox-subprocess-contract PYTHON=$(PYTHON) diff --git a/docs/execution-status.md b/docs/execution-status.md index 4edfa03..afb9807 100644 --- a/docs/execution-status.md +++ b/docs/execution-status.md @@ -28,6 +28,7 @@ The committed implementation now includes: - `make milestone-c-internal-checks` composes the current internal Milestone C artifact-validation path across RAG chunk and security-report gates; CI/static guard scripts fail closed if that command wiring or the dated closeout record drifts. - Milestone D source-only pre-alpha contract work has started with `docs/milestone-d-verify-citations-contract.md`. It defines `verify_citations` v1 as the citation-input, verification-config, grounding-source, and verification-report contract currently carried by `ethos verify`; schema/example validation checks that the minimal citation example and verification-report example stay coherent. Focused validation is `make milestone-d-verify-citations-contract PYTHON=/bin/python`. - Milestone D `capability_downgrade` v1 contract prep has started with `docs/milestone-d-capability-downgrade-contract.md`. It defines `capability_downgrade` as the grounding-source capability declaration to verification-report downgrade contract currently carried by `ethos verify`; schema/example validation checks the inventory at `examples/verify/capability_downgrade_v1_contract.json`, and the repository guard checks that it stays coherent with report goldens. Focused validation is `make milestone-d-capability-downgrade-contract PYTHON=/bin/python`. +- Milestone D `opendataloader_adapter_shape` v1 contract prep has started with `docs/milestone-d-opendataloader-adapter-shape-contract.md`. It defines `opendataloader_adapter_shape` as the OpenDataLoader-style input-shape to `GroundingSource` contract currently carried by `ethos-grounding-opendataloader-json` and `ethos verify --grounding opendataloader-json`; schema/example validation checks the inventory at `examples/verify/opendataloader_adapter_shape_v1_contract.json`, and the repository guard checks that it stays coherent with adapter tests, CLI grounding tests, report goldens, and usage diagnostics. Focused validation is `make milestone-d-opendataloader-adapter-shape-contract PYTHON=/bin/python`. - Milestone D `crop_element` v1 contract prep has started with `docs/milestone-d-crop-element-contract.md`. It defines `crop_element` as the future element-to-crop-descriptor contract currently represented by the existing `ethos verify --crop-dir` carrier; schema/example validation checks the request envelope at `schemas/examples/crop-element-request.example.json` and inventory at `examples/crop/crop_element_v1_contract.json`, and the repository guard checks that request identity, document, and crop-descriptor examples stay coherent. Focused validation is `make milestone-d-crop-element-contract PYTHON=/bin/python`. - Milestone D `sandbox_subprocess` v1 contract prep has started with `docs/milestone-d-sandbox-subprocess-contract.md`. It defines `sandbox_subprocess` as the future worker-boundary contract currently represented by the existing PDF worker process behind `ethos doc parse` and `ethos fingerprint`; schema/example validation checks the request envelopes under `schemas/examples/sandbox-subprocess-*.example.json` and inventory at `examples/sandbox/sandbox_subprocess_v1_contract.json`, and the repository guard checks that they stay coherent with the worker test slice. Focused validation is `make milestone-d-sandbox-subprocess-contract PYTHON=/bin/python`. - `make milestone-d-internal-contracts PYTHON=/bin/python` composes the current Milestone D source-only contract gates and has a static guard for that target's command wiring and contract registry. diff --git a/docs/milestone-d-opendataloader-adapter-shape-contract.md b/docs/milestone-d-opendataloader-adapter-shape-contract.md new file mode 100644 index 0000000..d66ee4b --- /dev/null +++ b/docs/milestone-d-opendataloader-adapter-shape-contract.md @@ -0,0 +1,62 @@ +# Milestone D `opendataloader_adapter_shape` v1 Contract + +Status: source-only pre-alpha contract work for internal Milestone D continuation. + +This note defines the narrow `opendataloader_adapter_shape` contract-prep slice for +Milestone D. It does not add a new public command, binding surface, adapter behavior, or +verification policy. + +The current executable carrier remains the `ethos-grounding-opendataloader-json` adapter +crate and `ethos verify --grounding opendataloader-json`. +`opendataloader_adapter_shape` names the current contract between OpenDataLoader-style JSON +shapes and the parser-neutral `GroundingSource` boundary used by `ethos verify`. + +## Contract Inputs + +`opendataloader_adapter_shape` v1 consumes: + +- the documented synthetic OpenDataLoader-style subset with `tool`, `pages`, `elements`, and + optional `tables`; +- the pinned real OpenDataLoader 2.4.x tree shape with `kids`, `children`, `list items`, + `list_items`, `rows`, and `cells`; +- deterministic malformed-input diagnostics for adapter shapes that cannot be mapped without + silent approximation. + +The executable inventory is `examples/verify/opendataloader_adapter_shape_v1_contract.json`. +It binds accepted and rejected adapter shapes to existing adapter crate tests, CLI grounding +tests, report goldens, usage diagnostics, and explicit blockers. + +## Validation Target + +- `make milestone-d-opendataloader-adapter-shape-contract PYTHON=/bin/python` + +The target runs the current adapter crate tests, OpenDataLoader-focused CLI verifier tests, +schema/example validation, status guards, roadmap guards, the contract guard, and whitespace +diff checks. + +## Boundaries Locked By This Slice + +- documented subset inputs retain parser identity, page geometry, elements, optional + tables/cells, centipoint bbox quantization, and explicit capability downgrades; +- real OpenDataLoader-style tree inputs retain parser name, unknown parser version, adapter + identity, derived page extents, deterministic element ids, text/child alias handling, and + explicit capability downgrades; +- real OpenDataLoader-style table nodes map only when row/cell page, bbox, and text fields are + explicit enough to produce deterministic cells; +- malformed bbox, unknown page references, malformed child containers, malformed table cells, + and unrecognized roots fail closed with deterministic diagnostics. + +## Explicit Blockers For This Slice + +This first `opendataloader_adapter_shape` slice does not add: + +- a new command or binding surface; +- a new foreign parser adapter; +- adapter behavior beyond committed fixtures; +- coordinate-origin inference; +- fingerprint, span, char-offset, or crop support; +- claim-kind expansion or semantic verification. + +Until those blockers are explicitly handled, public language remains limited to source-only +pre-alpha internal continuation, evidence grounding, diagnostics, fixture-backed validation, and +explicit blockers. diff --git a/docs/roadmap.md b/docs/roadmap.md index 20aceca..6ff98ad 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -31,6 +31,10 @@ The source-only binds existing grounding-source capability declarations to verification-report capability limits, warnings, and blocked-check diagnostics without adding a new command or binding surface. +The source-only +[`opendataloader_adapter_shape` v1 contract](milestone-d-opendataloader-adapter-shape-contract.md) +binds the existing OpenDataLoader-style adapter shape boundary to `GroundingSource` +identity, capabilities, accepted fixture shapes, and deterministic diagnostics. The source-only [`sandbox_subprocess` v1 contract](milestone-d-sandbox-subprocess-contract.md) classifies the existing PDF worker-process boundary behind `ethos doc parse` and `ethos fingerprint`; it does not add hardened sandbox rules or a new command or diff --git a/examples/verify/opendataloader_adapter_shape_v1_contract.json b/examples/verify/opendataloader_adapter_shape_v1_contract.json new file mode 100644 index 0000000..573becd --- /dev/null +++ b/examples/verify/opendataloader_adapter_shape_v1_contract.json @@ -0,0 +1,203 @@ +{ + "schema_version": 1, + "contract": "opendataloader_adapter_shape.v1", + "status": "source-only-pre-alpha", + "carrier": "opendataloader-json adapter", + "accepted_shapes": [ + { + "name": "documented-subset", + "category": "documented-subset", + "input_shape": "tool/pages/elements/tables subset", + "source_fixture": "examples/verify/opendataloader.json", + "adapter_tests": [ + "maps_the_documented_subset" + ], + "cli_verify_tests": [ + "opendataloader_verify_adapter_produces_capability_aware_report" + ], + "cli_verify_cases": [ + "opendataloader-grounded" + ], + "expected_parser": { + "name": "opendataloader-pdf", + "version": "0.0.0-synthetic", + "adapter": "opendataloader-json", + "adapter_version": "0.1.0" + }, + "expected_capabilities": { + "spans": false, + "char_offsets": false, + "tables": true, + "fingerprint": false, + "coordinate_origin": "unknown", + "crop_support": false + } + }, + { + "name": "real-basic-tree", + "category": "real-odl-tree", + "input_shape": "kids tree with page numbers, bounding boxes, and content", + "source_fixture": "fixtures/foreign/opendataloader/real/opendataloader-output.json", + "adapter_tests": [ + "maps_real_opendataloader_json_shape" + ], + "cli_verify_tests": [ + "real_opendataloader_fixture_verifies_against_golden", + "real_opendataloader_ungrounded_fixture_verifies_against_golden" + ], + "cli_verify_cases": [ + "real-opendataloader-grounded", + "real-opendataloader-ungrounded" + ], + "expected_parser": { + "name": "opendataloader-pdf", + "version": "unknown", + "adapter": "opendataloader-json", + "adapter_version": "0.1.0" + }, + "expected_capabilities": { + "spans": false, + "char_offsets": false, + "tables": false, + "fingerprint": false, + "coordinate_origin": "unknown", + "crop_support": false + } + }, + { + "name": "real-text-child-aliases", + "category": "real-odl-aliases", + "input_shape": "text/content aliases plus kids/children/list_items aliases", + "adapter_tests": [ + "maps_real_text_and_child_aliases_in_preorder" + ], + "cli_verify_tests": [ + "real_opendataloader_text_and_child_alias_claim_grounds" + ], + "cli_verify_cases": [], + "expected_parser": { + "name": "opendataloader-pdf", + "version": "unknown", + "adapter": "opendataloader-json", + "adapter_version": "0.1.0" + }, + "expected_capabilities": { + "spans": false, + "char_offsets": false, + "tables": true, + "fingerprint": false, + "coordinate_origin": "unknown", + "crop_support": false + } + }, + { + "name": "real-table-cells", + "category": "real-odl-table", + "input_shape": "rows/cells table nodes with explicit page, bbox, and text", + "adapter_tests": [ + "maps_real_nested_child_structures_in_preorder" + ], + "cli_verify_tests": [ + "real_opendataloader_style_table_cell_claim_grounds" + ], + "cli_verify_cases": [], + "expected_parser": { + "name": "opendataloader-pdf", + "version": "unknown", + "adapter": "opendataloader-json", + "adapter_version": "0.1.0" + }, + "expected_capabilities": { + "spans": false, + "char_offsets": false, + "tables": true, + "fingerprint": false, + "coordinate_origin": "unknown", + "crop_support": false + } + }, + { + "name": "real-structural-containers", + "category": "real-odl-structural", + "input_shape": "pure child containers without table capability", + "adapter_tests": [ + "maps_real_structural_containers_without_table_capability" + ], + "cli_verify_tests": [], + "cli_verify_cases": [], + "expected_parser": { + "name": "opendataloader-pdf", + "version": "unknown", + "adapter": "opendataloader-json", + "adapter_version": "0.1.0" + }, + "expected_capabilities": { + "spans": false, + "char_offsets": false, + "tables": false, + "fingerprint": false, + "coordinate_origin": "unknown", + "crop_support": false + } + } + ], + "rejected_shapes": [ + { + "name": "documented-malformed-bbox", + "category": "documented-subset-diagnostics", + "source_fixture": "examples/verify/opendataloader_malformed_bbox.json", + "adapter_tests": [], + "cli_verify_tests": [], + "cli_usage_error_case": "opendataloader-malformed-bbox-input", + "expected_error_contains": "opendataloader-json adapter: bbox is malformed (x0>x1 or y0>y1)" + }, + { + "name": "documented-unknown-page", + "category": "documented-subset-diagnostics", + "source_fixture": "examples/verify/opendataloader_unknown_page.json", + "adapter_tests": [ + "rejects_invalid_page_numbers_and_references" + ], + "cli_verify_tests": [ + "opendataloader_adapter_errors_are_usage_errors" + ], + "cli_usage_error_case": "opendataloader-unknown-page-input", + "expected_error_contains": "opendataloader-json adapter: element.page references unknown page" + }, + { + "name": "real-malformed-child-containers", + "category": "real-odl-diagnostics", + "adapter_tests": [ + "rejects_malformed_real_child_containers" + ], + "cli_verify_tests": [], + "expected_error_contains": "kids must be an array" + }, + { + "name": "real-malformed-table-cells", + "category": "real-odl-diagnostics", + "adapter_tests": [ + "rejects_malformed_real_table_cells" + ], + "cli_verify_tests": [], + "expected_error_contains": "missing cell content" + }, + { + "name": "unrecognized-root", + "category": "recognition-diagnostics", + "adapter_tests": [ + "rejects_unrecognizable_input" + ], + "cli_verify_tests": [], + "expected_error_contains": "missing 'tool' object" + } + ], + "explicit_blockers": [ + "a new command or binding surface", + "a new foreign parser adapter", + "adapter behavior beyond committed fixtures", + "coordinate-origin inference", + "fingerprint, span, char-offset, or crop support", + "claim-kind expansion or semantic verification" + ] +} diff --git a/schemas/README.md b/schemas/README.md index 1076013..ba3f626 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -16,6 +16,7 @@ bumps and downstream sign-off; output-changing heuristics are semver events (PRD | `ethos-crop-element-request.schema.json` | source-only request envelope for Milestone D `crop_element` v1 contract work | | `ethos-verify-citations-contract.schema.json` | Milestone D `verify_citations` v1 source-only contract inventory | | `ethos-capability-downgrade-contract.schema.json` | Milestone D `capability_downgrade` v1 source-only contract inventory | +| `ethos-opendataloader-adapter-shape-contract.schema.json` | Milestone D `opendataloader_adapter_shape` v1 source-only contract inventory | | `ethos-crop-element-contract.schema.json` | Milestone D `crop_element` v1 source-only contract inventory | | `ethos-sandbox-subprocess-request.schema.json` | source-only request envelope for Milestone D `sandbox_subprocess` v1 contract work | | `ethos-sandbox-subprocess-contract.schema.json` | Milestone D `sandbox_subprocess` v1 source-only contract inventory | @@ -50,6 +51,15 @@ surface. The contract inventory at `examples/verify/capability_downgrade_v1_cont schema-validated here; its alignment with report goldens is checked by the Milestone D repository guard. +Milestone D `opendataloader_adapter_shape` v1 contract work is tracked in +`docs/milestone-d-opendataloader-adapter-shape-contract.md`. In this source-only pre-alpha +slice, `opendataloader_adapter_shape` names the adapter input-shape to `GroundingSource` +contract currently carried by `ethos-grounding-opendataloader-json` and +`ethos verify --grounding opendataloader-json`; it does not add a new command or binding surface. +The contract inventory at `examples/verify/opendataloader_adapter_shape_v1_contract.json` is +schema-validated here; its alignment with adapter tests, CLI grounding tests, report goldens, and +usage diagnostics is checked by the Milestone D repository guard. + Milestone D `crop_element` v1 contract work is tracked in `docs/milestone-d-crop-element-contract.md`. In this source-only pre-alpha slice, `crop_element` names the future element-to-crop-descriptor contract currently represented by the diff --git a/schemas/ethos-opendataloader-adapter-shape-contract.schema.json b/schemas/ethos-opendataloader-adapter-shape-contract.schema.json new file mode 100644 index 0000000..909673f --- /dev/null +++ b/schemas/ethos-opendataloader-adapter-shape-contract.schema.json @@ -0,0 +1,155 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:ethos:schema:opendataloader-adapter-shape-contract:1", + "title": "Ethos opendataloader_adapter_shape v1 contract inventory", + "description": "Source-only pre-alpha Milestone D inventory for the current opendataloader_adapter_shape v1 contract carried by the opendataloader-json adapter. This validates inventory shape and vocabulary; fixture/test/golden alignment stays in the repository guard.", + "type": "object", + "required": [ + "schema_version", + "contract", + "status", + "carrier", + "accepted_shapes", + "rejected_shapes", + "explicit_blockers" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "const": 1 }, + "contract": { "const": "opendataloader_adapter_shape.v1" }, + "status": { "const": "source-only-pre-alpha" }, + "carrier": { "const": "opendataloader-json adapter" }, + "accepted_shapes": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/accepted_shape" } + }, + "rejected_shapes": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/rejected_shape" } + }, + "explicit_blockers": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + } + }, + "$defs": { + "accepted_shape": { + "type": "object", + "required": [ + "name", + "category", + "input_shape", + "adapter_tests", + "cli_verify_tests", + "cli_verify_cases", + "expected_parser", + "expected_capabilities" + ], + "additionalProperties": false, + "properties": { + "name": { "$ref": "#/$defs/case_name" }, + "category": { + "enum": [ + "documented-subset", + "real-odl-tree", + "real-odl-aliases", + "real-odl-table", + "real-odl-structural" + ] + }, + "input_shape": { "type": "string", "minLength": 1 }, + "source_fixture": { "$ref": "#/$defs/repo_path" }, + "adapter_tests": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/rust_test_name" }, + "uniqueItems": true + }, + "cli_verify_tests": { + "type": "array", + "items": { "$ref": "#/$defs/rust_test_name" }, + "uniqueItems": true + }, + "cli_verify_cases": { + "type": "array", + "items": { "$ref": "#/$defs/case_name" }, + "uniqueItems": true + }, + "expected_parser": { "$ref": "#/$defs/parser" }, + "expected_capabilities": { "$ref": "#/$defs/capabilities" } + } + }, + "rejected_shape": { + "type": "object", + "required": [ + "name", + "category", + "adapter_tests", + "cli_verify_tests", + "expected_error_contains" + ], + "additionalProperties": false, + "properties": { + "name": { "$ref": "#/$defs/case_name" }, + "category": { + "enum": [ + "documented-subset-diagnostics", + "real-odl-diagnostics", + "recognition-diagnostics" + ] + }, + "source_fixture": { "$ref": "#/$defs/repo_path" }, + "adapter_tests": { + "type": "array", + "items": { "$ref": "#/$defs/rust_test_name" }, + "uniqueItems": true + }, + "cli_verify_tests": { + "type": "array", + "items": { "$ref": "#/$defs/rust_test_name" }, + "uniqueItems": true + }, + "cli_usage_error_case": { "$ref": "#/$defs/case_name" }, + "expected_error_contains": { "type": "string", "minLength": 1 } + } + }, + "parser": { + "type": "object", + "required": ["name", "version", "adapter", "adapter_version"], + "additionalProperties": false, + "properties": { + "name": { "const": "opendataloader-pdf" }, + "version": { "type": "string", "minLength": 1 }, + "adapter": { "const": "opendataloader-json" }, + "adapter_version": { "const": "0.1.0" } + } + }, + "capabilities": { + "type": "object", + "required": [ + "spans", + "char_offsets", + "tables", + "fingerprint", + "coordinate_origin", + "crop_support" + ], + "additionalProperties": false, + "properties": { + "spans": { "const": false }, + "char_offsets": { "const": false }, + "tables": { "type": "boolean" }, + "fingerprint": { "const": false }, + "coordinate_origin": { "const": "unknown" }, + "crop_support": { "const": false } + } + }, + "case_name": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, + "repo_path": { "type": "string", "minLength": 1, "pattern": "^[A-Za-z0-9_./-]+$" }, + "rust_test_name": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" } + } +} diff --git a/schemas/validate_examples.py b/schemas/validate_examples.py index 8429e4b..4dc8408 100644 --- a/schemas/validate_examples.py +++ b/schemas/validate_examples.py @@ -89,6 +89,9 @@ ("ethos-capability-downgrade-contract.schema.json", [ ROOT / "examples" / "verify" / "capability_downgrade_v1_contract.json", ]), + ("ethos-opendataloader-adapter-shape-contract.schema.json", [ + ROOT / "examples" / "verify" / "opendataloader_adapter_shape_v1_contract.json", + ]), ("ethos-crop-element-contract.schema.json", [ ROOT / "examples" / "crop" / "crop_element_v1_contract.json", ]), From 5a2b9494183bbd2198962d53cfbdd6a1988913b0 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 18:52:23 +0530 Subject: [PATCH 26/39] Add grounding source contract guard Signed-off-by: docushell-admin --- .github/scripts/test_execution_status.py | 2 + ...t_milestone_d_grounding_source_contract.py | 269 ++++++++++++++++++ .../test_milestone_d_internal_contracts.py | 8 + Makefile | 12 + docs/execution-status.md | 1 + docs/milestone-d-grounding-source-contract.md | 61 ++++ docs/roadmap.md | 4 + .../verify/grounding_source_v1_contract.json | 112 ++++++++ schemas/README.md | 10 + ...thos-grounding-source-contract.schema.json | 132 +++++++++ schemas/validate_examples.py | 3 + 11 files changed, 614 insertions(+) create mode 100644 .github/scripts/test_milestone_d_grounding_source_contract.py create mode 100644 docs/milestone-d-grounding-source-contract.md create mode 100644 examples/verify/grounding_source_v1_contract.json create mode 100644 schemas/ethos-grounding-source-contract.schema.json diff --git a/.github/scripts/test_execution_status.py b/.github/scripts/test_execution_status.py index 894d498..87f090b 100644 --- a/.github/scripts/test_execution_status.py +++ b/.github/scripts/test_execution_status.py @@ -39,6 +39,7 @@ def test_status_is_scoped_to_internal_continuation(self) -> None: text, ) self.assertIn("docs/milestone-d-verify-citations-contract.md", text) + self.assertIn("docs/milestone-d-grounding-source-contract.md", text) self.assertIn("docs/milestone-d-capability-downgrade-contract.md", text) self.assertIn("docs/milestone-d-opendataloader-adapter-shape-contract.md", text) self.assertIn("docs/milestone-d-crop-element-contract.md", text) @@ -51,6 +52,7 @@ def test_internal_check_command_is_documented(self) -> None: self.assertIn("make milestone-b-internal-checks", text) self.assertIn("make milestone-c-internal-checks", text) self.assertIn("make milestone-d-verify-citations-contract", text) + self.assertIn("make milestone-d-grounding-source-contract", text) self.assertIn("make milestone-d-capability-downgrade-contract", text) self.assertIn("make milestone-d-opendataloader-adapter-shape-contract", text) self.assertIn("make milestone-d-crop-element-contract", text) diff --git a/.github/scripts/test_milestone_d_grounding_source_contract.py b/.github/scripts/test_milestone_d_grounding_source_contract.py new file mode 100644 index 0000000..af824ec --- /dev/null +++ b/.github/scripts/test_milestone_d_grounding_source_contract.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python3 +# +# Copyright 2026 The Ethos maintainers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import annotations + +import json +import re +import unittest +from pathlib import Path + +from jsonschema import Draft202012Validator +from makefile_guard import makefile_text, target_block + + +ROOT = Path(__file__).resolve().parents[2] +CONTRACT = ROOT / "docs/milestone-d-grounding-source-contract.md" +CONTRACT_INVENTORY = ROOT / "examples/verify/grounding_source_v1_contract.json" +CONTRACT_INVENTORY_SCHEMA = ROOT / "schemas/ethos-grounding-source-contract.schema.json" +GROUNDING_SOURCE = ROOT / "crates/ethos-core/src/grounding.rs" +MODEL_SOURCE = ROOT / "crates/ethos-core/src/model.rs" +ODL_SOURCE = ROOT / "adapters/grounding/opendataloader-json/src/lib.rs" +CLI_VERIFY_TEST = ROOT / "crates/ethos-cli/tests/verify.rs" +VERIFICATION_REPORT_SCHEMA = ROOT / "schemas/ethos-verification-report.schema.json" +ROADMAP = ROOT / "docs/roadmap.md" +EXECUTION_STATUS = ROOT / "docs/execution-status.md" +SCHEMAS_README = ROOT / "schemas/README.md" +EXPECTED_EXPLICIT_BLOCKERS = [ + "a new command or binding surface", + "a new foreign parser adapter", + "adapter behavior beyond committed fixtures", + "first-class crop API behavior", + "sandbox backend behavior", + "claim-kind expansion or semantic verification", +] +EXPECTED_TRAIT_METHODS = [ + "parser", + "capabilities", + "fingerprint", + "pages", + "elements", + "spans", + "tables", + "crop_ref", + "element_by_id", +] + + +def load_json(path: Path): + return json.loads(path.read_text(encoding="utf-8")) + + +def contract_text() -> str: + return CONTRACT.read_text(encoding="utf-8") + + +def normalized_contract_text() -> str: + return re.sub(r"\s+", " ", contract_text()) + + +def contract_explicit_blockers() -> list[str]: + match = re.search( + r"## Explicit Blockers For This Slice\n\n" + r"This first `grounding_source` slice does not add:\n\n" + r"(?P(?:- .+\n)+)", + contract_text(), + ) + if match is None: + raise AssertionError("missing grounding_source explicit blocker list") + return [ + line.removeprefix("- ").rstrip(";.") + for line in match.group("bullets").strip().splitlines() + ] + + +def schema_errors(schema: dict, instance: dict) -> list: + return sorted( + Draft202012Validator(schema).iter_errors(instance), + key=lambda error: list(error.absolute_path), + ) + + +def rust_test_names(path: Path) -> set[str]: + text = path.read_text(encoding="utf-8") + return set(re.findall(r"(?m)^\s*fn ([a-z][a-z0-9_]*)\(", text)) + + +def trait_method_names() -> list[str]: + text = GROUNDING_SOURCE.read_text(encoding="utf-8") + match = re.search(r"pub trait GroundingSource \{(?P.*?)\n\}", text, re.DOTALL) + if match is None: + raise AssertionError("missing GroundingSource trait body") + return re.findall(r"(?m)^\s*fn ([a-z][a-z0-9_]*)\(", match.group("body")) + + +class MilestoneDGroundingSourceContractTests(unittest.TestCase): + def test_target_is_declared_phony(self) -> None: + text = makefile_text() + + self.assertIn(".PHONY:", text) + self.assertIn("milestone-d-grounding-source-contract", text) + + def test_target_composes_contract_gates(self) -> None: + block = target_block("milestone-d-grounding-source-contract") + + required = [ + "cargo test --locked -p ethos-core grounding", + "cargo test --locked -p ethos-cli --test verify native_ethos_verify_produces_non_empty_checks", + "cargo test --locked -p ethos-cli --test verify opendataloader_verify_adapter_produces_capability_aware_report", + "$(PYTHON) schemas/validate_examples.py", + "$(PYTHON) .github/scripts/test_execution_status.py", + "$(PYTHON) .github/scripts/test_roadmap_status.py", + "$(PYTHON) .github/scripts/test_milestone_d_grounding_source_contract.py", + "git diff --check", + ] + self.assertEqual(required, [line.strip() for line in block.splitlines() if line.strip()]) + + def test_target_stays_contract_scoped(self) -> None: + block = target_block("milestone-d-grounding-source-contract") + + for out_of_scope in [ + "verify-alpha", + "rag-chunk-alpha", + "security-report-alpha", + "verify-rendered-crops", + "compare-rendered-crops", + "layout-evaluator-alpha", + "python-surface-test", + "milestone-b-internal-checks", + "milestone-c-internal-checks", + "npm", + "mcp", + ]: + self.assertNotIn(out_of_scope, block) + + def test_contract_is_linked_from_status_docs(self) -> None: + for path in [ROADMAP, EXECUTION_STATUS, SCHEMAS_README]: + text = path.read_text(encoding="utf-8") + self.assertIn("milestone-d-grounding-source-contract.md", text, path) + + def test_contract_defines_existing_carrier_not_new_surface(self) -> None: + text = normalized_contract_text() + + self.assertIn("source-only pre-alpha contract work", text) + self.assertIn("does not add a new public command", text) + self.assertIn("The current executable carrier remains the `GroundingSource` trait", text) + self.assertIn("native Ethos document JSON, foreign adapter output, and the verifier", text) + + def test_contract_pins_supported_boundaries(self) -> None: + text = normalized_contract_text() + + for required in [ + "`examples/verify/grounding_source_v1_contract.json`", + "`ParserIdentity`", + "`Capabilities`", + "`verification_report.grounding`", + "default optional trait methods remain safe", + "`make milestone-d-grounding-source-contract PYTHON=/bin/python`", + ]: + self.assertIn(required, text) + self.assertEqual(EXPECTED_EXPLICIT_BLOCKERS, contract_explicit_blockers()) + + def test_contract_inventory_schema_validates_inventory(self) -> None: + schema = load_json(CONTRACT_INVENTORY_SCHEMA) + inventory = load_json(CONTRACT_INVENTORY) + + Draft202012Validator.check_schema(schema) + self.assertEqual([], schema_errors(schema, inventory)) + + def test_trait_method_surface_matches_inventory(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + + self.assertEqual(EXPECTED_TRAIT_METHODS, inventory["required_trait_methods"]) + self.assertEqual(EXPECTED_TRAIT_METHODS, trait_method_names()) + + def test_report_schema_grounding_matches_trait_metadata(self) -> None: + schema = load_json(VERIFICATION_REPORT_SCHEMA) + grounding = schema["properties"]["grounding"] + capabilities = grounding["properties"]["capabilities"] + + self.assertEqual(["parser", "capabilities"], grounding["required"]) + self.assertEqual( + [ + "char_offsets", + "coordinate_origin", + "crop_support", + "fingerprint", + "spans", + "tables", + ], + sorted(capabilities["properties"].keys()), + ) + self.assertEqual( + ["bottom-left", "top-left", "unknown"], + sorted(capabilities["properties"]["coordinate_origin"]["enum"]), + ) + + def test_contract_inventory_binds_existing_sources_and_tests(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + core_tests = rust_test_names(GROUNDING_SOURCE) + cli_tests = rust_test_names(CLI_VERIFY_TEST) + + self.assertEqual(inventory["schema_version"], 1) + self.assertEqual(inventory["contract"], "grounding_source.v1") + self.assertEqual(inventory["status"], "source-only-pre-alpha") + self.assertEqual(inventory["carrier"], "GroundingSource trait") + self.assertEqual(EXPECTED_EXPLICIT_BLOCKERS, inventory["explicit_blockers"]) + self.assertTrue((ROOT / inventory["trait_module"]).is_file()) + self.assertTrue((ROOT / inventory["report_schema"]).is_file()) + + for case in inventory["source_cases"]: + self.assertTrue((ROOT / case["implementation"]).is_file(), case["name"]) + self.assertTrue((ROOT / case["report_golden"]).is_file(), case["name"]) + for test_name in case["rust_tests"]: + self.assertIn(test_name, cli_tests, case["name"]) + + default_case = inventory["trait_default_case"] + self.assertEqual("safe-defaults", default_case["name"]) + self.assertTrue((ROOT / default_case["implementation"]).is_file()) + for test_name in default_case["rust_tests"]: + self.assertIn(test_name, core_tests) + self.assertEqual( + ["spans", "tables", "crop_ref", "element_by_id"], + default_case["expected_defaults"], + ) + + def test_current_implementations_are_named(self) -> None: + model = MODEL_SOURCE.read_text(encoding="utf-8") + odl = ODL_SOURCE.read_text(encoding="utf-8") + grounding = GROUNDING_SOURCE.read_text(encoding="utf-8") + + self.assertIn("impl crate::grounding::GroundingSource for Document", model) + self.assertIn("impl GroundingSource for OdlJsonSource", odl) + self.assertIn("fn spans(&self) -> Vec {\n Vec::new()", grounding) + self.assertIn("fn tables(&self) -> Vec {\n Vec::new()", grounding) + self.assertIn("fn crop_ref(&self, _page: &str, _bbox: [i64; 4]) -> Option", grounding) + + def test_source_cases_match_report_goldens(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + + for case in inventory["source_cases"]: + golden = load_json(ROOT / case["report_golden"]) + self.assertEqual(case["expected_parser"], golden["grounding"]["parser"], case["name"]) + self.assertEqual( + case["expected_capabilities"], + golden["grounding"]["capabilities"], + case["name"], + ) + if case["document_fingerprint_required"]: + self.assertIn("document_fingerprint", golden, case["name"]) + else: + self.assertNotIn("document_fingerprint", golden, case["name"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/scripts/test_milestone_d_internal_contracts.py b/.github/scripts/test_milestone_d_internal_contracts.py index 937c435..5dec9b1 100644 --- a/.github/scripts/test_milestone_d_internal_contracts.py +++ b/.github/scripts/test_milestone_d_internal_contracts.py @@ -35,6 +35,14 @@ "inventory": "examples/verify/verify_citations_v1_contract.json", "schema": "schemas/ethos-verify-citations-contract.schema.json", }, + { + "contract": "grounding_source.v1", + "carrier": "GroundingSource trait", + "target": "milestone-d-grounding-source-contract", + "doc": "docs/milestone-d-grounding-source-contract.md", + "inventory": "examples/verify/grounding_source_v1_contract.json", + "schema": "schemas/ethos-grounding-source-contract.schema.json", + }, { "contract": "opendataloader_adapter_shape.v1", "carrier": "opendataloader-json adapter", diff --git a/Makefile b/Makefile index ca0a972..c60158e 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ LAYOUT_EVALUATOR_OUT ?= $(ROOT)/target/layout-evaluator-alpha .PHONY: verify-alpha verify-alpha-tree rag-chunk-alpha security-report-alpha milestone-d-verify-citations-contract milestone-d-crop-element-contract milestone-d-sandbox-subprocess-contract milestone-d-internal-contracts verify-rendered-crops compare-rendered-crops layout-evaluator-alpha python-surface-test milestone-b-internal-checks milestone-c-internal-checks release-hygiene release-advisory third-party-license-manifest release-notice-draft .PHONY: milestone-d-capability-downgrade-contract .PHONY: milestone-d-opendataloader-adapter-shape-contract +.PHONY: milestone-d-grounding-source-contract $(ETHOS_BIN): cargo build --locked -p ethos-cli @@ -57,6 +58,16 @@ milestone-d-verify-citations-contract: $(PYTHON) .github/scripts/test_milestone_d_verify_citations_contract.py git diff --check +milestone-d-grounding-source-contract: + cargo test --locked -p ethos-core grounding + cargo test --locked -p ethos-cli --test verify native_ethos_verify_produces_non_empty_checks + cargo test --locked -p ethos-cli --test verify opendataloader_verify_adapter_produces_capability_aware_report + $(PYTHON) schemas/validate_examples.py + $(PYTHON) .github/scripts/test_execution_status.py + $(PYTHON) .github/scripts/test_roadmap_status.py + $(PYTHON) .github/scripts/test_milestone_d_grounding_source_contract.py + git diff --check + milestone-d-crop-element-contract: cargo test --locked -p ethos-cli --test verify native_verify_crop_dir_writes_deterministic_crop_descriptors $(PYTHON) schemas/validate_examples.py @@ -93,6 +104,7 @@ milestone-d-opendataloader-adapter-shape-contract: milestone-d-internal-contracts: $(MAKE) milestone-d-verify-citations-contract PYTHON=$(PYTHON) + $(MAKE) milestone-d-grounding-source-contract PYTHON=$(PYTHON) $(MAKE) milestone-d-opendataloader-adapter-shape-contract PYTHON=$(PYTHON) $(MAKE) milestone-d-capability-downgrade-contract PYTHON=$(PYTHON) $(MAKE) milestone-d-crop-element-contract PYTHON=$(PYTHON) diff --git a/docs/execution-status.md b/docs/execution-status.md index afb9807..4662dc5 100644 --- a/docs/execution-status.md +++ b/docs/execution-status.md @@ -27,6 +27,7 @@ The committed implementation now includes: - `ethos security report` has a source-only pre-alpha artifact check over the committed document example. The current internal checks cover deterministic report output, report/source identity grounding, security-warning lane and message diagnostics, locator grounding, inventory/report parity, summary drift, warning id uniqueness, deterministic warning numbering, and explicit rejection of unsupported current source-warning references. - `make milestone-c-internal-checks` composes the current internal Milestone C artifact-validation path across RAG chunk and security-report gates; CI/static guard scripts fail closed if that command wiring or the dated closeout record drifts. - Milestone D source-only pre-alpha contract work has started with `docs/milestone-d-verify-citations-contract.md`. It defines `verify_citations` v1 as the citation-input, verification-config, grounding-source, and verification-report contract currently carried by `ethos verify`; schema/example validation checks that the minimal citation example and verification-report example stay coherent. Focused validation is `make milestone-d-verify-citations-contract PYTHON=/bin/python`. +- Milestone D `grounding_source` v1 contract prep has started with `docs/milestone-d-grounding-source-contract.md`. It defines `grounding_source` as the parser-neutral evidence boundary currently carried by the `GroundingSource` trait and `ethos verify` report grounding metadata; schema/example validation checks the inventory at `examples/verify/grounding_source_v1_contract.json`, and the repository guard checks that it stays coherent with trait methods, current source implementations, focused verifier tests, and report goldens. Focused validation is `make milestone-d-grounding-source-contract PYTHON=/bin/python`. - Milestone D `capability_downgrade` v1 contract prep has started with `docs/milestone-d-capability-downgrade-contract.md`. It defines `capability_downgrade` as the grounding-source capability declaration to verification-report downgrade contract currently carried by `ethos verify`; schema/example validation checks the inventory at `examples/verify/capability_downgrade_v1_contract.json`, and the repository guard checks that it stays coherent with report goldens. Focused validation is `make milestone-d-capability-downgrade-contract PYTHON=/bin/python`. - Milestone D `opendataloader_adapter_shape` v1 contract prep has started with `docs/milestone-d-opendataloader-adapter-shape-contract.md`. It defines `opendataloader_adapter_shape` as the OpenDataLoader-style input-shape to `GroundingSource` contract currently carried by `ethos-grounding-opendataloader-json` and `ethos verify --grounding opendataloader-json`; schema/example validation checks the inventory at `examples/verify/opendataloader_adapter_shape_v1_contract.json`, and the repository guard checks that it stays coherent with adapter tests, CLI grounding tests, report goldens, and usage diagnostics. Focused validation is `make milestone-d-opendataloader-adapter-shape-contract PYTHON=/bin/python`. - Milestone D `crop_element` v1 contract prep has started with `docs/milestone-d-crop-element-contract.md`. It defines `crop_element` as the future element-to-crop-descriptor contract currently represented by the existing `ethos verify --crop-dir` carrier; schema/example validation checks the request envelope at `schemas/examples/crop-element-request.example.json` and inventory at `examples/crop/crop_element_v1_contract.json`, and the repository guard checks that request identity, document, and crop-descriptor examples stay coherent. Focused validation is `make milestone-d-crop-element-contract PYTHON=/bin/python`. diff --git a/docs/milestone-d-grounding-source-contract.md b/docs/milestone-d-grounding-source-contract.md new file mode 100644 index 0000000..c9925f2 --- /dev/null +++ b/docs/milestone-d-grounding-source-contract.md @@ -0,0 +1,61 @@ +# Milestone D `grounding_source` v1 Contract + +Status: source-only pre-alpha contract work for internal Milestone D continuation. + +This note defines the narrow `grounding_source` contract-prep slice for Milestone D. It does +not add a new public command, binding surface, adapter behavior, crop surface, or sandbox +behavior. + +The current executable carrier remains the `GroundingSource` trait in `ethos-core`, plus the +existing `ethos verify` reports that embed parser identity and declared capabilities. +`grounding_source` names the parser-neutral evidence boundary between native Ethos document +JSON, foreign adapter output, and the verifier. + +## Contract Surface + +`grounding_source` v1 covers: + +- parser identity through `ParserIdentity`; +- capability declarations through `Capabilities`; +- optional document fingerprint exposure; +- deterministic page, element, span, and table evidence ordering; +- optional crop references that remain opaque audit pointers; +- report grounding metadata under `verification_report.grounding`. + +The executable inventory is `examples/verify/grounding_source_v1_contract.json`. It binds the +current trait surface to native Ethos and OpenDataLoader-style report goldens plus the trait +default-safety test. + +## Validation Target + +- `make milestone-d-grounding-source-contract PYTHON=/bin/python` + +The target runs the focused core grounding test, native and OpenDataLoader-style CLI verifier +checks, schema/example validation, status guards, roadmap guards, the contract guard, and +whitespace diff checks. + +## Boundaries Locked By This Slice + +- native Ethos document JSON remains a `GroundingSource` with parser name `ethos`, a declared + fingerprint, top-left coordinate origin, spans, char offsets, and tables; +- OpenDataLoader-style JSON remains a foreign `GroundingSource` through the `opendataloader-json` + adapter, with explicit missing fingerprint/span/char-offset/crop capabilities and unknown + coordinate origin; +- `ethos verify` reports echo only parser identity and capabilities under `grounding`; +- default optional trait methods remain safe: empty spans, empty tables, no crop reference, and + linear element lookup. + +## Explicit Blockers For This Slice + +This first `grounding_source` slice does not add: + +- a new command or binding surface; +- a new foreign parser adapter; +- adapter behavior beyond committed fixtures; +- first-class crop API behavior; +- sandbox backend behavior; +- claim-kind expansion or semantic verification. + +Until those blockers are explicitly handled, public language remains limited to source-only +pre-alpha internal continuation, evidence grounding, diagnostics, fixture-backed validation, and +explicit blockers. diff --git a/docs/roadmap.md b/docs/roadmap.md index 6ff98ad..4fa6c52 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -21,6 +21,10 @@ Milestone D source-only pre-alpha contract work has started with the narrow current executable carrier remains `ethos verify`; the first D slice is a contract and fixture-backed validation boundary, not a new public command, binding, crop API, sandbox backend, Node beta, or MCP experimental scope. +The source-only +[`grounding_source` v1 contract](milestone-d-grounding-source-contract.md) +binds the parser-neutral evidence boundary to current native and foreign-source +report grounding metadata without adding a new command or binding surface. The next D contract-prep slice defines the source-only [`crop_element` v1 contract](milestone-d-crop-element-contract.md) over the existing `ethos verify --crop-dir` crop descriptor carrier; it does not add a diff --git a/examples/verify/grounding_source_v1_contract.json b/examples/verify/grounding_source_v1_contract.json new file mode 100644 index 0000000..436c6b6 --- /dev/null +++ b/examples/verify/grounding_source_v1_contract.json @@ -0,0 +1,112 @@ +{ + "schema_version": 1, + "contract": "grounding_source.v1", + "status": "source-only-pre-alpha", + "carrier": "GroundingSource trait", + "trait_module": "crates/ethos-core/src/grounding.rs", + "report_schema": "schemas/ethos-verification-report.schema.json", + "required_trait_methods": [ + "parser", + "capabilities", + "fingerprint", + "pages", + "elements", + "spans", + "tables", + "crop_ref", + "element_by_id" + ], + "source_cases": [ + { + "name": "native-ethos-document", + "category": "native", + "implementation": "crates/ethos-core/src/model.rs", + "rust_tests": [ + "native_ethos_verify_produces_non_empty_checks" + ], + "report_golden": "examples/verify/goldens/native_grounded_report.json", + "document_fingerprint_required": true, + "expected_parser": { + "name": "ethos", + "version": "0.1.0" + }, + "expected_capabilities": { + "spans": true, + "char_offsets": true, + "tables": true, + "fingerprint": true, + "coordinate_origin": "top-left", + "crop_support": false + } + }, + { + "name": "opendataloader-documented-subset", + "category": "foreign-adapter", + "implementation": "adapters/grounding/opendataloader-json/src/lib.rs", + "rust_tests": [ + "opendataloader_verify_adapter_produces_capability_aware_report" + ], + "report_golden": "examples/verify/goldens/opendataloader_grounded_report.json", + "document_fingerprint_required": false, + "expected_parser": { + "name": "opendataloader-pdf", + "version": "0.0.0-synthetic", + "adapter": "opendataloader-json", + "adapter_version": "0.1.0" + }, + "expected_capabilities": { + "spans": false, + "char_offsets": false, + "tables": true, + "fingerprint": false, + "coordinate_origin": "unknown", + "crop_support": false + } + }, + { + "name": "opendataloader-real-tree", + "category": "foreign-adapter", + "implementation": "adapters/grounding/opendataloader-json/src/lib.rs", + "rust_tests": [ + "real_opendataloader_fixture_verifies_against_golden" + ], + "report_golden": "fixtures/foreign/opendataloader/real/expected.verification_report.json", + "document_fingerprint_required": false, + "expected_parser": { + "name": "opendataloader-pdf", + "version": "unknown", + "adapter": "opendataloader-json", + "adapter_version": "0.1.0" + }, + "expected_capabilities": { + "spans": false, + "char_offsets": false, + "tables": false, + "fingerprint": false, + "coordinate_origin": "unknown", + "crop_support": false + } + } + ], + "trait_default_case": { + "name": "safe-defaults", + "implementation": "crates/ethos-core/src/grounding.rs", + "rust_tests": [ + "defaults_are_safe" + ], + "expected_defaults": [ + "spans", + "tables", + "crop_ref", + "element_by_id" + ] + }, + "explicit_blockers": [ + "a new command or binding surface", + "a new foreign parser adapter", + "adapter behavior beyond committed fixtures", + "first-class crop API behavior", + "sandbox backend behavior", + "claim-kind expansion or semantic verification" + ] +} diff --git a/schemas/README.md b/schemas/README.md index ba3f626..847785d 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -15,6 +15,7 @@ bumps and downstream sign-off; output-changing heuristics are semver events (PRD | `ethos-crop-descriptor.schema.json` | crop descriptor JSON emitted by `ethos verify --crop-dir` | | `ethos-crop-element-request.schema.json` | source-only request envelope for Milestone D `crop_element` v1 contract work | | `ethos-verify-citations-contract.schema.json` | Milestone D `verify_citations` v1 source-only contract inventory | +| `ethos-grounding-source-contract.schema.json` | Milestone D `grounding_source` v1 source-only contract inventory | | `ethos-capability-downgrade-contract.schema.json` | Milestone D `capability_downgrade` v1 source-only contract inventory | | `ethos-opendataloader-adapter-shape-contract.schema.json` | Milestone D `opendataloader_adapter_shape` v1 source-only contract inventory | | `ethos-crop-element-contract.schema.json` | Milestone D `crop_element` v1 source-only contract inventory | @@ -43,6 +44,15 @@ Milestone D `verify_citations` v1 contract work is tracked in `examples/verify/verify_citations_v1_contract.json` is schema-validated here; its alignment with the executable case inventory and report goldens is checked by the Milestone D repository guard. +Milestone D `grounding_source` v1 contract work is tracked in +`docs/milestone-d-grounding-source-contract.md`. In this source-only pre-alpha slice, +`grounding_source` names the parser-neutral evidence boundary currently carried by the +`GroundingSource` trait and `ethos verify` report grounding metadata; it does not add a new +command or binding surface. The contract inventory at +`examples/verify/grounding_source_v1_contract.json` is schema-validated here; its alignment with +trait methods, source implementations, focused verifier tests, and report goldens is checked by +the Milestone D repository guard. + Milestone D `capability_downgrade` v1 contract work is tracked in `docs/milestone-d-capability-downgrade-contract.md`. In this source-only pre-alpha slice, `capability_downgrade` names the grounding-source capability declaration to verification-report diff --git a/schemas/ethos-grounding-source-contract.schema.json b/schemas/ethos-grounding-source-contract.schema.json new file mode 100644 index 0000000..17ac530 --- /dev/null +++ b/schemas/ethos-grounding-source-contract.schema.json @@ -0,0 +1,132 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:ethos:schema:grounding-source-contract:1", + "title": "Ethos grounding_source v1 contract inventory", + "description": "Source-only pre-alpha Milestone D inventory for the current grounding_source v1 contract carried by the GroundingSource trait and ethos verify report grounding metadata. This validates inventory shape and vocabulary; trait/report alignment stays in the repository guard.", + "type": "object", + "required": [ + "schema_version", + "contract", + "status", + "carrier", + "trait_module", + "report_schema", + "required_trait_methods", + "source_cases", + "trait_default_case", + "explicit_blockers" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "const": 1 }, + "contract": { "const": "grounding_source.v1" }, + "status": { "const": "source-only-pre-alpha" }, + "carrier": { "const": "GroundingSource trait" }, + "trait_module": { "$ref": "#/$defs/repo_path" }, + "report_schema": { "$ref": "#/$defs/repo_path" }, + "required_trait_methods": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/rust_method_name" }, + "uniqueItems": true + }, + "source_cases": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/source_case" } + }, + "trait_default_case": { "$ref": "#/$defs/trait_default_case" }, + "explicit_blockers": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + } + }, + "$defs": { + "source_case": { + "type": "object", + "required": [ + "name", + "category", + "implementation", + "rust_tests", + "report_golden", + "document_fingerprint_required", + "expected_parser", + "expected_capabilities" + ], + "additionalProperties": false, + "properties": { + "name": { "$ref": "#/$defs/case_name" }, + "category": { "enum": ["native", "foreign-adapter"] }, + "implementation": { "$ref": "#/$defs/repo_path" }, + "rust_tests": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/rust_method_name" }, + "uniqueItems": true + }, + "report_golden": { "$ref": "#/$defs/repo_path" }, + "document_fingerprint_required": { "type": "boolean" }, + "expected_parser": { "$ref": "#/$defs/parser" }, + "expected_capabilities": { "$ref": "#/$defs/capabilities" } + } + }, + "trait_default_case": { + "type": "object", + "required": ["name", "implementation", "rust_tests", "expected_defaults"], + "additionalProperties": false, + "properties": { + "name": { "$ref": "#/$defs/case_name" }, + "implementation": { "$ref": "#/$defs/repo_path" }, + "rust_tests": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/rust_method_name" }, + "uniqueItems": true + }, + "expected_defaults": { + "type": "array", + "minItems": 1, + "items": { "enum": ["spans", "tables", "crop_ref", "element_by_id"] }, + "uniqueItems": true + } + } + }, + "parser": { + "type": "object", + "required": ["name", "version"], + "additionalProperties": false, + "properties": { + "name": { "type": "string", "minLength": 1 }, + "version": { "type": "string", "minLength": 1 }, + "adapter": { "const": "opendataloader-json" }, + "adapter_version": { "const": "0.1.0" } + } + }, + "capabilities": { + "type": "object", + "required": [ + "spans", + "char_offsets", + "tables", + "fingerprint", + "coordinate_origin", + "crop_support" + ], + "additionalProperties": false, + "properties": { + "spans": { "type": "boolean" }, + "char_offsets": { "type": "boolean" }, + "tables": { "type": "boolean" }, + "fingerprint": { "type": "boolean" }, + "coordinate_origin": { "enum": ["top-left", "bottom-left", "unknown"] }, + "crop_support": { "type": "boolean" } + } + }, + "case_name": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, + "repo_path": { "type": "string", "minLength": 1, "pattern": "^[A-Za-z0-9_./-]+$" }, + "rust_method_name": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" } + } +} diff --git a/schemas/validate_examples.py b/schemas/validate_examples.py index 4dc8408..d839283 100644 --- a/schemas/validate_examples.py +++ b/schemas/validate_examples.py @@ -86,6 +86,9 @@ ("ethos-verify-citations-contract.schema.json", [ ROOT / "examples" / "verify" / "verify_citations_v1_contract.json", ]), + ("ethos-grounding-source-contract.schema.json", [ + ROOT / "examples" / "verify" / "grounding_source_v1_contract.json", + ]), ("ethos-capability-downgrade-contract.schema.json", [ ROOT / "examples" / "verify" / "capability_downgrade_v1_contract.json", ]), From ec53e1dac683ca327713595c34fd14cf25b748e9 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 19:00:07 +0530 Subject: [PATCH 27/39] Add crop element surface shape contract guard Signed-off-by: docushell-admin --- .github/scripts/test_execution_status.py | 2 + ...e_d_crop_element_surface_shape_contract.py | 240 ++++++++++++++++++ .../test_milestone_d_internal_contracts.py | 8 + Makefile | 9 + docs/execution-status.md | 1 + ...e-d-crop-element-surface-shape-contract.md | 62 +++++ docs/roadmap.md | 4 + ...rop_element_surface_shape_v1_contract.json | 57 +++++ schemas/README.md | 9 + ...element-surface-shape-contract.schema.json | 91 +++++++ schemas/validate_examples.py | 3 + 11 files changed, 486 insertions(+) create mode 100644 .github/scripts/test_milestone_d_crop_element_surface_shape_contract.py create mode 100644 docs/milestone-d-crop-element-surface-shape-contract.md create mode 100644 examples/crop/crop_element_surface_shape_v1_contract.json create mode 100644 schemas/ethos-crop-element-surface-shape-contract.schema.json diff --git a/.github/scripts/test_execution_status.py b/.github/scripts/test_execution_status.py index 87f090b..1f37d3a 100644 --- a/.github/scripts/test_execution_status.py +++ b/.github/scripts/test_execution_status.py @@ -43,6 +43,7 @@ def test_status_is_scoped_to_internal_continuation(self) -> None: self.assertIn("docs/milestone-d-capability-downgrade-contract.md", text) self.assertIn("docs/milestone-d-opendataloader-adapter-shape-contract.md", text) self.assertIn("docs/milestone-d-crop-element-contract.md", text) + self.assertIn("docs/milestone-d-crop-element-surface-shape-contract.md", text) self.assertIn("docs/milestone-d-sandbox-subprocess-contract.md", text) self.assertNotIn("Status: Pre-alpha / Milestone B entry.", text) @@ -56,6 +57,7 @@ def test_internal_check_command_is_documented(self) -> None: self.assertIn("make milestone-d-capability-downgrade-contract", text) self.assertIn("make milestone-d-opendataloader-adapter-shape-contract", text) self.assertIn("make milestone-d-crop-element-contract", text) + self.assertIn("make milestone-d-crop-element-surface-shape-contract", text) self.assertIn("make milestone-d-sandbox-subprocess-contract", text) self.assertIn("make milestone-d-internal-contracts", text) self.assertIn("CI has a static guard for that target's command wiring", text) diff --git a/.github/scripts/test_milestone_d_crop_element_surface_shape_contract.py b/.github/scripts/test_milestone_d_crop_element_surface_shape_contract.py new file mode 100644 index 0000000..75672f3 --- /dev/null +++ b/.github/scripts/test_milestone_d_crop_element_surface_shape_contract.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +# +# Copyright 2026 The Ethos maintainers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import annotations + +import json +import re +import unittest +from pathlib import Path + +from jsonschema import Draft202012Validator +from makefile_guard import makefile_text, target_block + + +ROOT = Path(__file__).resolve().parents[2] +CONTRACT = ROOT / "docs/milestone-d-crop-element-surface-shape-contract.md" +CONTRACT_INVENTORY = ROOT / "examples/crop/crop_element_surface_shape_v1_contract.json" +CONTRACT_INVENTORY_SCHEMA = ROOT / "schemas/ethos-crop-element-surface-shape-contract.schema.json" +CROP_ELEMENT_CONTRACT_INVENTORY = ROOT / "examples/crop/crop_element_v1_contract.json" +CROP_ELEMENT_REQUEST_SCHEMA = ROOT / "schemas/ethos-crop-element-request.schema.json" +CROP_DESCRIPTOR_SCHEMA = ROOT / "schemas/ethos-crop-descriptor.schema.json" +CLI_MAIN = ROOT / "crates/ethos-cli/src/main.rs" +PYTHON_CLI = ROOT / "python/ethos_pdf/_cli.py" +ROADMAP = ROOT / "docs/roadmap.md" +EXECUTION_STATUS = ROOT / "docs/execution-status.md" +SCHEMAS_README = ROOT / "schemas/README.md" +EXPECTED_EXPLICIT_BLOCKERS = [ + "a first-class `crop_element` CLI command", + "a Python crop method", + "Node, MCP, or hosted crop surfaces", + "rendered-crop backend changes", + "sandbox backend behavior", + "foreign-adapter crop coordinate hardening", +] +EXPECTED_SURFACE_MAP = { + "document_fingerprint": "request.document_fingerprint", + "element_id": "request.element_id", + "rendering": "request.rendering", + "source_pdf_fingerprint": "request.source_pdf_fingerprint", + "crop_descriptor": "descriptor.crop_ref", + "rendered_artifact": "descriptor.rendered_ref", +} + + +def load_json(path: Path): + return json.loads(path.read_text(encoding="utf-8")) + + +def contract_text() -> str: + return CONTRACT.read_text(encoding="utf-8") + + +def normalized_contract_text() -> str: + return re.sub(r"\s+", " ", contract_text()) + + +def contract_explicit_blockers() -> list[str]: + match = re.search( + r"## Explicit Blockers For This Slice\n\n" + r"This first `crop_element_surface_shape` slice does not add:\n\n" + r"(?P(?:- .+\n)+)", + contract_text(), + ) + if match is None: + raise AssertionError("missing crop_element_surface_shape explicit blocker list") + return [ + line.removeprefix("- ").rstrip(";.") + for line in match.group("bullets").strip().splitlines() + ] + + +def schema_errors(schema: dict, instance: dict) -> list: + return sorted( + Draft202012Validator(schema).iter_errors(instance), + key=lambda error: list(error.absolute_path), + ) + + +def schema_property_names(schema: dict) -> set[str]: + return set(schema["properties"].keys()) + + +class MilestoneDCropElementSurfaceShapeContractTests(unittest.TestCase): + def test_target_is_declared_phony(self) -> None: + text = makefile_text() + + self.assertIn(".PHONY:", text) + self.assertIn("milestone-d-crop-element-surface-shape-contract", text) + + def test_target_composes_contract_gates(self) -> None: + block = target_block("milestone-d-crop-element-surface-shape-contract") + + required = [ + "$(PYTHON) schemas/validate_examples.py", + "$(PYTHON) .github/scripts/test_execution_status.py", + "$(PYTHON) .github/scripts/test_roadmap_status.py", + "$(PYTHON) .github/scripts/test_milestone_d_crop_element_surface_shape_contract.py", + "git diff --check", + ] + self.assertEqual(required, [line.strip() for line in block.splitlines() if line.strip()]) + + def test_target_stays_surface_shape_scoped(self) -> None: + block = target_block("milestone-d-crop-element-surface-shape-contract") + + for out_of_scope in [ + "cargo test", + "verify-rendered-crops", + "compare-rendered-crops", + "layout-evaluator-alpha", + "python-surface-test", + "milestone-b-internal-checks", + "milestone-c-internal-checks", + "npm", + "mcp", + ]: + self.assertNotIn(out_of_scope, block) + + def test_contract_is_linked_from_status_docs(self) -> None: + for path in [ROADMAP, EXECUTION_STATUS, SCHEMAS_README]: + text = path.read_text(encoding="utf-8") + self.assertIn("milestone-d-crop-element-surface-shape-contract.md", text, path) + + def test_contract_defines_surface_shape_not_implementation(self) -> None: + text = normalized_contract_text() + + self.assertIn("source-only pre-alpha contract work", text) + self.assertIn("does not add a first-class CLI command", text) + self.assertIn("The current executable crop carrier remains `ethos verify --crop-dir`", text) + self.assertIn("names the future callable surface shape", text) + self.assertIn("does not implement that surface", text) + + def test_contract_pins_supported_boundaries(self) -> None: + text = normalized_contract_text() + + for required in [ + "`examples/crop/crop_element_surface_shape_v1_contract.json`", + "`schemas/ethos-crop-element-request.schema.json`", + "`schemas/ethos-crop-descriptor.schema.json`", + "`document_fingerprint`", + "`element_id`", + "`source_pdf_fingerprint`", + "current CLI still has no first-class `crop_element` command", + "current Python scaffold still has no crop method", + "`make milestone-d-crop-element-surface-shape-contract PYTHON=/bin/python`", + ]: + self.assertIn(required, text) + self.assertEqual(EXPECTED_EXPLICIT_BLOCKERS, contract_explicit_blockers()) + + def test_contract_inventory_schema_validates_inventory(self) -> None: + schema = load_json(CONTRACT_INVENTORY_SCHEMA) + inventory = load_json(CONTRACT_INVENTORY) + + Draft202012Validator.check_schema(schema) + self.assertEqual([], schema_errors(schema, inventory)) + + def test_contract_inventory_binds_existing_crop_contract_files(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + + self.assertEqual(inventory["schema_version"], 1) + self.assertEqual(inventory["contract"], "crop_element_surface_shape.v1") + self.assertEqual(inventory["status"], "source-only-pre-alpha") + self.assertEqual(inventory["carrier"], "source-only crop_element surface shape") + self.assertEqual(EXPECTED_EXPLICIT_BLOCKERS, inventory["explicit_blockers"]) + + for key in ["request_schema", "descriptor_schema", "base_contract_inventory"]: + self.assertTrue((ROOT / inventory[key]).is_file(), key) + self.assertEqual(CROP_ELEMENT_CONTRACT_INVENTORY, ROOT / inventory["base_contract_inventory"]) + + def test_surface_fields_map_to_existing_request_and_descriptor_schema_fields(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + request_schema = load_json(CROP_ELEMENT_REQUEST_SCHEMA) + descriptor_schema = load_json(CROP_DESCRIPTOR_SCHEMA) + request_fields = schema_property_names(request_schema) + descriptor_fields = schema_property_names(descriptor_schema) + + actual = {field["name"]: field["maps_to"] for field in inventory["planned_surface_fields"]} + self.assertEqual(EXPECTED_SURFACE_MAP, actual) + + for mapped in actual.values(): + prefix, name = mapped.split(".", 1) + if prefix == "request": + self.assertIn(name, request_fields) + elif prefix == "descriptor": + self.assertIn(name, descriptor_fields) + else: + raise AssertionError(f"unexpected surface mapping prefix: {prefix}") + + def test_rendering_conditions_reuse_existing_schema_modes(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + request_schema = load_json(CROP_ELEMENT_REQUEST_SCHEMA) + descriptor_schema = load_json(CROP_DESCRIPTOR_SCHEMA) + conditional = { + field["name"]: field.get("required_when") + for field in inventory["planned_surface_fields"] + if "required_when" in field + } + + self.assertEqual( + { + "source_pdf_fingerprint": "rendering=rendered", + "rendered_artifact": "rendering=rendered", + }, + conditional, + ) + self.assertEqual(["descriptor_only", "rendered"], request_schema["properties"]["rendering"]["enum"]) + self.assertEqual( + ["descriptor_only", "rendered"], + descriptor_schema["properties"]["rendering_status"]["enum"], + ) + + def test_current_cli_and_python_surface_absence_is_guarded(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + checked_files = [ROOT / path for path in inventory["current_surface_absence"]["checked_files"]] + self.assertEqual([CLI_MAIN, PYTHON_CLI], checked_files) + + cli_text = CLI_MAIN.read_text(encoding="utf-8") + python_text = PYTHON_CLI.read_text(encoding="utf-8") + self.assertNotIn("CropElement", cli_text) + self.assertNotIn("crop-element", cli_text) + self.assertNotIn("crop_element", cli_text) + self.assertNotIn("def crop", python_text) + self.assertNotIn("crop_element", python_text) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/scripts/test_milestone_d_internal_contracts.py b/.github/scripts/test_milestone_d_internal_contracts.py index 5dec9b1..edd8e48 100644 --- a/.github/scripts/test_milestone_d_internal_contracts.py +++ b/.github/scripts/test_milestone_d_internal_contracts.py @@ -67,6 +67,14 @@ "inventory": "examples/crop/crop_element_v1_contract.json", "schema": "schemas/ethos-crop-element-contract.schema.json", }, + { + "contract": "crop_element_surface_shape.v1", + "carrier": "source-only crop_element surface shape", + "target": "milestone-d-crop-element-surface-shape-contract", + "doc": "docs/milestone-d-crop-element-surface-shape-contract.md", + "inventory": "examples/crop/crop_element_surface_shape_v1_contract.json", + "schema": "schemas/ethos-crop-element-surface-shape-contract.schema.json", + }, { "contract": "sandbox_subprocess.v1", "carrier": "pdfium worker process", diff --git a/Makefile b/Makefile index c60158e..6539ae0 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,7 @@ LAYOUT_EVALUATOR_OUT ?= $(ROOT)/target/layout-evaluator-alpha .PHONY: milestone-d-capability-downgrade-contract .PHONY: milestone-d-opendataloader-adapter-shape-contract .PHONY: milestone-d-grounding-source-contract +.PHONY: milestone-d-crop-element-surface-shape-contract $(ETHOS_BIN): cargo build --locked -p ethos-cli @@ -76,6 +77,13 @@ milestone-d-crop-element-contract: $(PYTHON) .github/scripts/test_milestone_d_crop_element_contract.py git diff --check +milestone-d-crop-element-surface-shape-contract: + $(PYTHON) schemas/validate_examples.py + $(PYTHON) .github/scripts/test_execution_status.py + $(PYTHON) .github/scripts/test_roadmap_status.py + $(PYTHON) .github/scripts/test_milestone_d_crop_element_surface_shape_contract.py + git diff --check + milestone-d-sandbox-subprocess-contract: cargo test --locked -p ethos-cli --test pdf_parse worker $(PYTHON) schemas/validate_examples.py @@ -108,6 +116,7 @@ milestone-d-internal-contracts: $(MAKE) milestone-d-opendataloader-adapter-shape-contract PYTHON=$(PYTHON) $(MAKE) milestone-d-capability-downgrade-contract PYTHON=$(PYTHON) $(MAKE) milestone-d-crop-element-contract PYTHON=$(PYTHON) + $(MAKE) milestone-d-crop-element-surface-shape-contract PYTHON=$(PYTHON) $(MAKE) milestone-d-sandbox-subprocess-contract PYTHON=$(PYTHON) $(PYTHON) .github/scripts/test_milestone_d_internal_contracts.py git diff --check diff --git a/docs/execution-status.md b/docs/execution-status.md index 4662dc5..ceaf700 100644 --- a/docs/execution-status.md +++ b/docs/execution-status.md @@ -31,6 +31,7 @@ The committed implementation now includes: - Milestone D `capability_downgrade` v1 contract prep has started with `docs/milestone-d-capability-downgrade-contract.md`. It defines `capability_downgrade` as the grounding-source capability declaration to verification-report downgrade contract currently carried by `ethos verify`; schema/example validation checks the inventory at `examples/verify/capability_downgrade_v1_contract.json`, and the repository guard checks that it stays coherent with report goldens. Focused validation is `make milestone-d-capability-downgrade-contract PYTHON=/bin/python`. - Milestone D `opendataloader_adapter_shape` v1 contract prep has started with `docs/milestone-d-opendataloader-adapter-shape-contract.md`. It defines `opendataloader_adapter_shape` as the OpenDataLoader-style input-shape to `GroundingSource` contract currently carried by `ethos-grounding-opendataloader-json` and `ethos verify --grounding opendataloader-json`; schema/example validation checks the inventory at `examples/verify/opendataloader_adapter_shape_v1_contract.json`, and the repository guard checks that it stays coherent with adapter tests, CLI grounding tests, report goldens, and usage diagnostics. Focused validation is `make milestone-d-opendataloader-adapter-shape-contract PYTHON=/bin/python`. - Milestone D `crop_element` v1 contract prep has started with `docs/milestone-d-crop-element-contract.md`. It defines `crop_element` as the future element-to-crop-descriptor contract currently represented by the existing `ethos verify --crop-dir` carrier; schema/example validation checks the request envelope at `schemas/examples/crop-element-request.example.json` and inventory at `examples/crop/crop_element_v1_contract.json`, and the repository guard checks that request identity, document, and crop-descriptor examples stay coherent. Focused validation is `make milestone-d-crop-element-contract PYTHON=/bin/python`. +- Milestone D `crop_element_surface_shape` v1 contract prep has started with `docs/milestone-d-crop-element-surface-shape-contract.md`. It defines `crop_element_surface_shape` as the future callable crop surface shape over the existing crop request and descriptor schemas without adding that surface; schema/example validation checks the inventory at `examples/crop/crop_element_surface_shape_v1_contract.json`, and the repository guard checks that it stays coherent with the request schema, descriptor schema, and current surface absence. Focused validation is `make milestone-d-crop-element-surface-shape-contract PYTHON=/bin/python`. - Milestone D `sandbox_subprocess` v1 contract prep has started with `docs/milestone-d-sandbox-subprocess-contract.md`. It defines `sandbox_subprocess` as the future worker-boundary contract currently represented by the existing PDF worker process behind `ethos doc parse` and `ethos fingerprint`; schema/example validation checks the request envelopes under `schemas/examples/sandbox-subprocess-*.example.json` and inventory at `examples/sandbox/sandbox_subprocess_v1_contract.json`, and the repository guard checks that they stay coherent with the worker test slice. Focused validation is `make milestone-d-sandbox-subprocess-contract PYTHON=/bin/python`. - `make milestone-d-internal-contracts PYTHON=/bin/python` composes the current Milestone D source-only contract gates and has a static guard for that target's command wiring and contract registry. diff --git a/docs/milestone-d-crop-element-surface-shape-contract.md b/docs/milestone-d-crop-element-surface-shape-contract.md new file mode 100644 index 0000000..36fb5cb --- /dev/null +++ b/docs/milestone-d-crop-element-surface-shape-contract.md @@ -0,0 +1,62 @@ +# Milestone D `crop_element_surface_shape` v1 Contract + +Status: source-only pre-alpha contract work for internal Milestone D continuation. + +This note defines the narrow `crop_element_surface_shape` contract-prep slice for Milestone D. +It does not add a first-class CLI command, Python method, Node binding, MCP method, hosted +surface, crop renderer, or sandbox behavior. + +The current executable crop carrier remains `ethos verify --crop-dir` and optional +`--crop-source-pdf`. `crop_element_surface_shape` names the future callable surface shape that +must preserve the existing `crop_element` request and crop descriptor audit bindings before any +first-class surface is added. + +## Surface Shape + +`crop_element_surface_shape` v1 is a shape contract over existing artifacts: + +- document identity maps to `document_fingerprint` in + `schemas/ethos-crop-element-request.schema.json`; +- the explicit element locator maps to `element_id`; +- rendering mode maps to `rendering`; +- optional source PDF identity maps to `source_pdf_fingerprint` when rendering is requested; +- descriptor output maps to `schemas/ethos-crop-descriptor.schema.json`; +- rendered artifact metadata remains descriptor-owned when rendered output exists. + +The executable inventory is `examples/crop/crop_element_surface_shape_v1_contract.json`. It binds +the future surface fields to the existing request and descriptor schemas, and records that the +current CLI and Python surfaces intentionally do not expose a first-class crop call. + +## Validation Target + +- `make milestone-d-crop-element-surface-shape-contract PYTHON=/bin/python` + +The target runs schema/example validation, status guards, roadmap guards, the surface-shape +contract guard, and whitespace diff checks. It intentionally does not run rendered crop comparison +or Python surface tests because this slice does not implement that surface. + +## Boundaries Locked By This Slice + +- the surface shape uses the existing request and descriptor schemas instead of defining new + geometry semantics; +- descriptor-only and rendered modes keep the same conditional source-PDF requirements as the + request and descriptor schemas; +- the future callable boundary remains native Ethos document plus explicit element id; +- the current CLI still has no first-class `crop_element` command; +- the current Python scaffold still has no crop method; +- Node, MCP, and hosted crop surfaces remain explicit blockers. + +## Explicit Blockers For This Slice + +This first `crop_element_surface_shape` slice does not add: + +- a first-class `crop_element` CLI command; +- a Python crop method; +- Node, MCP, or hosted crop surfaces; +- rendered-crop backend changes; +- sandbox backend behavior; +- foreign-adapter crop coordinate hardening. + +Until those blockers are explicitly handled, public language remains limited to source-only +pre-alpha internal continuation, evidence grounding, diagnostics, fixture-backed validation, and +explicit blockers. diff --git a/docs/roadmap.md b/docs/roadmap.md index 4fa6c52..0df9648 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -31,6 +31,10 @@ existing `ethos verify --crop-dir` crop descriptor carrier; it does not add a first-class crop command, binding surface, sandbox backend, Node beta, or MCP experimental scope. The source-only +[`crop_element_surface_shape` v1 contract](milestone-d-crop-element-surface-shape-contract.md) +binds the future callable crop surface shape to the existing request and descriptor schemas +without adding that surface. +The source-only [`capability_downgrade` v1 contract](milestone-d-capability-downgrade-contract.md) binds existing grounding-source capability declarations to verification-report capability limits, warnings, and blocked-check diagnostics without adding a new diff --git a/examples/crop/crop_element_surface_shape_v1_contract.json b/examples/crop/crop_element_surface_shape_v1_contract.json new file mode 100644 index 0000000..bb844da --- /dev/null +++ b/examples/crop/crop_element_surface_shape_v1_contract.json @@ -0,0 +1,57 @@ +{ + "schema_version": 1, + "contract": "crop_element_surface_shape.v1", + "status": "source-only-pre-alpha", + "carrier": "source-only crop_element surface shape", + "request_schema": "schemas/ethos-crop-element-request.schema.json", + "descriptor_schema": "schemas/ethos-crop-descriptor.schema.json", + "base_contract_inventory": "examples/crop/crop_element_v1_contract.json", + "planned_surface_fields": [ + { + "name": "document_fingerprint", + "maps_to": "request.document_fingerprint", + "required": true + }, + { + "name": "element_id", + "maps_to": "request.element_id", + "required": true + }, + { + "name": "rendering", + "maps_to": "request.rendering", + "required": true + }, + { + "name": "source_pdf_fingerprint", + "maps_to": "request.source_pdf_fingerprint", + "required_when": "rendering=rendered" + }, + { + "name": "crop_descriptor", + "maps_to": "descriptor.crop_ref", + "required": true + }, + { + "name": "rendered_artifact", + "maps_to": "descriptor.rendered_ref", + "required_when": "rendering=rendered" + } + ], + "current_surface_absence": { + "cli_command": "crop_element", + "python_method": "crop_element", + "checked_files": [ + "crates/ethos-cli/src/main.rs", + "python/ethos_pdf/_cli.py" + ] + }, + "explicit_blockers": [ + "a first-class `crop_element` CLI command", + "a Python crop method", + "Node, MCP, or hosted crop surfaces", + "rendered-crop backend changes", + "sandbox backend behavior", + "foreign-adapter crop coordinate hardening" + ] +} diff --git a/schemas/README.md b/schemas/README.md index 847785d..71dcebd 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -19,6 +19,7 @@ bumps and downstream sign-off; output-changing heuristics are semver events (PRD | `ethos-capability-downgrade-contract.schema.json` | Milestone D `capability_downgrade` v1 source-only contract inventory | | `ethos-opendataloader-adapter-shape-contract.schema.json` | Milestone D `opendataloader_adapter_shape` v1 source-only contract inventory | | `ethos-crop-element-contract.schema.json` | Milestone D `crop_element` v1 source-only contract inventory | +| `ethos-crop-element-surface-shape-contract.schema.json` | Milestone D `crop_element_surface_shape` v1 source-only contract inventory | | `ethos-sandbox-subprocess-request.schema.json` | source-only request envelope for Milestone D `sandbox_subprocess` v1 contract work | | `ethos-sandbox-subprocess-contract.schema.json` | Milestone D `sandbox_subprocess` v1 source-only contract inventory | | `ethos-deterministic-profile.schema.json` | `profiles/ethos-deterministic-v*.json` checker | @@ -80,6 +81,14 @@ their alignment with the document and crop-descriptor examples is checked by the repository guard. The request envelope carries a c14n-derived `request_ref` identity guarded in that same source-only contract check. +Milestone D `crop_element_surface_shape` v1 contract work is tracked in +`docs/milestone-d-crop-element-surface-shape-contract.md`. In this source-only pre-alpha slice, +`crop_element_surface_shape` names the future callable crop surface shape that must preserve the +existing crop request and descriptor bindings; it does not add a first-class command or binding +surface. The contract inventory at `examples/crop/crop_element_surface_shape_v1_contract.json` is +schema-validated here; its alignment with the request schema, descriptor schema, and current +surface absence is checked by the Milestone D repository guard. + Milestone D `sandbox_subprocess` v1 contract work is tracked in `docs/milestone-d-sandbox-subprocess-contract.md`. In this source-only pre-alpha slice, `sandbox_subprocess` names the future worker-boundary contract currently represented by the diff --git a/schemas/ethos-crop-element-surface-shape-contract.schema.json b/schemas/ethos-crop-element-surface-shape-contract.schema.json new file mode 100644 index 0000000..07802d5 --- /dev/null +++ b/schemas/ethos-crop-element-surface-shape-contract.schema.json @@ -0,0 +1,91 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:ethos:schema:crop-element-surface-shape-contract:1", + "title": "Ethos crop_element_surface_shape v1 contract inventory", + "description": "Source-only pre-alpha Milestone D inventory for the future crop_element callable surface shape. This validates inventory shape and vocabulary; schema/surface absence alignment stays in the repository guard.", + "type": "object", + "required": [ + "schema_version", + "contract", + "status", + "carrier", + "request_schema", + "descriptor_schema", + "base_contract_inventory", + "planned_surface_fields", + "current_surface_absence", + "explicit_blockers" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "const": 1 }, + "contract": { "const": "crop_element_surface_shape.v1" }, + "status": { "const": "source-only-pre-alpha" }, + "carrier": { "const": "source-only crop_element surface shape" }, + "request_schema": { "$ref": "#/$defs/repo_path" }, + "descriptor_schema": { "$ref": "#/$defs/repo_path" }, + "base_contract_inventory": { "$ref": "#/$defs/repo_path" }, + "planned_surface_fields": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/surface_field" } + }, + "current_surface_absence": { + "type": "object", + "required": ["cli_command", "python_method", "checked_files"], + "additionalProperties": false, + "properties": { + "cli_command": { "const": "crop_element" }, + "python_method": { "const": "crop_element" }, + "checked_files": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/repo_path" }, + "uniqueItems": true + } + } + }, + "explicit_blockers": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + } + }, + "$defs": { + "surface_field": { + "type": "object", + "required": ["name", "maps_to"], + "oneOf": [ + { "required": ["required"] }, + { "required": ["required_when"] } + ], + "additionalProperties": false, + "properties": { + "name": { + "enum": [ + "document_fingerprint", + "element_id", + "rendering", + "source_pdf_fingerprint", + "crop_descriptor", + "rendered_artifact" + ] + }, + "maps_to": { + "enum": [ + "request.document_fingerprint", + "request.element_id", + "request.rendering", + "request.source_pdf_fingerprint", + "descriptor.crop_ref", + "descriptor.rendered_ref" + ] + }, + "required": { "type": "boolean" }, + "required_when": { "const": "rendering=rendered" } + } + }, + "repo_path": { "type": "string", "minLength": 1, "pattern": "^[A-Za-z0-9_./-]+$" } + } +} diff --git a/schemas/validate_examples.py b/schemas/validate_examples.py index d839283..5a77cb9 100644 --- a/schemas/validate_examples.py +++ b/schemas/validate_examples.py @@ -98,6 +98,9 @@ ("ethos-crop-element-contract.schema.json", [ ROOT / "examples" / "crop" / "crop_element_v1_contract.json", ]), + ("ethos-crop-element-surface-shape-contract.schema.json", [ + ROOT / "examples" / "crop" / "crop_element_surface_shape_v1_contract.json", + ]), ("ethos-sandbox-subprocess-request.schema.json", [ EXAMPLES / "sandbox-subprocess-doc-parse-request.example.json", EXAMPLES / "sandbox-subprocess-doc-parse-timeout-request.example.json", From 14136333a48e779d1208bc638544d1743edf851c Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 19:10:36 +0530 Subject: [PATCH 28/39] Add claim kind boundary contract guard Signed-off-by: docushell-admin --- .github/scripts/test_execution_status.py | 2 + ...ilestone_d_claim_kind_boundary_contract.py | 275 ++++++++++++++++++ .../test_milestone_d_internal_contracts.py | 8 + Makefile | 11 + docs/execution-status.md | 1 + ...ilestone-d-claim-kind-boundary-contract.md | 59 ++++ docs/roadmap.md | 6 +- .../claim_kind_boundary_v1_contract.json | 34 +++ schemas/README.md | 9 + ...s-claim-kind-boundary-contract.schema.json | 173 +++++++++++ schemas/validate_examples.py | 3 + 11 files changed, 580 insertions(+), 1 deletion(-) create mode 100644 .github/scripts/test_milestone_d_claim_kind_boundary_contract.py create mode 100644 docs/milestone-d-claim-kind-boundary-contract.md create mode 100644 examples/verify/claim_kind_boundary_v1_contract.json create mode 100644 schemas/ethos-claim-kind-boundary-contract.schema.json diff --git a/.github/scripts/test_execution_status.py b/.github/scripts/test_execution_status.py index 1f37d3a..3d5ad12 100644 --- a/.github/scripts/test_execution_status.py +++ b/.github/scripts/test_execution_status.py @@ -39,6 +39,7 @@ def test_status_is_scoped_to_internal_continuation(self) -> None: text, ) self.assertIn("docs/milestone-d-verify-citations-contract.md", text) + self.assertIn("docs/milestone-d-claim-kind-boundary-contract.md", text) self.assertIn("docs/milestone-d-grounding-source-contract.md", text) self.assertIn("docs/milestone-d-capability-downgrade-contract.md", text) self.assertIn("docs/milestone-d-opendataloader-adapter-shape-contract.md", text) @@ -53,6 +54,7 @@ def test_internal_check_command_is_documented(self) -> None: self.assertIn("make milestone-b-internal-checks", text) self.assertIn("make milestone-c-internal-checks", text) self.assertIn("make milestone-d-verify-citations-contract", text) + self.assertIn("make milestone-d-claim-kind-boundary-contract", text) self.assertIn("make milestone-d-grounding-source-contract", text) self.assertIn("make milestone-d-capability-downgrade-contract", text) self.assertIn("make milestone-d-opendataloader-adapter-shape-contract", text) diff --git a/.github/scripts/test_milestone_d_claim_kind_boundary_contract.py b/.github/scripts/test_milestone_d_claim_kind_boundary_contract.py new file mode 100644 index 0000000..8055802 --- /dev/null +++ b/.github/scripts/test_milestone_d_claim_kind_boundary_contract.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python3 +# +# Copyright 2026 The Ethos maintainers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import annotations + +import json +import re +import unittest +from pathlib import Path + +from jsonschema import Draft202012Validator +from makefile_guard import makefile_text, target_block + + +ROOT = Path(__file__).resolve().parents[2] +CONTRACT = ROOT / "docs/milestone-d-claim-kind-boundary-contract.md" +CONTRACT_INVENTORY = ROOT / "examples/verify/claim_kind_boundary_v1_contract.json" +CONTRACT_INVENTORY_SCHEMA = ROOT / "schemas/ethos-claim-kind-boundary-contract.schema.json" +CITATIONS_SCHEMA = ROOT / "schemas/ethos-citations.schema.json" +VERIFICATION_CONFIG_SCHEMA = ROOT / "schemas/ethos-verification-config.schema.json" +VERIFICATION_REPORT_SCHEMA = ROOT / "schemas/ethos-verification-report.schema.json" +VERIFY_CITATIONS_INVENTORY = ROOT / "examples/verify/verify_citations_v1_contract.json" +VERIFY_CASES = ROOT / "examples/verify/cases.json" +RUST_VERIFY_TYPES = ROOT / "crates/ethos-core/src/verify_types.rs" +RUST_VERIFY_LIB = ROOT / "crates/ethos-verify/src/lib.rs" +RUST_CLI_VERIFY = ROOT / "crates/ethos-cli/src/cmd/verify.rs" +CLI_VERIFY_TESTS = ROOT / "crates/ethos-cli/tests/verify.rs" +ROADMAP = ROOT / "docs/roadmap.md" +EXECUTION_STATUS = ROOT / "docs/execution-status.md" +SCHEMAS_README = ROOT / "schemas/README.md" +EXPECTED_SUPPORTED = ["quote", "value", "presence", "table_cell"] +EXPECTED_UNSUPPORTED = ["region", "other"] +EXPECTED_EXPLICIT_BLOCKERS = [ + "new claim-kind support", + "semantic, visual, arithmetic, or cross-region verification", + "a new command or binding surface", + "crop API changes", + "sandbox backend changes", + "foreign-adapter broadening beyond committed fixtures", +] + + +def load_json(path: Path): + return json.loads(path.read_text(encoding="utf-8")) + + +def contract_text() -> str: + return CONTRACT.read_text(encoding="utf-8") + + +def normalized_contract_text() -> str: + return re.sub(r"\s+", " ", contract_text()) + + +def schema_errors(schema: dict, instance: dict) -> list: + return sorted( + Draft202012Validator(schema).iter_errors(instance), + key=lambda error: list(error.absolute_path), + ) + + +def contract_explicit_blockers() -> list[str]: + match = re.search( + r"## Explicit Blockers For This Slice\n\n" + r"This first `claim_kind_boundary` slice does not add:\n\n" + r"(?P(?:- .+\n)+)", + contract_text(), + ) + if match is None: + raise AssertionError("missing claim_kind_boundary explicit blocker list") + return [ + line.removeprefix("- ").rstrip(";.") + for line in match.group("bullets").strip().splitlines() + ] + + +def citation_claims(citations: dict) -> list[dict]: + return citations["claims"] + + +class MilestoneDClaimKindBoundaryContractTests(unittest.TestCase): + def test_target_is_declared_phony(self) -> None: + text = makefile_text() + + self.assertIn(".PHONY:", text) + self.assertIn("milestone-d-claim-kind-boundary-contract", text) + + def test_target_composes_contract_gates(self) -> None: + block = target_block("milestone-d-claim-kind-boundary-contract") + + required = [ + "cargo test --locked -p ethos-verify claim_kind", + "cargo test --locked -p ethos-cli --test verify invalid_config_constraints_are_usage_errors", + "$(PYTHON) schemas/validate_examples.py", + "$(PYTHON) .github/scripts/test_execution_status.py", + "$(PYTHON) .github/scripts/test_roadmap_status.py", + "$(PYTHON) .github/scripts/test_milestone_d_claim_kind_boundary_contract.py", + "git diff --check", + ] + self.assertEqual(required, [line.strip() for line in block.splitlines() if line.strip()]) + + def test_target_stays_contract_scoped(self) -> None: + block = target_block("milestone-d-claim-kind-boundary-contract") + + for out_of_scope in [ + "verify-alpha", + "rag-chunk-alpha", + "security-report-alpha", + "verify-rendered-crops", + "compare-rendered-crops", + "layout-evaluator-alpha", + "python-surface-test", + "milestone-b-internal-checks", + "milestone-c-internal-checks", + "npm", + "mcp", + ]: + self.assertNotIn(out_of_scope, block) + + def test_contract_is_linked_from_status_docs(self) -> None: + for path in [ROADMAP, EXECUTION_STATUS, SCHEMAS_README]: + text = path.read_text(encoding="utf-8") + self.assertIn("milestone-d-claim-kind-boundary-contract.md", text, path) + + def test_contract_defines_boundary_not_new_behavior(self) -> None: + text = normalized_contract_text() + + self.assertIn("source-only pre-alpha contract work", text) + self.assertIn("The current executable carrier remains `ethos verify`", text) + self.assertIn("does not add new claim-kind support", text) + self.assertIn("future claim-kind expansion from silently changing", text) + self.assertIn("does not add or broaden verification behavior", text) + self.assertEqual(EXPECTED_EXPLICIT_BLOCKERS, contract_explicit_blockers()) + + def test_contract_inventory_schema_validates_inventory(self) -> None: + schema = load_json(CONTRACT_INVENTORY_SCHEMA) + inventory = load_json(CONTRACT_INVENTORY) + + Draft202012Validator.check_schema(schema) + self.assertEqual([], schema_errors(schema, inventory)) + + def test_inventory_binds_current_claim_kind_sets(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + + self.assertEqual(inventory["schema_version"], 1) + self.assertEqual(inventory["contract"], "claim_kind_boundary.v1") + self.assertEqual(inventory["status"], "source-only-pre-alpha") + self.assertEqual(inventory["carrier"], "ethos verify") + self.assertEqual(EXPECTED_SUPPORTED, inventory["supported_claim_kinds"]) + self.assertEqual(EXPECTED_UNSUPPORTED, inventory["unsupported_claim_kinds"]) + self.assertEqual(EXPECTED_EXPLICIT_BLOCKERS, inventory["explicit_blockers"]) + + def test_schema_boundaries_match_inventory(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + citations_schema = load_json(CITATIONS_SCHEMA) + config_schema = load_json(VERIFICATION_CONFIG_SCHEMA) + report_schema = load_json(VERIFICATION_REPORT_SCHEMA) + + citation_kinds = citations_schema["$defs"]["claim"]["properties"]["kind"]["enum"] + config_kinds = config_schema["properties"]["claim_kinds"]["items"]["enum"] + report_kinds = ( + report_schema["properties"]["checks"]["items"]["properties"]["claim"]["properties"]["kind"]["enum"] + ) + + self.assertEqual(EXPECTED_SUPPORTED + EXPECTED_UNSUPPORTED, citation_kinds) + self.assertEqual(EXPECTED_SUPPORTED, config_kinds) + self.assertEqual(EXPECTED_SUPPORTED + EXPECTED_UNSUPPORTED, report_kinds) + self.assertEqual(EXPECTED_SUPPORTED, inventory["config_boundary"]["accepted_claim_kinds"]) + self.assertEqual(EXPECTED_UNSUPPORTED, inventory["config_boundary"]["rejected_claim_kinds"]) + self.assertEqual(VERIFICATION_CONFIG_SCHEMA, ROOT / inventory["config_boundary"]["schema"]) + + def test_report_case_matches_committed_fixture_pair(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + case = inventory["report_case"] + citations = load_json(ROOT / case["citations"]) + report = load_json(ROOT / case["golden"]) + claims = citation_claims(citations) + checks = report["checks"] + + self.assertEqual("native-non-v1-claims", case["name"]) + self.assertEqual(len(claims), len(checks)) + self.assertEqual([claim["kind"] for claim in claims], ["presence", "region", "other"]) + self.assertEqual([check["claim"] for check in checks], claims) + self.assertEqual(case["expected_statuses"], [check["status"] for check in checks]) + self.assertEqual( + case["expected_reasons"], + [check["reason"] for check in checks if "reason" in check], + ) + self.assertEqual( + case["expected_match_methods"], + [check["match_method"] for check in checks], + ) + self.assertEqual( + case["expected_unsupported_claim_kinds"], + report["unsupported_claim_kinds"], + ) + self.assertEqual(case["all_evidence_grounded"], report["all_evidence_grounded"]) + + def test_unsupported_checks_fail_closed_without_evidence_or_semantic_state(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + report = load_json(ROOT / inventory["report_case"]["golden"]) + unsupported_checks = [ + check for check in report["checks"] if check["status"] == "unsupported_claim_kind" + ] + + self.assertEqual(2, len(unsupported_checks)) + for check in unsupported_checks: + self.assertEqual("unsupported_claim_kind", check["reason"]) + self.assertEqual("none", check["match_method"]) + self.assertFalse(check["semantic_unverified"]) + self.assertNotIn("evidence", check) + self.assertEqual([], check["warnings"]) + + def test_verify_citations_inventory_keeps_non_v1_case_classified(self) -> None: + inventory = load_json(VERIFY_CITATIONS_INVENTORY) + matching = [ + case for case in inventory["report_cases"] if case["name"] == "native-non-v1-claims" + ] + + self.assertEqual(1, len(matching)) + self.assertEqual("unsupported-non-v1", matching[0]["category"]) + self.assertEqual(False, matching[0]["all_evidence_grounded"]) + self.assertEqual( + ["grounded", "unsupported_claim_kind", "unsupported_claim_kind"], + matching[0]["statuses"], + ) + self.assertEqual( + ["unsupported_claim_kind", "unsupported_claim_kind"], + matching[0]["reasons"], + ) + + def test_executable_case_inventory_points_to_same_fixture_pair(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + cases = load_json(VERIFY_CASES) + matching = [ + case for case in cases["report_cases"] if case["name"] == inventory["report_case"]["name"] + ] + + self.assertEqual(1, len(matching)) + self.assertEqual(inventory["report_case"]["citations"], matching[0]["citations"]) + self.assertEqual(inventory["report_case"]["golden"], matching[0]["golden"]) + + def test_rust_surfaces_keep_claim_kind_boundary_explicit(self) -> None: + verify_types = RUST_VERIFY_TYPES.read_text(encoding="utf-8") + verify_lib = RUST_VERIFY_LIB.read_text(encoding="utf-8") + cli_verify = RUST_CLI_VERIFY.read_text(encoding="utf-8") + cli_tests = CLI_VERIFY_TESTS.read_text(encoding="utf-8") + + for variant in ["Quote", "Value", "Presence", "TableCell", "Region", "Other"]: + self.assertIn(variant, verify_types) + self.assertIn("ClaimKind::Region => \"region\"", verify_lib) + self.assertIn("ClaimKind::Other => \"other\"", verify_lib) + self.assertIn("non_v1_claim_kinds_are_deduped_and_keep_gate_false", verify_lib) + self.assertIn("unsupported_claim_kinds_are_explicit", verify_lib) + self.assertIn("ClaimKind::Region | ClaimKind::Other", cli_verify) + self.assertIn("verification config claim_kinds must include only quote", cli_verify) + self.assertIn("invalid_config_constraints_are_usage_errors", cli_tests) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/scripts/test_milestone_d_internal_contracts.py b/.github/scripts/test_milestone_d_internal_contracts.py index edd8e48..fd658af 100644 --- a/.github/scripts/test_milestone_d_internal_contracts.py +++ b/.github/scripts/test_milestone_d_internal_contracts.py @@ -35,6 +35,14 @@ "inventory": "examples/verify/verify_citations_v1_contract.json", "schema": "schemas/ethos-verify-citations-contract.schema.json", }, + { + "contract": "claim_kind_boundary.v1", + "carrier": "ethos verify", + "target": "milestone-d-claim-kind-boundary-contract", + "doc": "docs/milestone-d-claim-kind-boundary-contract.md", + "inventory": "examples/verify/claim_kind_boundary_v1_contract.json", + "schema": "schemas/ethos-claim-kind-boundary-contract.schema.json", + }, { "contract": "grounding_source.v1", "carrier": "GroundingSource trait", diff --git a/Makefile b/Makefile index 6539ae0..858a0c8 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ LAYOUT_EVALUATOR_OUT ?= $(ROOT)/target/layout-evaluator-alpha .PHONY: milestone-d-opendataloader-adapter-shape-contract .PHONY: milestone-d-grounding-source-contract .PHONY: milestone-d-crop-element-surface-shape-contract +.PHONY: milestone-d-claim-kind-boundary-contract $(ETHOS_BIN): cargo build --locked -p ethos-cli @@ -59,6 +60,15 @@ milestone-d-verify-citations-contract: $(PYTHON) .github/scripts/test_milestone_d_verify_citations_contract.py git diff --check +milestone-d-claim-kind-boundary-contract: + cargo test --locked -p ethos-verify claim_kind + cargo test --locked -p ethos-cli --test verify invalid_config_constraints_are_usage_errors + $(PYTHON) schemas/validate_examples.py + $(PYTHON) .github/scripts/test_execution_status.py + $(PYTHON) .github/scripts/test_roadmap_status.py + $(PYTHON) .github/scripts/test_milestone_d_claim_kind_boundary_contract.py + git diff --check + milestone-d-grounding-source-contract: cargo test --locked -p ethos-core grounding cargo test --locked -p ethos-cli --test verify native_ethos_verify_produces_non_empty_checks @@ -112,6 +122,7 @@ milestone-d-opendataloader-adapter-shape-contract: milestone-d-internal-contracts: $(MAKE) milestone-d-verify-citations-contract PYTHON=$(PYTHON) + $(MAKE) milestone-d-claim-kind-boundary-contract PYTHON=$(PYTHON) $(MAKE) milestone-d-grounding-source-contract PYTHON=$(PYTHON) $(MAKE) milestone-d-opendataloader-adapter-shape-contract PYTHON=$(PYTHON) $(MAKE) milestone-d-capability-downgrade-contract PYTHON=$(PYTHON) diff --git a/docs/execution-status.md b/docs/execution-status.md index ceaf700..1db29f8 100644 --- a/docs/execution-status.md +++ b/docs/execution-status.md @@ -27,6 +27,7 @@ The committed implementation now includes: - `ethos security report` has a source-only pre-alpha artifact check over the committed document example. The current internal checks cover deterministic report output, report/source identity grounding, security-warning lane and message diagnostics, locator grounding, inventory/report parity, summary drift, warning id uniqueness, deterministic warning numbering, and explicit rejection of unsupported current source-warning references. - `make milestone-c-internal-checks` composes the current internal Milestone C artifact-validation path across RAG chunk and security-report gates; CI/static guard scripts fail closed if that command wiring or the dated closeout record drifts. - Milestone D source-only pre-alpha contract work has started with `docs/milestone-d-verify-citations-contract.md`. It defines `verify_citations` v1 as the citation-input, verification-config, grounding-source, and verification-report contract currently carried by `ethos verify`; schema/example validation checks that the minimal citation example and verification-report example stay coherent. Focused validation is `make milestone-d-verify-citations-contract PYTHON=/bin/python`. +- Milestone D `claim_kind_boundary` v1 contract prep has started with `docs/milestone-d-claim-kind-boundary-contract.md`. It defines the supported v1 claim-kind boundary currently carried by `ethos verify`; schema/example validation checks the inventory at `examples/verify/claim_kind_boundary_v1_contract.json`, and the repository guard checks that the non-v1 claim fixture and report golden stay coherent. Focused validation is `make milestone-d-claim-kind-boundary-contract PYTHON=/bin/python`. - Milestone D `grounding_source` v1 contract prep has started with `docs/milestone-d-grounding-source-contract.md`. It defines `grounding_source` as the parser-neutral evidence boundary currently carried by the `GroundingSource` trait and `ethos verify` report grounding metadata; schema/example validation checks the inventory at `examples/verify/grounding_source_v1_contract.json`, and the repository guard checks that it stays coherent with trait methods, current source implementations, focused verifier tests, and report goldens. Focused validation is `make milestone-d-grounding-source-contract PYTHON=/bin/python`. - Milestone D `capability_downgrade` v1 contract prep has started with `docs/milestone-d-capability-downgrade-contract.md`. It defines `capability_downgrade` as the grounding-source capability declaration to verification-report downgrade contract currently carried by `ethos verify`; schema/example validation checks the inventory at `examples/verify/capability_downgrade_v1_contract.json`, and the repository guard checks that it stays coherent with report goldens. Focused validation is `make milestone-d-capability-downgrade-contract PYTHON=/bin/python`. - Milestone D `opendataloader_adapter_shape` v1 contract prep has started with `docs/milestone-d-opendataloader-adapter-shape-contract.md`. It defines `opendataloader_adapter_shape` as the OpenDataLoader-style input-shape to `GroundingSource` contract currently carried by `ethos-grounding-opendataloader-json` and `ethos verify --grounding opendataloader-json`; schema/example validation checks the inventory at `examples/verify/opendataloader_adapter_shape_v1_contract.json`, and the repository guard checks that it stays coherent with adapter tests, CLI grounding tests, report goldens, and usage diagnostics. Focused validation is `make milestone-d-opendataloader-adapter-shape-contract PYTHON=/bin/python`. diff --git a/docs/milestone-d-claim-kind-boundary-contract.md b/docs/milestone-d-claim-kind-boundary-contract.md new file mode 100644 index 0000000..afcab14 --- /dev/null +++ b/docs/milestone-d-claim-kind-boundary-contract.md @@ -0,0 +1,59 @@ +# Milestone D `claim_kind_boundary` v1 Contract + +Status: source-only pre-alpha contract work for internal Milestone D continuation. + +This note defines the narrow `claim_kind_boundary` contract-prep slice for Milestone D. It does +not add new claim-kind support, a new command, a binding surface, Node surface, MCP surface, hosted +surface, crop behavior, or sandbox behavior. The current executable carrier remains `ethos verify`. + +`claim_kind_boundary` names the v1 boundary between supported literal claim kinds and explicit +non-v1 claim diagnostics. This keeps future claim-kind expansion from silently changing the +current trust loop. + +## Contract Surface + +`claim_kind_boundary` v1 is defined by the current schemas and fixtures: + +- `schemas/ethos-citations.schema.json` admits `quote`, `value`, `presence`, `table_cell`, + `region`, and `other` citation input kinds so non-v1 claims can be reported explicitly; +- `schemas/ethos-verification-config.schema.json` accepts only `quote`, `value`, `presence`, and + `table_cell` in `claim_kinds`; +- `schemas/ethos-verification-report.schema.json` reports non-v1 claim checks with + `unsupported_claim_kind`, `match_method: none`, no evidence, and `all_evidence_grounded: false`; +- `examples/verify/native_non_v1_claims_citations.json` and + `examples/verify/goldens/native_non_v1_claims_report.json` are the current fixture pair. + +The executable inventory is `examples/verify/claim_kind_boundary_v1_contract.json`. It binds the +supported and unsupported claim-kind sets to the existing schemas and the committed fixture pair. + +## Validation Target + +- `make milestone-d-claim-kind-boundary-contract PYTHON=/bin/python` + +The target runs the focused current verifier tests for claim-kind boundaries, schema/example +validation, status guards, roadmap guards, this contract guard, and whitespace diff checks. It +does not add or broaden verification behavior. + +## Boundaries Locked By This Slice + +- supported v1 claim kinds remain `quote`, `value`, `presence`, and `table_cell`; +- `region` and `other` remain accepted citation input kinds only so they can fail closed with + explicit diagnostics; +- verification configs cannot enable `region` or `other`; +- unsupported non-v1 checks carry no evidence and do not become semantic checks; +- unsupported non-v1 checks keep `all_evidence_grounded` false even when other checks ground. + +## Explicit Blockers For This Slice + +This first `claim_kind_boundary` slice does not add: + +- new claim-kind support; +- semantic, visual, arithmetic, or cross-region verification; +- a new command or binding surface; +- crop API changes; +- sandbox backend changes; +- foreign-adapter broadening beyond committed fixtures. + +Until those blockers are explicitly handled, public language remains limited to source-only +pre-alpha internal continuation, evidence grounding, diagnostics, fixture-backed validation, and +explicit blockers. diff --git a/docs/roadmap.md b/docs/roadmap.md index 0df9648..ee0011c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -22,6 +22,10 @@ current executable carrier remains `ethos verify`; the first D slice is a contract and fixture-backed validation boundary, not a new public command, binding, crop API, sandbox backend, Node beta, or MCP experimental scope. The source-only +[`claim_kind_boundary` v1 contract](milestone-d-claim-kind-boundary-contract.md) +binds the supported v1 claim-kind boundary to current citation/config/report +fixtures so non-v1 claim inputs remain explicit diagnostics until deliberately expanded. +The source-only [`grounding_source` v1 contract](milestone-d-grounding-source-contract.md) binds the parser-neutral evidence boundary to current native and foreign-source report grounding metadata without adding a new command or binding surface. @@ -54,7 +58,7 @@ binding surface. | A | weeks 1-8 | Contracts (5 schemas, c14n, deterministic profile), trust-boundary artifacts (`GroundingSource`, verification schemas, OpenDataLoader adapter stub, `ethos verify` CLI stub), PDFium Phase 1 spike, harness + competitor adapters, CLI skeleton | **Gate Zero**: ADR-0005 is accepted as `PROCEED` for internal Milestone B continuation. This is not public benchmark, release, package, production, or claim approval. | | B | weeks 9-14 | **`ethos verify` alpha first**: native Ethos JSON + synthetic and pinned real OpenDataLoader verification demos, stale fingerprint checks, capability-limited reports, deterministic evidence matching including split-quote coverage, explicit unsupported non-v1 claim reporting, adapter structure diagnostics; then reading order, blocks, headings, lists, Markdown/text exporters, Python wheel scaffold, quality dashboard, Windows x64 nightly determinism | [13-B exit checklist](milestone-b-exit-checklist.md) | | C | weeks 15-22 | Simple/bordered tables; RAG chunker + citations; non-text region coordinates; security report + default-chunk exclusion; debug overlay; internal benchmark snapshot | Current artifact-validation checkpoint recorded in [Milestone C closeout validation](validation/milestone-c-closeout-validation-2026-06-18.md); broader debug/crop/table follow-ups remain explicit | -| D | weeks 23-30 | [`verify_citations` v1](milestone-d-verify-citations-contract.md); [`crop_element` v1 contract prep](milestone-d-crop-element-contract.md); [`sandbox_subprocess` v1 contract prep](milestone-d-sandbox-subprocess-contract.md); crop API; sandbox/subprocess backend; Node beta and MCP experimental only if staffed or accepted by release-scope ADR | 13-D exit | +| D | weeks 23-30 | [`verify_citations` v1](milestone-d-verify-citations-contract.md); [`claim_kind_boundary` v1 contract prep](milestone-d-claim-kind-boundary-contract.md); [`crop_element` v1 contract prep](milestone-d-crop-element-contract.md); [`sandbox_subprocess` v1 contract prep](milestone-d-sandbox-subprocess-contract.md); crop API; sandbox/subprocess backend; Node beta and MCP experimental only if staffed or accepted by release-scope ADR | 13-D exit | | E | weeks 31-40 | Public benchmark report (reproducible, labeled tiers); PDFium Phase 2 project-maintained builds; stable CLI/Python docs; proof-of-trust demos; **Public Beta** | Release 1 claim audit + public-beta checkpoint | | F / Release 2 | post-E | Complex tables, formula/LaTeX, chart classification, optional enrichment modules (never base) | Scoped after E from beta fixtures | diff --git a/examples/verify/claim_kind_boundary_v1_contract.json b/examples/verify/claim_kind_boundary_v1_contract.json new file mode 100644 index 0000000..802dc0e --- /dev/null +++ b/examples/verify/claim_kind_boundary_v1_contract.json @@ -0,0 +1,34 @@ +{ + "schema_version": 1, + "contract": "claim_kind_boundary.v1", + "status": "source-only-pre-alpha", + "carrier": "ethos verify", + "supported_claim_kinds": ["quote", "value", "presence", "table_cell"], + "unsupported_claim_kinds": ["region", "other"], + "config_boundary": { + "schema": "schemas/ethos-verification-config.schema.json", + "accepted_claim_kinds": ["quote", "value", "presence", "table_cell"], + "rejected_claim_kinds": ["region", "other"], + "usage_error": "verification config claim_kinds must include only quote, value, presence, and table_cell" + }, + "report_case": { + "name": "native-non-v1-claims", + "citations": "examples/verify/native_non_v1_claims_citations.json", + "golden": "examples/verify/goldens/native_non_v1_claims_report.json", + "expected_statuses": ["grounded", "unsupported_claim_kind", "unsupported_claim_kind"], + "expected_reasons": ["unsupported_claim_kind", "unsupported_claim_kind"], + "expected_match_methods": ["presence_only", "none", "none"], + "expected_unsupported_claim_kinds": ["region", "other"], + "all_evidence_grounded": false, + "unsupported_checks_have_no_evidence": true, + "unsupported_checks_semantic_unverified": false + }, + "explicit_blockers": [ + "new claim-kind support", + "semantic, visual, arithmetic, or cross-region verification", + "a new command or binding surface", + "crop API changes", + "sandbox backend changes", + "foreign-adapter broadening beyond committed fixtures" + ] +} diff --git a/schemas/README.md b/schemas/README.md index 71dcebd..43307dd 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -15,6 +15,7 @@ bumps and downstream sign-off; output-changing heuristics are semver events (PRD | `ethos-crop-descriptor.schema.json` | crop descriptor JSON emitted by `ethos verify --crop-dir` | | `ethos-crop-element-request.schema.json` | source-only request envelope for Milestone D `crop_element` v1 contract work | | `ethos-verify-citations-contract.schema.json` | Milestone D `verify_citations` v1 source-only contract inventory | +| `ethos-claim-kind-boundary-contract.schema.json` | Milestone D `claim_kind_boundary` v1 source-only contract inventory | | `ethos-grounding-source-contract.schema.json` | Milestone D `grounding_source` v1 source-only contract inventory | | `ethos-capability-downgrade-contract.schema.json` | Milestone D `capability_downgrade` v1 source-only contract inventory | | `ethos-opendataloader-adapter-shape-contract.schema.json` | Milestone D `opendataloader_adapter_shape` v1 source-only contract inventory | @@ -45,6 +46,14 @@ Milestone D `verify_citations` v1 contract work is tracked in `examples/verify/verify_citations_v1_contract.json` is schema-validated here; its alignment with the executable case inventory and report goldens is checked by the Milestone D repository guard. +Milestone D `claim_kind_boundary` v1 contract work is tracked in +`docs/milestone-d-claim-kind-boundary-contract.md`. In this source-only pre-alpha slice, +`claim_kind_boundary` names the supported v1 claim-kind boundary currently carried by +`ethos verify`; it does not add new claim-kind support or a new command/binding surface. The +contract inventory at `examples/verify/claim_kind_boundary_v1_contract.json` is schema-validated +here; its alignment with the committed non-v1 claim fixture and report golden is checked by the +Milestone D repository guard. + Milestone D `grounding_source` v1 contract work is tracked in `docs/milestone-d-grounding-source-contract.md`. In this source-only pre-alpha slice, `grounding_source` names the parser-neutral evidence boundary currently carried by the diff --git a/schemas/ethos-claim-kind-boundary-contract.schema.json b/schemas/ethos-claim-kind-boundary-contract.schema.json new file mode 100644 index 0000000..796efea --- /dev/null +++ b/schemas/ethos-claim-kind-boundary-contract.schema.json @@ -0,0 +1,173 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:ethos:schema:claim-kind-boundary-contract:1", + "title": "Ethos claim_kind_boundary v1 contract inventory", + "description": "Source-only pre-alpha Milestone D inventory for the current claim_kind_boundary v1 contract carried by ethos verify. This validates inventory shape and vocabulary; fixture alignment stays in the repository guard.", + "type": "object", + "required": [ + "schema_version", + "contract", + "status", + "carrier", + "supported_claim_kinds", + "unsupported_claim_kinds", + "config_boundary", + "report_case", + "explicit_blockers" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "const": 1 }, + "contract": { "const": "claim_kind_boundary.v1" }, + "status": { "const": "source-only-pre-alpha" }, + "carrier": { "const": "ethos verify" }, + "supported_claim_kinds": { + "type": "array", + "minItems": 4, + "maxItems": 4, + "items": { "$ref": "#/$defs/supported_claim_kind" }, + "uniqueItems": true + }, + "unsupported_claim_kinds": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { "$ref": "#/$defs/unsupported_claim_kind" }, + "uniqueItems": true + }, + "config_boundary": { + "type": "object", + "required": [ + "schema", + "accepted_claim_kinds", + "rejected_claim_kinds", + "usage_error" + ], + "additionalProperties": false, + "properties": { + "schema": { "$ref": "#/$defs/repo_path" }, + "accepted_claim_kinds": { + "type": "array", + "minItems": 4, + "maxItems": 4, + "items": { "$ref": "#/$defs/supported_claim_kind" }, + "uniqueItems": true + }, + "rejected_claim_kinds": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { "$ref": "#/$defs/unsupported_claim_kind" }, + "uniqueItems": true + }, + "usage_error": { "type": "string", "minLength": 1 } + } + }, + "report_case": { + "type": "object", + "required": [ + "name", + "citations", + "golden", + "expected_statuses", + "expected_reasons", + "expected_match_methods", + "expected_unsupported_claim_kinds", + "all_evidence_grounded", + "unsupported_checks_have_no_evidence", + "unsupported_checks_semantic_unverified" + ], + "additionalProperties": false, + "properties": { + "name": { "$ref": "#/$defs/case_name" }, + "citations": { "$ref": "#/$defs/repo_path" }, + "golden": { "$ref": "#/$defs/repo_path" }, + "expected_statuses": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/check_status" } + }, + "expected_reasons": { + "type": "array", + "items": { "$ref": "#/$defs/check_reason" } + }, + "expected_match_methods": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/match_method" } + }, + "expected_unsupported_claim_kinds": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { "$ref": "#/$defs/unsupported_claim_kind" }, + "uniqueItems": true + }, + "all_evidence_grounded": { "const": false }, + "unsupported_checks_have_no_evidence": { "const": true }, + "unsupported_checks_semantic_unverified": { "const": false } + } + }, + "explicit_blockers": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + } + }, + "$defs": { + "case_name": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, + "repo_path": { "type": "string", "minLength": 1, "pattern": "^[A-Za-z0-9_./-]+$" }, + "supported_claim_kind": { + "enum": ["quote", "value", "presence", "table_cell"] + }, + "unsupported_claim_kind": { + "enum": ["region", "other"] + }, + "check_status": { + "enum": [ + "grounded", + "not_found", + "mismatch", + "stale", + "unsupported_claim_kind", + "capability_blocked", + "error" + ] + }, + "check_reason": { + "enum": [ + "missing_locator", + "missing_required_text", + "unsupported_claim_kind", + "stale_fingerprint", + "missing_source_fingerprint", + "missing_citation_fingerprint", + "missing_span_capability", + "missing_table_capability", + "unknown_coordinate_origin", + "element_not_found", + "span_not_found", + "page_not_found", + "bbox_not_found", + "missing_page_for_bbox", + "missing_table_cell_locator", + "table_not_found", + "table_cell_not_found", + "text_mismatch" + ] + }, + "match_method": { + "enum": [ + "exact_text", + "normalized_text", + "exact_text_contains", + "normalized_text_contains", + "table_cell_lookup", + "bbox_containment", + "presence_only", + "none" + ] + } + } +} diff --git a/schemas/validate_examples.py b/schemas/validate_examples.py index 5a77cb9..bb6f59f 100644 --- a/schemas/validate_examples.py +++ b/schemas/validate_examples.py @@ -86,6 +86,9 @@ ("ethos-verify-citations-contract.schema.json", [ ROOT / "examples" / "verify" / "verify_citations_v1_contract.json", ]), + ("ethos-claim-kind-boundary-contract.schema.json", [ + ROOT / "examples" / "verify" / "claim_kind_boundary_v1_contract.json", + ]), ("ethos-grounding-source-contract.schema.json", [ ROOT / "examples" / "verify" / "grounding_source_v1_contract.json", ]), From 6e4c88fcff237813aaa4bdd06720d700d28c592f Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 19:17:30 +0530 Subject: [PATCH 29/39] Guard sandbox worker outcome contract Signed-off-by: docushell-admin --- ...milestone_d_sandbox_subprocess_contract.py | 88 +++++++++++++++++++ crates/ethos-cli/tests/pdf_parse.rs | 1 + ...milestone-d-sandbox-subprocess-contract.md | 5 +- .../sandbox_subprocess_v1_contract.json | 56 ++++++++++-- ...os-sandbox-subprocess-contract.schema.json | 55 +++++++++++- 5 files changed, 197 insertions(+), 8 deletions(-) diff --git a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py index fb7b87a..eb2c3d4 100644 --- a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py +++ b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py @@ -51,6 +51,52 @@ "stable_error_envelope", "diagnostics_gated_stderr", ] +EXPECTED_FAILURES = { + "doc-parse-timeout": { + "exit_code": 10, + "error_code": "parse_timeout", + "error_message": "parse exceeded max_parse_ms", + "stdout": "empty", + "diagnostics": False, + }, + "fingerprint-timeout": { + "exit_code": 10, + "error_code": "parse_timeout", + "error_message": "parse exceeded max_parse_ms", + "stdout": "empty", + "diagnostics": False, + }, + "doc-parse-memory-limit": { + "exit_code": 11, + "error_code": "memory_limit_exceeded", + "error_message": "parse exceeded memory limit", + "stdout": "empty", + "diagnostics": False, + }, + "doc-parse-stable-error-envelope": { + "exit_code": 3, + "error_code": "invalid_pdf", + "error_message": "input does not contain a PDF header", + "stdout": "empty", + "diagnostics": False, + }, + "doc-parse-stderr-hidden-by-default": { + "exit_code": 12, + "error_code": "internal_error", + "error_message": "pdfium worker failed", + "stdout": "empty", + "diagnostics": False, + }, + "doc-parse-diagnostics-gated-stderr": { + "exit_code": 12, + "error_code": "internal_error", + "error_message": "pdfium worker failed", + "stdout": "empty", + "diagnostics": True, + "worker_exit_code": 101, + "worker_stderr": "native pdfium stderr sentinel\nsecond line", + }, +} def load_json(path: Path): @@ -333,6 +379,11 @@ def test_contract_inventory_matches_existing_worker_tests(self) -> None: self.assertEqual([], schema_errors(request_schema, request), case["name"]) self.assertEqual([], request_ref_drift_diagnostics(request), case["name"]) self.assertEqual([], request_case_diagnostics(request, case), case["name"]) + self.assertEqual( + EXPECTED_FAILURES[case["name"]], + case["expected_failure"], + case["name"], + ) self.assertIn(f"fn {case['test_filter']}()", test_source, case["name"]) def test_contract_inventory_case_order_is_deterministic(self) -> None: @@ -473,6 +524,43 @@ def test_inventory_worker_failure_tests_keep_stdout_empty(self) -> None: case["name"], ) + def test_inventory_worker_failure_tests_pin_expected_outcomes(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + test_source = PDF_PARSE_TESTS.read_text(encoding="utf-8") + + for case in inventory["cases"]: + failure = case["expected_failure"] + body = rust_test_body(test_source, case["test_filter"]) + self.assertIn( + f"assert_eq!(output.status.code(), Some({failure['exit_code']}));", + body, + case["name"], + ) + self.assertIn(failure["error_code"], body, case["name"]) + self.assertIn(failure["error_message"], body, case["name"]) + + if failure["diagnostics"]: + self.assertIn("--diagnostics", body, case["name"]) + self.assertIn( + f'error["diagnostics"]["pdfium_worker"]["exit_code"], {failure["worker_exit_code"]}', + body, + case["name"], + ) + self.assertIn( + failure["worker_stderr"].replace("\n", "\\n"), + body, + case["name"], + ) + else: + self.assertNotIn('error["diagnostics"]', body, case["name"]) + + if case["name"] == "doc-parse-stderr-hidden-by-default": + self.assertIn( + r'{\"error\":{\"code\":\"internal_error\",\"message\":\"pdfium worker failed\"}}\n', + body, + case["name"], + ) + def test_inventory_keeps_current_command_surfaces_narrow(self) -> None: inventory = load_json(CONTRACT_INVENTORY) diff --git a/crates/ethos-cli/tests/pdf_parse.rs b/crates/ethos-cli/tests/pdf_parse.rs index fd1e15c..629042a 100644 --- a/crates/ethos-cli/tests/pdf_parse.rs +++ b/crates/ethos-cli/tests/pdf_parse.rs @@ -529,6 +529,7 @@ fn pdf_fingerprint_timeout_kills_pdfium_worker() { assert!(output.stdout.is_empty()); let error: Value = serde_json::from_slice(&output.stderr).unwrap(); assert_eq!(error["error"]["code"], "parse_timeout"); + assert_eq!(error["error"]["message"], "parse exceeded max_parse_ms"); } #[cfg(debug_assertions)] diff --git a/docs/milestone-d-sandbox-subprocess-contract.md b/docs/milestone-d-sandbox-subprocess-contract.md index 1a8ed28..a935797 100644 --- a/docs/milestone-d-sandbox-subprocess-contract.md +++ b/docs/milestone-d-sandbox-subprocess-contract.md @@ -24,7 +24,8 @@ The current source-tree inventory for this contract boundary is `examples/sandbox/sandbox_subprocess_v1_contract.json`. It classifies existing worker tests that exercise timeout handling, memory-limit error reporting, stable error relay, and diagnostics-gated stderr behavior. Each inventory case binds to a request envelope under -`schemas/examples/sandbox-subprocess-*.example.json`. +`schemas/examples/sandbox-subprocess-*.example.json` and records the expected process exit code, +stable error code, stable error message, and diagnostics policy. Focused validation command: @@ -46,6 +47,8 @@ The v1 contract boundary is fail-closed and error-envelope-first: - request envelopes bind each failure case to the intended operation, timeout limit, diagnostics mode, failure-output policy, and c14n-derived request identity `ethos.sandbox_subprocess_request_ref.v1`; +- inventory outcome fields bind each failure case to its current exit code and stable error + envelope; - stdout remains empty on worker failures covered by this contract inventory. ## Explicit Blockers For This Slice diff --git a/examples/sandbox/sandbox_subprocess_v1_contract.json b/examples/sandbox/sandbox_subprocess_v1_contract.json index 238a64b..9af52c9 100644 --- a/examples/sandbox/sandbox_subprocess_v1_contract.json +++ b/examples/sandbox/sandbox_subprocess_v1_contract.json @@ -9,42 +9,86 @@ "command_surface": "ethos doc parse", "request": "schemas/examples/sandbox-subprocess-doc-parse-timeout-request.example.json", "test_filter": "doc_parse_timeout_kills_pdfium_worker", - "boundary": "max_parse_ms_timeout" + "boundary": "max_parse_ms_timeout", + "expected_failure": { + "exit_code": 10, + "error_code": "parse_timeout", + "error_message": "parse exceeded max_parse_ms", + "stdout": "empty", + "diagnostics": false + } }, { "name": "fingerprint-timeout", "command_surface": "ethos fingerprint", "request": "schemas/examples/sandbox-subprocess-fingerprint-timeout-request.example.json", "test_filter": "pdf_fingerprint_timeout_kills_pdfium_worker", - "boundary": "max_parse_ms_timeout" + "boundary": "max_parse_ms_timeout", + "expected_failure": { + "exit_code": 10, + "error_code": "parse_timeout", + "error_message": "parse exceeded max_parse_ms", + "stdout": "empty", + "diagnostics": false + } }, { "name": "doc-parse-memory-limit", "command_surface": "ethos doc parse", "request": "schemas/examples/sandbox-subprocess-doc-parse-request.example.json", "test_filter": "doc_parse_memory_limit_worker_failure_is_stable", - "boundary": "memory_limit_error" + "boundary": "memory_limit_error", + "expected_failure": { + "exit_code": 11, + "error_code": "memory_limit_exceeded", + "error_message": "parse exceeded memory limit", + "stdout": "empty", + "diagnostics": false + } }, { "name": "doc-parse-stable-error-envelope", "command_surface": "ethos doc parse", "request": "schemas/examples/sandbox-subprocess-doc-parse-request.example.json", "test_filter": "doc_parse_relays_worker_stable_error_envelope", - "boundary": "stable_error_envelope" + "boundary": "stable_error_envelope", + "expected_failure": { + "exit_code": 3, + "error_code": "invalid_pdf", + "error_message": "input does not contain a PDF header", + "stdout": "empty", + "diagnostics": false + } }, { "name": "doc-parse-stderr-hidden-by-default", "command_surface": "ethos doc parse", "request": "schemas/examples/sandbox-subprocess-doc-parse-request.example.json", "test_filter": "doc_parse_non_envelope_worker_failure_stays_canonical_without_diagnostics", - "boundary": "diagnostics_gated_stderr" + "boundary": "diagnostics_gated_stderr", + "expected_failure": { + "exit_code": 12, + "error_code": "internal_error", + "error_message": "pdfium worker failed", + "stdout": "empty", + "diagnostics": false + } }, { "name": "doc-parse-diagnostics-gated-stderr", "command_surface": "ethos doc parse", "request": "schemas/examples/sandbox-subprocess-doc-parse-diagnostics-request.example.json", "test_filter": "doc_parse_non_envelope_worker_failure_includes_stderr_with_diagnostics", - "boundary": "diagnostics_gated_stderr" + "boundary": "diagnostics_gated_stderr", + "expected_failure": { + "exit_code": 12, + "error_code": "internal_error", + "error_message": "pdfium worker failed", + "stdout": "empty", + "diagnostics": true, + "worker_exit_code": 101, + "worker_stderr": "native pdfium stderr sentinel\nsecond line" + } } ], "explicit_blockers": [ diff --git a/schemas/ethos-sandbox-subprocess-contract.schema.json b/schemas/ethos-sandbox-subprocess-contract.schema.json index 527f9de..2d5f532 100644 --- a/schemas/ethos-sandbox-subprocess-contract.schema.json +++ b/schemas/ethos-sandbox-subprocess-contract.schema.json @@ -28,7 +28,8 @@ "command_surface", "request", "test_filter", - "boundary" + "boundary", + "expected_failure" ], "additionalProperties": false, "properties": { @@ -45,6 +46,58 @@ "stable_error_envelope", "diagnostics_gated_stderr" ] + }, + "expected_failure": { + "type": "object", + "required": [ + "exit_code", + "error_code", + "error_message", + "stdout", + "diagnostics" + ], + "additionalProperties": false, + "properties": { + "exit_code": { "type": "integer", "minimum": 1 }, + "error_code": { + "enum": [ + "parse_timeout", + "memory_limit_exceeded", + "invalid_pdf", + "internal_error" + ] + }, + "error_message": { "type": "string", "minLength": 1 }, + "stdout": { "const": "empty" }, + "diagnostics": { "type": "boolean" }, + "worker_exit_code": { "type": "integer", "minimum": 1 }, + "worker_stderr": { "type": "string", "minLength": 1 } + }, + "allOf": [ + { + "if": { + "properties": { "diagnostics": { "const": true } }, + "required": ["diagnostics"] + }, + "then": { + "required": ["worker_exit_code", "worker_stderr"] + } + }, + { + "if": { + "properties": { "diagnostics": { "const": false } }, + "required": ["diagnostics"] + }, + "then": { + "not": { + "anyOf": [ + { "required": ["worker_exit_code"] }, + { "required": ["worker_stderr"] } + ] + } + } + } + ] } } } From 8b5873c073f12e07835b6d7a26d172e6a2828c04 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 19:25:19 +0530 Subject: [PATCH 30/39] Guard crop element diagnostic contract Signed-off-by: docushell-admin --- .../test_milestone_d_crop_element_contract.py | 246 ++++++++++++++++++ examples/crop/crop_element_v1_contract.json | 103 ++++++++ .../ethos-crop-element-contract.schema.json | 48 ++++ 3 files changed, 397 insertions(+) diff --git a/.github/scripts/test_milestone_d_crop_element_contract.py b/.github/scripts/test_milestone_d_crop_element_contract.py index 7ecc9ca..17a4f9d 100644 --- a/.github/scripts/test_milestone_d_crop_element_contract.py +++ b/.github/scripts/test_milestone_d_crop_element_contract.py @@ -49,6 +49,117 @@ "foreign-adapter crop coordinate hardening", "cross-platform rendered-crop byte identity claims", ] +EXPECTED_DIAGNOSTIC_MESSAGES = [ + "request_ref is missing", + "request_ref does not match crop element request identity tuple", + "descriptor must bind exactly one logical check id", + "descriptor crop_ref does not match logical identity tuple", + "request document_fingerprint does not match document fingerprint", + "descriptor document_fingerprint does not match request", + "request element_id does not match contract inventory case", + "request element_id does not resolve in document", + "resolved element is missing page", + "descriptor page does not match resolved element", + "resolved element is missing bbox", + "descriptor bbox does not match resolved element", + "request rendering does not match contract inventory case", + "descriptor rendering_status does not match request", +] +EXPECTED_DIAGNOSTIC_CASES = [ + { + "name": "request-ref-missing", + "surface": "request_ref", + "expected_diagnostics": ["request_ref is missing"], + }, + { + "name": "request-ref-identity-drift", + "surface": "request_ref", + "expected_diagnostics": [ + "request_ref does not match crop element request identity tuple" + ], + }, + { + "name": "request-element-unresolved", + "surface": "request_binding", + "expected_diagnostics": [ + "request element_id does not match contract inventory case", + "request element_id does not resolve in document", + ], + }, + { + "name": "request-document-fingerprint-mismatch", + "surface": "request_binding", + "expected_diagnostics": [ + "request document_fingerprint does not match document fingerprint", + "descriptor document_fingerprint does not match request", + ], + }, + { + "name": "request-element-mismatch", + "surface": "request_binding", + "expected_diagnostics": [ + "request element_id does not match contract inventory case", + "descriptor bbox does not match resolved element", + ], + }, + { + "name": "descriptor-document-fingerprint-mismatch", + "surface": "request_binding", + "expected_diagnostics": [ + "descriptor document_fingerprint does not match request", + ], + }, + { + "name": "descriptor-page-mismatch", + "surface": "request_binding", + "expected_diagnostics": [ + "descriptor page does not match resolved element", + ], + }, + { + "name": "descriptor-bbox-mismatch", + "surface": "request_binding", + "expected_diagnostics": [ + "descriptor bbox does not match resolved element", + ], + }, + { + "name": "request-rendering-mismatch", + "surface": "request_binding", + "expected_diagnostics": [ + "request rendering does not match contract inventory case", + ], + }, + { + "name": "descriptor-rendering-status-mismatch", + "surface": "request_binding", + "expected_diagnostics": [ + "descriptor rendering_status does not match request", + ], + }, + { + "name": "resolved-element-missing-page", + "surface": "request_binding", + "expected_diagnostics": ["resolved element is missing page"], + }, + { + "name": "resolved-element-missing-bbox", + "surface": "request_binding", + "expected_diagnostics": ["resolved element is missing bbox"], + }, + { + "name": "descriptor-crop-ref-identity-drift", + "surface": "descriptor_ref", + "expected_diagnostics": [ + "descriptor crop_ref does not match logical identity tuple" + ], + }, + { + "name": "descriptor-ambiguous-checks", + "surface": "descriptor_ref", + "expected_diagnostics": ["descriptor must bind exactly one logical check id"], + }, +] def load_json(path: Path): @@ -199,6 +310,106 @@ def request_case_diagnostics( return diagnostics +def inventory_diagnostic_outputs(inventory: dict) -> dict[str, list[str]]: + case = inventory["cases"][0] + request = load_json(ROOT / case["request"]) + document = load_json(ROOT / case["document"]) + descriptor = load_json(ROOT / case["descriptor"]) + + missing_request_ref = dict(request) + del missing_request_ref["request_ref"] + + stale_request_ref = dict(request, request_ref="request-" + "0" * 64) + unknown_element_request = dict(request, element_id="e999999") + stale_request = dict(request, document_fingerprint="sha256:" + "0" * 64) + mismatched_element_request = dict(request, element_id="e000001") + mismatched_rendering_case = dict(case, rendering_status="rendered") + + stale_descriptor_fingerprint = dict( + descriptor, + document_fingerprint="sha256:" + "0" * 64, + ) + stale_descriptor_page = dict(descriptor, page="p999999") + stale_descriptor_bbox = dict(descriptor, bbox=[0, 0, 1, 1]) + stale_descriptor_rendering = dict(descriptor, rendering_status="rendered") + + document_without_page = deepcopy(document) + del elements_by_id(document_without_page)[request["element_id"]]["page"] + + document_without_bbox = deepcopy(document) + del elements_by_id(document_without_bbox)[request["element_id"]]["bbox"] + + stale_crop_ref = dict(descriptor, crop_ref="crop-" + "0" * 64 + ".json") + ambiguous_checks = dict(descriptor, check_ids=["v0001", "v0002"]) + + return { + "request-ref-missing": request_ref_drift_diagnostics(missing_request_ref), + "request-ref-identity-drift": request_ref_drift_diagnostics(stale_request_ref), + "request-element-unresolved": request_case_diagnostics( + unknown_element_request, + document, + descriptor, + case, + ), + "request-document-fingerprint-mismatch": request_case_diagnostics( + stale_request, + document, + descriptor, + case, + ), + "request-element-mismatch": request_case_diagnostics( + mismatched_element_request, + document, + descriptor, + case, + ), + "descriptor-document-fingerprint-mismatch": request_case_diagnostics( + request, + document, + stale_descriptor_fingerprint, + case, + ), + "descriptor-page-mismatch": request_case_diagnostics( + request, + document, + stale_descriptor_page, + case, + ), + "descriptor-bbox-mismatch": request_case_diagnostics( + request, + document, + stale_descriptor_bbox, + case, + ), + "request-rendering-mismatch": request_case_diagnostics( + request, + document, + descriptor, + mismatched_rendering_case, + ), + "descriptor-rendering-status-mismatch": request_case_diagnostics( + request, + document, + stale_descriptor_rendering, + case, + ), + "resolved-element-missing-page": request_case_diagnostics( + request, + document_without_page, + descriptor, + case, + ), + "resolved-element-missing-bbox": request_case_diagnostics( + request, + document_without_bbox, + descriptor, + case, + ), + "descriptor-crop-ref-identity-drift": crop_ref_drift_diagnostics(stale_crop_ref), + "descriptor-ambiguous-checks": crop_ref_drift_diagnostics(ambiguous_checks), + } + + class MilestoneDCropElementContractTests(unittest.TestCase): def test_target_is_declared_phony(self) -> None: text = makefile_text() @@ -403,6 +614,41 @@ def test_contract_inventory_binds_element_to_descriptor_example(self) -> None: self.assertEqual([], crop_ref_drift_diagnostics(descriptor), case["name"]) self.assertEqual([], request_case_diagnostics(request, document, descriptor, case)) + def test_contract_inventory_pins_fail_closed_diagnostics(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + + self.assertEqual(EXPECTED_DIAGNOSTIC_CASES, inventory["diagnostic_cases"]) + + diagnostic_names = [case["name"] for case in inventory["diagnostic_cases"]] + self.assertEqual(len(diagnostic_names), len(set(diagnostic_names))) + self.assertEqual( + ["descriptor_ref", "request_binding", "request_ref"], + sorted({case["surface"] for case in inventory["diagnostic_cases"]}), + ) + self.assertEqual( + set(EXPECTED_DIAGNOSTIC_MESSAGES), + { + diagnostic + for case in inventory["diagnostic_cases"] + for diagnostic in case["expected_diagnostics"] + }, + ) + + def test_contract_inventory_diagnostics_match_current_helpers(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + actual_diagnostics = inventory_diagnostic_outputs(inventory) + + self.assertEqual( + {case["name"] for case in inventory["diagnostic_cases"]}, + set(actual_diagnostics), + ) + for case in inventory["diagnostic_cases"]: + self.assertEqual( + case["expected_diagnostics"], + actual_diagnostics[case["name"]], + case["name"], + ) + def test_request_binding_guard_fails_closed_on_stale_or_unresolved_inputs(self) -> None: inventory = load_json(CONTRACT_INVENTORY) case = inventory["cases"][0] diff --git a/examples/crop/crop_element_v1_contract.json b/examples/crop/crop_element_v1_contract.json index 992a85e..e454f7f 100644 --- a/examples/crop/crop_element_v1_contract.json +++ b/examples/crop/crop_element_v1_contract.json @@ -13,6 +13,109 @@ "rendering_status": "descriptor_only" } ], + "diagnostic_cases": [ + { + "name": "request-ref-missing", + "surface": "request_ref", + "expected_diagnostics": [ + "request_ref is missing" + ] + }, + { + "name": "request-ref-identity-drift", + "surface": "request_ref", + "expected_diagnostics": [ + "request_ref does not match crop element request identity tuple" + ] + }, + { + "name": "request-element-unresolved", + "surface": "request_binding", + "expected_diagnostics": [ + "request element_id does not match contract inventory case", + "request element_id does not resolve in document" + ] + }, + { + "name": "request-document-fingerprint-mismatch", + "surface": "request_binding", + "expected_diagnostics": [ + "request document_fingerprint does not match document fingerprint", + "descriptor document_fingerprint does not match request" + ] + }, + { + "name": "request-element-mismatch", + "surface": "request_binding", + "expected_diagnostics": [ + "request element_id does not match contract inventory case", + "descriptor bbox does not match resolved element" + ] + }, + { + "name": "descriptor-document-fingerprint-mismatch", + "surface": "request_binding", + "expected_diagnostics": [ + "descriptor document_fingerprint does not match request" + ] + }, + { + "name": "descriptor-page-mismatch", + "surface": "request_binding", + "expected_diagnostics": [ + "descriptor page does not match resolved element" + ] + }, + { + "name": "descriptor-bbox-mismatch", + "surface": "request_binding", + "expected_diagnostics": [ + "descriptor bbox does not match resolved element" + ] + }, + { + "name": "request-rendering-mismatch", + "surface": "request_binding", + "expected_diagnostics": [ + "request rendering does not match contract inventory case" + ] + }, + { + "name": "descriptor-rendering-status-mismatch", + "surface": "request_binding", + "expected_diagnostics": [ + "descriptor rendering_status does not match request" + ] + }, + { + "name": "resolved-element-missing-page", + "surface": "request_binding", + "expected_diagnostics": [ + "resolved element is missing page" + ] + }, + { + "name": "resolved-element-missing-bbox", + "surface": "request_binding", + "expected_diagnostics": [ + "resolved element is missing bbox" + ] + }, + { + "name": "descriptor-crop-ref-identity-drift", + "surface": "descriptor_ref", + "expected_diagnostics": [ + "descriptor crop_ref does not match logical identity tuple" + ] + }, + { + "name": "descriptor-ambiguous-checks", + "surface": "descriptor_ref", + "expected_diagnostics": [ + "descriptor must bind exactly one logical check id" + ] + } + ], "explicit_blockers": [ "a first-class `crop_element` CLI command or binding surface", "Python, Node, MCP, or hosted crop API surfaces", diff --git a/schemas/ethos-crop-element-contract.schema.json b/schemas/ethos-crop-element-contract.schema.json index 4a4e7c7..50a1ac2 100644 --- a/schemas/ethos-crop-element-contract.schema.json +++ b/schemas/ethos-crop-element-contract.schema.json @@ -10,6 +10,7 @@ "status", "carrier", "cases", + "diagnostic_cases", "explicit_blockers" ], "additionalProperties": false, @@ -42,6 +43,35 @@ } } }, + "diagnostic_cases": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "name", + "surface", + "expected_diagnostics" + ], + "additionalProperties": false, + "properties": { + "name": { "$ref": "#/$defs/case_name" }, + "surface": { + "enum": [ + "request_ref", + "request_binding", + "descriptor_ref" + ] + }, + "expected_diagnostics": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/diagnostic_message" }, + "uniqueItems": true + } + } + } + }, "explicit_blockers": { "type": "array", "minItems": 1, @@ -51,6 +81,24 @@ }, "$defs": { "case_name": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, + "diagnostic_message": { + "enum": [ + "request_ref is missing", + "request_ref does not match crop element request identity tuple", + "descriptor must bind exactly one logical check id", + "descriptor crop_ref does not match logical identity tuple", + "request document_fingerprint does not match document fingerprint", + "descriptor document_fingerprint does not match request", + "request element_id does not match contract inventory case", + "request element_id does not resolve in document", + "resolved element is missing page", + "descriptor page does not match resolved element", + "resolved element is missing bbox", + "descriptor bbox does not match resolved element", + "request rendering does not match contract inventory case", + "descriptor rendering_status does not match request" + ] + }, "repo_path": { "type": "string", "minLength": 1, "pattern": "^[A-Za-z0-9_./-]+$" } } } From da51ed6749dc7311c47343ec98a4d816ce57f54a Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 19:30:23 +0530 Subject: [PATCH 31/39] Guard verify citations usage diagnostics Signed-off-by: docushell-admin --- ...t_milestone_d_verify_citations_contract.py | 54 +++++++++++++++++++ .../verify/verify_citations_v1_contract.json | 26 +++++++++ ...thos-verify-citations-contract.schema.json | 29 ++++++++++ 3 files changed, 109 insertions(+) diff --git a/.github/scripts/test_milestone_d_verify_citations_contract.py b/.github/scripts/test_milestone_d_verify_citations_contract.py index 7b8d3ae..7bda11d 100644 --- a/.github/scripts/test_milestone_d_verify_citations_contract.py +++ b/.github/scripts/test_milestone_d_verify_citations_contract.py @@ -44,6 +44,32 @@ "sandbox/subprocess backend expansion", "semantic or arithmetic verification", ] +EXPECTED_USAGE_DIAGNOSTIC_CASES = [ + { + "name": "invalid-table-cell-citation", + "exit_code": 2, + "stdout": "empty", + "stderr_contains": "table_cell citation must include table_id and cell", + }, + { + "name": "invalid-bbox-citation", + "exit_code": 2, + "stdout": "empty", + "stderr_contains": "citation bbox requires page unless another target locator is present", + }, + { + "name": "opendataloader-malformed-bbox-input", + "exit_code": 2, + "stdout": "empty", + "stderr_contains": "opendataloader-json adapter: bbox is malformed (x0>x1 or y0>y1)", + }, + { + "name": "opendataloader-unknown-page-input", + "exit_code": 2, + "stdout": "empty", + "stderr_contains": "opendataloader-json adapter: element.page references unknown page", + }, +] def contract_text() -> str: @@ -133,6 +159,18 @@ def derived_category(report: dict) -> str: return "diagnostic-non-grounded" +def expected_usage_diagnostic_cases(cases: dict) -> list[dict]: + return [ + { + "name": case["name"], + "exit_code": 2, + "stdout": "empty", + "stderr_contains": case["stderr_contains"], + } + for case in cases["usage_error_cases"] + ] + + class MilestoneDVerifyCitationsContractTests(unittest.TestCase): def test_target_is_declared_phony(self) -> None: text = makefile_text() @@ -258,6 +296,22 @@ def test_contract_inventory_matches_executable_case_inventory(self) -> None: assert_unique(self, usage_case_names, "cases.json usage_error_cases") self.assertEqual(inventory["usage_error_cases"], usage_case_names) + usage_diagnostic_names = case_names(inventory["usage_diagnostic_cases"]) + assert_unique( + self, + usage_diagnostic_names, + "contract inventory usage_diagnostic_cases", + ) + self.assertEqual(usage_case_names, usage_diagnostic_names) + self.assertEqual( + EXPECTED_USAGE_DIAGNOSTIC_CASES, + inventory["usage_diagnostic_cases"], + ) + self.assertEqual( + expected_usage_diagnostic_cases(cases), + inventory["usage_diagnostic_cases"], + ) + summary_case_names = case_names(cases["summary_cases"]) assert_unique(self, summary_case_names, "cases.json summary_cases") self.assertEqual(inventory["summary_cases"], summary_case_names) diff --git a/examples/verify/verify_citations_v1_contract.json b/examples/verify/verify_citations_v1_contract.json index 9365602..9d48cfd 100644 --- a/examples/verify/verify_citations_v1_contract.json +++ b/examples/verify/verify_citations_v1_contract.json @@ -81,6 +81,32 @@ "opendataloader-malformed-bbox-input", "opendataloader-unknown-page-input" ], + "usage_diagnostic_cases": [ + { + "name": "invalid-table-cell-citation", + "exit_code": 2, + "stdout": "empty", + "stderr_contains": "table_cell citation must include table_id and cell" + }, + { + "name": "invalid-bbox-citation", + "exit_code": 2, + "stdout": "empty", + "stderr_contains": "citation bbox requires page unless another target locator is present" + }, + { + "name": "opendataloader-malformed-bbox-input", + "exit_code": 2, + "stdout": "empty", + "stderr_contains": "opendataloader-json adapter: bbox is malformed (x0>x1 or y0>y1)" + }, + { + "name": "opendataloader-unknown-page-input", + "exit_code": 2, + "stdout": "empty", + "stderr_contains": "opendataloader-json adapter: element.page references unknown page" + } + ], "summary_cases": ["native-ungrounded-summary"], "explicit_blockers": [ "a new `verify_citations` CLI alias", diff --git a/schemas/ethos-verify-citations-contract.schema.json b/schemas/ethos-verify-citations-contract.schema.json index 83be6e3..8e4de25 100644 --- a/schemas/ethos-verify-citations-contract.schema.json +++ b/schemas/ethos-verify-citations-contract.schema.json @@ -11,6 +11,7 @@ "carrier", "report_cases", "usage_error_cases", + "usage_diagnostic_cases", "summary_cases", "explicit_blockers" ], @@ -64,6 +65,26 @@ "items": { "$ref": "#/$defs/case_name" }, "uniqueItems": true }, + "usage_diagnostic_cases": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "name", + "exit_code", + "stdout", + "stderr_contains" + ], + "additionalProperties": false, + "properties": { + "name": { "$ref": "#/$defs/case_name" }, + "exit_code": { "const": 2 }, + "stdout": { "const": "empty" }, + "stderr_contains": { "$ref": "#/$defs/usage_diagnostic_message" } + } + } + }, "summary_cases": { "type": "array", "minItems": 1, @@ -111,6 +132,14 @@ "table_cell_not_found", "text_mismatch" ] + }, + "usage_diagnostic_message": { + "enum": [ + "table_cell citation must include table_id and cell", + "citation bbox requires page unless another target locator is present", + "opendataloader-json adapter: bbox is malformed (x0>x1 or y0>y1)", + "opendataloader-json adapter: element.page references unknown page" + ] } } } From 23edd737a759c6d1a3a9595041f83201b62cc6af Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 19:34:46 +0530 Subject: [PATCH 32/39] Guard sandbox request diagnostics Signed-off-by: docushell-admin --- ...milestone_d_sandbox_subprocess_contract.py | 169 ++++++++++++++++++ ...milestone-d-sandbox-subprocess-contract.md | 6 +- .../sandbox_subprocess_v1_contract.json | 65 +++++++ ...os-sandbox-subprocess-contract.schema.json | 42 +++++ 4 files changed, 281 insertions(+), 1 deletion(-) diff --git a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py index eb2c3d4..e072e56 100644 --- a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py +++ b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py @@ -51,6 +51,68 @@ "stable_error_envelope", "diagnostics_gated_stderr", ] +EXPECTED_DIAGNOSTIC_MESSAGES = [ + "request_ref is missing", + "request_ref does not match sandbox subprocess request identity tuple", + "request operation does not match command surface", + "request diagnostics does not match inventory case", + "request stderr policy does not match diagnostics mode", + "request failure stdout policy is not empty", + "request max_parse_ms does not match inventory case", + "doc_parse request must bind explicit page selection", + "fingerprint request must not carry page selection", +] +EXPECTED_DIAGNOSTIC_CASES = [ + { + "name": "request-ref-missing", + "surface": "request_ref", + "expected_diagnostics": ["request_ref is missing"], + }, + { + "name": "request-ref-identity-drift", + "surface": "request_ref", + "expected_diagnostics": [ + "request_ref does not match sandbox subprocess request identity tuple" + ], + }, + { + "name": "request-operation-mismatch", + "surface": "request_policy", + "expected_diagnostics": ["request operation does not match command surface"], + }, + { + "name": "request-diagnostics-mismatch", + "surface": "request_policy", + "expected_diagnostics": ["request diagnostics does not match inventory case"], + }, + { + "name": "request-stderr-policy-mismatch", + "surface": "request_policy", + "expected_diagnostics": [ + "request stderr policy does not match diagnostics mode" + ], + }, + { + "name": "request-stdout-policy-mismatch", + "surface": "request_policy", + "expected_diagnostics": ["request failure stdout policy is not empty"], + }, + { + "name": "request-max-parse-ms-mismatch", + "surface": "request_policy", + "expected_diagnostics": ["request max_parse_ms does not match inventory case"], + }, + { + "name": "doc-parse-page-selection-missing", + "surface": "request_policy", + "expected_diagnostics": ["doc_parse request must bind explicit page selection"], + }, + { + "name": "fingerprint-page-selection-present", + "surface": "request_policy", + "expected_diagnostics": ["fingerprint request must not carry page selection"], + }, +] EXPECTED_FAILURES = { "doc-parse-timeout": { "exit_code": 10, @@ -204,6 +266,76 @@ def request_case_diagnostics(request: dict, case: dict) -> list[str]: return diagnostics +def contract_case_by_name(inventory: dict, name: str) -> dict: + return next(case for case in inventory["cases"] if case["name"] == name) + + +def inventory_diagnostic_outputs(inventory: dict) -> dict[str, list[str]]: + doc_parse_case = contract_case_by_name(inventory, "doc-parse-stable-error-envelope") + timeout_case = contract_case_by_name(inventory, "doc-parse-timeout") + diagnostics_case = contract_case_by_name(inventory, "doc-parse-diagnostics-gated-stderr") + fingerprint_case = contract_case_by_name(inventory, "fingerprint-timeout") + + doc_parse_request = load_json(ROOT / doc_parse_case["request"]) + timeout_request = load_json(ROOT / timeout_case["request"]) + diagnostics_request = load_json(ROOT / diagnostics_case["request"]) + fingerprint_request = load_json(ROOT / fingerprint_case["request"]) + + missing_request_ref = dict(doc_parse_request) + del missing_request_ref["request_ref"] + + stale_request_ref = dict(doc_parse_request, request_ref="request-" + "0" * 64) + + wrong_operation = dict(timeout_request, operation="fingerprint") + del wrong_operation["page_selection"] + + diagnostics_disabled = dict(diagnostics_request, diagnostics=False) + wrong_stderr_policy = dict( + diagnostics_request, + stderr_policy="stable_error_envelope", + ) + stdout_not_empty = dict(diagnostics_request, stdout_on_failure="not-empty") + wrong_timeout = dict(diagnostics_request, limits={"max_parse_ms": 25}) + + doc_parse_without_pages = dict(doc_parse_request) + del doc_parse_without_pages["page_selection"] + + fingerprint_with_pages = dict(fingerprint_request, page_selection="all") + + return { + "request-ref-missing": request_ref_drift_diagnostics(missing_request_ref), + "request-ref-identity-drift": request_ref_drift_diagnostics(stale_request_ref), + "request-operation-mismatch": request_case_diagnostics( + wrong_operation, + timeout_case, + ), + "request-diagnostics-mismatch": request_case_diagnostics( + diagnostics_disabled, + diagnostics_case, + ), + "request-stderr-policy-mismatch": request_case_diagnostics( + wrong_stderr_policy, + diagnostics_case, + ), + "request-stdout-policy-mismatch": request_case_diagnostics( + stdout_not_empty, + diagnostics_case, + ), + "request-max-parse-ms-mismatch": request_case_diagnostics( + wrong_timeout, + diagnostics_case, + ), + "doc-parse-page-selection-missing": request_case_diagnostics( + doc_parse_without_pages, + doc_parse_case, + ), + "fingerprint-page-selection-present": request_case_diagnostics( + fingerprint_with_pages, + fingerprint_case, + ), + } + + def rust_test_body(source: str, test_name: str) -> str: match = re.search( rf"(?:#\[[^\n]+\]\n)*\s*#\[test\]\s*fn {re.escape(test_name)}\(\) \{{", @@ -294,10 +426,12 @@ def test_contract_pins_fail_closed_boundaries(self) -> None: "`schemas/examples/sandbox-subprocess-*.example.json`", "`request_ref`", "`ethos.sandbox_subprocess_request_ref.v1`", + "request identity and request-policy diagnostics", "stable worker error envelopes are relayed", "non-envelope worker stderr is hidden by default", "explicit diagnostics", "request envelopes bind each failure case", + "source-tree fixture validation pins expected diagnostics", "stdout remains empty on worker failures", "`make milestone-d-sandbox-subprocess-contract PYTHON=/bin/python`", ]: @@ -386,6 +520,41 @@ def test_contract_inventory_matches_existing_worker_tests(self) -> None: ) self.assertIn(f"fn {case['test_filter']}()", test_source, case["name"]) + def test_contract_inventory_pins_fail_closed_diagnostics(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + + self.assertEqual(EXPECTED_DIAGNOSTIC_CASES, inventory["diagnostic_cases"]) + + diagnostic_names = [case["name"] for case in inventory["diagnostic_cases"]] + self.assertEqual(len(diagnostic_names), len(set(diagnostic_names))) + self.assertEqual( + ["request_policy", "request_ref"], + sorted({case["surface"] for case in inventory["diagnostic_cases"]}), + ) + self.assertEqual( + set(EXPECTED_DIAGNOSTIC_MESSAGES), + { + diagnostic + for case in inventory["diagnostic_cases"] + for diagnostic in case["expected_diagnostics"] + }, + ) + + def test_contract_inventory_diagnostics_match_current_helpers(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + actual_diagnostics = inventory_diagnostic_outputs(inventory) + + self.assertEqual( + {case["name"] for case in inventory["diagnostic_cases"]}, + set(actual_diagnostics), + ) + for case in inventory["diagnostic_cases"]: + self.assertEqual( + case["expected_diagnostics"], + actual_diagnostics[case["name"]], + case["name"], + ) + def test_contract_inventory_case_order_is_deterministic(self) -> None: inventory = load_json(CONTRACT_INVENTORY) diff --git a/docs/milestone-d-sandbox-subprocess-contract.md b/docs/milestone-d-sandbox-subprocess-contract.md index a935797..30190b7 100644 --- a/docs/milestone-d-sandbox-subprocess-contract.md +++ b/docs/milestone-d-sandbox-subprocess-contract.md @@ -25,7 +25,9 @@ The current source-tree inventory for this contract boundary is exercise timeout handling, memory-limit error reporting, stable error relay, and diagnostics-gated stderr behavior. Each inventory case binds to a request envelope under `schemas/examples/sandbox-subprocess-*.example.json` and records the expected process exit code, -stable error code, stable error message, and diagnostics policy. +stable error code, stable error message, and diagnostics policy. The inventory also records the +request identity and request-policy diagnostics that the source-tree guard exercises for this +contract boundary. Focused validation command: @@ -47,6 +49,8 @@ The v1 contract boundary is fail-closed and error-envelope-first: - request envelopes bind each failure case to the intended operation, timeout limit, diagnostics mode, failure-output policy, and c14n-derived request identity `ethos.sandbox_subprocess_request_ref.v1`; +- source-tree fixture validation pins expected diagnostics for request identity drift and request + policy drift; - inventory outcome fields bind each failure case to its current exit code and stable error envelope; - stdout remains empty on worker failures covered by this contract inventory. diff --git a/examples/sandbox/sandbox_subprocess_v1_contract.json b/examples/sandbox/sandbox_subprocess_v1_contract.json index 9af52c9..c756c2a 100644 --- a/examples/sandbox/sandbox_subprocess_v1_contract.json +++ b/examples/sandbox/sandbox_subprocess_v1_contract.json @@ -91,6 +91,71 @@ } } ], + "diagnostic_cases": [ + { + "name": "request-ref-missing", + "surface": "request_ref", + "expected_diagnostics": [ + "request_ref is missing" + ] + }, + { + "name": "request-ref-identity-drift", + "surface": "request_ref", + "expected_diagnostics": [ + "request_ref does not match sandbox subprocess request identity tuple" + ] + }, + { + "name": "request-operation-mismatch", + "surface": "request_policy", + "expected_diagnostics": [ + "request operation does not match command surface" + ] + }, + { + "name": "request-diagnostics-mismatch", + "surface": "request_policy", + "expected_diagnostics": [ + "request diagnostics does not match inventory case" + ] + }, + { + "name": "request-stderr-policy-mismatch", + "surface": "request_policy", + "expected_diagnostics": [ + "request stderr policy does not match diagnostics mode" + ] + }, + { + "name": "request-stdout-policy-mismatch", + "surface": "request_policy", + "expected_diagnostics": [ + "request failure stdout policy is not empty" + ] + }, + { + "name": "request-max-parse-ms-mismatch", + "surface": "request_policy", + "expected_diagnostics": [ + "request max_parse_ms does not match inventory case" + ] + }, + { + "name": "doc-parse-page-selection-missing", + "surface": "request_policy", + "expected_diagnostics": [ + "doc_parse request must bind explicit page selection" + ] + }, + { + "name": "fingerprint-page-selection-present", + "surface": "request_policy", + "expected_diagnostics": [ + "fingerprint request must not carry page selection" + ] + } + ], "explicit_blockers": [ "hardened OS sandbox rules", "network-denying runtime proof", diff --git a/schemas/ethos-sandbox-subprocess-contract.schema.json b/schemas/ethos-sandbox-subprocess-contract.schema.json index 2d5f532..c551b1f 100644 --- a/schemas/ethos-sandbox-subprocess-contract.schema.json +++ b/schemas/ethos-sandbox-subprocess-contract.schema.json @@ -10,6 +10,7 @@ "status", "carrier", "cases", + "diagnostic_cases", "explicit_blockers" ], "additionalProperties": false, @@ -102,6 +103,34 @@ } } }, + "diagnostic_cases": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "name", + "surface", + "expected_diagnostics" + ], + "additionalProperties": false, + "properties": { + "name": { "$ref": "#/$defs/case_name" }, + "surface": { + "enum": [ + "request_ref", + "request_policy" + ] + }, + "expected_diagnostics": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/diagnostic_message" }, + "uniqueItems": true + } + } + } + }, "explicit_blockers": { "type": "array", "minItems": 1, @@ -111,6 +140,19 @@ }, "$defs": { "case_name": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, + "diagnostic_message": { + "enum": [ + "request_ref is missing", + "request_ref does not match sandbox subprocess request identity tuple", + "request operation does not match command surface", + "request diagnostics does not match inventory case", + "request stderr policy does not match diagnostics mode", + "request failure stdout policy is not empty", + "request max_parse_ms does not match inventory case", + "doc_parse request must bind explicit page selection", + "fingerprint request must not carry page selection" + ] + }, "repo_path": { "type": "string", "minLength": 1, "pattern": "^[A-Za-z0-9_./-]+$" }, "rust_test_filter": { "type": "string", "pattern": "^[a-z0-9_]+$" } } From 4b8972b126d890d99171f96b60d3cb2ccb2d89d7 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 19:39:13 +0530 Subject: [PATCH 33/39] Guard capability downgrade category invariants Signed-off-by: docushell-admin --- ...lestone_d_capability_downgrade_contract.py | 91 +++++++++++++++++++ ...lestone-d-capability-downgrade-contract.md | 5 +- .../capability_downgrade_v1_contract.json | 34 +++++++ ...-capability-downgrade-contract.schema.json | 47 ++++++++-- 4 files changed, 168 insertions(+), 9 deletions(-) diff --git a/.github/scripts/test_milestone_d_capability_downgrade_contract.py b/.github/scripts/test_milestone_d_capability_downgrade_contract.py index 73c16fe..92554b3 100644 --- a/.github/scripts/test_milestone_d_capability_downgrade_contract.py +++ b/.github/scripts/test_milestone_d_capability_downgrade_contract.py @@ -42,6 +42,40 @@ "sandbox backend expansion", "semantic or arithmetic verification", ] +EXPECTED_CATEGORY_INVARIANTS = [ + { + "category": "no-downgrade-control", + "capability_limits": "absent", + "report_warning": "absent", + "blocked_checks": "absent", + "non_grounded_checks": "absent", + "all_evidence_grounded": True, + }, + { + "category": "report-only-downgrade", + "capability_limits": "present", + "report_warning": "capability_limited", + "blocked_checks": "absent", + "non_grounded_checks": "absent", + "all_evidence_grounded": True, + }, + { + "category": "non-grounded-with-downgrade", + "capability_limits": "present", + "report_warning": "capability_limited", + "blocked_checks": "absent", + "non_grounded_checks": "present", + "all_evidence_grounded": False, + }, + { + "category": "check-blocked-downgrade", + "capability_limits": "present", + "report_warning": "capability_limited", + "blocked_checks": "present", + "non_grounded_checks": "present", + "all_evidence_grounded": False, + }, +] def load_json(path: Path): @@ -82,6 +116,25 @@ def blocked_checks(report: dict) -> list[dict]: return [check for check in report["checks"] if check["status"] == "capability_blocked"] +def present_or_absent(value: bool) -> str: + return "present" if value else "absent" + + +def category_invariant(case: dict, report: dict) -> dict: + return { + "category": case["category"], + "capability_limits": present_or_absent(bool(report["capability_limits"])), + "report_warning": ( + "capability_limited" if "capability_limited" in report["warnings"] else "absent" + ), + "blocked_checks": present_or_absent(bool(blocked_checks(report))), + "non_grounded_checks": present_or_absent( + any(check["status"] != "grounded" for check in report["checks"]) + ), + "all_evidence_grounded": report["all_evidence_grounded"], + } + + class MilestoneDCapabilityDowngradeContractTests(unittest.TestCase): def test_target_is_declared_phony(self) -> None: text = makefile_text() @@ -148,8 +201,10 @@ def test_contract_pins_supported_boundaries(self) -> None: "`capability_limited`", "`capability_blocked`", "`missing_table_capability`", + "category invariants", "native Ethos grounding", "OpenDataLoader-style grounding", + "source-tree fixture validation pins the category invariants", "`make milestone-d-capability-downgrade-contract PYTHON=/bin/python`", ]: self.assertIn(required, text) @@ -207,6 +262,15 @@ def test_contract_inventory_case_order_is_deterministic(self) -> None: }, {case["category"] for case in inventory["report_cases"]}, ) + self.assertEqual( + [ + "no-downgrade-control", + "report-only-downgrade", + "non-grounded-with-downgrade", + "check-blocked-downgrade", + ], + [case["category"] for case in inventory["category_invariants"]], + ) def test_contract_inventory_matches_report_goldens(self) -> None: inventory = load_json(CONTRACT_INVENTORY) @@ -264,6 +328,33 @@ def test_contract_inventory_matches_report_goldens(self) -> None: case["name"], ) + def test_contract_inventory_pins_category_invariants(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + + self.assertEqual(EXPECTED_CATEGORY_INVARIANTS, inventory["category_invariants"]) + + categories = [case["category"] for case in inventory["category_invariants"]] + self.assertEqual(len(categories), len(set(categories))) + self.assertEqual( + {case["category"] for case in inventory["report_cases"]}, + set(categories), + ) + + def test_category_invariants_match_report_goldens(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + expected_by_category = { + case["category"]: case for case in inventory["category_invariants"] + } + seen: dict[str, dict] = {} + + for case in inventory["report_cases"]: + report = load_json(ROOT / case["golden"]) + actual = category_invariant(case, report) + self.assertEqual(expected_by_category[case["category"]], actual, case["name"]) + + previous = seen.setdefault(case["category"], actual) + self.assertEqual(previous, actual, case["name"]) + def test_capability_warning_matches_structured_limits(self) -> None: inventory = load_json(CONTRACT_INVENTORY) diff --git a/docs/milestone-d-capability-downgrade-contract.md b/docs/milestone-d-capability-downgrade-contract.md index 9e52482..30810e3 100644 --- a/docs/milestone-d-capability-downgrade-contract.md +++ b/docs/milestone-d-capability-downgrade-contract.md @@ -23,7 +23,8 @@ capability-blocked checks. The current source-tree inventory for this contract boundary is `examples/verify/capability_downgrade_v1_contract.json`. It binds existing native and OpenDataLoader-style verification report goldens to expected capability limits, warnings, and -blocked-check reasons. +blocked-check reasons. The inventory also records category invariants for downgrade-free, +report-only downgrade, non-grounded downgrade, and check-blocked downgrade cases. Focused validation command: @@ -45,6 +46,8 @@ The v1 contract boundary is explicit and fail-closed: grounding source does not expose tables; - evidence mismatches and not-found checks retain their specific diagnostics while still carrying report-level capability downgrade warnings when the source is limited; +- source-tree fixture validation pins the category invariants for report-level downgrade warnings, + blocked checks, non-grounded checks, and `all_evidence_grounded`; - `all_evidence_grounded` remains true only when every supported check is grounded and no stale, unsupported, or blocked check is present. diff --git a/examples/verify/capability_downgrade_v1_contract.json b/examples/verify/capability_downgrade_v1_contract.json index 892160a..b51bfef 100644 --- a/examples/verify/capability_downgrade_v1_contract.json +++ b/examples/verify/capability_downgrade_v1_contract.json @@ -105,6 +105,40 @@ "expected_blocked_reasons": [] } ], + "category_invariants": [ + { + "category": "no-downgrade-control", + "capability_limits": "absent", + "report_warning": "absent", + "blocked_checks": "absent", + "non_grounded_checks": "absent", + "all_evidence_grounded": true + }, + { + "category": "report-only-downgrade", + "capability_limits": "present", + "report_warning": "capability_limited", + "blocked_checks": "absent", + "non_grounded_checks": "absent", + "all_evidence_grounded": true + }, + { + "category": "non-grounded-with-downgrade", + "capability_limits": "present", + "report_warning": "capability_limited", + "blocked_checks": "absent", + "non_grounded_checks": "present", + "all_evidence_grounded": false + }, + { + "category": "check-blocked-downgrade", + "capability_limits": "present", + "report_warning": "capability_limited", + "blocked_checks": "present", + "non_grounded_checks": "present", + "all_evidence_grounded": false + } + ], "explicit_blockers": [ "a new public command or binding surface", "Python, Node, MCP, or hosted capability surfaces", diff --git a/schemas/ethos-capability-downgrade-contract.schema.json b/schemas/ethos-capability-downgrade-contract.schema.json index d70fccf..28ce36b 100644 --- a/schemas/ethos-capability-downgrade-contract.schema.json +++ b/schemas/ethos-capability-downgrade-contract.schema.json @@ -10,6 +10,7 @@ "status", "carrier", "report_cases", + "category_invariants", "explicit_blockers" ], "additionalProperties": false, @@ -38,14 +39,7 @@ "additionalProperties": false, "properties": { "name": { "$ref": "#/$defs/case_name" }, - "category": { - "enum": [ - "no-downgrade-control", - "report-only-downgrade", - "non-grounded-with-downgrade", - "check-blocked-downgrade" - ] - }, + "category": { "$ref": "#/$defs/case_category" }, "golden": { "$ref": "#/$defs/repo_path" }, "all_evidence_grounded": { "type": "boolean" }, "expected_statuses": { @@ -79,6 +73,32 @@ } } }, + "category_invariants": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "category", + "capability_limits", + "report_warning", + "blocked_checks", + "non_grounded_checks", + "all_evidence_grounded" + ], + "additionalProperties": false, + "properties": { + "category": { "$ref": "#/$defs/case_category" }, + "capability_limits": { "$ref": "#/$defs/presence" }, + "report_warning": { + "enum": ["absent", "capability_limited"] + }, + "blocked_checks": { "$ref": "#/$defs/presence" }, + "non_grounded_checks": { "$ref": "#/$defs/presence" }, + "all_evidence_grounded": { "type": "boolean" } + } + } + }, "explicit_blockers": { "type": "array", "minItems": 1, @@ -90,6 +110,17 @@ "case_name": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "repo_path": { "type": "string", "minLength": 1, "pattern": "^[A-Za-z0-9_./-]+$" }, "check_id": { "type": "string", "pattern": "^v[0-9]{4}$" }, + "case_category": { + "enum": [ + "no-downgrade-control", + "report-only-downgrade", + "non-grounded-with-downgrade", + "check-blocked-downgrade" + ] + }, + "presence": { + "enum": ["absent", "present"] + }, "check_status": { "enum": [ "grounded", From 42c6cf7867f31f439eb664d4acb13a00013d12a6 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 19:47:35 +0530 Subject: [PATCH 34/39] Guard sandbox page selection diagnostics Signed-off-by: docushell-admin --- ...milestone_d_sandbox_subprocess_contract.py | 73 ++++++++++++++++++- ...milestone-d-sandbox-subprocess-contract.md | 4 +- .../sandbox_subprocess_v1_contract.json | 24 ++++++ ...os-sandbox-subprocess-contract.schema.json | 6 +- 4 files changed, 104 insertions(+), 3 deletions(-) diff --git a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py index e072e56..0b56c0e 100644 --- a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py +++ b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py @@ -61,6 +61,9 @@ "request max_parse_ms does not match inventory case", "doc_parse request must bind explicit page selection", "fingerprint request must not carry page selection", + "pages are 1-based", + "descending range in page selection", + "page number out of range", ] EXPECTED_DIAGNOSTIC_CASES = [ { @@ -112,6 +115,30 @@ "surface": "request_policy", "expected_diagnostics": ["fingerprint request must not carry page selection"], }, + { + "name": "doc-parse-page-selection-zero", + "surface": "page_selection", + "expected_diagnostics": [ + "doc_parse request must bind explicit page selection", + "pages are 1-based", + ], + }, + { + "name": "doc-parse-page-selection-descending-range", + "surface": "page_selection", + "expected_diagnostics": [ + "doc_parse request must bind explicit page selection", + "descending range in page selection", + ], + }, + { + "name": "doc-parse-page-selection-overflow", + "surface": "page_selection", + "expected_diagnostics": [ + "doc_parse request must bind explicit page selection", + "page number out of range", + ], + }, ] EXPECTED_FAILURES = { "doc-parse-timeout": { @@ -243,6 +270,31 @@ def request_ref_drift_diagnostics(request: dict) -> list[str]: return [] +def page_selection_diagnostics(page_selection: str) -> list[str]: + if page_selection == "all": + return [] + + for part in page_selection.split(","): + if "-" in part: + start, end = part.split("-", 1) + else: + start = part + end = part + if not start.isdigit() or not end.isdigit(): + return ["malformed page number"] + + low = int(start) + high = int(end) + if low > 2**32 - 1 or high > 2**32 - 1: + return ["page number out of range"] + if low == 0 or high == 0: + return ["pages are 1-based"] + if low > high: + return ["descending range in page selection"] + + return [] + + def request_case_diagnostics(request: dict, case: dict) -> list[str]: diagnostics: list[str] = [] want_diagnostics, want_stderr_policy, want_max_parse_ms = expected_request_policy(case) @@ -260,6 +312,10 @@ def request_case_diagnostics(request: dict, case: dict) -> list[str]: if request["operation"] == "doc_parse" and request.get("page_selection") != "all": diagnostics.append("doc_parse request must bind explicit page selection") + if "page_selection" in request: + diagnostics.extend( + page_selection_diagnostics(request["page_selection"]) + ) if request["operation"] == "fingerprint" and "page_selection" in request: diagnostics.append("fingerprint request must not carry page selection") @@ -301,6 +357,9 @@ def inventory_diagnostic_outputs(inventory: dict) -> dict[str, list[str]]: del doc_parse_without_pages["page_selection"] fingerprint_with_pages = dict(fingerprint_request, page_selection="all") + zero_page_selection = dict(doc_parse_request, page_selection="0") + descending_page_selection = dict(doc_parse_request, page_selection="5-2") + overflow_page_selection = dict(doc_parse_request, page_selection="4294967296") return { "request-ref-missing": request_ref_drift_diagnostics(missing_request_ref), @@ -333,6 +392,18 @@ def inventory_diagnostic_outputs(inventory: dict) -> dict[str, list[str]]: fingerprint_with_pages, fingerprint_case, ), + "doc-parse-page-selection-zero": request_case_diagnostics( + zero_page_selection, + doc_parse_case, + ), + "doc-parse-page-selection-descending-range": request_case_diagnostics( + descending_page_selection, + doc_parse_case, + ), + "doc-parse-page-selection-overflow": request_case_diagnostics( + overflow_page_selection, + doc_parse_case, + ), } @@ -528,7 +599,7 @@ def test_contract_inventory_pins_fail_closed_diagnostics(self) -> None: diagnostic_names = [case["name"] for case in inventory["diagnostic_cases"]] self.assertEqual(len(diagnostic_names), len(set(diagnostic_names))) self.assertEqual( - ["request_policy", "request_ref"], + ["page_selection", "request_policy", "request_ref"], sorted({case["surface"] for case in inventory["diagnostic_cases"]}), ) self.assertEqual( diff --git a/docs/milestone-d-sandbox-subprocess-contract.md b/docs/milestone-d-sandbox-subprocess-contract.md index 30190b7..dc3d6cb 100644 --- a/docs/milestone-d-sandbox-subprocess-contract.md +++ b/docs/milestone-d-sandbox-subprocess-contract.md @@ -27,7 +27,7 @@ stderr behavior. Each inventory case binds to a request envelope under `schemas/examples/sandbox-subprocess-*.example.json` and records the expected process exit code, stable error code, stable error message, and diagnostics policy. The inventory also records the request identity and request-policy diagnostics that the source-tree guard exercises for this -contract boundary. +contract boundary, including semantic page-selection diagnostics for schema-shaped request values. Focused validation command: @@ -51,6 +51,8 @@ The v1 contract boundary is fail-closed and error-envelope-first: `ethos.sandbox_subprocess_request_ref.v1`; - source-tree fixture validation pins expected diagnostics for request identity drift and request policy drift; +- page-selection diagnostics reject zero pages, descending ranges, and overflowing page numbers + without changing the request schema shape; - inventory outcome fields bind each failure case to its current exit code and stable error envelope; - stdout remains empty on worker failures covered by this contract inventory. diff --git a/examples/sandbox/sandbox_subprocess_v1_contract.json b/examples/sandbox/sandbox_subprocess_v1_contract.json index c756c2a..570a25e 100644 --- a/examples/sandbox/sandbox_subprocess_v1_contract.json +++ b/examples/sandbox/sandbox_subprocess_v1_contract.json @@ -154,6 +154,30 @@ "expected_diagnostics": [ "fingerprint request must not carry page selection" ] + }, + { + "name": "doc-parse-page-selection-zero", + "surface": "page_selection", + "expected_diagnostics": [ + "doc_parse request must bind explicit page selection", + "pages are 1-based" + ] + }, + { + "name": "doc-parse-page-selection-descending-range", + "surface": "page_selection", + "expected_diagnostics": [ + "doc_parse request must bind explicit page selection", + "descending range in page selection" + ] + }, + { + "name": "doc-parse-page-selection-overflow", + "surface": "page_selection", + "expected_diagnostics": [ + "doc_parse request must bind explicit page selection", + "page number out of range" + ] } ], "explicit_blockers": [ diff --git a/schemas/ethos-sandbox-subprocess-contract.schema.json b/schemas/ethos-sandbox-subprocess-contract.schema.json index c551b1f..01f4f95 100644 --- a/schemas/ethos-sandbox-subprocess-contract.schema.json +++ b/schemas/ethos-sandbox-subprocess-contract.schema.json @@ -118,6 +118,7 @@ "name": { "$ref": "#/$defs/case_name" }, "surface": { "enum": [ + "page_selection", "request_ref", "request_policy" ] @@ -150,7 +151,10 @@ "request failure stdout policy is not empty", "request max_parse_ms does not match inventory case", "doc_parse request must bind explicit page selection", - "fingerprint request must not carry page selection" + "fingerprint request must not carry page selection", + "pages are 1-based", + "descending range in page selection", + "page number out of range" ] }, "repo_path": { "type": "string", "minLength": 1, "pattern": "^[A-Za-z0-9_./-]+$" }, From 79f6e9ed4794a3716033d7b74c6444bf1d56e9c7 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 19:54:30 +0530 Subject: [PATCH 35/39] Guard crop descriptor text hash diagnostics Signed-off-by: docushell-admin --- .../test_milestone_d_crop_element_contract.py | 45 ++++++++++++++++++- docs/milestone-d-crop-element-contract.md | 2 + examples/crop/crop_element_v1_contract.json | 7 +++ .../ethos-crop-element-contract.schema.json | 2 + 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/.github/scripts/test_milestone_d_crop_element_contract.py b/.github/scripts/test_milestone_d_crop_element_contract.py index 17a4f9d..c444fe2 100644 --- a/.github/scripts/test_milestone_d_crop_element_contract.py +++ b/.github/scripts/test_milestone_d_crop_element_contract.py @@ -54,6 +54,7 @@ "request_ref does not match crop element request identity tuple", "descriptor must bind exactly one logical check id", "descriptor crop_ref does not match logical identity tuple", + "descriptor text_sha256 does not match verification evidence", "request document_fingerprint does not match document fingerprint", "descriptor document_fingerprint does not match request", "request element_id does not match contract inventory case", @@ -154,6 +155,13 @@ "descriptor crop_ref does not match logical identity tuple" ], }, + { + "name": "descriptor-text-sha256-mismatch", + "surface": "descriptor_binding", + "expected_diagnostics": [ + "descriptor text_sha256 does not match verification evidence" + ], + }, { "name": "descriptor-ambiguous-checks", "surface": "descriptor_ref", @@ -265,6 +273,24 @@ def crop_ref_drift_diagnostics(descriptor: dict) -> list[str]: return diagnostics +def descriptor_evidence_diagnostics(descriptor: dict, report: dict) -> list[str]: + if "text_sha256" not in descriptor: + return [] + + report_checks = checks_by_id(report) + for check_id in descriptor.get("check_ids", []): + check = report_checks.get(check_id) + if check is None: + continue + evidence_text = check.get("evidence", {}).get("text") + if evidence_text is None: + continue + if descriptor["text_sha256"] != sha256_text(evidence_text): + return ["descriptor text_sha256 does not match verification evidence"] + + return [] + + def schema_errors(schema: dict, instance: dict) -> list: return sorted( Draft202012Validator(schema).iter_errors(instance), @@ -315,6 +341,7 @@ def inventory_diagnostic_outputs(inventory: dict) -> dict[str, list[str]]: request = load_json(ROOT / case["request"]) document = load_json(ROOT / case["document"]) descriptor = load_json(ROOT / case["descriptor"]) + report = load_json(VERIFICATION_REPORT_EXAMPLE) missing_request_ref = dict(request) del missing_request_ref["request_ref"] @@ -340,6 +367,7 @@ def inventory_diagnostic_outputs(inventory: dict) -> dict[str, list[str]]: del elements_by_id(document_without_bbox)[request["element_id"]]["bbox"] stale_crop_ref = dict(descriptor, crop_ref="crop-" + "0" * 64 + ".json") + stale_text_sha256 = dict(descriptor, text_sha256="0" * 64) ambiguous_checks = dict(descriptor, check_ids=["v0001", "v0002"]) return { @@ -406,6 +434,10 @@ def inventory_diagnostic_outputs(inventory: dict) -> dict[str, list[str]]: case, ), "descriptor-crop-ref-identity-drift": crop_ref_drift_diagnostics(stale_crop_ref), + "descriptor-text-sha256-mismatch": descriptor_evidence_diagnostics( + stale_text_sha256, + report, + ), "descriptor-ambiguous-checks": crop_ref_drift_diagnostics(ambiguous_checks), } @@ -622,7 +654,7 @@ def test_contract_inventory_pins_fail_closed_diagnostics(self) -> None: diagnostic_names = [case["name"] for case in inventory["diagnostic_cases"]] self.assertEqual(len(diagnostic_names), len(set(diagnostic_names))) self.assertEqual( - ["descriptor_ref", "request_binding", "request_ref"], + ["descriptor_binding", "descriptor_ref", "request_binding", "request_ref"], sorted({case["surface"] for case in inventory["diagnostic_cases"]}), ) self.assertEqual( @@ -715,6 +747,17 @@ def test_contract_inventory_binds_descriptor_to_verification_evidence(self) -> N sha256_text(evidence["text"]), case["name"], ) + self.assertEqual([], descriptor_evidence_diagnostics(descriptor, report)) + + def test_descriptor_text_hash_fails_closed_on_evidence_mismatch(self) -> None: + descriptor = load_json(ROOT / "schemas/examples/crop-descriptor.example.json") + report = load_json(VERIFICATION_REPORT_EXAMPLE) + + stale_text_sha256 = dict(descriptor, text_sha256="0" * 64) + self.assertIn( + "descriptor text_sha256 does not match verification evidence", + descriptor_evidence_diagnostics(stale_text_sha256, report), + ) def test_crop_descriptor_crop_ref_fails_closed_on_logical_identity_drift(self) -> None: descriptor = load_json(ROOT / "schemas/examples/crop-descriptor.example.json") diff --git a/docs/milestone-d-crop-element-contract.md b/docs/milestone-d-crop-element-contract.md index be9527c..0f6b6b3 100644 --- a/docs/milestone-d-crop-element-contract.md +++ b/docs/milestone-d-crop-element-contract.md @@ -51,6 +51,8 @@ The v1 contract boundary is native, explicit, and source-bound: - descriptor `crop_ref` values remain opaque artifact filenames for callers; - source-tree fixture validation binds descriptor filenames to the logical evidence identity tuple of `document_fingerprint`, check id, page id, and `ethos.logical_crop_ref.v1`; +- source-tree fixture validation binds descriptor `text_sha256` values to verification evidence + text when textual evidence is present; - descriptor JSON remains the canonical crop audit artifact; - rendered PNG output is optional and must be bound to a matching source PDF fingerprint; - missing elements, missing pages, missing bboxes, malformed bboxes, and source fingerprint diff --git a/examples/crop/crop_element_v1_contract.json b/examples/crop/crop_element_v1_contract.json index e454f7f..14ff315 100644 --- a/examples/crop/crop_element_v1_contract.json +++ b/examples/crop/crop_element_v1_contract.json @@ -108,6 +108,13 @@ "descriptor crop_ref does not match logical identity tuple" ] }, + { + "name": "descriptor-text-sha256-mismatch", + "surface": "descriptor_binding", + "expected_diagnostics": [ + "descriptor text_sha256 does not match verification evidence" + ] + }, { "name": "descriptor-ambiguous-checks", "surface": "descriptor_ref", diff --git a/schemas/ethos-crop-element-contract.schema.json b/schemas/ethos-crop-element-contract.schema.json index 50a1ac2..721e17e 100644 --- a/schemas/ethos-crop-element-contract.schema.json +++ b/schemas/ethos-crop-element-contract.schema.json @@ -58,6 +58,7 @@ "name": { "$ref": "#/$defs/case_name" }, "surface": { "enum": [ + "descriptor_binding", "request_ref", "request_binding", "descriptor_ref" @@ -87,6 +88,7 @@ "request_ref does not match crop element request identity tuple", "descriptor must bind exactly one logical check id", "descriptor crop_ref does not match logical identity tuple", + "descriptor text_sha256 does not match verification evidence", "request document_fingerprint does not match document fingerprint", "descriptor document_fingerprint does not match request", "request element_id does not match contract inventory case", From 1ded2bb58e0ea6bd01d8a80646279cccc5806be8 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 19:58:36 +0530 Subject: [PATCH 36/39] Guard malformed sandbox page selection diagnostics Signed-off-by: docushell-admin --- ...test_milestone_d_sandbox_subprocess_contract.py | 14 ++++++++++++++ docs/milestone-d-sandbox-subprocess-contract.md | 4 ++-- .../sandbox/sandbox_subprocess_v1_contract.json | 8 ++++++++ .../ethos-sandbox-subprocess-contract.schema.json | 1 + 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py index 0b56c0e..43af77f 100644 --- a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py +++ b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py @@ -61,6 +61,7 @@ "request max_parse_ms does not match inventory case", "doc_parse request must bind explicit page selection", "fingerprint request must not carry page selection", + "malformed page number", "pages are 1-based", "descending range in page selection", "page number out of range", @@ -115,6 +116,14 @@ "surface": "request_policy", "expected_diagnostics": ["fingerprint request must not carry page selection"], }, + { + "name": "doc-parse-page-selection-malformed", + "surface": "page_selection", + "expected_diagnostics": [ + "doc_parse request must bind explicit page selection", + "malformed page number", + ], + }, { "name": "doc-parse-page-selection-zero", "surface": "page_selection", @@ -357,6 +366,7 @@ def inventory_diagnostic_outputs(inventory: dict) -> dict[str, list[str]]: del doc_parse_without_pages["page_selection"] fingerprint_with_pages = dict(fingerprint_request, page_selection="all") + malformed_page_selection = dict(doc_parse_request, page_selection="1-a") zero_page_selection = dict(doc_parse_request, page_selection="0") descending_page_selection = dict(doc_parse_request, page_selection="5-2") overflow_page_selection = dict(doc_parse_request, page_selection="4294967296") @@ -392,6 +402,10 @@ def inventory_diagnostic_outputs(inventory: dict) -> dict[str, list[str]]: fingerprint_with_pages, fingerprint_case, ), + "doc-parse-page-selection-malformed": request_case_diagnostics( + malformed_page_selection, + doc_parse_case, + ), "doc-parse-page-selection-zero": request_case_diagnostics( zero_page_selection, doc_parse_case, diff --git a/docs/milestone-d-sandbox-subprocess-contract.md b/docs/milestone-d-sandbox-subprocess-contract.md index dc3d6cb..e80f852 100644 --- a/docs/milestone-d-sandbox-subprocess-contract.md +++ b/docs/milestone-d-sandbox-subprocess-contract.md @@ -51,8 +51,8 @@ The v1 contract boundary is fail-closed and error-envelope-first: `ethos.sandbox_subprocess_request_ref.v1`; - source-tree fixture validation pins expected diagnostics for request identity drift and request policy drift; -- page-selection diagnostics reject zero pages, descending ranges, and overflowing page numbers - without changing the request schema shape; +- page-selection diagnostics reject malformed page numbers, zero pages, descending ranges, and + overflowing page numbers without changing the request schema shape; - inventory outcome fields bind each failure case to its current exit code and stable error envelope; - stdout remains empty on worker failures covered by this contract inventory. diff --git a/examples/sandbox/sandbox_subprocess_v1_contract.json b/examples/sandbox/sandbox_subprocess_v1_contract.json index 570a25e..236136e 100644 --- a/examples/sandbox/sandbox_subprocess_v1_contract.json +++ b/examples/sandbox/sandbox_subprocess_v1_contract.json @@ -155,6 +155,14 @@ "fingerprint request must not carry page selection" ] }, + { + "name": "doc-parse-page-selection-malformed", + "surface": "page_selection", + "expected_diagnostics": [ + "doc_parse request must bind explicit page selection", + "malformed page number" + ] + }, { "name": "doc-parse-page-selection-zero", "surface": "page_selection", diff --git a/schemas/ethos-sandbox-subprocess-contract.schema.json b/schemas/ethos-sandbox-subprocess-contract.schema.json index 01f4f95..9fe235b 100644 --- a/schemas/ethos-sandbox-subprocess-contract.schema.json +++ b/schemas/ethos-sandbox-subprocess-contract.schema.json @@ -152,6 +152,7 @@ "request max_parse_ms does not match inventory case", "doc_parse request must bind explicit page selection", "fingerprint request must not carry page selection", + "malformed page number", "pages are 1-based", "descending range in page selection", "page number out of range" From 938595d3bfc86192d57e8cf35946f16998c46733 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 20:04:14 +0530 Subject: [PATCH 37/39] Bind sandbox artifact header contract Signed-off-by: docushell-admin --- ...milestone_d_sandbox_subprocess_contract.py | 53 +++++++++++++++++++ Makefile | 1 + ...milestone-d-sandbox-subprocess-contract.md | 5 +- .../sandbox_subprocess_v1_contract.json | 24 +++++++++ ...os-sandbox-subprocess-contract.schema.json | 48 +++++++++++++++++ 5 files changed, 130 insertions(+), 1 deletion(-) diff --git a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py index 43af77f..bf30e0c 100644 --- a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py +++ b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py @@ -36,6 +36,7 @@ EXECUTION_STATUS = ROOT / "docs/execution-status.md" SCHEMAS_README = ROOT / "schemas/README.md" PDF_PARSE_TESTS = ROOT / "crates/ethos-cli/tests/pdf_parse.rs" +WORKER_SOURCE = ROOT / "crates/ethos-cli/src/worker.rs" EXPECTED_EXPLICIT_BLOCKERS = [ "hardened OS sandbox rules", "network-denying runtime proof", @@ -51,6 +52,30 @@ "stable_error_envelope", "diagnostics_gated_stderr", ] +EXPECTED_ARTIFACT_HEADER_CASES = [ + { + "name": "json-artifact-header-valid", + "test_filter": "validates_json_artifact_header_against_file_hash", + "boundary": "json_artifact_header_integrity", + "expected_result": "accepted", + }, + { + "name": "json-artifact-header-hash-mismatch", + "test_filter": "rejects_json_artifact_header_hash_mismatch", + "boundary": "json_artifact_header_integrity", + "expected_result": "rejected", + "error_code": "internal_error", + "error_message": "pdfium worker JSON artifact hash mismatch", + }, + { + "name": "json-artifact-header-envelope-mismatch", + "test_filter": "rejects_json_artifact_header_envelope_mismatch", + "boundary": "json_artifact_header_integrity", + "expected_result": "rejected", + "error_code": "internal_error", + "error_message": "pdfium worker JSON artifact header does not match artifact", + }, +] EXPECTED_DIAGNOSTIC_MESSAGES = [ "request_ref is missing", "request_ref does not match sandbox subprocess request identity tuple", @@ -455,6 +480,7 @@ def test_target_composes_contract_gates(self) -> None: block = target_block("milestone-d-sandbox-subprocess-contract") required = [ + "cargo test --locked -p ethos-cli json_artifact_header", "cargo test --locked -p ethos-cli --test pdf_parse worker", "$(PYTHON) schemas/validate_examples.py", "$(PYTHON) .github/scripts/test_execution_status.py", @@ -513,6 +539,8 @@ def test_contract_pins_fail_closed_boundaries(self) -> None: "`ethos.sandbox_subprocess_request_ref.v1`", "request identity and request-policy diagnostics", "stable worker error envelopes are relayed", + "worker JSON artifact headers bind output byte count, output hash, document " + "fingerprint, and payload hash", "non-envelope worker stderr is hidden by default", "explicit diagnostics", "request envelopes bind each failure case", @@ -584,6 +612,7 @@ def test_contract_inventory_matches_existing_worker_tests(self) -> None: self.assertEqual(inventory["status"], "source-only-pre-alpha") self.assertEqual(inventory["carrier"], "pdfium worker process") self.assertEqual(EXPECTED_EXPLICIT_BLOCKERS, inventory["explicit_blockers"]) + self.assertEqual(EXPECTED_ARTIFACT_HEADER_CASES, inventory["artifact_header_cases"]) case_names = [case["name"] for case in inventory["cases"]] self.assertEqual(len(case_names), len(set(case_names))) @@ -605,6 +634,30 @@ def test_contract_inventory_matches_existing_worker_tests(self) -> None: ) self.assertIn(f"fn {case['test_filter']}()", test_source, case["name"]) + def test_contract_inventory_matches_existing_artifact_header_tests(self) -> None: + inventory = load_json(CONTRACT_INVENTORY) + worker_source = WORKER_SOURCE.read_text(encoding="utf-8") + + self.assertEqual( + EXPECTED_ARTIFACT_HEADER_CASES, + inventory["artifact_header_cases"], + ) + + case_names = [case["name"] for case in inventory["artifact_header_cases"]] + self.assertEqual(len(case_names), len(set(case_names))) + for case in inventory["artifact_header_cases"]: + body = rust_test_body(worker_source, case["test_filter"]) + self.assertIn("worker_json_artifact_from_header", body, case["name"]) + + if case["expected_result"] == "accepted": + self.assertIn("artifact header did not validate", body, case["name"]) + self.assertIn("document_fingerprint()", body, case["name"]) + self.assertIn("artifact is cleaned up on drop", body, case["name"]) + else: + self.assertIn("InternalError", body, case["name"]) + self.assertIn(case["error_message"], body, case["name"]) + self.assertIn("temp dir is cleaned up on rejection", body, case["name"]) + def test_contract_inventory_pins_fail_closed_diagnostics(self) -> None: inventory = load_json(CONTRACT_INVENTORY) diff --git a/Makefile b/Makefile index 858a0c8..22ddf77 100644 --- a/Makefile +++ b/Makefile @@ -95,6 +95,7 @@ milestone-d-crop-element-surface-shape-contract: git diff --check milestone-d-sandbox-subprocess-contract: + cargo test --locked -p ethos-cli json_artifact_header cargo test --locked -p ethos-cli --test pdf_parse worker $(PYTHON) schemas/validate_examples.py $(PYTHON) .github/scripts/test_execution_status.py diff --git a/docs/milestone-d-sandbox-subprocess-contract.md b/docs/milestone-d-sandbox-subprocess-contract.md index e80f852..6941cf2 100644 --- a/docs/milestone-d-sandbox-subprocess-contract.md +++ b/docs/milestone-d-sandbox-subprocess-contract.md @@ -23,7 +23,8 @@ PDF input, bounded worker execution, normalized document output, and stable erro The current source-tree inventory for this contract boundary is `examples/sandbox/sandbox_subprocess_v1_contract.json`. It classifies existing worker tests that exercise timeout handling, memory-limit error reporting, stable error relay, and diagnostics-gated -stderr behavior. Each inventory case binds to a request envelope under +stderr behavior. It also records worker JSON artifact-header integrity cases for the existing +temporary artifact handoff. Each inventory case binds to a request envelope under `schemas/examples/sandbox-subprocess-*.example.json` and records the expected process exit code, stable error code, stable error message, and diagnostics policy. The inventory also records the request identity and request-policy diagnostics that the source-tree guard exercises for this @@ -44,6 +45,8 @@ The v1 contract boundary is fail-closed and error-envelope-first: - `max_parse_ms` timeout exits through the stable `parse_timeout` error code; - worker memory-limit failures exit through the stable `memory_limit_exceeded` error code; - stable worker error envelopes are relayed without converting them to generic failures; +- worker JSON artifact headers bind output byte count, output hash, document fingerprint, and + payload hash to the temporary JSON artifact before the parent process accepts it; - non-envelope worker stderr is hidden by default; - non-envelope worker stderr is exposed only under explicit diagnostics; - request envelopes bind each failure case to the intended operation, timeout limit, diagnostics diff --git a/examples/sandbox/sandbox_subprocess_v1_contract.json b/examples/sandbox/sandbox_subprocess_v1_contract.json index 236136e..bc58a0c 100644 --- a/examples/sandbox/sandbox_subprocess_v1_contract.json +++ b/examples/sandbox/sandbox_subprocess_v1_contract.json @@ -91,6 +91,30 @@ } } ], + "artifact_header_cases": [ + { + "name": "json-artifact-header-valid", + "test_filter": "validates_json_artifact_header_against_file_hash", + "boundary": "json_artifact_header_integrity", + "expected_result": "accepted" + }, + { + "name": "json-artifact-header-hash-mismatch", + "test_filter": "rejects_json_artifact_header_hash_mismatch", + "boundary": "json_artifact_header_integrity", + "expected_result": "rejected", + "error_code": "internal_error", + "error_message": "pdfium worker JSON artifact hash mismatch" + }, + { + "name": "json-artifact-header-envelope-mismatch", + "test_filter": "rejects_json_artifact_header_envelope_mismatch", + "boundary": "json_artifact_header_integrity", + "expected_result": "rejected", + "error_code": "internal_error", + "error_message": "pdfium worker JSON artifact header does not match artifact" + } + ], "diagnostic_cases": [ { "name": "request-ref-missing", diff --git a/schemas/ethos-sandbox-subprocess-contract.schema.json b/schemas/ethos-sandbox-subprocess-contract.schema.json index 9fe235b..1d45a6b 100644 --- a/schemas/ethos-sandbox-subprocess-contract.schema.json +++ b/schemas/ethos-sandbox-subprocess-contract.schema.json @@ -10,6 +10,7 @@ "status", "carrier", "cases", + "artifact_header_cases", "diagnostic_cases", "explicit_blockers" ], @@ -103,6 +104,53 @@ } } }, + "artifact_header_cases": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "name", + "test_filter", + "boundary", + "expected_result" + ], + "additionalProperties": false, + "properties": { + "name": { "$ref": "#/$defs/case_name" }, + "test_filter": { "$ref": "#/$defs/rust_test_filter" }, + "boundary": { "const": "json_artifact_header_integrity" }, + "expected_result": { "enum": ["accepted", "rejected"] }, + "error_code": { "const": "internal_error" }, + "error_message": { "type": "string", "minLength": 1 } + }, + "allOf": [ + { + "if": { + "properties": { "expected_result": { "const": "rejected" } }, + "required": ["expected_result"] + }, + "then": { + "required": ["error_code", "error_message"] + } + }, + { + "if": { + "properties": { "expected_result": { "const": "accepted" } }, + "required": ["expected_result"] + }, + "then": { + "not": { + "anyOf": [ + { "required": ["error_code"] }, + { "required": ["error_message"] } + ] + } + } + } + ] + } + }, "diagnostic_cases": { "type": "array", "minItems": 1, From 54d87012ffa3f6d994bb40223f671b57d5ce1403 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 20:12:56 +0530 Subject: [PATCH 38/39] Guard sandbox artifact header rejection diagnostics Signed-off-by: docushell-admin --- ...milestone_d_sandbox_subprocess_contract.py | 45 +++++++++++++- crates/ethos-cli/src/worker.rs | 62 +++++++++++++++++++ ...milestone-d-sandbox-subprocess-contract.md | 1 + .../sandbox_subprocess_v1_contract.json | 24 +++++++ 4 files changed, 129 insertions(+), 3 deletions(-) diff --git a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py index bf30e0c..318eb13 100644 --- a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py +++ b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py @@ -59,6 +59,30 @@ "boundary": "json_artifact_header_integrity", "expected_result": "accepted", }, + { + "name": "json-artifact-header-invalid-json", + "test_filter": "rejects_json_artifact_header_invalid_json", + "boundary": "json_artifact_header_integrity", + "expected_result": "rejected", + "error_code": "internal_error", + "error_message": "pdfium worker returned an invalid JSON artifact header", + }, + { + "name": "json-artifact-header-unsupported-schema", + "test_filter": "rejects_json_artifact_header_unsupported_schema", + "boundary": "json_artifact_header_integrity", + "expected_result": "rejected", + "error_code": "internal_error", + "error_message": "pdfium worker returned an unsupported JSON artifact header", + }, + { + "name": "json-artifact-header-missing-output-bytes", + "test_filter": "rejects_json_artifact_header_missing_output_bytes", + "boundary": "json_artifact_header_integrity", + "expected_result": "rejected", + "error_code": "internal_error", + "error_message": "pdfium worker JSON artifact header missing output_bytes", + }, { "name": "json-artifact-header-hash-mismatch", "test_filter": "rejects_json_artifact_header_hash_mismatch", @@ -645,18 +669,33 @@ def test_contract_inventory_matches_existing_artifact_header_tests(self) -> None case_names = [case["name"] for case in inventory["artifact_header_cases"]] self.assertEqual(len(case_names), len(set(case_names))) + self.assertIn("fn assert_json_artifact_header_rejected", worker_source) + self.assertIn("worker_json_artifact_from_header", worker_source) for case in inventory["artifact_header_cases"]: body = rust_test_body(worker_source, case["test_filter"]) - self.assertIn("worker_json_artifact_from_header", body, case["name"]) if case["expected_result"] == "accepted": + self.assertIn("worker_json_artifact_from_header", body, case["name"]) self.assertIn("artifact header did not validate", body, case["name"]) self.assertIn("document_fingerprint()", body, case["name"]) self.assertIn("artifact is cleaned up on drop", body, case["name"]) else: - self.assertIn("InternalError", body, case["name"]) + self.assertTrue( + "worker_json_artifact_from_header" in body + or "assert_json_artifact_header_rejected" in body, + case["name"], + ) + self.assertIn( + "InternalError", + body if "InternalError" in body else worker_source, + case["name"], + ) self.assertIn(case["error_message"], body, case["name"]) - self.assertIn("temp dir is cleaned up on rejection", body, case["name"]) + self.assertIn( + "temp dir is cleaned up on rejection", + body if "temp dir is cleaned up on rejection" in body else worker_source, + case["name"], + ) def test_contract_inventory_pins_fail_closed_diagnostics(self) -> None: inventory = load_json(CONTRACT_INVENTORY) diff --git a/crates/ethos-cli/src/worker.rs b/crates/ethos-cli/src/worker.rs index b5592d6..ae2f1c2 100644 --- a/crates/ethos-cli/src/worker.rs +++ b/crates/ethos-cli/src/worker.rs @@ -520,6 +520,32 @@ mod tests { bytes } + fn test_header_bytes(value: serde_json::Value) -> Vec { + let mut bytes = ethos_core::c14n::c14n_bytes(&value).expect("test value is canonical"); + bytes.push(b'\n'); + bytes + } + + fn assert_json_artifact_header_rejected(header: Vec, message: &str) { + let (temp_dir, path) = test_artifact(); + let bytes = test_document_bytes(TEST_FINGERPRINT, TEST_PAYLOAD_SHA256); + std::fs::write(&path, &bytes).expect("test artifact can be written"); + + let error = match worker_json_artifact_from_header(&header, temp_dir, path.clone()) { + Ok(_) => panic!("invalid artifact header was accepted"), + Err(error) => error, + }; + + match error { + Failure::Ethos(error) => { + assert_eq!(error.code, ErrorCode::InternalError); + assert_eq!(error.message, message); + } + _ => panic!("expected Ethos failure"), + } + assert!(!path.exists(), "temp dir is cleaned up on rejection"); + } + #[test] fn validates_json_artifact_header_against_file_hash() { let (temp_dir, path) = test_artifact(); @@ -545,6 +571,42 @@ mod tests { assert!(!path.exists(), "artifact is cleaned up on drop"); } + #[test] + fn rejects_json_artifact_header_invalid_json() { + assert_json_artifact_header_rejected( + b"{".to_vec(), + "pdfium worker returned an invalid JSON artifact header", + ); + } + + #[test] + fn rejects_json_artifact_header_unsupported_schema() { + let header = test_header_bytes(serde_json::json!({ + "schema_version": "unsupported-worker-json-artifact", + })); + + assert_json_artifact_header_rejected( + header, + "pdfium worker returned an unsupported JSON artifact header", + ); + } + + #[test] + fn rejects_json_artifact_header_missing_output_bytes() { + let bytes = test_document_bytes(TEST_FINGERPRINT, TEST_PAYLOAD_SHA256); + let header = test_header_bytes(serde_json::json!({ + "schema_version": WORKER_JSON_ARTIFACT_SCHEMA, + "document_fingerprint": TEST_FINGERPRINT, + "payload_sha256": TEST_PAYLOAD_SHA256, + "output_sha256": sha256_hex(&bytes), + })); + + assert_json_artifact_header_rejected( + header, + "pdfium worker JSON artifact header missing output_bytes", + ); + } + #[test] fn rejects_json_artifact_header_hash_mismatch() { let (temp_dir, path) = test_artifact(); diff --git a/docs/milestone-d-sandbox-subprocess-contract.md b/docs/milestone-d-sandbox-subprocess-contract.md index 6941cf2..9dfa302 100644 --- a/docs/milestone-d-sandbox-subprocess-contract.md +++ b/docs/milestone-d-sandbox-subprocess-contract.md @@ -47,6 +47,7 @@ The v1 contract boundary is fail-closed and error-envelope-first: - stable worker error envelopes are relayed without converting them to generic failures; - worker JSON artifact headers bind output byte count, output hash, document fingerprint, and payload hash to the temporary JSON artifact before the parent process accepts it; +- malformed, unsupported, or incomplete worker JSON artifact headers fail closed; - non-envelope worker stderr is hidden by default; - non-envelope worker stderr is exposed only under explicit diagnostics; - request envelopes bind each failure case to the intended operation, timeout limit, diagnostics diff --git a/examples/sandbox/sandbox_subprocess_v1_contract.json b/examples/sandbox/sandbox_subprocess_v1_contract.json index bc58a0c..405a3c1 100644 --- a/examples/sandbox/sandbox_subprocess_v1_contract.json +++ b/examples/sandbox/sandbox_subprocess_v1_contract.json @@ -98,6 +98,30 @@ "boundary": "json_artifact_header_integrity", "expected_result": "accepted" }, + { + "name": "json-artifact-header-invalid-json", + "test_filter": "rejects_json_artifact_header_invalid_json", + "boundary": "json_artifact_header_integrity", + "expected_result": "rejected", + "error_code": "internal_error", + "error_message": "pdfium worker returned an invalid JSON artifact header" + }, + { + "name": "json-artifact-header-unsupported-schema", + "test_filter": "rejects_json_artifact_header_unsupported_schema", + "boundary": "json_artifact_header_integrity", + "expected_result": "rejected", + "error_code": "internal_error", + "error_message": "pdfium worker returned an unsupported JSON artifact header" + }, + { + "name": "json-artifact-header-missing-output-bytes", + "test_filter": "rejects_json_artifact_header_missing_output_bytes", + "boundary": "json_artifact_header_integrity", + "expected_result": "rejected", + "error_code": "internal_error", + "error_message": "pdfium worker JSON artifact header missing output_bytes" + }, { "name": "json-artifact-header-hash-mismatch", "test_filter": "rejects_json_artifact_header_hash_mismatch", From 09edd383b6744dbb0db469bdf68ada53425f7946 Mon Sep 17 00:00:00 2001 From: docushell-admin Date: Thu, 18 Jun 2026 20:16:32 +0530 Subject: [PATCH 39/39] Guard sandbox request identity drift fields Signed-off-by: docushell-admin --- ...milestone_d_sandbox_subprocess_contract.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py index 318eb13..8bf4a1b 100644 --- a/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py +++ b/.github/scripts/test_milestone_d_sandbox_subprocess_contract.py @@ -791,6 +791,33 @@ def test_request_ref_fails_closed_on_identity_drift(self) -> None: request_ref_drift_diagnostics(stale_limit), ) + stale_input = dict(request, input={"kind": "fixture_path"}) + self.assertNotEqual(request["request_ref"], logical_sandbox_request_ref(stale_input)) + self.assertEqual( + ["request_ref does not match sandbox subprocess request identity tuple"], + request_ref_drift_diagnostics(stale_input), + ) + + stale_diagnostics = dict(request, diagnostics=True) + self.assertNotEqual( + request["request_ref"], + logical_sandbox_request_ref(stale_diagnostics), + ) + self.assertEqual( + ["request_ref does not match sandbox subprocess request identity tuple"], + request_ref_drift_diagnostics(stale_diagnostics), + ) + + stale_stdout_policy = dict(request, stdout_on_failure="not-empty") + self.assertNotEqual( + request["request_ref"], + logical_sandbox_request_ref(stale_stdout_policy), + ) + self.assertEqual( + ["request_ref does not match sandbox subprocess request identity tuple"], + request_ref_drift_diagnostics(stale_stdout_policy), + ) + stale_stderr_policy = dict( request, stderr_policy="stable_error_envelope_with_worker_stderr",