diff --git a/crates/ethos-cli/tests/pdf_parse.rs b/crates/ethos-cli/tests/pdf_parse.rs index 7c59767f..0738521b 100644 --- a/crates/ethos-cli/tests/pdf_parse.rs +++ b/crates/ethos-cli/tests/pdf_parse.rs @@ -94,6 +94,10 @@ fn two_line_fixture_pdf() -> PathBuf { fixture_pdf_by_id("synthetic-two-lines") } +fn heading_export_fixture_pdf() -> PathBuf { + fixture_pdf_by_id("synthetic-heading-export") +} + fn two_column_fixture_pdf() -> PathBuf { fixture_pdf_by_id("synthetic-two-columns") } @@ -342,6 +346,51 @@ fn doc_parse_text_and_markdown_exports_match_fixture_goldens_when_pdfium_is_conf } } +#[test] +fn parses_heading_fixture_and_exports_markdown_when_pdfium_is_configured() { + if !pdfium_configured() { + eprintln!( + "skipping heading export fixture test: ETHOS_PDFIUM_LIBRARY_PATH is not configured" + ); + return; + } + + let fixture = heading_export_fixture_pdf(); + let doc = parse_json(&[ + "doc", + "parse", + fixture.to_str().unwrap(), + "--format", + "json", + ]); + let elements = doc["payload"]["elements"].as_array().unwrap(); + assert_eq!(elements.len(), 2); + assert_eq!(elements[0]["type"], "heading"); + assert_eq!(elements[0]["heading_level"], 1); + assert_eq!(elements[0]["text"], "Alpha Overview"); + assert_eq!(elements[1]["type"], "text_block"); + assert_eq!(elements[1]["text"], "Trust loop evidence stays explicit"); + + let output = run_ethos(&[ + "doc", + "parse", + fixture.to_str().unwrap(), + "--format", + "markdown", + ]); + assert!( + output.status.success(), + "ethos doc parse --format markdown failed for heading fixture\nstatus: {:?}\nstderr:\n{}\nstdout:\n{}", + output.status.code(), + String::from_utf8_lossy(&output.stderr), + String::from_utf8_lossy(&output.stdout) + ); + assert_eq!( + String::from_utf8(output.stdout).expect("markdown stdout is UTF-8"), + "# Alpha Overview\n\nTrust loop evidence stays explicit\n" + ); +} + #[cfg(debug_assertions)] #[test] fn doc_parse_timeout_kills_pdfium_worker() { diff --git a/docs/execution-status.md b/docs/execution-status.md index 07e51a48..67de83b5 100644 --- a/docs/execution-status.md +++ b/docs/execution-status.md @@ -14,7 +14,7 @@ The committed implementation now includes: - A pinned Phase 1 PDFium profile in `docs/pdfium-profile.md` and `profiles/ethos-deterministic-v1.json`: `chromium/7881`, V8/XFA disabled, platform artifact hashes, runtime library hashes, and provenance are recorded. - Runtime checks that reject missing or mismatched PDFium versions, release artifacts, and extracted libraries with stable errors before dynamic loading. - `ethos doc parse` / `ethos fingerprint` PDF execution through a worker process with `max_parse_ms` timeout enforcement, stable error-envelope relay, diagnostics-gated worker stderr, and page-range validation/filtering. -- Quantized page/span extraction at the backend boundary, plus a basic deterministic layout pass that assembles paragraph `text_block` elements and simple column reading order for the current born-digital fixtures. Fixture validation binds selected `fixture.json` expectations to committed extraction/layout goldens and binds current alpha text/Markdown exports to committed layout output so current read-order, element-type, and export cases fail closed on drift. +- Quantized page/span extraction at the backend boundary, plus a basic deterministic layout pass that assembles paragraph `text_block` elements, fixture-backed alpha heading elements, and simple column reading order for the current born-digital fixtures. Fixture validation binds selected `fixture.json` expectations to committed extraction/layout goldens and binds current alpha text/Markdown exports to committed layout output so current read-order, element-type, heading-export, and export cases fail closed on drift. - Schema/example/profile validation is green through `schemas/validate_examples.py` using `jsonschema` draft 2020-12 validation, including the crop descriptor artifact contract plus referential-integrity and bbox sanity checks outside JSON Schema. - `ethos verify` now produces non-empty quote, value, presence, and table-cell verification checks over native Ethos document JSON and synthetic OpenDataLoader-style JSON through `--grounding opendataloader-json`; it also verifies quote/value/presence citations over pinned real OpenDataLoader 2.4.7 JSON, including grounded and ungrounded cases. Citation/config inputs are rejected when they drift outside the closed schemas. The public demo harness covers grounded, ungrounded, split-quote, not-found, stale-fingerprint, unsupported non-v1 claim, capability-limited, malformed-citation, malformed OpenDataLoader-style input, and summary-format reject paths. - Verification semantics are now trust-honest at alpha scope: quote containment is explicitly labeled, value/table-cell checks require normalized equality, fingerprint-pinned citations fail closed when source fingerprints are unavailable, and structured capability limits explain why a run is downgraded. @@ -49,7 +49,7 @@ Milestone A has an accepted internal Gate Zero decision for roadmap control, so | PDFium Phase 1 profile | Landed: pinned profile, V8/XFA-disabled state, platform hashes, runtime library hashes, and provenance are recorded | Phase 2 project-maintained builds still block Public Beta | | PDFium loader/runtime checks | Landed: missing/mismatched version, artifact, and runtime library hashes fail deterministically | Release packaging and operator setup path still need hardening | | Real PDF backend | Landed for simple born-digital PDFs: page count, quantized spans, worker execution, timeout, page filtering, and fingerprint path exist | Wider corpus coverage, failure fixtures, memory-limit behavior, quirk log, and Gate Zero run are still missing | -| Layout groundwork | Landed: basic paragraph text blocks, simple column reading order over quantized spans, fixture metadata checks against committed extraction/layout goldens for current read-order and element-type expectations, and alpha text/Markdown export goldens derived from committed layout output | Tables, headings, lists, rotation/quirk handling, and confidence policy remain future work | +| Layout groundwork | Landed: basic paragraph text blocks, fixture-backed alpha heading elements, simple column reading order over quantized spans, fixture metadata checks against committed extraction/layout goldens for current read-order and element-type expectations, and alpha text/Markdown export goldens derived from committed layout output | Tables, lists, rotation/quirk handling, and confidence policy remain future work | | Font policy groundwork | Partially landed: substitution table and profile policy are present; fixture output uses deterministic substitution IDs | Bundled fallback asset hashing and broader font/CID validation remain open | | Schema/example validation | Landed: schemas, examples, deterministic profile, referential integrity, and bbox sanity pass the `jsonschema` validation gate | Contract changes still require explicit versioning and compatibility review | | Trust-layer implementation | Landed: `ethos verify` quote/value/presence/table-cell checks, explicit quote-containment labeling, normalized equality for value/table-cell checks, stale and unverifiable fingerprint handling, unsupported claim reporting, structured capability limits, native Ethos JSON path, ODL-style adapter path with synthetic table/cell mapping, pinned real OpenDataLoader 2.4.7 grounded/ungrounded fixtures, foreign fixture manifest hash validation, crop-ref evidence plumbing, stable logical native crop refs, native crop descriptor artifacts, raw BGRA crop rendering in `ethos-pdf`, CLI PNG crop artifact production for bound native source PDFs, same-host rendered crop repeatability check, rendered-crop run comparison helper, strict citation/config input validation, citation input schema, split-quote fixture coverage, explicit unsupported non-v1 claim reporting, OpenDataLoader-style structure diagnostics for malformed bbox and unknown-page references, verify-alpha case inventory checks, and demo fixtures | Still needed: real OpenDataLoader table-cell grounding, additional adapter hardening against broader real output shapes, future claim-kind expansion outside the current v1 alpha policy, and a decision on whether cross-platform rendered crop artifact equality is worth pursuing after the current macOS/Linux bbox drift finding | diff --git a/fixtures/README.md b/fixtures/README.md index f01d497c..05f52a17 100644 --- a/fixtures/README.md +++ b/fixtures/README.md @@ -37,9 +37,8 @@ Successful parse fixtures also carry c14n stage goldens: - `extraction.json`: `ethos_core::traits::Extraction` after the PDF backend boundary. - `layout.json`: `ethos_core::traits::LayoutOutput` after deterministic layout grouping. - `text.txt`: current alpha plain-text export rendered from `layout.json` element text order. -- `markdown.md`: current alpha Markdown export rendered from `layout.json`. The current - committed synthetic fixture set contains text blocks, so these files mirror the same block - order as `text.txt`. +- `markdown.md`: current alpha Markdown export rendered from `layout.json`, including heading + prefixes for committed heading elements. For successful fixtures, `validate_fixtures.py` also binds selected `fixture.json` expectations to those committed goldens: diff --git a/fixtures/manifest.json b/fixtures/manifest.json index cc5fc9f0..e0a4b122 100644 --- a/fixtures/manifest.json +++ b/fixtures/manifest.json @@ -5,6 +5,7 @@ "born_digital", "failure", "fonts", + "headings", "hyphenation", "ligatures", "multi_column", @@ -66,6 +67,18 @@ "provenance": "Synthetic password-protected PDF generated by Ethos maintainers from the simple-text fixture with qpdf 256-bit encryption for stable password-protected error-envelope tests.", "license": "CC0-1.0" }, + { + "id": "synthetic-heading-export", + "file": "synthetic/heading-export/document.pdf", + "sha256": "e5692f6b236da48f85790bffa68787a2d30ec2bcadf4e2d1e0ef9626787537b4", + "pages": 1, + "subsets": [ + "born_digital", + "headings" + ], + "provenance": "Synthetic PDF generated by Ethos maintainers for Milestone B alpha heading export fixture coverage.", + "license": "CC0-1.0" + }, { "id": "synthetic-hyphenated-line-break", "file": "synthetic/hyphenated-line-break/document.pdf", diff --git a/fixtures/synthetic/heading-export/document.pdf b/fixtures/synthetic/heading-export/document.pdf new file mode 100644 index 00000000..5146443a Binary files /dev/null and b/fixtures/synthetic/heading-export/document.pdf differ diff --git a/fixtures/synthetic/heading-export/extraction.json b/fixtures/synthetic/heading-export/extraction.json new file mode 100644 index 00000000..2cd5725e --- /dev/null +++ b/fixtures/synthetic/heading-export/extraction.json @@ -0,0 +1 @@ +{"pages":[{"height":79200,"id":"p0001","index":1,"rotation":0,"width":61200}],"regions":[],"spans":[{"bbox":[7200,5482,13236,7675],"font_id":"subst:liberation-sans-regular","font_size_q":2400,"id":"s000001","origin_locator":{"first_origin":[7200,7200],"last_origin":[12002,7200],"policy":"origin-run-locator-v1"},"page":"p0001","text":"Alpha"},{"bbox":[14119,5450,23986,7226],"font_id":"subst:liberation-sans-regular","font_size_q":2400,"id":"s000002","origin_locator":{"first_origin":[14004,7200],"last_origin":[22272,7200],"policy":"origin-run-locator-v1"},"page":"p0001","text":"Overview"},{"bbox":[7228,10541,9925,11413],"font_id":"subst:liberation-sans-regular","font_size_q":1200,"id":"s000003","origin_locator":{"first_origin":[7200,11400],"last_origin":[9600,11400],"policy":"origin-run-locator-v1"},"page":"p0001","text":"Trust"},{"bbox":[10344,10541,12487,11638],"font_id":"subst:liberation-sans-regular","font_size_q":1200,"id":"s000004","origin_locator":{"first_origin":[10267,11400],"last_origin":[11868,11400],"policy":"origin-run-locator-v1"},"page":"p0001","text":"loop"},{"bbox":[12913,10541,17622,11413],"font_id":"subst:liberation-sans-regular","font_size_q":1200,"id":"s000005","origin_locator":{"first_origin":[12869,11400],"last_origin":[17004,11400],"policy":"origin-run-locator-v1"},"page":"p0001","text":"evidence"},{"bbox":[18042,10560,20759,11651],"font_id":"subst:liberation-sans-regular","font_size_q":1200,"id":"s000006","origin_locator":{"first_origin":[18005,11400],"last_origin":[20206,11400],"policy":"origin-run-locator-v1"},"page":"p0001","text":"stays"},{"bbox":[21184,10541,24798,11638],"font_id":"subst:liberation-sans-regular","font_size_q":1200,"id":"s000007","origin_locator":{"first_origin":[21139,11400],"last_origin":[24473,11400],"policy":"origin-run-locator-v1"},"page":"p0001","text":"explicit"}],"warnings":[]} diff --git a/fixtures/synthetic/heading-export/fixture.json b/fixtures/synthetic/heading-export/fixture.json new file mode 100644 index 00000000..667356b8 --- /dev/null +++ b/fixtures/synthetic/heading-export/fixture.json @@ -0,0 +1,16 @@ +{ + "id": "synthetic-heading-export", + "kind": "pdf", + "provenance": "Synthetic PDF generated by Ethos maintainers for Milestone B alpha heading export fixture coverage.", + "license": "CC0-1.0", + "subsets": ["born_digital", "headings"], + "description": "One-page synthetic PDF with a large heading followed by body text for alpha text and Markdown export coverage.", + "document": "document.pdf", + "sha256": "e5692f6b236da48f85790bffa68787a2d30ec2bcadf4e2d1e0ef9626787537b4", + "pages": 1, + "expected_text": ["Alpha Overview", "Trust loop evidence stays explicit"], + "expected_span_text": ["Alpha", "Overview", "Trust", "loop", "evidence", "stays", "explicit"], + "expected_pages": 1, + "expected_elements": 2, + "expected_element_types": ["heading", "text_block"] +} diff --git a/fixtures/synthetic/heading-export/layout.json b/fixtures/synthetic/heading-export/layout.json new file mode 100644 index 00000000..a44820e5 --- /dev/null +++ b/fixtures/synthetic/heading-export/layout.json @@ -0,0 +1 @@ +{"elements":[{"bbox":[7200,5450,23986,7675],"confidence":850,"heading_level":1,"id":"e000001","page":"p0001","span_refs":["s000001","s000002"],"text":"Alpha Overview","type":"heading"},{"bbox":[7228,10541,24798,11651],"id":"e000002","page":"p0001","span_refs":["s000003","s000004","s000005","s000006","s000007"],"text":"Trust loop evidence stays explicit","type":"text_block"}],"warnings":[]} diff --git a/fixtures/synthetic/heading-export/markdown.md b/fixtures/synthetic/heading-export/markdown.md new file mode 100644 index 00000000..555d5990 --- /dev/null +++ b/fixtures/synthetic/heading-export/markdown.md @@ -0,0 +1,3 @@ +# Alpha Overview + +Trust loop evidence stays explicit diff --git a/fixtures/synthetic/heading-export/text.txt b/fixtures/synthetic/heading-export/text.txt new file mode 100644 index 00000000..e23e9a0c --- /dev/null +++ b/fixtures/synthetic/heading-export/text.txt @@ -0,0 +1,3 @@ +Alpha Overview + +Trust loop evidence stays explicit