From 0224ef62b1d7b873fc779f762420e1be7b003944 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 1 Nov 2026 15:47:22 +0000 Subject: [PATCH 1/7] fix: reject malformed PDF widget geometry Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- EXPLAINME.adoc | 12 +- TEST-NEEDS.adoc | 23 +- TOPOLOGY.adoc | 18 +- docs/ci/CHECK-DETERMINATIONS.adoc | 115 ++- ...0-10-ultraplan-formatrix-ffp-geometry.adoc | 762 ++++++++++++++++++ rust/pdftool_core/README.adoc | 7 +- rust/pdftool_core/src/lib.rs | 294 ++++++- wiki/Architecture.md | 7 +- wiki/CI-and-Gates.md | 11 + wiki/Contributing--Dev-Setup.md | 7 +- wiki/Home.md | 6 +- 11 files changed, 1183 insertions(+), 79 deletions(-) create mode 100644 docs/plans/2026-10-10-ultraplan-formatrix-ffp-geometry.adoc diff --git a/EXPLAINME.adoc b/EXPLAINME.adoc index 632b7c6..15087af 100644 --- a/EXPLAINME.adoc +++ b/EXPLAINME.adoc @@ -27,7 +27,7 @@ declaration of anything. |=== | Path | What's There -| `rust/pdftool_core/src/lib.rs` | The whole core: `detect_blocks`, `fill_blocks`, the field walkers, the `BW_*` error taxonomy, and 6 unit tests. ~1030 lines. +| `rust/pdftool_core/src/lib.rs` | The whole core: `detect_blocks`, `fill_blocks`, the field walkers, the `BW_*` error taxonomy, and 17 test functions (6 original + 11 detector tests in the current unverified diff). ~1290 lines. | `src/*.affine` | AffineScript prototype: popup, content script, background script, `core/PdfTool`, `core/Storage`, `core/ProvenMount`, `components/Block`, `components/FormFiller`. Not buildable in this checkout. | `public/` | `manifest.json`, `popup.html`, icons. | `tests/aspect/` | Aspect-oriented test shell script. @@ -47,11 +47,11 @@ declaration of anything. |=== | Claim in the README | Where it is checked -| The Rust core is implemented and has unit tests -| `cargo test --manifest-path rust/pdftool_core/Cargo.toml --locked` — 6 tests, including AcroForm writeback for text/select and button widgets and the structured error taxonomy. +| The Rust core has unit tests +| The current source contains 17 test functions: 6 original fill/error tests and 11 direct detector tests. Latest main baseline run `38818494415` at `3fd5ba1` passed; the current working diff has not been compiled or run because `cargo`/`rustc` are unavailable here. -| `cargo fmt`, `cargo test` and `cargo clippy -D warnings` all pass -| `.github/workflows/ci.yml`, job `Rust core (tests, formatting, lint)`. As of 2026-09-27 this job is green on `main` at `e1b6225`. +| `cargo fmt`, `cargo test` and `cargo clippy -D warnings` pass on baseline `main` +| `.github/workflows/ci.yml`, job `Rust core (tests, formatting, lint)`. Run `38818494415` on `main` at `3fd5ba1` passed all three; this does not verify the current branch diff. | The frontend is a prototype with no build pipeline | There is no AffineScript compiler configuration and no JavaScript package manifest in the tree. `deno.json` was deleted by #43. @@ -60,7 +60,7 @@ declaration of anything. | `apply_field_value` in `rust/pdftool_core/src/lib.rs`, dispatched on field type `Tx` / `Ch` / `Btn`. | Errors carry structured codes -| `CoreErrorPayload { code, message, context }`; 20 `BW_*` codes. +| `CoreErrorPayload { code, message, context }`; 24 unique `BW_*` codes in the current source. Four new detector codes remain unverified and need the compatibility/version review required for public API changes. |=== diff --git a/TEST-NEEDS.adoc b/TEST-NEEDS.adoc index 761b2a3..cacb73a 100644 --- a/TEST-NEEDS.adoc +++ b/TEST-NEEDS.adoc @@ -3,7 +3,7 @@ === CRG Grade: C — ACHIEVED 2026-04-04 -Last reviewed: 2026-09-27. +Last reviewed: 2026-11-01. === Current Test State @@ -12,17 +12,24 @@ Last reviewed: 2026-09-27. |Category |Count |Notes |Test directories |1 |Location(s): /tests (aspect + fuzz placeholder) |CI workflows |14 |`.github/workflows/*.yml`. `ci.yml` runs the Rust core checks; `actions.lock` is a file, not a workflow. See docs/ci/CHECK-DETERMINATIONS.adoc -|Unit tests |6 |Rust `pdftool_core` — `cargo test --manifest-path rust/pdftool_core/Cargo.toml --locked` +|Unit tests |17 in source |Rust `pdftool_core` — 6 existing fill/error tests + 11 direct detector tests in the current working diff. Current diff is uncompiled and unrun. |=== === What's Covered -* [x] Rust core unit tests (block detection, `fill_blocks_*` AcroForm writeback for - `Tx`/`Ch`/`Btn` fields, taxonomy errors) — 6 tests, all green on `main` at `e1b6225` -* [x] `cargo fmt -- --check` in CI — green -* [x] `cargo clippy --all-targets -- -D warnings` in CI — green (the two remaining - lints were fixed in #73) -* [x] `actions.lock` synchronisation gate (`lock-sync-gate.yml`) — green on `main` +* [x] Rust core baseline CI on `main` — run `38818494415` at `3fd5ba1` + passed formatting, tests, and Clippy (actor `hyperpolymath`). That run predates + this branch's detector changes. +* [ ] Current working diff — 17 test functions are present, including 11 direct + detector tests for valid widgets, indirect rectangles, malformed annotation + arrays/references, invalid rectangles, and non-widget annotations. They have + not been compiled or run because this sandbox has no Rust toolchain. +* [ ] `actions.lock` synchronisation gate — failing on current `main` at run + `38818494312`; the YAML uses `smtp-notify-action@v0.5.0` while generated lock + metadata records `v0.3.0`. Do not hand-edit the generated file. +* [ ] Governance Actions lock verification — failed at run `38818495000` for + the same lock drift; other job outcomes and additional current failures are + recorded in `docs/ci/CHECK-DETERMINATIONS.adoc`. === Still Missing (for CRG B+) diff --git a/TOPOLOGY.adoc b/TOPOLOGY.adoc index beb35f1..3b130f9 100644 --- a/TOPOLOGY.adoc +++ b/TOPOLOGY.adoc @@ -1,8 +1,8 @@ == blocky-writer — Project Topology .... -Last updated: 2026-09-27 -Status: Rust core green (fmt + 6 tests + clippy); frontend prototype, no build pipeline +Last updated: 2026-11-01 +Status: main Rust CI passed at `3fd5ba1`; current 17-test source diff is unverified; frontend prototype, no build pipeline .... === System Architecture @@ -65,20 +65,20 @@ EXTENSION LAYERS Extension bundle pipeline ░░░░░░░░░░ 0% deno.json removed in #43; no toolchain CORE (RUST/WASM) - pdftool_core (WASM) ████████░░ 80% detect_blocks + fill_blocks, 6 tests green + pdftool_core (WASM) ████████░░ 80% Main baseline passes CI; 17 tests in source, 11 new detector tests unverified AcroForm Writeback ████████░░ 80% Tx / Ch / Btn only; no Sig Ruled-line & cell detection ░░░░░░░░░░ 0% NOT IMPLEMENTED — the "hand spacing" case - Error Taxonomy ██████████ 100% 20 stable BW_* codes + Error Taxonomy ████████░░ 80% 24 codes in source; 4 new codes need Rust + compatibility review REPO INFRASTRUCTURE - Rust CI (fmt/test/clippy) ██████████ 100% ci.yml rust-core job — green - Lock Sync Gate ██████████ 100% lock-sync-gate.yml — green on main - Documentation ████████░░ 80% README/TOPOLOGY/wiki corrected 2026-09-27 - .machine_readable/ ██████████ 100% STATE.a2ml tracking + Rust CI (fmt/test/clippy) ██████████ 100% main run 38818494415 passed at 3fd5ba1; branch unverified + Lock Sync Gate ░░░░░░░░░░ 0% main run 38818494312 failed on generated-lock drift + Documentation ████████░░ 80% Human-readable status refreshed 2026-11-01; machine state remains stale + .machine_readable/ ████░░░░░░ 40% STATE.a2ml exists but is stale; A2ML/DEED owner ruling pending Containerfile ██████████ 100% Reproducible dev env ───────────────────────────────────────────────────────────────────────────── -OVERALL: █████░░░░░ ~50% Rust core ready; extension not release-ready +OVERALL: █████░░░░░ ~50% Main Rust baseline passes; branch validation pending; extension not release-ready .... === Key Dependencies diff --git a/docs/ci/CHECK-DETERMINATIONS.adoc b/docs/ci/CHECK-DETERMINATIONS.adoc index ca3fee7..f10fa37 100644 --- a/docs/ci/CHECK-DETERMINATIONS.adoc +++ b/docs/ci/CHECK-DETERMINATIONS.adoc @@ -7,6 +7,9 @@ The record required by the estate stopping rule (hyperpolymath/standards#968, #994): every CI check that has ever been red on `main` has exactly one of three determinations -- *fixed*, *retired*, or *exempt* -- and none is ever closed by muting it (`continue-on-error`, demotion to a warning, or silent removal). +The current-main snapshot below identifies active failures that are still +unresolved; this ledger remains incomplete until each receives an evidence-based +*fixed*, *retired*, or *exempt* determination. “Open” is not a fourth closure. == Ledger @@ -37,39 +40,97 @@ muting it (`continue-on-error`, demotion to a warning, or silent removal). gone until the thing it would build exists. | `rust-core` (CI) -| *Fixed -- awaiting one owner-triggered green run on `main`* -| 2026-09-27 +| *Fixed on `main`; current feature branch unverified* +| 2026-11-01 | Replacement for both retired jobs: `cargo fmt -- --check`, `cargo test - --locked`, `cargo clippy --all-targets -- -D warnings` on - `rust/pdftool_core`. It did execute once, contrary to the previous revision - of this row: run 36296845297 (push, `e1b6225`, actor `hyperpolymath`) got as - far as three of its steps -- *Check formatting* `success`, *Run unit tests* - `success` (6/6), *Run Clippy* `failure`. Clippy was the only red step and it - reported exactly two lints, both in `rust/pdftool_core/src/lib.rs`: - `clippy::map_clone` on `doc.get_object(*id).map(Clone::clone)` and - `clippy::unnecessary_cast` on `Object::Real(v) => Some(*v as f32)`. Both are - repaired in #73 (`.cloned()` and `Some(*v)` respectively); neither repair - weakens the gate, and the adjacent `Object::Integer(v) => Some(*v as f32)` - is deliberately left alone because that `i64` -> `f32` cast is real. The job - has *not* run with the repair in it: every run on #73's branch is - bot-triggered and refused at startup (<>), so the step is verified by - inspection only. This row therefore stays open until one run triggered by - `hyperpolymath` is green on `main`; if that run is red, the crate has a real - defect and this row becomes a repair item. It is not closed until such a run - exists. + --locked`, and `cargo clippy --all-targets -- -D warnings` on + `rust/pdftool_core`. The historical first run at `e1b6225` passed formatting + and six tests but failed two Clippy lints; #73 repaired those without + weakening the gate. The latest main push run, `38818494415` at + `3fd5ba1ed3deee22288d3441b1f3a833246995e2` (actor `hyperpolymath`), passed all + three Rust steps. This closes the old “awaiting first owner-triggered green” + status for `main` only. The current Arena branch contains uncompiled detector + changes and has no test/format/Clippy run; do not infer its status from this + baseline. | `Lock Sync Gate` / `actions.lock is in sync with the workflow YAML` -| *Fixed* -| 2026-09-27 -| Red on `main` from #69 (2026-09-24) onward, and the cause of every other - workflow on `main` reporting `startup_failure` at d50a857 -- see - <>. Resynchronised in the fix for #67. +| *Open — failing on current `main`* +| 2026-11-01 +| The fix for #67 was valid at the time, but the lock drifted again after #74. + On current main, run `38818494312` at `3fd5ba1` failed its + `Verify lockfile synchronisation` step. The current workflow uses + `hyperpolymath/smtp-notify-action@v0.5.0`; the generated lock still records + `@v0.3.0`. The supported `gh actions-lock` generator is unavailable in this + sandbox, so the lock has deliberately not been hand-edited. +|=== + +== Current `main` run snapshot (2026-11-01) + +Current `origin/main` is `3fd5ba1` (#75); this session branch tip is +`c2b073e` (#74). Both commits are shallow/grafted in this checkout, so their +ancestry is not inferred. The following are latest observed outcomes on that +SHA or later scheduled runs against it, not evidence for the untested Arena +branch. + +[cols="2,1,1,3",options="header"] |=== +| Check / workflow | Run | Result | Evidence and current disposition + +| Rust core (`CI`) +| `38818494415` +| Passed +| Push on `3fd5ba1`, actor `hyperpolymath`; formatting, tests, and Clippy all + succeeded. Current branch changes remain untested. + +| Governance / Actions lockfile verify +| `38818495000` +| Failed +| Push at `3fd5ba1`, actor `hyperpolymath`. Only `governance / Actions lockfile + verify` failed; its verification step failed. Other Governance jobs passed or + were explicitly skipped. Same visible lock drift as the standalone gate. + +| Lock Sync Gate +| `38818494312` +| Failed +| Push at `3fd5ba1`, actor `hyperpolymath`; `Verify lockfile synchronisation` + failed. The workflow/lock mismatch is observable; do not replace the generated + lock by hand. + +| Mirror to Git Forges +| `38818495057` +| Failed +| Push at `3fd5ba1`, actor `hyperpolymath`. mirror-bitbucket, mirror-gitea, + mirror-disroot, and mirror-codeberg failed; mirror-gitlab, mirror-sourcehut, + and mirror-radicle succeeded. Log retrieval + returned EOF; failure causes remain unknown. + +| CodeQL Advanced +| `41099036814` +| Failed +| Scheduled run on `3fd5ba1`, actor `hyperpolymath`; both Analyze jobs failed at + `Set up job`. Log retrieval returned EOF, so cause is unknown. + +| Hypatia Security Scan +| `41255373342` +| Failed +| Scheduled run on `3fd5ba1`, actor `hyperpolymath`; + `Hypatia Neurosymbolic Analysis` failed at `Set up job`. Log retrieval + returned EOF, so cause is unknown. + +| Secret Scanner / GitHub Pages +| `38818495110` / `38818494311` +| Passed +| Both passed on the `3fd5ba1` main push, actor `hyperpolymath`. +|=== + +The only root cause established in this snapshot is the generated Actions lock +drift. Do not attribute the setup-step or mirror failures to code, credentials, +network, or remote policy without accessible evidence. -== Also red on `main`? +== Historical red checks for #67 -Yes, for both checks named in #67, and for reasons that have nothing to do -with the `actions.lock` cure (#66) that surfaced them: +At the time of #67, both checks named in that fix were red for reasons that had +nothing to do with the `actions.lock` cure (#66) that surfaced them: * `core-fill-tests` and `extension-build` were red on every run since #43, which predates #66 by a month. #66 revived the workflow that runs them; it diff --git a/docs/plans/2026-10-10-ultraplan-formatrix-ffp-geometry.adoc b/docs/plans/2026-10-10-ultraplan-formatrix-ffp-geometry.adoc new file mode 100644 index 0000000..0795f94 --- /dev/null +++ b/docs/plans/2026-10-10-ultraplan-formatrix-ffp-geometry.adoc @@ -0,0 +1,762 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 += Blocky Writer ultraplan: FFP, geometry fidelity, and suite boundaries +:doctype: book +:toc: left +:toclevels: 3 +:sectnums: +:revdate: 2026-11-01 +:status: in-progress + +== Executive disposition + +This remains an evidence-backed plan with partial implementation in the session +branch, not a completed change. The requested work has four separate +owners/scopes and must not be collapsed into a new integration product: + +. Restore trustworthy repository health records and repair the current Actions + lock mismatch. +. Bring Blocky Writer's *producer* into conformance with active, ratified FFP + 1.0, then declare and test the existing Blocky Writer -> Presswerk handoff + under the estate's active SEAMS-SPEC. +. Establish direct detection tests and resolve geometry/field-identity behavior + against the DocMatrix composition contract before changing that behavior. +. Handle Formatrix Docs#61 and RSR-template-repo#200 as bounded, informational + follow-ups owned by those repositories; do not build a viewer/parser + integration or edit another repository here. + +The boundary remains explicit: Formatrix Docs owns multi-format viewing and +editing; Blocky Writer owns fixed-layout PDF/form placement. DocMatrix#71 rejects +a new runtime integration engine. A test-only producer-to-consumer check is not +a runtime integration, but it must not create a runtime dependency or a new +universal document serializer. + +== Current implementation status (2026-11-01) + +[cols="1,3",options="header"] +|=== +| Work area | Current state on `arena/d32cfaf0-blocky-writer` + +| Detector geometry +| The working diff rejects reversed, non-finite, and lossy integer widget + rectangles; resolves an indirect `/Rect`; and reports malformed annotation + arrays, unresolved/invalid annotations, and invalid widget rectangles with + structured errors. This adds four public `BW_*` codes; compatibility/version + review remains pending. Eleven direct detector tests and the six original + fill/error tests are present (17 test functions total). None has been + compiled or run on this branch. + +| FFP producer +| Not implemented. `fill_blocks_impl` still writes no FFP XMP marker. Safe XMP + merging and marker tests have not been implemented or validated; no-op + semantics and truthful appearance metadata also remain open. Do not claim + producer conformance. This remains local implementation work once it can be + developed and verified without replacing or damaging existing XMP. + +| Producer-to-Presswerk seam +| Not implemented. The standards-owner questions on contract/test paths, + `fail-degrade`/`external-trust`, and circuit-breaker applicability remain + unresolved. No Trustfile or Mustfile seam declaration has been added. + +| CI / lock +| `origin/main` is at `3fd5ba1` (#75); this session branch is at `c2b073e` + (#74). Both commits are shallow/grafted in this checkout, so their ancestry is + not inferred. Current `main` still has `push-email-notify.yml` at v0.5.0 while the generated + lock records v0.3.0. Main's latest Rust CI run passed, but its Governance and + Lock Sync Gate runs failed on lock verification. The prescribed + `gh actions-lock` generator is unavailable here; the lock has not been edited. + +| Validation environment +| `cargo`, `rustc`, `rustfmt`, `gawk`, `go`, and `asciidoctor` are absent from + `PATH`. Rust formatting, compilation, tests, Clippy, lock verification, and + AsciiDoc rendering therefore remain pending. Do not describe the working diff + as passing. + +| Estate follow-ups +| No other repository or issue has been edited or closed. The A2ML/DEED + interpretation and SEAMS-SPEC gates remain as recorded below. +|=== + +== Recon snapshot and limits + +[cols="1,3",options="header"] +|=== +| Item | Snapshot used + +| Blocky Writer +| `c2b073eaf5e204f466adc0797484478ce94ef95b`, on the session branch + `arena/d32cfaf0-blocky-writer`; the checkout was clean before this plan was + written. + +| Estate standards +| `hyperpolymath/standards` at + `3108c1b01186741eea80c00da3f4beda912286f9` (2026-10-09). + +| RSR template +| `hyperpolymath/rsr-template-repo` at + `df09a6c50c4c77e93abac0fdf162509bf5648611` (2026-10-09). + +| Formatrix Docs +| `hyperpolymath/formatrix-docs` at + `85e6692aaa544c0b2b7b41f156c11f3de25d95fc` (2026-10-09). + +| Presswerk +| `hyperpolymath/presswerk` at + `f17620192b69ccffb9454cd49aac8d5ec9454c48` (2026-10-05). +|=== + +No Rust tests, build, format, lint, FFP conformance run, browser build, or +AsciiDoc render has run on the current working tree. `cargo`, `rustc`, `rustfmt`, +`gawk`, `go`, `asciidoctor`, `just`, `wasm-pack`, and `deno` are not on `PATH`. +The plan therefore distinguishes source inspection and recorded remote CI +evidence from checks actually executed in this sandbox. Remote CI results below +are for `origin/main` at `3fd5ba1`, not this feature branch. + +== Binding rules and authority + +Use the status of each source, not just its location in the estate: + +* **FFP 1.0.0** is active and ratified (2026-10-04). Its MARKER producer + obligations are normative. FFP says a producer that wrote any field value + MUST set `ffp:filledBy=machine` in an XMP packet in the PDF catalog's + `/Metadata`; it MUST preserve, not remove or overwrite, existing XMP. FFP + recommends appearance generation, but that recommendation is not the marker + obligation. +* **SEAMS-SPEC 1.0.0** is active and normative. Its seam register belongs in + the canonical Trustfile at + `.machine_readable/contractiles/trust/Trustfile.a2ml`; it requires a local + P2P `test_ref`, and says a Mustfile `seam-check` must run the P2P test until + the planned `contractile trust verify` exists. +* **DocMatrix#71** is the live composition contract and the current owner ruling + is controlling: products remain independently useful and own their schemas; + Groove handles discovery/compatibility; bootstrap is static typed + records/direct invocation; do not introduce another runtime integration + engine. ForthWall remains proposed and audit/propose/verify-only. +* **Blocky Writer's own contractiles remain binding.** `INTENT.contractile` + says the WASM boundary is exactly two functions, filling is name-driven, and + page geometry is never silently normalized. `MUST.contractile` forbids new + TypeScript/Python/Go and NPM dependencies, removal/weakening of tests or CI, + loss of `.machine_readable/`, and unapproved unsafe Rust. Preserve these + constraints while implementing the plan. +* **The A2ML/DEED format position is unresolved.** Current standards A2ML + `meta`, `state`, and `ecosystem` documentation carries a warning that the old + TOML-like characterization is superseded and points to DEED. But + `DEED-GRAMMAR-SPEC.adoc` v0.2.2 explicitly says DRAFT and not ratified. Do + not present the old TOML-like examples as current conformance grammar, and do + not present the draft DEED grammar as ratified. This is a standards-owner + decision gate, not permission to infer a universal parser from local files. +* `0-canon/rsr/RSR-SPEC-v2.adoc` in the standards checkout is explicitly DRAFT. + Do not cite it as a ratified requirement. The current RSR template's executable + minting/conformance scripts and the open template issue are evidence for the + narrowly scoped follow-up below. + +== Findings that determine the order + +[cols="1,2,3",options="header"] +|=== +| Area | Verified finding | Consequence + +| CI and state +| https://github.com/hyperpolymath/blocky-writer/pull/74[PR#74] merged + `smtp-notify-action` v0.5.0 into `.github/workflows/push-email-notify.yml`; + `.github/workflows/actions.lock` still records v0.3.0. The latest observed + Rust CI run at `c2b073e` passed, + while Governance and Lock Sync Gate failed. Four mirror jobs also failed; + their causes are not established. +| Repair the generated lock first, then refresh the health ledger from + owner-triggered/current runs. Do not disable checks or attribute mirror + failures without their evidence. + +| FFP producer +| `fill_blocks_impl` writes AcroForm values (`/V`, `/DV`, and/or `/AS`) and sets + `/NeedAppearances=true`; it emits no FFP XMP marker. Presswerk has a detector + and print-path consumer. Its documented 14/14 conformance result was not + independently run in this sandbox. +| This is Blocky Writer's own producer-conformance work, not an invitation to + add a Presswerk runtime integration. Without a marker, the standard's + structural detector may only classify the output as suspected. + +| FFP appearances +| Blocky Writer does not generate `/AP` appearance streams; it relies on + `/NeedAppearances`. FFP says producers SHOULD generate appearances and says + that a producer writing values without `/AP` MUST declare + `appearancesGenerated=false` if it emits that property (or may omit it). +| Mark the current hazard truthfully. Assess appearance generation separately; + do not claim that a provenance marker makes the PDF safe to print. + +| Seam register +| Blocky Writer's canonical Trustfile has no `[SEAMS]`; its declarative + `contractiles/must/Mustfile.a2ml` has no `seam-check`; no local P2P test was + found. The inspected Presswerk Trustfile also has no FFP seam entry. +| Both sides need a coordinated declaration and runnable P2P evidence. The FFP + worked example also conflicts with SEAMS-SPEC on paths, the `fail-degrade` / + `external-trust` pairing, and possible circuit-breaker expectations; resolve + those before claiming full seam conformance (see WP-1). + +| Detection tests +| At the inspected base `c2b073e`, the six Rust unit tests all exercised + fill/error paths; none called `detect_blocks`, although `TEST-NEEDS.adoc` + marked block detection as covered. The working diff now adds eleven direct + detector tests, bringing the source count to 17; these remain uncompiled and + unrun. +| Correct the coverage record, keep the unverified tests clearly marked, and + complete the remaining geometry/identity cases only after their contracts are + agreed. + +| Geometry +| At `c2b073e`, `rect_from_object` used `min` and `abs`, normalizing reversed + corners, while `detect_blocks_impl` silently skipped some malformed or + unresolved annotation/rectangle cases. The working diff rejects reversed, + non-finite, and lossy integer coordinates, resolves one-level indirect + rectangles, and returns structured errors for malformed annotations and + widget rectangles. Coordinates remain `f32` PDF user-space values. +| The diff removes silent normalization and several silent skips, but remains + uncompiled and does not resolve page transforms, identity, Unicode decoding, + or the full consumer precision contract. Preserve those as explicit review + gates; do not claim complete geometry conformance. + +| Field identity +| `Block` is `{label,x,y,width,height}`. Detection does not return an explicit + page, a full hierarchical field name, or a stable widget identity. Text + extraction uses lossy UTF-8 and trims NUL/whitespace. The `fill_blocks` + `blocks` argument is validated but intentionally unused; filling is + name-driven. +| Treat the JSON in Formatrix Docs#61 as an informational example, not a stable + interchange API. Unicode and identity handling are risks to test and contract, + not yet proven defects. + +| Formatrix/A2ML +| Formatrix Docs' current README and TUI enum list TXT, MD, ADOC, DJOT, ORG, + RST, and TYP; neither lists A2ML. Targeted source search found no A2ML parser. + The open #61 body says A2ML is tabbed and suggests this repository's files as + a corpus. +| The premise in #61 is stale against the inspected Formatrix snapshot. Ask the + maintainer to confirm intent. The 17 local `.a2ml` and four `.contractile` + files are not a canonical positive parser corpus under the unresolved + standards position. + +| RSR template +| Blocky Writer's local CONTRIBUTING guide was already rewritten in merged + PR#73. RSR-template-repo#200 remains open. Its current mint process performs + identity substitutions, but inspected checks do not verify that a generated + guide's commands, paths, and repository identity are substantively correct. +| Keep #200 external and narrowly scoped. Do not re-edit Blocky Writer's guide + or patch the template from this repository. +|=== + +== Work package 0 — restore evidence-quality CI and health records + +=== Goal + +Return to a state where the repository's green/red claims, lockfile, and +stopping-rule ledger describe the exact checked revision. This work is +independent of the FFP and geometry changes and should be a separate change set. + +=== Actions + +Remote evidence rechecked on 2026-11-01. `origin/main` is +`3fd5ba1ed3deee22288d3441b1f3a833246995e2` (#75); this session branch is at +`c2b073e` (#74). Both tips are shallow/grafted in this checkout, so ancestry is +not inferred. Main's +latest push-triggered Rust CI run, `38818494415` (2026-10-15, actor +`hyperpolymath`), passed formatting, unit tests, and Clippy. That run does not +include this branch's detector changes. + +The same main push (actor `hyperpolymath`) failed Governance run `38818495000` +at `governance / Actions lockfile verify` and Lock Sync Gate run `38818494312` +at `Verify lockfile synchronisation`. Main's generated `actions.lock` is now format +v0.0.3 after #75, but still records `hyperpolymath/smtp-notify-action@v0.3.0` +while the workflow uses v0.5.0. Mirror run `38818495057` (actor `hyperpolymath`) failed in +mirror-bitbucket, mirror-gitea, mirror-disroot, and mirror-codeberg; GitLab, +SourceHut, and Radicle succeeded. Causes remain unknown. + +Scheduled CodeQL run `41099036814` (2026-10-30, actor `hyperpolymath`) failed at +`Set up job` for both analysis jobs; Hypatia run `41255373342` (2026-11-01, +actor `hyperpolymath`) failed at `Set up job`. Log downloads for these and the +mirror/lock runs returned EOF, so their causes +are not established. Secret Scanner run `38818495110` and Pages run +`38818494311` passed on the same main push, also triggered by `hyperpolymath`. +These are remote observations, not local test execution. The current branch +still has no passing CI evidence. + +. Regenerate `.github/workflows/actions.lock` with the prescribed + `gh actions-lock` tooling after the #74 action bump. Do not hand-edit the + generated file and do not invent a resolved SHA. The extension command is not + installed here (`gh actions-lock` is unknown; installing the extension failed + while downloading its release asset), and `gawk`/`go` are absent. Leave the + generated file unchanged until the supported generator is available; then + verify with `scripts/check-lock-sync.sh` and keep YAML plus lock metadata in + the same change. +. Have an allowed owner/actor run repository checks after lock regeneration. + Inspect the run-page annotation as well as REST/run summaries: a lockfile + refusal and an org policy refusing bot-triggered Actions are distinct. A run + with zero jobs is not evidence that Rust tests passed or failed. +. Refresh `docs/ci/CHECK-DETERMINATIONS.adoc` from exact current evidence. The + Rust core row predates passing main run `38818494415` at `3fd5ba1`; the Lock + Sync Gate row incorrectly says “Fixed” despite the post-#74 drift. Record the + new IDs, actor, step results, and unresolved scheduled/mirror failures without + claiming that this branch was tested. +. Triage mirror failures separately. Run `38818495057` failed in + mirror-bitbucket, mirror-gitea, mirror-disroot, and mirror-codeberg; GitLab, + SourceHut, and Radicle passed. Record those outcomes, but do not guess whether + failures are credential, network, remote-policy, or code failures. Do not + weaken or delete mirror checks. +. Reconcile human-readable repository claims against the checked tree and runs: + `README.adoc`, `docs/ecosystem/ECOSYSTEM.adoc`, `TEST-NEEDS.adoc`, + `TOPOLOGY.adoc`, `EXPLAINME.adoc`, `rust/pdftool_core/README.adoc`, and + `docs/ci/CHECK-DETERMINATIONS.adoc`. Keep the product boundary explicit and + preserve `REQUIRES_INITIALISATION.adoc`'s resolved-template status. Do not + rewrite `.machine_readable/6a2/STATE.a2ml` or contractiles to settle the + unresolved A2ML/DEED syntax; label that status snapshot stale instead. Review + the old `quality.yml` enforcement references, but do not make an unapproved + contract change. +. **State-format stop condition:** before extending + `.machine_readable/6a2/STATE.a2ml`, get an explicit standards-owner ruling on + the A2ML/DEED status conflict above. Until then, make human-readable + AsciiDoc health/CI records accurate and clearly mark the machine-readable + status file as stale; do not silently declare its legacy TOML-like syntax + standards-conforming or convert it to an unratified draft grammar. + +=== Acceptance + +* `scripts/check-lock-sync.sh` passes against the tracked workflow YAML and + generated lockfile. +* A current, owner-triggered CI run reports the exact Rust, Governance, and + Lock Sync outcomes; no run is described as green solely from a bot + `startup_failure`. +* `CHECK-DETERMINATIONS.adoc` contains a dated determination for every currently + red-on-main check, with evidence and no muted/deleted failure. +* Health documentation does not claim detection tests that do not exist or + report stale run IDs as current. +* Mirror failure causes remain explicitly unknown until independently + evidenced. + +== Work package 1 — FFP producer and declared seam + +=== Goal + +Make Blocky Writer's filled output honestly declare machine-written field +values under ratified FFP 1.0, preserve existing metadata, and provide a +runnable producer-to-Presswerk seam test without adding runtime coupling. + +=== 1A. Producer behavior + +Implement within the existing `fill_blocks` path; do not add a WASM export or a +new runtime service. + +* Track actual successful field writes. Add the FFP marker only after the fill + operation has successfully written at least one value. The marker belongs in + an XMP packet carried by the catalog `/Metadata` stream, never `/Info`. +* Use the FFP 1.0 namespace URI exactly and set `ffp:filledBy=machine`. The + optional `ffp:tool` SHOULD identify `blocky-writer`. Omit `filledAt` and + `toolVersion` in the first pass: neither is required, the former is + privacy-bearing, and Presswerk's release-preparation note recommends consent + before emitting `toolVersion`/`filledAt`. +* The current writer does not generate `/AP`. If it emits + `ffp:appearancesGenerated`, it must be `false`; do not declare `true` based + only on `/NeedAppearances` or pre-existing streams. Keep the blank-print + hazard visible to Presswerk. +* Preserve all existing XMP content. Add/merge the FFP description without + replacing unrelated RDF, namespaces, or packet data. If an existing packet is + malformed or cannot be safely merged, fail with a structured `BW_*` error + rather than silently replacing it. If a pre-existing FFP declaration + conflicts with the new producer metadata, get a standards-owner resolution + before choosing overwrite/duplicate semantics. +* Define no-op behavior explicitly. `fill_blocks_impl` currently sets + `/NeedAppearances=true` before it knows whether any supplied field matches; + an empty fields map can therefore serialize a mutated document with zero + field writes. Tests must establish whether this is retained or corrected, and + prove that no new FFP marker is asserted when no field value was written. + +=== 1B. Producer-side tests + +Add narrow tests for the marker and metadata semantics, including: + +[cols="2,3",options="header"] +|=== +| Input case | Required assertion + +| Successful text/select write +| Catalog `/Metadata` contains FFP namespace + `filledBy=machine`; no new + `/Info` claim. + +| Successful button write +| The same marker condition holds for `/V`/`/AS`-based writeback. + +| Existing non-FFP XMP packet +| Existing packet content survives and FFP is added; it is not replaced. + +| No matching/empty values or fill error +| No false machine-filled declaration is added; error paths do not return a + partially serialized output. + +| Existing malformed or conflicting FFP metadata +| No silent metadata loss. The approved policy is exercised and the failure is + structured if safe merge is impossible. + +| Appearance declaration +| If present while Blocky Writer still relies on `NeedAppearances`, the value is + `false`; tests do not imply that `/AP` was generated. +|=== + +Also run the standards FFP detector conformance vectors where available, but +label them correctly: the existing shared 14-vector runner verifies a +*detector's classification semantics*. It does not by itself prove that +Blocky Writer writes a conforming marker or that its actual output crosses the +Presswerk seam. + +=== 1C. Seam declaration and P2P gate + +The seam ID is `blocky-writer-presswerk-form-handoff`. The FFP PRINT-PATH +worked example supplies the intended direction, `fail-degrade` behavior, and +`external-trust` tier. Both product repositories need their own declaration in +the canonical Trustfile; the inspected Blocky Writer and Presswerk Trustfiles +currently lack the entry. + +There are standards-example mismatches that must be resolved rather than copied +blindly: + +* SEAMS-SPEC requires `contract` to be a path relative to the repository root + and `test_ref` to name a local, runnable P2P test. +* FFP PRINT-PATH's worked example points both fields at paths in the separate + `standards` checkout; its conformance runner exercises detector vectors and + is not a test of a Blocky Writer-produced PDF passing through Presswerk. +* FFP's example pairs `failure_mode: fail-degrade` with `tier: external-trust`, + while SEAMS-SPEC §7 says `fail-degrade` seams require `elixir-disciplined` + tier or higher. The intended tier ordering/exception for this handoff is not + established by the inspected texts. +* SEAMS-SPEC also says `external-trust` seams must be isolated by a circuit + breaker. The FFP handoff is a PDF/file handoff, not an identified network + service; requiring a new runtime circuit breaker may conflict with DocMatrix#71 + and is not justified by current evidence. + +Ask the standards owner to clarify how a child repository pins an external FFP +contract under SEAMS-SPEC, whether the FFP example's test reference is intended +as the local P2P test, how its tier/failure-mode pair is reconciled with SEAMS-SPEC +§7, and whether the circuit-breaker clause applies to this file handoff. Do not +duplicate or silently fork the standard. Do not build a runtime circuit breaker +to paper over the ambiguity. Subject to an owner-approved interpretation, +create an in-repository P2P test that: + +. produces a deterministic filled AcroForm fixture using Blocky Writer; +. passes that output to Presswerk's detector/print-path classifier (for example, + via a test-only `FFP_DETECTOR` interface); and +. asserts declared machine-filled provenance and preserves any incomplete + appearance hazard. + +The test is test-only. It must not add Presswerk as a runtime dependency, make +Presswerk mandatory to use Blocky Writer, or create another integration engine. +Coordinate the matching declaration/test in Presswerk through its maintainers; +this plan does not modify that repository. + +The Blocky Writer seam record must include the fields required by SEAMS-SPEC: +`id`, `description`, `from`, `to`, `contract`, `test_ref`, `failure_mode`, +`tier`, and `notes`. Resolve the contract/test path discrepancy first; then wire +the P2P test into the current Mustfile/CI mechanism. The inspected +`contractiles/must/Mustfile.a2ml` contains only license/readme/banned-path checks, +not `seam-check`, and the inspected `.machine_readable/MUST.contractile` +references `quality.yml`. Confirm the active runner mapping before asserting +that CI already enforces SEAMS-SPEC. Until a real seam gate runs, record the +deferral honestly rather than claiming conformance. + +=== Acceptance + +* A non-empty successful fill carries the correct FFP declaration in catalog + XMP and retains pre-existing XMP. +* No FFP declaration is added to an output on which Blocky Writer wrote no + field value. +* Existing FFP conformance-vector results are not represented as proof of the + producer or the end-to-end seam. +* Both sides declare the seam under their own canonical Trustfiles, and a local + runnable P2P test is named by `test_ref` and is invoked by the approved + Mustfile/CI `seam-check` mechanism. +* The chosen `failure_mode`/`tier` pair and the circuit-breaker applicability + are explicitly reconciled with both FFP PRINT-PATH and SEAMS-SPEC; the worked + example is not copied without resolving their differing prescriptions. +* A no-AP output remains truthfully flagged; marker presence never hides + Presswerk's FFP-E-AP-INCOMPLETE hazard. + +== Work package 2 — detection tests, geometry, and field identity + +=== Goal + +Make detection behavior observable and contractual before changing geometry or +field identity. Preserve the two-function WASM boundary and the documented +name-driven fill semantics. + +=== 2A. Test the current detector directly + +The current working diff adds eleven direct `detect_blocks_impl` tests using a +synthetic PDF: valid user-space widgets and labels, a valid non-widget +annotation, empty/malformed/unresolved page `/Annots`, direct and indirect +`/Rect`, malformed and reversed rectangles, precision-losing integer values, +and unresolved/non-dictionary annotations. Together with the six original +fill/error tests there are 17 test functions in source. None has run yet. + +Before treating this coverage as accepted, run it and add or explicitly defer the +remaining cases: + +* multiple pages, annotation ordering, duplicate partial labels, nested field + parents, full versus partial names, multiple widgets per field, and fallback + labels; +* non-zero/negative page boxes, page rotation and inherited page geometry; +* non-finite numeric inputs where the PDF parser permits constructing them; and +* PDFDocEncoding and UTF-16BE/BOM text, non-ASCII labels, significant leading/ + trailing spaces, and embedded NUL handling. + +The existing tests assert faithful results or structured refusals. Do not bake +in `min`/`abs`, lossy decoding, trim, or skip behavior merely because that is +what the implementation used to do. Keep coordinate transforms, field identity, +and text decoding unchanged until their contracts are reviewed. + +=== 2B. Resolve the contract before changing behavior + +The current composition contract says no component silently normalizes page +geometry. The working diff removes `min`/`abs` normalization and returns a +structured error for reversed or unrepresentable widget rectangles. This is a +provisional refusal policy, not a claim that the complete PDF rectangle and +coordinate contract has been approved. Review it against the PDF contract and +consumer needs before merging. + +Before finalizing the behavior, agree on: + +. the accepted PDF rectangle invariant and whether malformed geometry yields a + structured error, an explicit unsupported record, or a documented refusal; +. the coordinate reference frame and units (the current values are raw PDF user + space, not transformed screen coordinates); +. page identity and ordering, including rotations/page boxes and whether + coordinates are returned as authored or transformed; +. numeric representation/rounding and acceptable precision; and +. text decoding/normalization rules for PDF field names and display labels. + +Do not expose a best-effort number as exact geometry. If the existing `f32` +shape cannot satisfy the selected precision contract, version the response +shape deliberately before consumers rely on it; do not add a third WASM export. + +=== 2C. Separate display label from field identity + +`Block.label` is currently selected from the widget's `/T`, then a parent `/T`, +then a page/annotation fallback. It is not the full AcroForm hierarchy and is +not guaranteed unique. `object_to_text` uses `String::from_utf8_lossy` and trims +NUL/whitespace, so it can conflate identity-bearing strings. Treat this as a +fidelity risk pending PDF-string contract review. + +If a real consumer needs stable placement identity, define it with that +consumer and DocMatrix#71 first. Candidate dimensions to evaluate are full +field path, page number, and widget occurrence; PDF object numbers are not +stable across rewrites and should not be advertised as stable IDs. Keep a raw +identity representation distinct from any human-friendly label. Do not claim +the current `{label,x,y,width,height}` object is sufficient to locate a value +across pages or a versioned exchange format. + +`fill_blocks` is intentionally keyed by field name; its `blocks` parameter is +validated but unused. Do not make the operation coordinate-driven or use the +Formatrix #61 example as an established input contract. Keep exactly the +existing `detect_blocks` and `fill_blocks` exports unless an owner-approved, +versioned contract explicitly changes that surface. + +=== 2D. Documentation and coverage ledger + +After tests land, correct `TEST-NEEDS.adoc` so its coverage table says detection +was previously untested and now names the actual detection cases. Update the +relevant `rust/pdftool_core/README.adoc`, `TOPOLOGY.adoc`, and +`EXPLAINME.adoc` claims only to the level verified by tests. Keep test counts, +CI run IDs, and the exact contract shape synchronized; do not claim OCR, +visual-block, ruled-line, baseline, or per-character-cell detection. + +=== Acceptance + +* Each detection behavior is asserted in a direct Rust test; malformed geometry + and annotations cannot disappear silently without an explicit contract. +* Valid page coordinates are preserved under the agreed coordinate contract; + no unannounced `min`/`abs`, lossy cast, coordinate transform, or text rewrite + is introduced. +* The output either carries an explicitly versioned identity/coordinate shape + accepted by its consumer, or remains documented as a widget listing that is + not an interchange API. +* WASM still has exactly two exports; fill remains name-driven; no existing + public `BW_*` code is renamed or repurposed, and every new code is covered. +* The four-code API expansion receives the compatibility/version review required + by repository guidance before merge. +* `TEST-NEEDS.adoc` and other status docs reflect only tests that actually run. + +== Work package 3 — Formatrix Docs#61 and the A2ML question + +=== Disposition + +#61 is informational and open with no comments. Its boundary statement is +consistent with Blocky Writer#73 and DocMatrix#71; it does not request or block +an integration. The A2ML-support claim in its body is not corroborated by the +inspected Formatrix main snapshot: README and TUI enumerate seven other formats, +and targeted source searches found no A2ML parser. + +The local corpus is 17 `.a2ml` files and four `.contractile` files. It is +structurally varied as repository material, but not a standards-conformance +suite. At least the local `STATE.a2ml` is in the old TOML-like key/value shape +that the current standards documentation warns against implementing as the +family's current grammar; the standards warning reports only 0.2% conformance +for the measured legacy TOML-shaped corpus. Since the pointed-to DEED grammar is +explicitly DRAFT/unratified, neither parser target should be silently assumed. + +=== Follow-up sequence + +. Ask Formatrix Docs maintainers whether A2ML support is still planned and, if + so, whether they mean opaque pass-through/display or parsing/editing. +. Ask the standards owner to reconcile the supersession warning with the + unratified DEED grammar and identify the intended conformance source/version. +. Only after those answers, decide whether any local files are useful as + *legacy-compatibility* fixtures. Classify each fixture by dialect/status; + do not call the whole corpus canonical positive conformance data. +. If a real Formatrix consumer requests Blocky Writer placement data, bring the + request to DocMatrix#71's versioned-interchange owner. Require an explicit + versioned contract for format identity, parser/renderer versions, native-input + authority, content-addressed/inspectable intermediates, exact input/output + hashes, evidence spans/page coordinates, provenance, loss/refusal semantics, + and replayable operation records. No silent normalization; ambiguous or lossy + transformations require approval. Require positive, negative, ambiguous, + stale-input, lossy, round-trip, and fault-injection fixtures before a producer + schema is committed. + +No parser, converter, viewer, editor, shared serializer, or runtime integration +is planned here. Do not close #61 on behalf of Formatrix Docs. + +=== Suggested non-blocking response for #61 + +[source,text] +---- +Thanks — the boundary still stands: Formatrix Docs owns viewing/editing, while +Blocky Writer owns fixed-layout form placement. We are not proposing a code-level +integration. One update: the Formatrix README/TUI at the current main snapshot +list TXT, MD, ADOC, DJOT, ORG, RST, and TYP, not A2ML, and we did not find an +A2ML parser there. Can you confirm whether A2ML is still planned and whether a +real consumer needs placement data? Our current detect_blocks object is not a +stable interchange contract: it has no explicit page or stable full-field +identity. Any future exchange would need DocMatrix#71 owner approval and a +versioned contract. +---- + +This is a proposed communication only; do not post it without maintainer +approval. + +== Work package 4 — RSR-template-repo#200 follow-up + +Blocky Writer already rewrote its local `.github/CONTRIBUTING.md` in merged +PR#73. Do not reapply that fix. #200 is still an external template issue; the +current template's `repo-init` performs identity substitution and its mint +checks cover placeholders/instruction blocks and repository structure, but the +inspected tests do not validate that the generated contributor guide's actual +clone target, commands, and documented paths fit the minted child. + +The only planned follow-up is to offer the template maintainers a narrow +regression-test suggestion: mint a child fixture and assert that the resulting +guide points at the child repository, names paths and commands that exist, and +contains no unrelated template-project identity. Keep it external; do not edit +`rsr-template-repo` or broaden Blocky Writer's template obligations here. Do +not use draft `RSR-SPEC-v2` as if it were ratified. + +== Work package 5 — appearance generation follow-on + +FFP 1.0 recommends that the producer generate normal `/AP` streams because +viewers/rasterizers can ignore `/NeedAppearances`; Presswerk records and surfaces +the incomplete-appearance hazard and MUST NOT repair the PDF silently. The +minimal producer-marker work must not be mistaken for a rendering fix. + +After FFP marker behavior and tests are stable, open a separate, scoped +correctness decision for generating appearances for the currently supported +`Tx`, `Ch`, and `Btn` fields. Establish supported fonts, resources, widget +states, preservation of pre-existing `/AP`, errors for unsupported appearances, +and a real-viewer/print-rasterizer verification matrix first. Do not fold that +work into the XMP marker change, do not claim PDF/A suitability, and do not +remove the hazard declaration until generated appearances are verified. + +== Cross-repository references and ownership + +[cols="1,2,3",options="header"] +|=== +| Reference | Status / ownership | Use in this plan + +| https://github.com/hyperpolymath/formatrix-docs/issues/61[Formatrix Docs#61] +| Open, informational; current A2ML-support premise needs maintainer + confirmation. +| Boundary note and A2ML follow-up only; no integration ticket is implied. + +| https://github.com/hyperpolymath/blocky-writer/pull/73[Blocky Writer#73] +| Merged 2026-09-27. +| Boundary docs and local CONTRIBUTING correction already landed; not a + consumer integration or geometry contract. + +| https://github.com/hyperpolymath/docmatrix/issues/71[DocMatrix#71] +| Open architecture/composition contract with owner clarification against a + new runtime integration engine. +| Governs any future versioned interop, geometry fidelity, provenance, loss + reporting, replay, and independent verification. + +| https://github.com/hyperpolymath/presswerk/issues/118[Presswerk#118] and + https://github.com/hyperpolymath/standards/tree/3108c1b01186741eea80c00da3f4beda912286f9/1-formats/sub-specs/form-fill-provenance[FFP] +| #118 is closed after ruling D189; FFP 1.0.0 is active/ratified. Presswerk is + the consumer; Blocky Writer owes the producer marker. +| Do not re-ask D189. Implement producer behavior locally and coordinate the + explicit test/register across owners. + +| https://github.com/hyperpolymath/rsr-template-repo/issues/200[RSR template#200] +| Open and owned by the template maintainers. +| External mint-test follow-up only; child repo already has its correction. +|=== + +== Merge sequencing and stop conditions + +[arabic] +. **First:** repair the Actions lock and re-establish trustworthy health + records. Do not stack feature work on an unexplained workflow-file failure. +. **Second:** get the standards owner answer for the seam contract/test path and + the A2ML/DEED status conflict. FFP marker implementation can be developed + independently, but do not declare the seam conformant until its P2P gate and + both Trustfile entries are real. +. **Third:** add detector tests and agree the geometry/identity contract before + changing normalization, page semantics, or label decoding. +. **Fourth:** carry out only the local producer and detector changes approved by + those gates, each in a scoped change set with tests and updated status docs. +. **Finally:** send the optional #61 and #200 follow-ups to their owners; do not + edit or close issues in those repositories as part of this plan. + +Stop and ask the relevant owner if any proposal requires a new runtime engine, +a new mandatory hub/serializer, a third WASM export, silent geometry repair, +lossy field-name normalization, a parser for an unspecified A2ML dialect, +weakening a CI gate, or changing another repository. Those outcomes are not +accepted by this plan. + +== Source index + +* Blocky Writer implementation/tests: `rust/pdftool_core/src/lib.rs`; + `rust/pdftool_core/README.adoc`; `TEST-NEEDS.adoc`; `TOPOLOGY.adoc`; + `EXPLAINME.adoc`; `docs/ci/CHECK-DETERMINATIONS.adoc`; + `.machine_readable/6a2/STATE.a2ml`; `.machine_readable/MUST.contractile`; + `.machine_readable/contractiles/trust/Trustfile.a2ml`; + `contractiles/must/Mustfile.a2ml`; + `.github/workflows/push-email-notify.yml` and + `.github/workflows/actions.lock`. +* Active seam rules: + https://github.com/hyperpolymath/standards/blob/3108c1b01186741eea80c00da3f4beda912286f9/docs/SEAMS-SPEC.adoc[SEAMS-SPEC]. +* FFP producer marker, print path, and vectors: + https://github.com/hyperpolymath/standards/tree/3108c1b01186741eea80c00da3f4beda912286f9/1-formats/sub-specs/form-fill-provenance[FFP 1.0.0]. +* A2ML/DEED status evidence: + https://github.com/hyperpolymath/standards/blob/3108c1b01186741eea80c00da3f4beda912286f9/1-formats/a2ml/state/README.adoc[STATE supersession warning], + https://github.com/hyperpolymath/standards/blob/3108c1b01186741eea80c00da3f4beda912286f9/1-formats/a2ml/meta/README.adoc[META supersession warning], and + https://github.com/hyperpolymath/standards/blob/3108c1b01186741eea80c00da3f4beda912286f9/1-formats/deed/spec/DEED-GRAMMAR-SPEC.adoc[DEED draft status]. +* Formatrix snapshot evidence: + https://github.com/hyperpolymath/formatrix-docs/blob/85e6692aaa544c0b2b7b41f156c11f3de25d95fc/README.adoc[supported formats] and + https://github.com/hyperpolymath/formatrix-docs/blob/85e6692aaa544c0b2b7b41f156c11f3de25d95fc/tui/src/formatrix_tui-app.ads[TUI format enum]. +* Presswerk snapshot evidence: + https://github.com/hyperpolymath/presswerk/blob/f17620192b69ccffb9454cd49aac8d5ec9454c48/docs/APP_STORE_PREP.md[FFP report and privacy note], + https://github.com/hyperpolymath/presswerk/blob/f17620192b69ccffb9454cd49aac8d5ec9454c48/crates/presswerk-document/src/provenance.rs[detector implementation], and + https://github.com/hyperpolymath/presswerk/blob/f17620192b69ccffb9454cd49aac8d5ec9454c48/.machine_readable/contractiles/trust/Trustfile.a2ml[Trustfile inspected for seam registration]. +* Current main run pages (rechecked 2026-11-01): + https://github.com/hyperpolymath/blocky-writer/actions/runs/38818494415[Rust CI], + https://github.com/hyperpolymath/blocky-writer/actions/runs/38818495000[Governance], + https://github.com/hyperpolymath/blocky-writer/actions/runs/38818494312[Lock Sync Gate], + https://github.com/hyperpolymath/blocky-writer/actions/runs/38818495057[Mirror], + https://github.com/hyperpolymath/blocky-writer/actions/runs/41099036814[scheduled CodeQL], and + https://github.com/hyperpolymath/blocky-writer/actions/runs/41255373342[scheduled Hypatia]. +* Current template mint/conformance evidence: + https://github.com/hyperpolymath/rsr-template-repo/tree/df09a6c50c4c77e93abac0fdf162509bf5648611/build/just[repo-init], + https://github.com/hyperpolymath/rsr-template-repo/tree/df09a6c50c4c77e93abac0fdf162509bf5648611/tests/e2e[template E2E], and + https://github.com/hyperpolymath/rsr-template-repo/tree/df09a6c50c4c77e93abac0fdf162509bf5648611/scripts[conformance checks]. diff --git a/rust/pdftool_core/README.adoc b/rust/pdftool_core/README.adoc index 1d33ae9..d579ab1 100644 --- a/rust/pdftool_core/README.adoc +++ b/rust/pdftool_core/README.adoc @@ -10,8 +10,11 @@ Rust/WASM core for `blocky-writer`. * `fill_blocks` updates matching AcroForm field values and returns serialized PDF bytes. It supports the field types and validation covered by the unit tests; it is not a PDF renderer. -* Core parsing and writeback behavior is covered by Rust unit tests in - `src/lib.rs`. +* Synthetic-PDF unit tests in `src/lib.rs` cover representative widget detection, + rectangle/annotation validation, and selected `Tx` / `Ch` / `Btn` writeback + paths. They are not general PDF conformance tests or real-world corpus + coverage. The detector additions in the current session branch still require + formatting, compilation, tests, and Clippy before they can be called verified. == Native development checks diff --git a/rust/pdftool_core/src/lib.rs b/rust/pdftool_core/src/lib.rs index c6ceba9..65b325e 100644 --- a/rust/pdftool_core/src/lib.rs +++ b/rust/pdftool_core/src/lib.rs @@ -55,8 +55,14 @@ fn resolve_object(doc: &Document, obj: &Object) -> Result fn object_to_number(obj: &Object) -> Option { match obj { - Object::Integer(v) => Some(*v as f32), - Object::Real(v) => Some(*v), + Object::Integer(value) => { + let converted = *value as f32; + // Converting back through i128 avoids saturation at the i64 bounds + // and lets us refuse values that `f32` would silently change at the + // Block/WASM boundary. + ((converted as i128) == (*value as i128)).then_some(converted) + } + Object::Real(value) => value.is_finite().then_some(*value), _ => None, } } @@ -75,11 +81,20 @@ fn rect_from_object(obj: &Object) -> Option<(f32, f32, f32, f32)> { let urx = object_to_number(&values[2])?; let ury = object_to_number(&values[3])?; - let x = llx.min(urx); - let y = lly.min(ury); - let width = (urx - llx).abs(); - let height = (ury - lly).abs(); - Some((x, y, width, height)) + // PDF Rect is ordered as lower-left then upper-right. Do not normalize a + // malformed/reversed rectangle: that changes page geometry without the + // caller's knowledge. + if llx > urx || lly > ury { + return None; + } + + let width = urx - llx; + let height = ury - lly; + if !width.is_finite() || !height.is_finite() { + return None; + } + + Some((llx, lly, width, height)) } fn object_to_text(obj: &Object) -> Option { @@ -613,19 +628,41 @@ fn detect_blocks_impl(pdf_data: &[u8]) -> CoreResult> { Err(_) => continue, }; - let annots = match resolve_object(&doc, annots_obj) { - Ok(Object::Array(arr)) => arr, - Ok(_) | Err(_) => continue, + let annots = resolve_object(&doc, annots_obj).map_err(|err| { + core_error_with_context( + "BW_PDF_ANNOTATIONS_INVALID", + format!("could not resolve page Annots array: {err}"), + Some(format!("page {}", page_number)), + ) + })?; + let annots = match annots { + Object::Array(arr) => arr, + _ => { + return Err(core_error_with_context( + "BW_PDF_ANNOTATIONS_INVALID", + "page Annots entry is not an array", + Some(format!("page {}", page_number)), + )); + } }; for (annot_index, annot_ref) in annots.iter().enumerate() { - let annot_obj = match resolve_object(&doc, annot_ref) { - Ok(obj) => obj, - Err(_) => continue, - }; + let annot_obj = resolve_object(&doc, annot_ref).map_err(|err| { + core_error_with_context( + "BW_PDF_ANNOTATION_UNRESOLVED", + format!("could not resolve page annotation: {err}"), + Some(format!("page {} annotation {}", page_number, annot_index + 1)), + ) + })?; let widget = match annot_obj { Object::Dictionary(dict) => dict, - _ => continue, + _ => { + return Err(core_error_with_context( + "BW_PDF_ANNOTATION_INVALID", + "page annotation is not a dictionary", + Some(format!("page {} annotation {}", page_number, annot_index + 1)), + )); + } }; let is_widget = is_widget_dict(&widget); @@ -633,10 +670,27 @@ fn detect_blocks_impl(pdf_data: &[u8]) -> CoreResult> { continue; } - let rect = match widget.get(b"Rect").ok().and_then(rect_from_object) { - Some(rect) => rect, - None => continue, - }; + let rect_obj = widget.get(b"Rect").map_err(|err| { + core_error_with_context( + "BW_PDF_WIDGET_RECT_INVALID", + format!("widget Rect is missing: {err}"), + Some(format!("page {} annotation {}", page_number, annot_index + 1)), + ) + })?; + let rect_obj = resolve_object(&doc, rect_obj).map_err(|err| { + core_error_with_context( + "BW_PDF_WIDGET_RECT_INVALID", + format!("could not resolve widget Rect: {err}"), + Some(format!("page {} annotation {}", page_number, annot_index + 1)), + ) + })?; + let rect = rect_from_object(&rect_obj).ok_or_else(|| { + core_error_with_context( + "BW_PDF_WIDGET_RECT_INVALID", + "widget Rect must contain four ordered, finite, representable numbers", + Some(format!("page {} annotation {}", page_number, annot_index + 1)), + ) + })?; let fallback = format!("field_{}_{}", page_number, annot_index + 1); let label = widget_label(&doc, &widget, fallback); @@ -794,8 +848,8 @@ mod tests { Object::Array(vec![llx.into(), lly.into(), urx.into(), ury.into()]) } - fn assert_error_code(result: CoreResult>, expected_code: &str) { - let payload = result.expect_err("expected fill_blocks to fail"); + fn assert_error_code(result: CoreResult, expected_code: &str) { + let payload = result.expect_err("expected operation to fail"); assert_eq!(payload.code, expected_code); assert!( !payload.message.is_empty(), @@ -803,6 +857,91 @@ mod tests { ); } + fn save_fixture_doc(doc: &mut Document) -> Vec { + let mut bytes = Vec::new(); + doc.save_to(&mut bytes).expect("serialize fixture PDF"); + bytes + } + + fn fixture_with_first_widget_rect(pdf_data: &[u8], rect: Object) -> Vec { + let mut doc = Document::load_mem(pdf_data).expect("load fixture PDF"); + let page_id = *doc + .get_pages() + .values() + .next() + .expect("fixture has one page"); + let widget_id = { + let page = doc + .get_object(page_id) + .expect("fixture page exists") + .as_dict() + .expect("fixture page is a dictionary"); + let annots = page + .get(b"Annots") + .expect("fixture page has Annots") + .as_array() + .expect("fixture Annots is an array"); + match annots.first().expect("fixture has a widget annotation") { + Object::Reference(id) => *id, + _ => panic!("fixture widget must be indirect"), + } + }; + doc.get_object_mut(widget_id) + .expect("fixture widget exists") + .as_dict_mut() + .expect("fixture widget is a dictionary") + .set(b"Rect", rect); + save_fixture_doc(&mut doc) + } + + fn fixture_with_first_widget_indirect_rect(pdf_data: &[u8], rect: Object) -> Vec { + let mut doc = Document::load_mem(pdf_data).expect("load fixture PDF"); + let page_id = *doc + .get_pages() + .values() + .next() + .expect("fixture has one page"); + let widget_id = { + let page = doc + .get_object(page_id) + .expect("fixture page exists") + .as_dict() + .expect("fixture page is a dictionary"); + let annots = page + .get(b"Annots") + .expect("fixture page has Annots") + .as_array() + .expect("fixture Annots is an array"); + match annots.first().expect("fixture has a widget annotation") { + Object::Reference(id) => *id, + _ => panic!("fixture widget must be indirect"), + } + }; + let rect_id = doc.new_object_id(); + doc.objects.insert(rect_id, rect); + doc.get_object_mut(widget_id) + .expect("fixture widget exists") + .as_dict_mut() + .expect("fixture widget is a dictionary") + .set(b"Rect", Object::Reference(rect_id)); + save_fixture_doc(&mut doc) + } + + fn fixture_with_page_annots(pdf_data: &[u8], annots: Object) -> Vec { + let mut doc = Document::load_mem(pdf_data).expect("load fixture PDF"); + let page_id = *doc + .get_pages() + .values() + .next() + .expect("fixture has one page"); + doc.get_object_mut(page_id) + .expect("fixture page exists") + .as_dict_mut() + .expect("fixture page is a dictionary") + .set(b"Annots", annots); + save_fixture_doc(&mut doc) + } + fn make_fixture_pdf() -> Vec { let mut doc = Document::with_version("1.7"); @@ -944,6 +1083,119 @@ mod tests { bytes } + #[test] + fn detect_blocks_returns_widget_rectangles_in_pdf_user_space() { + let pdf = make_fixture_pdf(); + let blocks = detect_blocks_impl(&pdf).expect("valid widget rectangles should detect"); + + assert_eq!(blocks.len(), 4); + assert_eq!(blocks[0].label, "Name"); + assert_eq!((blocks[0].x, blocks[0].y), (50.0, 700.0)); + assert_eq!((blocks[0].width, blocks[0].height), (200.0, 24.0)); + assert_eq!(blocks[1].label, "Consent"); + assert_eq!(blocks[2].label, "Choice"); + assert_eq!(blocks[3].label, "Choice"); + assert_eq!((blocks[3].x, blocks[3].y), (120.0, 600.0)); + } + + #[test] + fn detect_blocks_resolves_indirect_widget_rectangles() { + let pdf = fixture_with_first_widget_indirect_rect( + &make_fixture_pdf(), + rect(50, 700, 250, 724), + ); + let blocks = detect_blocks_impl(&pdf).expect("indirect Rect arrays should resolve"); + assert_eq!((blocks[0].x, blocks[0].y), (50.0, 700.0)); + assert_eq!((blocks[0].width, blocks[0].height), (200.0, 24.0)); + } + + #[test] + fn detect_blocks_returns_empty_for_page_without_annotations() { + let pdf = fixture_with_page_annots(&make_fixture_pdf(), Object::Array(Vec::new())); + let blocks = detect_blocks_impl(&pdf).expect("empty Annots array is valid"); + assert!(blocks.is_empty()); + } + + #[test] + fn detect_blocks_rejects_non_array_page_annots() { + let pdf = fixture_with_page_annots(&make_fixture_pdf(), name("not-an-array")); + assert_error_code(detect_blocks_impl(&pdf), "BW_PDF_ANNOTATIONS_INVALID"); + } + + #[test] + fn detect_blocks_reports_unresolved_page_annots_reference() { + let missing_id = (u32::MAX, u16::MAX); + let pdf = fixture_with_page_annots( + &make_fixture_pdf(), + Object::Reference(missing_id), + ); + assert_error_code(detect_blocks_impl(&pdf), "BW_PDF_ANNOTATIONS_INVALID"); + } + + #[test] + fn detect_blocks_rejects_reversed_widget_rectangles() { + let pdf = fixture_with_first_widget_rect(&make_fixture_pdf(), rect(250, 700, 50, 724)); + assert_error_code(detect_blocks_impl(&pdf), "BW_PDF_WIDGET_RECT_INVALID"); + } + + #[test] + fn detect_blocks_rejects_malformed_widget_rectangles() { + let malformed_length = Object::Array(vec![ + Object::Integer(50), + Object::Integer(700), + Object::Integer(250), + ]); + let pdf = fixture_with_first_widget_rect(&make_fixture_pdf(), malformed_length); + assert_error_code(detect_blocks_impl(&pdf), "BW_PDF_WIDGET_RECT_INVALID"); + + let malformed_type = Object::Array(vec![ + Object::Integer(50), + name("not-a-number"), + Object::Integer(250), + Object::Integer(724), + ]); + let pdf = fixture_with_first_widget_rect(&make_fixture_pdf(), malformed_type); + assert_error_code(detect_blocks_impl(&pdf), "BW_PDF_WIDGET_RECT_INVALID"); + } + + #[test] + fn detect_blocks_rejects_integer_coordinates_that_lose_f32_precision() { + let imprecise_rect = rect(16_777_217, 700, 16_777_250, 724); + let pdf = fixture_with_first_widget_rect(&make_fixture_pdf(), imprecise_rect); + assert_error_code(detect_blocks_impl(&pdf), "BW_PDF_WIDGET_RECT_INVALID"); + } + + #[test] + fn detect_blocks_reports_unresolved_annotation_references() { + let missing_id = (u32::MAX, u16::MAX); + let pdf = fixture_with_page_annots( + &make_fixture_pdf(), + Object::Array(vec![Object::Reference(missing_id)]), + ); + assert_error_code(detect_blocks_impl(&pdf), "BW_PDF_ANNOTATION_UNRESOLVED"); + } + + #[test] + fn detect_blocks_ignores_valid_non_widget_annotations() { + let annotation = Object::Dictionary(dictionary! { + "Type" => name("Annot"), + "Subtype" => name("Text"), + "Rect" => rect(50, 700, 250, 724), + }); + let pdf = fixture_with_page_annots(&make_fixture_pdf(), Object::Array(vec![annotation])); + let blocks = detect_blocks_impl(&pdf).expect("non-widget annotations are valid"); + assert!(blocks.is_empty()); + } + + #[test] + fn detect_blocks_reports_non_dictionary_annotations() { + let pdf = fixture_with_page_annots( + &make_fixture_pdf(), + Object::Array(vec![name("not-a-dictionary")]), + ); + assert_error_code(detect_blocks_impl(&pdf), "BW_PDF_ANNOTATION_INVALID"); + } + #[test] fn fill_blocks_errors_on_empty_pdf() { let fields = HashMap::::new(); diff --git a/wiki/Architecture.md b/wiki/Architecture.md index a59ad17..436b43f 100644 --- a/wiki/Architecture.md +++ b/wiki/Architecture.md @@ -87,5 +87,8 @@ with a stable machine code, a human message, and optional context: { "code": "BW_FILL_NO_MATCHING_FIELDS", "message": "…", "context": "Choice" } ``` -There are 20 `BW_*` codes. Codes are stable API — treat adding or renaming one -as a breaking change, not a tidy-up. +There are 24 unique `BW_*` codes in the current source (20 baseline codes plus +four detector-validation codes). Error codes are external API; repository +guidance treats additions and renames as breaking changes, so this expansion +needs compatibility/version review before merge. The four additions also remain +unverified until the current Rust diff passes formatting, tests, and Clippy. diff --git a/wiki/CI-and-Gates.md b/wiki/CI-and-Gates.md index 08f599c..aae0534 100644 --- a/wiki/CI-and-Gates.md +++ b/wiki/CI-and-Gates.md @@ -63,6 +63,17 @@ only job in that workflow: ## Known problems — read before you trust a green +* **Current main CI state (2026-11-01).** Rust CI passed on `3fd5ba1` in run + `38818494415`, but Governance (`38818495000`) and Lock Sync Gate + (`38818494312`) failed at generated-lock verification. The workflow uses + `hyperpolymath/smtp-notify-action@v0.5.0`; the generated lock still records + v0.3.0. Do not hand-edit `actions.lock`; use the prescribed `gh actions-lock` + generator, then verify the result. The current Arena feature branch has not + been compiled or tested. +* **Other current failures.** Scheduled CodeQL run `41099036814` and Hypatia + run `41255373342` failed during job setup; mirror run `38818495057` had four + failing destinations. Logs were unavailable during the review, so causes are + unknown. See `docs/ci/CHECK-DETERMINATIONS.adoc` for step-level evidence. * **Two Pages workflows.** `pages.yml` and `casket-pages.yml` both build and deploy to the `github-pages` environment under the same `pages` concurrency group, so they cancel each other. `pages.yml` also looks for `README.md`, diff --git a/wiki/Contributing--Dev-Setup.md b/wiki/Contributing--Dev-Setup.md index 3ca9620..1b82242 100644 --- a/wiki/Contributing--Dev-Setup.md +++ b/wiki/Contributing--Dev-Setup.md @@ -53,8 +53,11 @@ cargo test --manifest-path rust/pdftool_core/Cargo.toml --locked cargo clippy --manifest-path rust/pdftool_core/Cargo.toml --locked --all-targets -- -D warnings ``` -`cargo test` runs 6 tests. If you get a different number, something changed and -the wiki is stale — fix the wiki. +The current working source contains 17 test functions: six existing fill/error +tests and eleven direct detector tests added in this branch. Those additions +have not been compiled or run in this environment because the Rust toolchain is +absent. Keep this count synchronized with `TEST-NEEDS.adoc`; CI, not the source +count alone, determines whether they pass. ## Building the WASM package (optional) diff --git a/wiki/Home.md b/wiki/Home.md index 4ee86e8..e90eded 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -38,8 +38,10 @@ is versioned, diffable and reviewable like any other file. See ## Current status in one paragraph -The Rust core (`rust/pdftool_core`) is implemented, unit-tested and green on -`cargo fmt`, `cargo test --locked` and `cargo clippy -D warnings`. The +The Rust core's baseline on `main` passed `cargo fmt`, `cargo test --locked`, +and `cargo clippy -D warnings` in run `38818494415` at `3fd5ba1`. The current +session branch adds detector validation and tests, but those edits have not been +compiled or run here because the Rust toolchain is absent. The browser-extension frontend is a **prototype**: this checkout has no AffineScript compiler configuration, no JavaScript package manifest and no reproducible extension bundle pipeline, so there is nothing to build or release From e58ed106e47872f78baed2ca1549e97bf246e45f Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 1 Nov 2026 15:55:55 +0000 Subject: [PATCH 2/7] style: apply rustfmt to geometry validation Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- rust/pdftool_core/src/lib.rs | 41 ++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/rust/pdftool_core/src/lib.rs b/rust/pdftool_core/src/lib.rs index 65b325e..54355ae 100644 --- a/rust/pdftool_core/src/lib.rs +++ b/rust/pdftool_core/src/lib.rs @@ -651,7 +651,11 @@ fn detect_blocks_impl(pdf_data: &[u8]) -> CoreResult> { core_error_with_context( "BW_PDF_ANNOTATION_UNRESOLVED", format!("could not resolve page annotation: {err}"), - Some(format!("page {} annotation {}", page_number, annot_index + 1)), + Some(format!( + "page {} annotation {}", + page_number, + annot_index + 1 + )), ) })?; let widget = match annot_obj { @@ -660,7 +664,11 @@ fn detect_blocks_impl(pdf_data: &[u8]) -> CoreResult> { return Err(core_error_with_context( "BW_PDF_ANNOTATION_INVALID", "page annotation is not a dictionary", - Some(format!("page {} annotation {}", page_number, annot_index + 1)), + Some(format!( + "page {} annotation {}", + page_number, + annot_index + 1 + )), )); } }; @@ -674,21 +682,33 @@ fn detect_blocks_impl(pdf_data: &[u8]) -> CoreResult> { core_error_with_context( "BW_PDF_WIDGET_RECT_INVALID", format!("widget Rect is missing: {err}"), - Some(format!("page {} annotation {}", page_number, annot_index + 1)), + Some(format!( + "page {} annotation {}", + page_number, + annot_index + 1 + )), ) })?; let rect_obj = resolve_object(&doc, rect_obj).map_err(|err| { core_error_with_context( "BW_PDF_WIDGET_RECT_INVALID", format!("could not resolve widget Rect: {err}"), - Some(format!("page {} annotation {}", page_number, annot_index + 1)), + Some(format!( + "page {} annotation {}", + page_number, + annot_index + 1 + )), ) })?; let rect = rect_from_object(&rect_obj).ok_or_else(|| { core_error_with_context( "BW_PDF_WIDGET_RECT_INVALID", "widget Rect must contain four ordered, finite, representable numbers", - Some(format!("page {} annotation {}", page_number, annot_index + 1)), + Some(format!( + "page {} annotation {}", + page_number, + annot_index + 1 + )), ) })?; @@ -1100,10 +1120,8 @@ mod tests { #[test] fn detect_blocks_resolves_indirect_widget_rectangles() { - let pdf = fixture_with_first_widget_indirect_rect( - &make_fixture_pdf(), - rect(50, 700, 250, 724), - ); + let pdf = + fixture_with_first_widget_indirect_rect(&make_fixture_pdf(), rect(50, 700, 250, 724)); let blocks = detect_blocks_impl(&pdf).expect("indirect Rect arrays should resolve"); assert_eq!((blocks[0].x, blocks[0].y), (50.0, 700.0)); assert_eq!((blocks[0].width, blocks[0].height), (200.0, 24.0)); @@ -1125,10 +1143,7 @@ mod tests { #[test] fn detect_blocks_reports_unresolved_page_annots_reference() { let missing_id = (u32::MAX, u16::MAX); - let pdf = fixture_with_page_annots( - &make_fixture_pdf(), - Object::Reference(missing_id), - ); + let pdf = fixture_with_page_annots(&make_fixture_pdf(), Object::Reference(missing_id)); assert_error_code(detect_blocks_impl(&pdf), "BW_PDF_ANNOTATIONS_INVALID"); } From c53d8ced437d6613cbb404d9a4853d832b737bba Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:11:10 +0000 Subject: [PATCH 3/7] fix(rust): follow PDF rectangle semantics Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- rust/pdftool_core/Cargo.lock | 2 +- rust/pdftool_core/Cargo.toml | 2 +- rust/pdftool_core/src/lib.rs | 312 +++++++++++++++++++++++++++-------- 3 files changed, 248 insertions(+), 68 deletions(-) diff --git a/rust/pdftool_core/Cargo.lock b/rust/pdftool_core/Cargo.lock index b5ebc3f..0ff0871 100644 --- a/rust/pdftool_core/Cargo.lock +++ b/rust/pdftool_core/Cargo.lock @@ -334,7 +334,7 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "pdftool_core" -version = "0.1.0" +version = "0.2.0" dependencies = [ "js-sys", "lopdf", diff --git a/rust/pdftool_core/Cargo.toml b/rust/pdftool_core/Cargo.toml index 02513b7..3869b36 100644 --- a/rust/pdftool_core/Cargo.toml +++ b/rust/pdftool_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pdftool_core" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "MPL-2.0" description = "WASM core for block detection and PDF filling" diff --git a/rust/pdftool_core/src/lib.rs b/rust/pdftool_core/src/lib.rs index 54355ae..7a37fd0 100644 --- a/rust/pdftool_core/src/lib.rs +++ b/rust/pdftool_core/src/lib.rs @@ -10,10 +10,10 @@ use wasm_bindgen::prelude::*; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Block { pub label: String, - pub x: f32, - pub y: f32, - pub width: f32, - pub height: f32, + pub x: f64, + pub y: f64, + pub width: f64, + pub height: f64, } #[derive(Debug, Serialize)] @@ -53,48 +53,74 @@ fn resolve_object(doc: &Document, obj: &Object) -> Result } } -fn object_to_number(obj: &Object) -> Option { - match obj { - Object::Integer(value) => { - let converted = *value as f32; - // Converting back through i128 avoids saturation at the i64 bounds - // and lets us refuse values that `f32` would silently change at the - // Block/WASM boundary. - ((converted as i128) == (*value as i128)).then_some(converted) +fn object_to_number(doc: &Document, obj: &Object) -> Option { + let mut resolved = obj.clone(); + let mut references = HashSet::new(); + + loop { + match resolved { + Object::Reference(id) => { + if !references.insert(id) { + return None; + } + resolved = doc.get_object(id).ok()?.clone(); + } + Object::Integer(value) => { + let converted = value as f64; + // Reject integers outside f64's exact range rather than + // silently presenting rounded coordinates to callers. + return ((converted as i128) == (value as i128)).then_some(converted); + } + Object::Real(value) => { + let converted = f64::from(value); + return converted.is_finite().then_some(converted); + } + _ => return None, } - Object::Real(value) => value.is_finite().then_some(*value), - _ => None, } } -fn rect_from_object(obj: &Object) -> Option<(f32, f32, f32, f32)> { - let Object::Array(values) = obj else { +fn rect_from_coordinates(x1: f64, y1: f64, x2: f64, y2: f64) -> Option<(f64, f64, f64, f64)> { + if ![x1, y1, x2, y2] + .iter() + .all(|coordinate| coordinate.is_finite()) + { return None; - }; + } - if values.len() != 4 { + // PDF Rect stores two diagonally opposite corners, which may be supplied + // in either order. Canonicalize the pair ordering required to expose an + // x/y/width/height block; this is PDF-defined interpretation, not a page + // coordinate transform. + let llx = x1.min(x2); + let lly = y1.min(y2); + let urx = x1.max(x2); + let ury = y1.max(y2); + let width = urx - llx; + let height = ury - lly; + + if !width.is_finite() || !height.is_finite() { return None; } - let llx = object_to_number(&values[0])?; - let lly = object_to_number(&values[1])?; - let urx = object_to_number(&values[2])?; - let ury = object_to_number(&values[3])?; + Some((llx, lly, width, height)) +} - // PDF Rect is ordered as lower-left then upper-right. Do not normalize a - // malformed/reversed rectangle: that changes page geometry without the - // caller's knowledge. - if llx > urx || lly > ury { +fn rect_from_object(doc: &Document, obj: &Object) -> Option<(f64, f64, f64, f64)> { + let Object::Array(values) = obj else { return None; - } + }; - let width = urx - llx; - let height = ury - lly; - if !width.is_finite() || !height.is_finite() { + if values.len() != 4 { return None; } - Some((llx, lly, width, height)) + rect_from_coordinates( + object_to_number(doc, &values[0])?, + object_to_number(doc, &values[1])?, + object_to_number(doc, &values[2])?, + object_to_number(doc, &values[3])?, + ) } fn object_to_text(obj: &Object) -> Option { @@ -628,16 +654,12 @@ fn detect_blocks_impl(pdf_data: &[u8]) -> CoreResult> { Err(_) => continue, }; - let annots = resolve_object(&doc, annots_obj).map_err(|err| { - core_error_with_context( - "BW_PDF_ANNOTATIONS_INVALID", - format!("could not resolve page Annots array: {err}"), - Some(format!("page {}", page_number)), - ) - })?; - let annots = match annots { - Object::Array(arr) => arr, - _ => { + let annots = match resolve_object(&doc, annots_obj) { + Ok(Object::Array(arr)) => arr, + // PDF null entries, including references to missing objects, have + // the same effect as an absent optional /Annots entry. + Ok(Object::Null) | Err(_) => continue, + Ok(_) => { return Err(core_error_with_context( "BW_PDF_ANNOTATIONS_INVALID", "page Annots entry is not an array", @@ -647,19 +669,13 @@ fn detect_blocks_impl(pdf_data: &[u8]) -> CoreResult> { }; for (annot_index, annot_ref) in annots.iter().enumerate() { - let annot_obj = resolve_object(&doc, annot_ref).map_err(|err| { - core_error_with_context( - "BW_PDF_ANNOTATION_UNRESOLVED", - format!("could not resolve page annotation: {err}"), - Some(format!( - "page {} annotation {}", - page_number, - annot_index + 1 - )), - ) - })?; + let annot_obj = match resolve_object(&doc, annot_ref) { + Ok(Object::Null) | Err(_) => continue, + Ok(obj) => obj, + }; let widget = match annot_obj { Object::Dictionary(dict) => dict, + Object::Null => continue, _ => { return Err(core_error_with_context( "BW_PDF_ANNOTATION_INVALID", @@ -700,10 +716,10 @@ fn detect_blocks_impl(pdf_data: &[u8]) -> CoreResult> { )), ) })?; - let rect = rect_from_object(&rect_obj).ok_or_else(|| { + let rect = rect_from_object(&doc, &rect_obj).ok_or_else(|| { core_error_with_context( "BW_PDF_WIDGET_RECT_INVALID", - "widget Rect must contain four ordered, finite, representable numbers", + "widget Rect must contain four finite, exactly representable numbers with finite extents", Some(format!( "page {} annotation {}", page_number, @@ -947,6 +963,74 @@ mod tests { save_fixture_doc(&mut doc) } + fn fixture_with_first_widget_indirect_rect_coordinates(pdf_data: &[u8]) -> Vec { + let mut doc = Document::load_mem(pdf_data).expect("load fixture PDF"); + let page_id = *doc + .get_pages() + .values() + .next() + .expect("fixture has one page"); + let widget_id = { + let page = doc + .get_object(page_id) + .expect("fixture page exists") + .as_dict() + .expect("fixture page is a dictionary"); + let annots = page + .get(b"Annots") + .expect("fixture page has Annots") + .as_array() + .expect("fixture Annots is an array"); + match annots.first().expect("fixture has a widget annotation") { + Object::Reference(id) => *id, + _ => panic!("fixture widget must be indirect"), + } + }; + let mut coordinates = Vec::new(); + for value in [50_i64, 700, 250, 724] { + let id = doc.new_object_id(); + doc.objects.insert(id, Object::Integer(value)); + coordinates.push(Object::Reference(id)); + } + doc.get_object_mut(widget_id) + .expect("fixture widget exists") + .as_dict_mut() + .expect("fixture widget is a dictionary") + .set(b"Rect", Object::Array(coordinates)); + save_fixture_doc(&mut doc) + } + + fn fixture_without_first_widget_rect(pdf_data: &[u8]) -> Vec { + let mut doc = Document::load_mem(pdf_data).expect("load fixture PDF"); + let page_id = *doc + .get_pages() + .values() + .next() + .expect("fixture has one page"); + let widget_id = { + let page = doc + .get_object(page_id) + .expect("fixture page exists") + .as_dict() + .expect("fixture page is a dictionary"); + let annots = page + .get(b"Annots") + .expect("fixture page has Annots") + .as_array() + .expect("fixture Annots is an array"); + match annots.first().expect("fixture has a widget annotation") { + Object::Reference(id) => *id, + _ => panic!("fixture widget must be indirect"), + } + }; + let widget = dictionary! { + "Type" => name("Annot"), + "Subtype" => name("Widget"), + }; + doc.objects.insert(widget_id, Object::Dictionary(widget)); + save_fixture_doc(&mut doc) + } + fn fixture_with_page_annots(pdf_data: &[u8], annots: Object) -> Vec { let mut doc = Document::load_mem(pdf_data).expect("load fixture PDF"); let page_id = *doc @@ -1127,6 +1211,14 @@ mod tests { assert_eq!((blocks[0].width, blocks[0].height), (200.0, 24.0)); } + #[test] + fn detect_blocks_resolves_indirect_widget_rect_coordinates() { + let pdf = fixture_with_first_widget_indirect_rect_coordinates(&make_fixture_pdf()); + let blocks = detect_blocks_impl(&pdf).expect("indirect Rect coordinates should resolve"); + assert_eq!((blocks[0].x, blocks[0].y), (50.0, 700.0)); + assert_eq!((blocks[0].width, blocks[0].height), (200.0, 24.0)); + } + #[test] fn detect_blocks_returns_empty_for_page_without_annotations() { let pdf = fixture_with_page_annots(&make_fixture_pdf(), Object::Array(Vec::new())); @@ -1141,16 +1233,26 @@ mod tests { } #[test] - fn detect_blocks_reports_unresolved_page_annots_reference() { + fn detect_blocks_treats_null_and_unresolved_page_annots_as_absent() { + let base = make_fixture_pdf(); + let null_annots = fixture_with_page_annots(&base, Object::Null); + assert!(detect_blocks_impl(&null_annots) + .expect("null Annots has the effect of an absent entry") + .is_empty()); + let missing_id = (u32::MAX, u16::MAX); - let pdf = fixture_with_page_annots(&make_fixture_pdf(), Object::Reference(missing_id)); - assert_error_code(detect_blocks_impl(&pdf), "BW_PDF_ANNOTATIONS_INVALID"); + let unresolved_annots = fixture_with_page_annots(&base, Object::Reference(missing_id)); + assert!(detect_blocks_impl(&unresolved_annots) + .expect("unresolved Annots is treated as null") + .is_empty()); } #[test] - fn detect_blocks_rejects_reversed_widget_rectangles() { - let pdf = fixture_with_first_widget_rect(&make_fixture_pdf(), rect(250, 700, 50, 724)); - assert_error_code(detect_blocks_impl(&pdf), "BW_PDF_WIDGET_RECT_INVALID"); + fn detect_blocks_normalizes_pdf_rect_corner_order() { + let pdf = fixture_with_first_widget_rect(&make_fixture_pdf(), rect(250, 724, 50, 700)); + let blocks = detect_blocks_impl(&pdf).expect("either diagonal-corner order is valid"); + assert_eq!((blocks[0].x, blocks[0].y), (50.0, 700.0)); + assert_eq!((blocks[0].width, blocks[0].height), (200.0, 24.0)); } #[test] @@ -1174,20 +1276,98 @@ mod tests { } #[test] - fn detect_blocks_rejects_integer_coordinates_that_lose_f32_precision() { - let imprecise_rect = rect(16_777_217, 700, 16_777_250, 724); - let pdf = fixture_with_first_widget_rect(&make_fixture_pdf(), imprecise_rect); + fn object_to_number_preserves_f64_integer_boundaries() { + let doc = Document::with_version("1.7"); + + assert_eq!( + object_to_number(&doc, &Object::Integer(16_777_217)), + Some(16_777_217.0) + ); + assert_eq!( + object_to_number(&doc, &Object::Integer(9_007_199_254_740_992)), + Some(9_007_199_254_740_992.0) + ); + assert_eq!( + object_to_number(&doc, &Object::Integer(i64::MIN)), + Some(i64::MIN as f64) + ); + assert_eq!( + object_to_number(&doc, &Object::Integer(9_007_199_254_740_993)), + None + ); + assert_eq!(object_to_number(&doc, &Object::Real(f32::NAN)), None); + assert_eq!(object_to_number(&doc, &Object::Real(f32::INFINITY)), None); + } + + #[test] + fn rect_from_object_rejects_non_finite_values_and_overflowing_extents() { + let doc = Document::with_version("1.7"); + let non_finite = Object::Array(vec![ + Object::Real(f32::NAN), + Object::Integer(0), + Object::Integer(10), + Object::Integer(10), + ]); + assert_eq!(rect_from_object(&doc, &non_finite), None); + + assert_eq!(rect_from_coordinates(-f64::MAX, 0.0, f64::MAX, 1.0), None); + assert_eq!(rect_from_coordinates(0.0, -f64::MAX, 1.0, f64::MAX), None); + assert_eq!( + rect_from_object(&doc, &rect(16_777_217, 0, 16_777_250, 1)), + Some((16_777_217.0, 0.0, 33.0, 1.0)) + ); + } + + #[test] + fn detect_blocks_reports_missing_widget_rectangles() { + let pdf = fixture_without_first_widget_rect(&make_fixture_pdf()); assert_error_code(detect_blocks_impl(&pdf), "BW_PDF_WIDGET_RECT_INVALID"); } #[test] - fn detect_blocks_reports_unresolved_annotation_references() { + fn detect_blocks_reports_unresolved_widget_rect_references() { + let missing_id = (u32::MAX, u16::MAX); + let unresolved_array = + fixture_with_first_widget_rect(&make_fixture_pdf(), Object::Reference(missing_id)); + assert_error_code( + detect_blocks_impl(&unresolved_array), + "BW_PDF_WIDGET_RECT_INVALID", + ); + + let unresolved_coordinate = fixture_with_first_widget_rect( + &make_fixture_pdf(), + Object::Array(vec![ + Object::Reference(missing_id), + Object::Integer(0), + Object::Integer(10), + Object::Integer(10), + ]), + ); + assert_error_code( + detect_blocks_impl(&unresolved_coordinate), + "BW_PDF_WIDGET_RECT_INVALID", + ); + } + + #[test] + fn detect_blocks_accepts_integer_coordinates_above_f32_precision_limit() { + let precise_rect = rect(16_777_217, 700, 16_777_250, 724); + let pdf = fixture_with_first_widget_rect(&make_fixture_pdf(), precise_rect); + let blocks = detect_blocks_impl(&pdf).expect("f64 preserves these valid PDF integers"); + assert_eq!(blocks[0].x, 16_777_217.0); + assert_eq!(blocks[0].width, 33.0); + } + + #[test] + fn detect_blocks_skips_null_and_unresolved_annotation_entries() { let missing_id = (u32::MAX, u16::MAX); let pdf = fixture_with_page_annots( &make_fixture_pdf(), - Object::Array(vec![Object::Reference(missing_id)]), + Object::Array(vec![Object::Null, Object::Reference(missing_id)]), ); - assert_error_code(detect_blocks_impl(&pdf), "BW_PDF_ANNOTATION_UNRESOLVED"); + assert!(detect_blocks_impl(&pdf) + .expect("null and unresolved annotation entries are absent") + .is_empty()); } #[test] From beb5daa745a156b5143e9e41712a85c9dbd948e0 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:12:11 +0000 Subject: [PATCH 4/7] Update TOPOLOGY.adoc Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com> --- TOPOLOGY.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TOPOLOGY.adoc b/TOPOLOGY.adoc index 3b130f9..e66e80c 100644 --- a/TOPOLOGY.adoc +++ b/TOPOLOGY.adoc @@ -65,7 +65,7 @@ EXTENSION LAYERS Extension bundle pipeline ░░░░░░░░░░ 0% deno.json removed in #43; no toolchain CORE (RUST/WASM) - pdftool_core (WASM) ████████░░ 80% Main baseline passes CI; 17 tests in source, 11 new detector tests unverified + pdftool_core (WASM) ████████░░ 80% Main baseline (3fd5ba1) passes CI; 80% covers main only; 17 tests in source, 11 new detector tests unverified AcroForm Writeback ████████░░ 80% Tx / Ch / Btn only; no Sig Ruled-line & cell detection ░░░░░░░░░░ 0% NOT IMPLEMENTED — the "hand spacing" case Error Taxonomy ████████░░ 80% 24 codes in source; 4 new codes need Rust + compatibility review From 1cf6ea3577ba75712d16e968efb5af54e54823ec Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:14:57 +0000 Subject: [PATCH 5/7] docs(rust): document PDF number resolution, rectangle validation, and widget detection --- rust/pdftool_core/src/lib.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/rust/pdftool_core/src/lib.rs b/rust/pdftool_core/src/lib.rs index 7a37fd0..9fbdd5c 100644 --- a/rust/pdftool_core/src/lib.rs +++ b/rust/pdftool_core/src/lib.rs @@ -53,6 +53,8 @@ fn resolve_object(doc: &Document, obj: &Object) -> Result } } +/// Resolves a PDF number, returning `None` for unresolved or cyclic references, +/// non-numbers, non-finite reals, or integers that cannot be represented exactly. fn object_to_number(doc: &Document, obj: &Object) -> Option { let mut resolved = obj.clone(); let mut references = HashSet::new(); @@ -80,6 +82,9 @@ fn object_to_number(doc: &Document, obj: &Object) -> Option { } } +/// Converts two diagonally opposite corners in either order to a rectangle +/// `(x, y, width, height)`, using the smaller coordinates as the origin. Zero +/// extents are accepted; non-finite coordinates or extents return `None`. fn rect_from_coordinates(x1: f64, y1: f64, x2: f64, y2: f64) -> Option<(f64, f64, f64, f64)> { if ![x1, y1, x2, y2] .iter() @@ -106,6 +111,9 @@ fn rect_from_coordinates(x1: f64, y1: f64, x2: f64, y2: f64) -> Option<(f64, f64 Some((llx, lly, width, height)) } +/// Converts a direct four-element PDF rectangle array to `(x, y, width, height)`. +/// Coordinates may be indirect. Other objects, invalid numbers or non-finite +/// extents yield `None`. fn rect_from_object(doc: &Document, obj: &Object) -> Option<(f64, f64, f64, f64)> { let Object::Array(values) = obj else { return None; @@ -619,6 +627,14 @@ fn apply_field_value( } } +/// Detects widget annotations in PDF page order and returns their labels and +/// rectangles in PDF user space. Corner order is normalised, and +/// missing labels fall back to `field__`. +/// +/// Missing, null or unresolved annotation entries and non-widget annotations +/// are skipped. Returns an error for empty or unreadable PDFs, unreadable or +/// invalid pages, non-array `Annots`, non-dictionary annotations, or missing, +/// unresolved or invalid widget rectangles. fn detect_blocks_impl(pdf_data: &[u8]) -> CoreResult> { if pdf_data.is_empty() { return Err(core_error("BW_PDF_EMPTY", "empty PDF payload")); From 9f9370e206ae8cc6dd3ac86cba3f0904e4838135 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:24:37 +0000 Subject: [PATCH 6/7] fix(rust): resolve chained PDF object references Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- rust/pdftool_core/src/lib.rs | 157 ++++++++++++++++++++++++++++++----- 1 file changed, 137 insertions(+), 20 deletions(-) diff --git a/rust/pdftool_core/src/lib.rs b/rust/pdftool_core/src/lib.rs index 9fbdd5c..f99e7ca 100644 --- a/rust/pdftool_core/src/lib.rs +++ b/rust/pdftool_core/src/lib.rs @@ -53,11 +53,9 @@ fn resolve_object(doc: &Document, obj: &Object) -> Result } } -/// Resolves a PDF number, returning `None` for unresolved or cyclic references, -/// non-numbers, non-finite reals, or integers that cannot be represented exactly. -fn object_to_number(doc: &Document, obj: &Object) -> Option { +fn resolve_object_chain(doc: &Document, obj: &Object) -> Option { let mut resolved = obj.clone(); - let mut references = HashSet::new(); + let mut references = HashSet::::new(); loop { match resolved { @@ -67,21 +65,29 @@ fn object_to_number(doc: &Document, obj: &Object) -> Option { } resolved = doc.get_object(id).ok()?.clone(); } - Object::Integer(value) => { - let converted = value as f64; - // Reject integers outside f64's exact range rather than - // silently presenting rounded coordinates to callers. - return ((converted as i128) == (value as i128)).then_some(converted); - } - Object::Real(value) => { - let converted = f64::from(value); - return converted.is_finite().then_some(converted); - } - _ => return None, + value => return Some(value), } } } +/// Resolves a PDF number, returning `None` for unresolved or cyclic references, +/// non-numbers, non-finite reals, or integers that cannot be represented exactly. +fn object_to_number(doc: &Document, obj: &Object) -> Option { + match resolve_object_chain(doc, obj)? { + Object::Integer(value) => { + let converted = value as f64; + // Reject integers outside f64's exact range rather than + // silently presenting rounded coordinates to callers. + ((converted as i128) == (value as i128)).then_some(converted) + } + Object::Real(value) => { + let converted = f64::from(value); + converted.is_finite().then_some(converted) + } + _ => None, + } +} + /// Converts two diagonally opposite corners in either order to a rectangle /// `(x, y, width, height)`, using the smaller coordinates as the origin. Zero /// extents are accepted; non-finite coordinates or extents return `None`. @@ -721,10 +727,10 @@ fn detect_blocks_impl(pdf_data: &[u8]) -> CoreResult> { )), ) })?; - let rect_obj = resolve_object(&doc, rect_obj).map_err(|err| { + let rect_obj = resolve_object_chain(&doc, rect_obj).ok_or_else(|| { core_error_with_context( "BW_PDF_WIDGET_RECT_INVALID", - format!("could not resolve widget Rect: {err}"), + "could not resolve widget Rect or encountered an indirect-reference cycle", Some(format!( "page {} annotation {}", page_number, @@ -979,6 +985,86 @@ mod tests { save_fixture_doc(&mut doc) } + fn fixture_with_first_widget_chained_indirect_rect(pdf_data: &[u8], rect: Object) -> Vec { + let indirect_pdf = fixture_with_first_widget_indirect_rect(pdf_data, rect); + let mut doc = Document::load_mem(&indirect_pdf).expect("load indirect-Rect fixture"); + let page_id = *doc + .get_pages() + .values() + .next() + .expect("fixture has one page"); + let widget_id = { + let page = doc + .get_object(page_id) + .expect("fixture page exists") + .as_dict() + .expect("fixture page is a dictionary"); + let annots = page + .get(b"Annots") + .expect("fixture page has Annots") + .as_array() + .expect("fixture Annots is an array"); + match annots.first().expect("fixture has a widget annotation") { + Object::Reference(id) => *id, + _ => panic!("fixture widget must be indirect"), + } + }; + let rect_id = { + let widget = doc + .get_object(widget_id) + .expect("fixture widget exists") + .as_dict() + .expect("fixture widget is a dictionary"); + match widget.get(b"Rect").expect("fixture widget has Rect") { + Object::Reference(id) => *id, + _ => panic!("fixture Rect must be indirect"), + } + }; + let chained_id = doc.new_object_id(); + doc.objects.insert(chained_id, Object::Reference(rect_id)); + doc.get_object_mut(widget_id) + .expect("fixture widget exists") + .as_dict_mut() + .expect("fixture widget is a dictionary") + .set(b"Rect", Object::Reference(chained_id)); + save_fixture_doc(&mut doc) + } + + fn fixture_with_first_widget_cyclic_indirect_rect(pdf_data: &[u8]) -> Vec { + let mut doc = Document::load_mem(pdf_data).expect("load fixture PDF"); + let page_id = *doc + .get_pages() + .values() + .next() + .expect("fixture has one page"); + let widget_id = { + let page = doc + .get_object(page_id) + .expect("fixture page exists") + .as_dict() + .expect("fixture page is a dictionary"); + let annots = page + .get(b"Annots") + .expect("fixture page has Annots") + .as_array() + .expect("fixture Annots is an array"); + match annots.first().expect("fixture has a widget annotation") { + Object::Reference(id) => *id, + _ => panic!("fixture widget must be indirect"), + } + }; + let first_id = doc.new_object_id(); + let second_id = doc.new_object_id(); + doc.objects.insert(first_id, Object::Reference(second_id)); + doc.objects.insert(second_id, Object::Reference(first_id)); + doc.get_object_mut(widget_id) + .expect("fixture widget exists") + .as_dict_mut() + .expect("fixture widget is a dictionary") + .set(b"Rect", Object::Reference(first_id)); + save_fixture_doc(&mut doc) + } + fn fixture_with_first_widget_indirect_rect_coordinates(pdf_data: &[u8]) -> Vec { let mut doc = Document::load_mem(pdf_data).expect("load fixture PDF"); let page_id = *doc @@ -1220,11 +1306,21 @@ mod tests { #[test] fn detect_blocks_resolves_indirect_widget_rectangles() { - let pdf = - fixture_with_first_widget_indirect_rect(&make_fixture_pdf(), rect(50, 700, 250, 724)); + let base = make_fixture_pdf(); + let pdf = fixture_with_first_widget_indirect_rect(&base, rect(50, 700, 250, 724)); let blocks = detect_blocks_impl(&pdf).expect("indirect Rect arrays should resolve"); assert_eq!((blocks[0].x, blocks[0].y), (50.0, 700.0)); assert_eq!((blocks[0].width, blocks[0].height), (200.0, 24.0)); + + let chained_pdf = + fixture_with_first_widget_chained_indirect_rect(&base, rect(50, 700, 250, 724)); + let chained_blocks = + detect_blocks_impl(&chained_pdf).expect("chained indirect Rect arrays should resolve"); + assert_eq!((chained_blocks[0].x, chained_blocks[0].y), (50.0, 700.0)); + assert_eq!( + (chained_blocks[0].width, chained_blocks[0].height), + (200.0, 24.0) + ); } #[test] @@ -1293,7 +1389,7 @@ mod tests { #[test] fn object_to_number_preserves_f64_integer_boundaries() { - let doc = Document::with_version("1.7"); + let mut doc = Document::with_version("1.7"); assert_eq!( object_to_number(&doc, &Object::Integer(16_777_217)), @@ -1311,6 +1407,21 @@ mod tests { object_to_number(&doc, &Object::Integer(9_007_199_254_740_993)), None ); + + let inner_id = doc.new_object_id(); + doc.objects.insert(inner_id, Object::Integer(42)); + let outer_id = doc.new_object_id(); + doc.objects.insert(outer_id, Object::Reference(inner_id)); + assert_eq!( + object_to_number(&doc, &Object::Reference(outer_id)), + Some(42.0) + ); + + let first_id = doc.new_object_id(); + let second_id = doc.new_object_id(); + doc.objects.insert(first_id, Object::Reference(second_id)); + doc.objects.insert(second_id, Object::Reference(first_id)); + assert_eq!(object_to_number(&doc, &Object::Reference(first_id)), None); assert_eq!(object_to_number(&doc, &Object::Real(f32::NAN)), None); assert_eq!(object_to_number(&doc, &Object::Real(f32::INFINITY)), None); } @@ -1363,6 +1474,12 @@ mod tests { detect_blocks_impl(&unresolved_coordinate), "BW_PDF_WIDGET_RECT_INVALID", ); + + let cyclic_rect = fixture_with_first_widget_cyclic_indirect_rect(&make_fixture_pdf()); + assert_error_code( + detect_blocks_impl(&cyclic_rect), + "BW_PDF_WIDGET_RECT_INVALID", + ); } #[test] From 2d5d2c86503acc6967c072b08cd6f2ce970a3f5b Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:34:01 +0000 Subject: [PATCH 7/7] docs: synchronize geometry evidence and proof policy Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- .github/CONTRIBUTING.md | 15 +- CHANGELOG.adoc | 12 +- EXPLAINME.adoc | 10 +- TEST-NEEDS.adoc | 27 +- TOPOLOGY.adoc | 12 +- docs/ci/CHECK-DETERMINATIONS.adoc | 91 +++- ...0-10-ultraplan-formatrix-ffp-geometry.adoc | 504 +++++++++++++----- rust/pdftool_core/README.adoc | 22 +- wiki/Architecture.md | 30 +- wiki/CI-and-Gates.md | 31 +- wiki/Contributing--Dev-Setup.md | 10 +- wiki/Glossary.md | 12 +- wiki/Home.md | 21 +- wiki/Rust-Core.md | 24 +- 14 files changed, 584 insertions(+), 237 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index dc54f48..4a3032f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -6,10 +6,11 @@ SPDX-License-Identifier: MPL-2.0 # Contributing to blocky-writer **Read `README.adoc` first.** The short version of where this project stands: -the Rust/WASM core (`rust/pdftool_core`) is implemented, unit-tested and green -in CI. The AffineScript frontend (`src/*.affine`) is a prototype with no build -pipeline in this checkout. Nothing here is release-ready as a Firefox extension -yet. +the Rust/WASM core (`rust/pdftool_core`) is implemented and unit-tested. The +main baseline passed and PR #78 implementation revision `9f9370e` passed the +Rust CI job (run `41326285595` for the latter); other PR gates remain red. The +AffineScript frontend (`src/*.affine`) is a prototype with no build pipeline in +this checkout. Nothing here is release-ready as a Firefox extension yet. --- @@ -135,8 +136,10 @@ See `SECURITY.adoc`. cargo clippy --manifest-path rust/pdftool_core/Cargo.toml --locked --all-targets -- -D warnings ``` - `cargo test` runs 6 tests. If you get a different number, something changed - and the wiki is stale — fix the wiki too. + The current `pdftool_core` source has 22 unit-test functions: 6 fill/error + tests, 14 detector tests, and 2 numeric-geometry tests. If the count changes, + update `TEST-NEEDS.adoc`, `EXPLAINME.adoc`, and the wiki in the same change. + Unit-test success is not a formal proof or general PDF conformance result. If you cannot run these — no Rust toolchain, no crates.io access — **say so explicitly in the PR** rather than implying you did. CI is the only authority diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 26a94c3..fd21caa 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -46,13 +46,18 @@ not repeated verbatim — where a commit subject was mangled or carried an inlin ==== Fixed +* `+rust/pdftool_core+`: `+detect_blocks+` now accepts either PDF-defined + diagonal `/Rect` corner order, resolves indirect rectangle and numeric-object + chains with cycle protection, and reports malformed widget geometry using a + structured `+BW_PDF_WIDGET_RECT_INVALID+` error. * `+rust/pdftool_core+`: resolved the two Clippy lints that were the only thing keeping the `+Rust core (tests, formatting, lint)+` job red — `+clippy::map_clone+` on `+doc.get_object(*id).map(Clone::clone)+` (now `+.cloned()+`) and `+clippy::unnecessary_cast+` on `+Object::Real(v) => Some(*v as f32)+` (now `+Some(*v)+`). The adjacent - `+Object::Integer+` cast was left alone because that `+i64+` → `+f32+` cast is real. - (#73) + `+Object::Integer+` cast was left alone because that `+i64+` → `+f32+` cast was real + at crate version 0.1.0. Version 0.2.0 subsequently widens the public `+Block+` + surface to `+f64+` (see *Changed* above). (#73) * `+rust/pdftool_core+`: `+rustfmt+` applied so the formatting step of the `+rust-core+` job passes. (e1b6225) * `+actions.lock+` resynchronised after the #69/#70/#71 desync that startup-killed @@ -77,6 +82,9 @@ not repeated verbatim — where a commit subject was mangled or carried an inlin ==== Changed +* `+rust/pdftool_core+` bumped to `+0.2.0+`; public `+Block+` coordinates now + use `+f64+`, and three new `+BW_*+` detector error codes are versioned as a + breaking contract change. Review compatibility before downstream adoption. * `+.machine_readable/+` migrated from 6SCM to 6A2 (`+.scm+` → `+.a2ml+`). * `+README.adoc+` gained an *Ecosystem position* section stating the boundary and linking the other suite components, satisfying the first acceptance criterion diff --git a/EXPLAINME.adoc b/EXPLAINME.adoc index 15087af..fda9c4d 100644 --- a/EXPLAINME.adoc +++ b/EXPLAINME.adoc @@ -27,7 +27,7 @@ declaration of anything. |=== | Path | What's There -| `rust/pdftool_core/src/lib.rs` | The whole core: `detect_blocks`, `fill_blocks`, the field walkers, the `BW_*` error taxonomy, and 17 test functions (6 original + 11 detector tests in the current unverified diff). ~1290 lines. +| `rust/pdftool_core/src/lib.rs` | The whole core: `detect_blocks`, `fill_blocks`, the field walkers, the `BW_*` error taxonomy, and 22 test functions (6 fill/error, 14 detector, 2 numeric-geometry). PR #78 implementation commit `9f9370e` passes Rust CI run `41326285595`. | `src/*.affine` | AffineScript prototype: popup, content script, background script, `core/PdfTool`, `core/Storage`, `core/ProvenMount`, `components/Block`, `components/FormFiller`. Not buildable in this checkout. | `public/` | `manifest.json`, `popup.html`, icons. | `tests/aspect/` | Aspect-oriented test shell script. @@ -48,10 +48,10 @@ declaration of anything. | Claim in the README | Where it is checked | The Rust core has unit tests -| The current source contains 17 test functions: 6 original fill/error tests and 11 direct detector tests. Latest main baseline run `38818494415` at `3fd5ba1` passed; the current working diff has not been compiled or run because `cargo`/`rustc` are unavailable here. +| The source contains 22 test functions: 6 fill/error tests, 14 detector tests, and 2 numeric-geometry guard tests. PR #78 implementation commit `9f9370e` passed all three Rust checks in run `41326285595`; synthetic tests do not establish general PDF conformance or formal proof. -| `cargo fmt`, `cargo test` and `cargo clippy -D warnings` pass on baseline `main` -| `.github/workflows/ci.yml`, job `Rust core (tests, formatting, lint)`. Run `38818494415` on `main` at `3fd5ba1` passed all three; this does not verify the current branch diff. +| `cargo fmt`, `cargo test` and `cargo clippy -D warnings` pass on the current implementation +| `.github/workflows/ci.yml`, job `Rust core (tests, formatting, lint)`. Run `41326285595` at `9f9370e` passed formatting, all 22 tests, and Clippy. Main baseline run `38818494415` at `3fd5ba1` is separate evidence. | The frontend is a prototype with no build pipeline | There is no AffineScript compiler configuration and no JavaScript package manifest in the tree. `deno.json` was deleted by #43. @@ -60,7 +60,7 @@ declaration of anything. | `apply_field_value` in `rust/pdftool_core/src/lib.rs`, dispatched on field type `Tx` / `Ch` / `Btn`. | Errors carry structured codes -| `CoreErrorPayload { code, message, context }`; 24 unique `BW_*` codes in the current source. Four new detector codes remain unverified and need the compatibility/version review required for public API changes. +| `CoreErrorPayload { code, message, context }`; 23 unique `BW_*` codes in the current source (20 baseline, 3 new). `pdftool_core` is bumped to `0.2.0` for the breaking public contract change; owner compatibility review remains open. |=== diff --git a/TEST-NEEDS.adoc b/TEST-NEEDS.adoc index cacb73a..1b78a3d 100644 --- a/TEST-NEEDS.adoc +++ b/TEST-NEEDS.adoc @@ -12,24 +12,25 @@ Last reviewed: 2026-11-01. |Category |Count |Notes |Test directories |1 |Location(s): /tests (aspect + fuzz placeholder) |CI workflows |14 |`.github/workflows/*.yml`. `ci.yml` runs the Rust core checks; `actions.lock` is a file, not a workflow. See docs/ci/CHECK-DETERMINATIONS.adoc -|Unit tests |17 in source |Rust `pdftool_core` — 6 existing fill/error tests + 11 direct detector tests in the current working diff. Current diff is uncompiled and unrun. +|Unit tests |22 in source |Rust `pdftool_core` — 6 fill/error tests, 14 detector tests, and 2 direct numeric-geometry tests. PR #78 implementation commit `9f9370e` passed formatting, all unit tests, and Clippy in run `41326285595`. |=== === What's Covered * [x] Rust core baseline CI on `main` — run `38818494415` at `3fd5ba1` - passed formatting, tests, and Clippy (actor `hyperpolymath`). That run predates - this branch's detector changes. -* [ ] Current working diff — 17 test functions are present, including 11 direct - detector tests for valid widgets, indirect rectangles, malformed annotation - arrays/references, invalid rectangles, and non-widget annotations. They have - not been compiled or run because this sandbox has no Rust toolchain. -* [ ] `actions.lock` synchronisation gate — failing on current `main` at run - `38818494312`; the YAML uses `smtp-notify-action@v0.5.0` while generated lock - metadata records `v0.3.0`. Do not hand-edit the generated file. -* [ ] Governance Actions lock verification — failed at run `38818495000` for - the same lock drift; other job outcomes and additional current failures are - recorded in `docs/ci/CHECK-DETERMINATIONS.adoc`. + passed formatting, tests, and Clippy (actor `hyperpolymath`). That run is + baseline evidence only. +* [x] PR #78 implementation revision `9f9370e` — run `41326285595` passed + formatting, all 22 unit tests, and Clippy. The synthetic fixtures exercise + PDF-defined corner ordering, chained indirect rectangles and numeric + references (including cycles), exact integer boundaries, null/dangling + annotations, and missing/unresolved/malformed widget geometry. + This is not real-world PDF corpus coverage or a formal proof receipt. +* [ ] `actions.lock` synchronisation gate — failing on PR run `41326285660` + and main run `38818494312`; the YAML uses `smtp-notify-action@v0.5.0` while + generated lock metadata records `v0.3.0`. Do not hand-edit the generated file. +* [ ] CodeQL and Hypatia — current PR jobs fail during `Set up job`; causes are + unknown, and the logs were unavailable. === Still Missing (for CRG B+) diff --git a/TOPOLOGY.adoc b/TOPOLOGY.adoc index e66e80c..07618c1 100644 --- a/TOPOLOGY.adoc +++ b/TOPOLOGY.adoc @@ -2,7 +2,7 @@ .... Last updated: 2026-11-01 -Status: main Rust CI passed at `3fd5ba1`; current 17-test source diff is unverified; frontend prototype, no build pipeline +Status: main baseline and PR #78 Rust CI pass; implementation `9f9370e` passed run `41326285595` (22 tests); other merge gates remain blocked; frontend prototype, no build pipeline .... === System Architecture @@ -65,20 +65,20 @@ EXTENSION LAYERS Extension bundle pipeline ░░░░░░░░░░ 0% deno.json removed in #43; no toolchain CORE (RUST/WASM) - pdftool_core (WASM) ████████░░ 80% Main baseline (3fd5ba1) passes CI; 80% covers main only; 17 tests in source, 11 new detector tests unverified + pdftool_core (WASM) ████████░░ 80% Project estimate; 9f9370e passes 22 Rust tests (run 41326285595), including chained-reference handling; no real-world PDF corpus or formal proof AcroForm Writeback ████████░░ 80% Tx / Ch / Btn only; no Sig Ruled-line & cell detection ░░░░░░░░░░ 0% NOT IMPLEMENTED — the "hand spacing" case - Error Taxonomy ████████░░ 80% 24 codes in source; 4 new codes need Rust + compatibility review + Error Taxonomy ████████░░ 80% 23 codes; 3 additions and f64 Block surface versioned in 0.2.0; owner review open REPO INFRASTRUCTURE - Rust CI (fmt/test/clippy) ██████████ 100% main run 38818494415 passed at 3fd5ba1; branch unverified - Lock Sync Gate ░░░░░░░░░░ 0% main run 38818494312 failed on generated-lock drift + Rust CI (fmt/test/clippy) ██████████ 100% PR #78 run 41326285595 passed on 9f9370e; main run 38818494415 also passed + Lock Sync Gate ░░░░░░░░░░ 0% PR run 41326285660 and main run 38818494312 fail on generated-lock drift Documentation ████████░░ 80% Human-readable status refreshed 2026-11-01; machine state remains stale .machine_readable/ ████░░░░░░ 40% STATE.a2ml exists but is stale; A2ML/DEED owner ruling pending Containerfile ██████████ 100% Reproducible dev env ───────────────────────────────────────────────────────────────────────────── -OVERALL: █████░░░░░ ~50% Main Rust baseline passes; branch validation pending; extension not release-ready +OVERALL: █████░░░░░ ~50% PR Rust checks pass; merge blocked by lock/analysis gates and review; extension not release-ready .... === Key Dependencies diff --git a/docs/ci/CHECK-DETERMINATIONS.adoc b/docs/ci/CHECK-DETERMINATIONS.adoc index f10fa37..beeb321 100644 --- a/docs/ci/CHECK-DETERMINATIONS.adoc +++ b/docs/ci/CHECK-DETERMINATIONS.adoc @@ -40,25 +40,24 @@ unresolved; this ledger remains incomplete until each receives an evidence-based gone until the thing it would build exists. | `rust-core` (CI) -| *Fixed on `main`; current feature branch unverified* +| *Fixed on `main` and PR #78 implementation revision `9f9370e`* | 2026-11-01 | Replacement for both retired jobs: `cargo fmt -- --check`, `cargo test --locked`, and `cargo clippy --all-targets -- -D warnings` on `rust/pdftool_core`. The historical first run at `e1b6225` passed formatting and six tests but failed two Clippy lints; #73 repaired those without - weakening the gate. The latest main push run, `38818494415` at - `3fd5ba1ed3deee22288d3441b1f3a833246995e2` (actor `hyperpolymath`), passed all - three Rust steps. This closes the old “awaiting first owner-triggered green” - status for `main` only. The current Arena branch contains uncompiled detector - changes and has no test/format/Clippy run; do not infer its status from this - baseline. + weakening the gate. Main push run `38818494415` at + `3fd5ba1ed3deee22288d3441b1f3a833246995e2` and PR run `41326285595` at + `9f9370e206ae8cc6dd3ac86cba3f0904e4838135` passed formatting, all 22 unit + tests, and Clippy. The latter validates the implementation revision only; + later Rust changes require a new receipt. Other PR gates remain unresolved. | `Lock Sync Gate` / `actions.lock is in sync with the workflow YAML` -| *Open — failing on current `main`* +| *Open — failing on current `main` and PR #78* | 2026-11-01 | The fix for #67 was valid at the time, but the lock drifted again after #74. - On current main, run `38818494312` at `3fd5ba1` failed its - `Verify lockfile synchronisation` step. The current workflow uses + Main run `38818494312` at `3fd5ba1` and latest PR run `41326285660` at + `9f9370e` failed `Verify lockfile synchronisation`. The current workflow uses `hyperpolymath/smtp-notify-action@v0.5.0`; the generated lock still records `@v0.3.0`. The supported `gh actions-lock` generator is unavailable in this sandbox, so the lock has deliberately not been hand-edited. @@ -66,11 +65,12 @@ unresolved; this ledger remains incomplete until each receives an evidence-based == Current `main` run snapshot (2026-11-01) -Current `origin/main` is `3fd5ba1` (#75); this session branch tip is -`c2b073e` (#74). Both commits are shallow/grafted in this checkout, so their -ancestry is not inferred. The following are latest observed outcomes on that -SHA or later scheduled runs against it, not evidence for the untested Arena -branch. +Current `origin/main` is `3fd5ba1` (#75). PR #78's fork point is `c2b073e` +(#74), and its implementation revision is `9f9370e` (after merge commit `4a7be9e`). +The fork point and main +snapshot are shallow/grafted in this checkout, so ancestry is not inferred. +The following table records only outcomes on the stated main snapshot; PR #78 +results are listed separately below. [cols="2,1,1,3",options="header"] |=== @@ -80,7 +80,8 @@ branch. | `38818494415` | Passed | Push on `3fd5ba1`, actor `hyperpolymath`; formatting, tests, and Clippy all - succeeded. Current branch changes remain untested. + succeeded. This is the main baseline; PR #78's separate Rust receipt is + `41326285595` at `9f9370e`. | Governance / Actions lockfile verify | `38818495000` @@ -127,6 +128,64 @@ The only root cause established in this snapshot is the generated Actions lock drift. Do not attribute the setup-step or mirror failures to code, credentials, network, or remote policy without accessible evidence. +== PR #78 run snapshot (implementation revision `9f9370e`) + +The session branch at `9f9370e206ae8cc6dd3ac86cba3f0904e4838135` passed the +Rust workflow in run `41326285595`: formatting, all 22 unit tests, and Clippy +passed. This includes chained indirect `/Rect` resolution and cycle checks. The +receipt confirms CI-tested behavior for that exact implementation commit; it is +not a formal proof, general PDF conformance result, or final merge receipt. + +[cols="2,1,1,3",options="header"] +|=== +| Check / workflow | Run | Result | Evidence and current disposition + +| Rust core (`CI`) +| `41326285595` +| Passed +| `Rust core (tests, formatting, lint)` passed all three steps at `9f9370e`. + +| Lock Sync Gate +| `41326285660` +| Failed +| `Verify lockfile synchronisation` failed. The generated action lock remains + out of sync; its visible `smtp-notify-action` entry is stale. Do not hand-edit it. + +| CodeQL Advanced +| `41326285683` +| Failed +| `Analyze (actions)` and `Analyze (javascript-typescript)` failed at `Set up job`. + Cause unknown; logs were unavailable. + +| Hypatia Security Scan +| `41326285619` +| Failed +| `Hypatia Neurosymbolic Analysis` failed at `Set up job`. Cause unknown; logs + were unavailable. + +| Governance / Secret Scanner (earlier PR runs) +| `41325016132` / `41325016140` +| Failed before jobs +| GitHub reported a generic workflow-file issue and created no jobs. Cause not + established; do not infer it from the lock gate alone. + +| GitGuardian / SonarCloud / Semgrep +| PR checks / scan `258901955` +| Passed +| These providers reported green on the `9f9370e` implementation revision. + +| CodeRabbit +| PR check / review +| Skipped / changes requested +| The check was skipped after review allowance was exhausted. The prior review + is still `CHANGES_REQUESTED` on `0224ef6`; it has not been refreshed after the + later code revisions. +|=== + +The pull request remains blocked. Do not bypass the failed lock/analysis gates or +the outstanding review decision. If the implementation changes, record and use +new run IDs rather than carrying these receipts forward. + == Historical red checks for #67 At the time of #67, both checks named in that fix were red for reasons that had diff --git a/docs/plans/2026-10-10-ultraplan-formatrix-ffp-geometry.adoc b/docs/plans/2026-10-10-ultraplan-formatrix-ffp-geometry.adoc index 0795f94..7d3f957 100644 --- a/docs/plans/2026-10-10-ultraplan-formatrix-ffp-geometry.adoc +++ b/docs/plans/2026-10-10-ultraplan-formatrix-ffp-geometry.adoc @@ -10,25 +10,29 @@ == Executive disposition This remains an evidence-backed plan with partial implementation in the session -branch, not a completed change. The requested work has four separate -owners/scopes and must not be collapsed into a new integration product: +branch, not a completed or merged change. Keep the work separated and do not +collapse it into a new integration product: . Restore trustworthy repository health records and repair the current Actions - lock mismatch. + lock mismatch; do not hand-edit generated lock metadata. +. Preserve the local detector correction, its PDF-spec semantics, and the + versioned error contract; resolve the remaining geometry/identity and owner + review gates before merge. . Bring Blocky Writer's *producer* into conformance with active, ratified FFP 1.0, then declare and test the existing Blocky Writer -> Presswerk handoff under the estate's active SEAMS-SPEC. -. Establish direct detection tests and resolve geometry/field-identity behavior - against the DocMatrix composition contract before changing that behavior. -. Handle Formatrix Docs#61 and RSR-template-repo#200 as bounded, informational - follow-ups owned by those repositories; do not build a viewer/parser - integration or edit another repository here. +. Handle Formatrix Docs#61 and RSR-template-repo#200 as bounded follow-ups owned + by those repositories; include Mathesis and Standards for proof-policy + coordination. Do not build a viewer/parser integration or edit another repo. +. Track proof claims at the exact Mathesis grade and use the Standards proof + policy. The current Rust CI result is `TESTED`, not a Grade 3 proof receipt. The boundary remains explicit: Formatrix Docs owns multi-format viewing and editing; Blocky Writer owns fixed-layout PDF/form placement. DocMatrix#71 rejects a new runtime integration engine. A test-only producer-to-consumer check is not a runtime integration, but it must not create a runtime dependency or a new -universal document serializer. +universal document serializer. Mathesis may catalogue proof receipts; the +original repository remains the owner of its proofs and engineering contracts. == Current implementation status (2026-11-01) @@ -37,13 +41,15 @@ universal document serializer. | Work area | Current state on `arena/d32cfaf0-blocky-writer` | Detector geometry -| The working diff rejects reversed, non-finite, and lossy integer widget - rectangles; resolves an indirect `/Rect`; and reports malformed annotation - arrays, unresolved/invalid annotations, and invalid widget rectangles with - structured errors. This adds four public `BW_*` codes; compatibility/version - review remains pending. Eleven direct detector tests and the six original - fill/error tests are present (17 test functions total). None has been - compiled or run on this branch. +| Implementation revision `9f9370e` follows PDF `/Rect` corner-pair semantics, + resolves chained indirect arrays and numeric elements with cycle protection, + uses `f64` for exactly representable integer coordinates, skips null/dangling + optional annotation entries, and reports malformed widget geometry with + structured errors. This adds three public `BW_*` codes and changes the numeric + surface; the crate is versioned `0.2.0`. There are 22 tests (6 fill/error, + 14 detector, 2 numeric guard). Rust CI run `41326285595` passed + fmt/tests/Clippy at that exact commit. Geometry-owner compatibility review + remains open; no Grade 3 proof is claimed. | FFP producer | Not implemented. `fill_blocks_impl` still writes no FFP XMP marker. Safe XMP @@ -58,22 +64,26 @@ universal document serializer. unresolved. No Trustfile or Mustfile seam declaration has been added. | CI / lock -| `origin/main` is at `3fd5ba1` (#75); this session branch is at `c2b073e` - (#74). Both commits are shallow/grafted in this checkout, so their ancestry is - not inferred. Current `main` still has `push-email-notify.yml` at v0.5.0 while the generated - lock records v0.3.0. Main's latest Rust CI run passed, but its Governance and - Lock Sync Gate runs failed on lock verification. The prescribed - `gh actions-lock` generator is unavailable here; the lock has not been edited. +| `origin/main` is at `3fd5ba1` (#75); the session fork point is `c2b073e` + (#74); PR #78 implementation revision `9f9370e` is the current tested code. + Main Rust run `38818494415` and PR Rust run `41326285595` passed. Main and PR + lock checks failed (`38818494312`, `41326285660`) because the workflow uses + `smtp-notify-action@v0.5.0` while the generated lock records `v0.3.0`. The + prescribed `gh actions-lock` generator is unavailable here; the lock remains + untouched. | Validation environment -| `cargo`, `rustc`, `rustfmt`, `gawk`, `go`, and `asciidoctor` are absent from - `PATH`. Rust formatting, compilation, tests, Clippy, lock verification, and - AsciiDoc rendering therefore remain pending. Do not describe the working diff - as passing. +| `cargo`, `rustc`, `rustfmt`, `gawk`, `go`, `asciidoctor`, `agda`, `rocq`, + `lean`, `idris2`, and `creusot` are absent from `PATH`. Rust + formatting/tests/Clippy have remote CI receipts for `9f9370e`; lock + verification and AsciiDoc rendering remain unverified here. CodeQL and + Hypatia fail during setup with unknown causes. No formal proof artifact or + Grade 3 receipt exists, and no proof checker was available locally. | Estate follow-ups | No other repository or issue has been edited or closed. The A2ML/DEED - interpretation and SEAMS-SPEC gates remain as recorded below. + interpretation, SEAMS-SPEC gates, and Mathesis/Standards proof-policy + coordination remain open as recorded below. |=== == Recon snapshot and limits @@ -82,15 +92,29 @@ universal document serializer. |=== | Item | Snapshot used -| Blocky Writer -| `c2b073eaf5e204f466adc0797484478ce94ef95b`, on the session branch - `arena/d32cfaf0-blocky-writer`; the checkout was clean before this plan was - written. +| Blocky Writer recon base +| Fork point `c2b073eaf5e204f466adc0797484478ce94ef95b`; this is the pre-change + base, not the implementation revision. -| Estate standards +| Blocky Writer implementation evidence +| PR #78 implementation commit `9f9370e206ae8cc6dd3ac86cba3f0904e4838135`; + Rust CI run `41326285595` passed at this SHA. This distinguishes the recon + base from the tested code revision. + +| Estate standards (FFP / SEAMS snapshot) | `hyperpolymath/standards` at `3108c1b01186741eea80c00da3f4beda912286f9` (2026-10-09). +| Estate standards proof policy +| `hyperpolymath/standards` at + `46ee18323305d5b36e2eef936e47dd54c39ea736`, file + `3-practice/PROOF-POLICY.adoc` (checked 2026-11-01). + +| Mathesis proof architecture +| `hyperpolymath/mathesis` at `50a012dba57fe90c9aca8d57241690c133ab117c`; + includes `estate-mathematical-architecture` and + `docs/status/PROOF-NEEDS.adoc` (T3 Standard). + | RSR template | `hyperpolymath/rsr-template-repo` at `df09a6c50c4c77e93abac0fdf162509bf5648611` (2026-10-09). @@ -104,12 +128,13 @@ universal document serializer. `f17620192b69ccffb9454cd49aac8d5ec9454c48` (2026-10-05). |=== -No Rust tests, build, format, lint, FFP conformance run, browser build, or -AsciiDoc render has run on the current working tree. `cargo`, `rustc`, `rustfmt`, -`gawk`, `go`, `asciidoctor`, `just`, `wasm-pack`, and `deno` are not on `PATH`. -The plan therefore distinguishes source inspection and recorded remote CI -evidence from checks actually executed in this sandbox. Remote CI results below -are for `origin/main` at `3fd5ba1`, not this feature branch. +No local Rust build/test/format/lint, FFP conformance run, browser build, +AsciiDoc render, or formal proof check has run in this sandbox: `cargo`, `rustc`, +`rustfmt`, `gawk`, `go`, `asciidoctor`, `just`, `wasm-pack`, `deno`, `agda`, +`rocq`, `lean`, `idris2`, and `creusot` are absent from `PATH`. Remote Rust CI +run `41326285595` passed for implementation revision `9f9370e`; main baseline +run `38818494415` at `3fd5ba1` is separate evidence. No FFP seam or formal proof +has been run. Do not collapse CI tests into a Mathesis Grade 3 proof claim. == Binding rules and authority @@ -133,10 +158,26 @@ Use the status of each source, not just its location in the estate: engine. ForthWall remains proposed and audit/propose/verify-only. * **Blocky Writer's own contractiles remain binding.** `INTENT.contractile` says the WASM boundary is exactly two functions, filling is name-driven, and - page geometry is never silently normalized. `MUST.contractile` forbids new + page geometry is never silently transformed. `MUST.contractile` forbids new TypeScript/Python/Go and NPM dependencies, removal/weakening of tests or CI, - loss of `.machine_readable/`, and unapproved unsafe Rust. Preserve these - constraints while implementing the plan. + loss of `.machine_readable/`, and unapproved unsafe Rust. PDF-defined + interpretation of `/Rect` corner order is not a screen/page transform; no + other normalization is introduced. +* **Standards proof policy is binding.** The current + `hyperpolymath/standards/3-practice/PROOF-POLICY.adoc` maps first-party Rust + code to Creusot, API/ABI engineering properties to Idris2, and mathematical + theory to Agda, with theory proofs rechecked in Rocq and Lean. Third-party + `lopdf` code is not edited or re-proved; behavior relied on at that boundary + is recorded as an assumption with version and evidence. +* **Mathesis Grade 3 is an evidence status, not a test grade.** The estate + mathematics architecture defines Grade 3 as a claim that type-checks locally + in Agda but has not been translated to an independent foundation. Mathesis + `T3 — Standard` is a separate repository proof-tier label. Mathesis catalogs + receipts while source repositories retain proof ownership. This PR's Rust CI + tests are `TESTED` evidence only: no formal proof artifact exists here, so no + Grade 3 or `PROVEN` claim is made. To satisfy the requested Grade 3 proofing, + formalizable theory claims need a locally checked Agda witness; code and ABI + obligations still follow the Standards verifier mapping. * **The A2ML/DEED format position is unresolved.** Current standards A2ML `meta`, `state`, and `ecosystem` documentation carries a warning that the old TOML-like characterization is superseded and points to DEED. But @@ -158,10 +199,11 @@ Use the status of each source, not just its location in the estate: | CI and state | https://github.com/hyperpolymath/blocky-writer/pull/74[PR#74] merged `smtp-notify-action` v0.5.0 into `.github/workflows/push-email-notify.yml`; - `.github/workflows/actions.lock` still records v0.3.0. The latest observed - Rust CI run at `c2b073e` passed, - while Governance and Lock Sync Gate failed. Four mirror jobs also failed; - their causes are not established. + `.github/workflows/actions.lock` still records v0.3.0. Historical Rust CI at + the fork point `c2b073e` passed; implementation revision `9f9370e` also passed + in run `41326285595`. Current main and PR Lock Sync runs (`38818494312`, + `41326285660`) still fail. Four mirror jobs failed on main; their causes are + not established. | Repair the generated lock first, then refresh the health ledger from owner-triggered/current runs. Do not disable checks or attribute mirror failures without their evidence. @@ -195,24 +237,32 @@ Use the status of each source, not just its location in the estate: | Detection tests | At the inspected base `c2b073e`, the six Rust unit tests all exercised fill/error paths; none called `detect_blocks`, although `TEST-NEEDS.adoc` - marked block detection as covered. The working diff now adds eleven direct - detector tests, bringing the source count to 17; these remain uncompiled and - unrun. -| Correct the coverage record, keep the unverified tests clearly marked, and - complete the remaining geometry/identity cases only after their contracts are - agreed. + marked block detection as covered. Implementation revision `9f9370e` has 22 + test functions: 6 fill/error, 14 detector, 2 direct numeric-geometry checks. + Rust CI run `41326285595` passed formatting, all tests, and Clippy on that + exact revision. +| The synthetic tests now cover PDF-defined corner ordering, chained indirect + arrays and coordinates with cycle protection, exact integer boundaries, + finite extents, null/dangling annotations, and invalid widget geometry. They + do not cover a real-world PDF corpus, all page transforms, text encodings, or + a formal proof. | Geometry -| At `c2b073e`, `rect_from_object` used `min` and `abs`, normalizing reversed - corners, while `detect_blocks_impl` silently skipped some malformed or - unresolved annotation/rectangle cases. The working diff rejects reversed, - non-finite, and lossy integer coordinates, resolves one-level indirect - rectangles, and returns structured errors for malformed annotations and - widget rectangles. Coordinates remain `f32` PDF user-space values. -| The diff removes silent normalization and several silent skips, but remains - uncompiled and does not resolve page transforms, identity, Unicode decoding, - or the full consumer precision contract. Preserve those as explicit review - gates; do not claim complete geometry conformance. +| At `c2b073e`, `rect_from_object` used `min` and `abs`; that behavior correctly + interpreted either order of the two diagonal PDF `/Rect` corners. The initial + session patch incorrectly rejected reversed corner pairs, rejected integers + outside `f32` exact range, and treated some null/dangling annotation objects + as errors. Revision `9f9370e` restores PDF corner-order semantics, widens the + `Block` numeric representation to `f64`, resolves chained indirect rectangle + and coordinate references with cycle protection, and preserves + null/dangling annotation skip behavior. Missing, unresolved, + malformed, non-finite, or unrepresentable widget rectangles fail with + `BW_PDF_WIDGET_RECT_INVALID`. Three new public error codes and the numeric + surface are versioned in `pdftool_core` `0.2.0`. +| Rust CI passed at `9f9370e`. The geometry/identity response contract, + error-code compatibility, and the application of the no-silent-transform + contract still require owner review. Page rotations/transforms, identity, + Unicode decoding, and full PDF corpus conformance remain out of scope here. | Field identity | `Block` is `{label,x,y,width,height}`. Detection does not return an explicit @@ -254,12 +304,13 @@ independent of the FFP and geometry changes and should be a separate change set. === Actions Remote evidence rechecked on 2026-11-01. `origin/main` is -`3fd5ba1ed3deee22288d3441b1f3a833246995e2` (#75); this session branch is at -`c2b073e` (#74). Both tips are shallow/grafted in this checkout, so ancestry is -not inferred. Main's -latest push-triggered Rust CI run, `38818494415` (2026-10-15, actor -`hyperpolymath`), passed formatting, unit tests, and Clippy. That run does not -include this branch's detector changes. +`3fd5ba1ed3deee22288d3441b1f3a833246995e2` (#75); this branch forked from +`c2b073e` (#74), and PR #78's current implementation revision is +`9f9370e206ae8cc6dd3ac86cba3f0904e4838135`. The branch incorporated main in +`4a7be9e` and documentation commit `1cf6ea3`; local recon had been taken before +those remote updates. Main Rust run `38818494415` and PR Rust run `41326285595` +both passed formatting, tests, and Clippy; the PR run executed all 22 tests at +`9f9370e`. The same main push (actor `hyperpolymath`) failed Governance run `38818495000` at `governance / Actions lockfile verify` and Lock Sync Gate run `38818494312` @@ -269,14 +320,18 @@ while the workflow uses v0.5.0. Mirror run `38818495057` (actor `hyperpolymath`) mirror-bitbucket, mirror-gitea, mirror-disroot, and mirror-codeberg; GitLab, SourceHut, and Radicle succeeded. Causes remain unknown. -Scheduled CodeQL run `41099036814` (2026-10-30, actor `hyperpolymath`) failed at -`Set up job` for both analysis jobs; Hypatia run `41255373342` (2026-11-01, -actor `hyperpolymath`) failed at `Set up job`. Log downloads for these and the -mirror/lock runs returned EOF, so their causes -are not established. Secret Scanner run `38818495110` and Pages run -`38818494311` passed on the same main push, also triggered by `hyperpolymath`. -These are remote observations, not local test execution. The current branch -still has no passing CI evidence. +Scheduled CodeQL run `41099036814` (2026-10-30) and Hypatia run `41255373342` +(2026-11-01) failed at `Set up job`; latest PR CodeQL run `41326285683` and +Hypatia run `41326285619` fail at the same setup boundary. PR Lock Sync run +`41326285660` also fails; its log download returned EOF, with lock drift still +visible in the checked files. Causes of the setup failures are unknown. Main +Secret Scanner run `38818495110` passed; on the latest PR update, Rust +`41326285595`, GitGuardian, SonarCloud, and Semgrep scan `258901955` passed. +CodeRabbit was skipped. Earlier PR Governance and Secret Scanner runs +`41325016132` and `41325016140` failed before jobs with a generic workflow-file +issue; cause is unconfirmed. These are remote observations, not local test +execution. The PR Rust check passes, but the overall PR remains blocked by the +lock/analysis failures and the stale `CHANGES_REQUESTED` review. . Regenerate `.github/workflows/actions.lock` with the prescribed `gh actions-lock` tooling after the #74 action bump. Do not hand-edit the @@ -290,11 +345,10 @@ still has no passing CI evidence. Inspect the run-page annotation as well as REST/run summaries: a lockfile refusal and an org policy refusing bot-triggered Actions are distinct. A run with zero jobs is not evidence that Rust tests passed or failed. -. Refresh `docs/ci/CHECK-DETERMINATIONS.adoc` from exact current evidence. The - Rust core row predates passing main run `38818494415` at `3fd5ba1`; the Lock - Sync Gate row incorrectly says “Fixed” despite the post-#74 drift. Record the - new IDs, actor, step results, and unresolved scheduled/mirror failures without - claiming that this branch was tested. +. Keep `docs/ci/CHECK-DETERMINATIONS.adoc` synchronized to the exact evidence. + It now records main and PR Rust successes separately, the PR lock failure, + setup-step failures, and no-job workflow runs. Preserve the unresolved causes; + do not infer a green overall PR from the Rust check. . Triage mirror failures separately. Run `38818495057` failed in mirror-bitbucket, mirror-gitea, mirror-disroot, and mirror-codeberg; GitLab, SourceHut, and Radicle passed. Record those outcomes, but do not guess whether @@ -458,6 +512,32 @@ references `quality.yml`. Confirm the active runner mapping before asserting that CI already enforces SEAMS-SPEC. Until a real seam gate runs, record the deferral honestly rather than claiming conformance. +=== Suggested prompt for Standards maintainers + +[source,text] +---- +Please resolve the following normative questions before Blocky Writer declares +its test-only producer-to-Presswerk seam conformant: + +1. SEAMS-SPEC requires repo-relative `contract` and runnable local `test_ref`, + while the FFP PRINT-PATH example points both at the Standards checkout. What + is the approved child-repository pinning/local-test pattern? +2. How should the FFP example's `failure_mode: fail-degrade` and + `tier: external-trust` be reconciled with SEAMS-SPEC §7's tier requirement? + Does the external-trust circuit-breaker clause apply to a file handoff, and + if so, what is the minimum non-runtime interpretation? +3. Please confirm the intended A2ML/DEED grammar and conformance source: the + A2ML supersession warning conflicts with DEED-GRAMMAR-SPEC still being draft. + We will not assume either an obsolete parser or an unratified dialect. +4. Please confirm the local proof-verifier mapping in + 3-practice/PROOF-POLICY.adoc for Rust implementation, API/ABI properties, + and mathematical theory, and what evidence must accompany a claimed Grade 3 + Agda result. + +No Standards files or issue are being changed by Blocky Writer. We will keep +any P2P integration test local and test-only, with no runtime engine. +---- + === Acceptance * A non-empty successful fill carries the correct FFP declaration in catalog @@ -479,21 +559,24 @@ deferral honestly rather than claiming conformance. === Goal -Make detection behavior observable and contractual before changing geometry or -field identity. Preserve the two-function WASM boundary and the documented -name-driven fill semantics. +Record and test the detector behavior now implemented, then hold further +geometry or identity changes behind owner-approved contracts. Preserve the +two-function WASM boundary and the documented name-driven fill semantics. === 2A. Test the current detector directly -The current working diff adds eleven direct `detect_blocks_impl` tests using a -synthetic PDF: valid user-space widgets and labels, a valid non-widget -annotation, empty/malformed/unresolved page `/Annots`, direct and indirect -`/Rect`, malformed and reversed rectangles, precision-losing integer values, -and unresolved/non-dictionary annotations. Together with the six original -fill/error tests there are 17 test functions in source. None has run yet. +Implementation revision `9f9370e` has 22 test functions using synthetic +PDFs: six fill/error tests, fourteen direct detector tests, and two direct +numeric-geometry tests. Coverage includes valid widget labels, PDF-permitted +corner orders, chained indirect arrays and coordinates with cycle protection, +exact integer boundaries, finite extent checks, null/dangling annotation +handling, missing/unresolved rectangles, malformed annotation objects, and +non-widget annotations. Rust CI run `41326285595` passed formatting, all tests, +and Clippy for that exact revision. These tests do not establish general PDF +conformance or a formal proof. -Before treating this coverage as accepted, run it and add or explicitly defer the -remaining cases: +Before treating the coverage as complete, add or explicitly defer the remaining +cases: * multiple pages, annotation ordering, duplicate partial labels, nested field parents, full versus partial names, multiple widgets per field, and fallback @@ -503,34 +586,61 @@ remaining cases: * PDFDocEncoding and UTF-16BE/BOM text, non-ASCII labels, significant leading/ trailing spaces, and embedded NUL handling. -The existing tests assert faithful results or structured refusals. Do not bake -in `min`/`abs`, lossy decoding, trim, or skip behavior merely because that is -what the implementation used to do. Keep coordinate transforms, field identity, -and text decoding unchanged until their contracts are reviewed. +The tests assert PDF-defined corner-pair interpretation or structured refusals. +Do not add transforms beyond the PDF object semantics; retain null/dangling +optional annotation handling, but do not silently discard malformed widget +geometry. Keep page transforms, field identity, and text decoding unchanged +until their contracts are reviewed. === 2B. Resolve the contract before changing behavior -The current composition contract says no component silently normalizes page -geometry. The working diff removes `min`/`abs` normalization and returns a -structured error for reversed or unrepresentable widget rectangles. This is a -provisional refusal policy, not a claim that the complete PDF rectangle and -coordinate contract has been approved. Review it against the PDF contract and -consumer needs before merging. - -Before finalizing the behavior, agree on: - -. the accepted PDF rectangle invariant and whether malformed geometry yields a - structured error, an explicit unsupported record, or a documented refusal; -. the coordinate reference frame and units (the current values are raw PDF user - space, not transformed screen coordinates); -. page identity and ordering, including rotations/page boxes and whether - coordinates are returned as authored or transformed; -. numeric representation/rounding and acceptable precision; and +The composition contract forbids silent geometry transformation. PDF `/Rect` +semantics permit the two diagonal corners in either order, so the detector's +`min`/`max` step is the format-defined interpretation required to produce +`{x,y,width,height}`—not an application-side coordinate repair. Revision +`9f9370e` applies that rule, resolves chained indirect rectangle and numeric +references with cycle protection, widens `Block` coordinates to `f64`, and +rejects missing, unresolved, malformed, non-finite, or unrepresentable widget +geometry with `BW_PDF_WIDGET_RECT_INVALID`. Null or dangling optional +annotation entries remain absent, matching existing behavior. This +implementation has remote Rust CI evidence, but geometry and compatibility +owner review remains open; treat the behavior as provisional until that review +is recorded. + +Before closing the contract gate, agree on: + +. the PDF `/Rect` pair semantics and refusal behavior for malformed geometry; +. the coordinate reference frame and units (raw PDF user space, with no screen, + page-rotation, or page-box transform in this response); +. page identity and ordering, including whether a future version returns page + numbers or hierarchical field identity; +. `f64` range/rounding behavior and the three new public error codes; and . text decoding/normalization rules for PDF field names and display labels. -Do not expose a best-effort number as exact geometry. If the existing `f32` -shape cannot satisfy the selected precision contract, version the response -shape deliberately before consumers rely on it; do not add a third WASM export. +The PR bumps `pdftool_core` to `0.2.0` to mark its breaking numeric/error +contract. Keep exactly the existing two WASM exports; do not expose a rounded +number as exact geometry or add an unversioned transform. + +=== Suggested prompt for DocMatrix maintainers + +[source,text] +---- +Please review the geometry and compatibility boundary for Blocky Writer's +`detect_blocks` result. PR #78 interprets PDF `/Rect` diagonal corner pairs in +either permitted order, resolves indirect numeric values, returns raw PDF +user-space `f64` coordinates, and fails malformed/unrepresentable widget +geometry with explicit `BW_*` errors. `pdftool_core` is bumped to 0.2.0; WASM +still exposes exactly two functions. Rust CI passes, but owner review is pending. + +Please confirm whether this is consistent with the DocMatrix composition +contract, especially the distinction between PDF-defined corner interpretation +and prohibited silent coordinate transforms. Also advise whether the present +widget-listing result should remain explicitly non-interchange data until a +consumer requests a versioned contract, and which future identity/page/label +fields must be specified before that API is treated as stable. We are not +proposing a runtime engine, shared serializer, third WASM export, page transform, +or cross-repository code change. +---- === 2C. Separate display label from field identity @@ -565,18 +675,21 @@ visual-block, ruled-line, baseline, or per-character-cell detection. === Acceptance -* Each detection behavior is asserted in a direct Rust test; malformed geometry - and annotations cannot disappear silently without an explicit contract. -* Valid page coordinates are preserved under the agreed coordinate contract; - no unannounced `min`/`abs`, lossy cast, coordinate transform, or text rewrite - is introduced. -* The output either carries an explicitly versioned identity/coordinate shape - accepted by its consumer, or remains documented as a widget listing that is - not an interchange API. +* The 22 current test functions pass in a recorded Rust CI run; malformed + widget geometry fails explicitly, while PDF-null/dangling annotation entries + retain their previous absent-entry handling. +* PDF-permitted corner ordering is interpreted by sorting each pair of + diagonally opposite coordinates. No screen/page transform, arbitrary geometry + repair, lossy integer cast, or text rewrite is introduced. +* The output remains a widget listing, not a stable interchange API; its + numeric type/error changes are versioned in `pdftool_core` `0.2.0`. * WASM still has exactly two exports; fill remains name-driven; no existing - public `BW_*` code is renamed or repurposed, and every new code is covered. -* The four-code API expansion receives the compatibility/version review required - by repository guidance before merge. + public `BW_*` code is renamed or repurposed, and all three new codes are + covered by tests. +* DocMatrix/standards-owner geometry and error-compatibility review is recorded + before merge; the Rust tests do not substitute for this decision. +* Proof claims follow Mathesis/Standards policy. A test receipt is not a Grade 3 + theorem receipt; no formal proof is claimed until the mapped verifier runs. * `TEST-NEEDS.adoc` and other status docs reflect only tests that actually run. == Work package 3 — Formatrix Docs#61 and the A2ML question @@ -653,6 +766,19 @@ contains no unrelated template-project identity. Keep it external; do not edit `rsr-template-repo` or broaden Blocky Writer's template obligations here. Do not use draft `RSR-SPEC-v2` as if it were ratified. +=== Suggested prompt for RSR-template-repo maintainers + +[source,text] +---- +For issue #200, could you consider a narrow mint-regression test: generate a +child repository from the current template, then assert that its contributor +guide targets the minted child (not the template), every documented path and +command exists in that child, and no unrelated template-project identity +remains? Blocky Writer has already corrected its own CONTRIBUTING guide in +merged PR #73; this is only a template-side suggestion. We are not changing +rsr-template-repo or treating the draft RSR-SPEC-v2 as ratified. +---- + == Work package 5 — appearance generation follow-on FFP 1.0 recommends that the producer generate normal `/AP` streams because @@ -668,6 +794,78 @@ and a real-viewer/print-rasterizer verification matrix first. Do not fold that work into the XMP marker change, do not claim PDF/A suitability, and do not remove the hazard declaration until generated appearances are verified. +=== Suggested prompt for Presswerk maintainers + +[source,text] +---- +Following the D189 ruling on Presswerk #118, could you confirm the expected +consumer-side behavior for a Blocky Writer-produced FFP PDF that declares +`filledBy=machine` but has no generated `/AP` streams? Blocky Writer's first +producer increment will truthfully omit or set `appearancesGenerated=false`, +preserve existing XMP, and avoid claiming appearance generation. Please also +identify the supported test-only invocation/interface for a deterministic +producer-to-Presswerk P2P test and what classification/hazard assertions it +should make. The test should not add a Presswerk runtime dependency or create a +new integration engine. We will not edit Presswerk or reopen #118 without an +owner request. +---- + +== Work package 6 — Mathesis proof-trace handoff + +The estate mathematics architecture at Mathesis commit `50a012d` says original +repositories remain the sovereign owners of their proofs and Mathesis catalogs +exact proof states and commit-level receipts. Standards proof policy at commit +`46ee183` maps first-party Rust engineering to Creusot, API/ABI properties to +Idris2, and mathematical theory to Agda (rechecked in Rocq and Lean). Do not +invent a mathematical relationship or treat CI tests as a proof receipt. + +The phrase “Grade 3” is used precisely: under the Mathesis architecture it is +an Agda proof that type-checks locally but has not been translated to an +independent foundational system. `T3 — Standard` in Mathesis `PROOF-NEEDS` is a +separate proof-tier label. This repository currently has no formal proof +artifacts and no Grade 3 status. The Rust CI receipt is `TESTED`, not `PROVEN`. + +=== Suggested prompt for Mathesis maintainers + +[source,text] +---- +Please review how this repository should be represented in Mathesis under the +estate-mathematical-architecture and the current Standards proof policy. The +Blocky Writer PR adds PDF /Rect corner interpretation, indirect numeric +resolution, f64 geometry output, and explicit malformed-widget errors; its +Rust CI tests pass, but there are no formal proof artifacts. Please clarify: + +1. Which theorem or engineering obligations belong in Blocky Writer's local + proof-status records, and which are merely test evidence or dependency + assumptions (lopdf 0.34)? +2. Is the repository proof tier T3 — Standard, and which applicable proof + categories/archetype should its local proof-needs record use? +3. For the geometry theorem, is a Grade 3 Agda witness appropriate, while + first-party Rust behavior and the WASM API/ABI follow the Standards + Creusot/Idris2 mapping? What exact statement and assumptions should be + recorded, and what receipt format/hash should Mathesis index? + +Please keep source proofs in Blocky Writer; Mathesis should catalogue their +exact statuses and revisions rather than becoming a proof repository or a +runtime dependency. Do not mark this work Grade 3 until the prescribed Agda +check and receipt exist. +---- + +This is a proposed coordination prompt, not a posted issue or proof claim. + +=== Grade 3 completion gate + +The requested Grade 3 proofing is **not yet satisfied** by this branch. The code +has no Agda witness or Mathesis receipt, and `agda` is unavailable in the +sandbox. Before any Grade 3 claim, select an exact mathematical theorem, state +its assumptions and its correspondence to the PDF geometry behavior, produce a +locally type-checking Agda proof, and record its revision and checker receipt +for Mathesis to catalogue. Track any Standards-required Rocq/Lean rechecks as +separate evidence; they do not turn Rust CI into a proof. Until those receipts +exist, status remains `TESTED`/`UNPROVEN`, not Grade 3 or `PROVEN`. If Grade 3 +is a merge prerequisite, this PR must remain blocked on that proof work as well +as the current CI/review gates. + == Cross-repository references and ownership [cols="1,2,3",options="header"] @@ -690,6 +888,17 @@ remove the hazard declaration until generated appearances are verified. | Governs any future versioned interop, geometry fidelity, provenance, loss reporting, replay, and independent verification. +| https://github.com/hyperpolymath/standards/blob/46ee18323305d5b36e2eef936e47dd54c39ea736/3-practice/PROOF-POLICY.adoc[Standards proof policy] +| Normative verifier mapping, checked at `46ee183` (2026-11-01). +| Apply locally: Rust -> Creusot; API/ABI engineering -> Idris2; theory -> Agda, + with theory rechecks in Rocq and Lean. Do not edit the standards repo here. + +| https://github.com/hyperpolymath/mathesis/tree/50a012dba57fe90c9aca8d57241690c133ab117c[hyperpolymath/mathesis] +| Estate proof catalogue; architecture defines Grade 3; `PROOF-NEEDS` records + its separate T3 — Standard proof tier. +| Coordinate proof inventory/status only. The source repo owns proofs; no + Mathesis runtime dependency or external edit is proposed. + | https://github.com/hyperpolymath/presswerk/issues/118[Presswerk#118] and https://github.com/hyperpolymath/standards/tree/3108c1b01186741eea80c00da3f4beda912286f9/1-formats/sub-specs/form-fill-provenance[FFP] | #118 is closed after ruling D189; FFP 1.0.0 is active/ratified. Presswerk is @@ -704,25 +913,34 @@ remove the hazard declaration until generated appearances are verified. == Merge sequencing and stop conditions +PR #78 is open at implementation revision `9f9370e`. Rust CI run +`41326285595` passed, but Lock Sync `41326285660`, CodeQL `41326285683`, and +Hypatia `41326285619` fail; the CodeRabbit check was skipped and its formal +review on `0224ef6` still says `CHANGES_REQUESTED`. Do not merge or bypass these +gates. + [arabic] -. **First:** repair the Actions lock and re-establish trustworthy health - records. Do not stack feature work on an unexplained workflow-file failure. -. **Second:** get the standards owner answer for the seam contract/test path and - the A2ML/DEED status conflict. FFP marker implementation can be developed - independently, but do not declare the seam conformant until its P2P gate and - both Trustfile entries are real. -. **Third:** add detector tests and agree the geometry/identity contract before - changing normalization, page semantics, or label decoding. -. **Fourth:** carry out only the local producer and detector changes approved by - those gates, each in a scoped change set with tests and updated status docs. -. **Finally:** send the optional #61 and #200 follow-ups to their owners; do not +. **First:** repair the generated Actions lock with the prescribed tool and + re-establish trustworthy health records. Resolve the setup failures or obtain + owner-side evidence; do not attribute causes without logs. +. **Second:** obtain DocMatrix/standards owner review of the PDF geometry + interpretation, three new `BW_*` codes, and `pdftool_core` `0.2.0` change. + PDF-defined corner-pair interpretation is supported; page transforms, + identity, and label decoding remain separate contract decisions. +. **Third:** agree local proof scope with Standards and Mathesis. Apply the + verifier mapping; if a mathematical theorem is claimed at Grade 3, create and + run the local Agda proof and record its exact source revision/assumptions. + Rust tests alone cannot satisfy that status. Keep source proofs in this repo. +. **Fourth:** complete FFP producer behavior and the producer-to-Presswerk seam + only after the normative test path and both Trustfile entries are real. +. **Finally:** send the bounded #61 and #200 follow-ups to their owners; do not edit or close issues in those repositories as part of this plan. Stop and ask the relevant owner if any proposal requires a new runtime engine, -a new mandatory hub/serializer, a third WASM export, silent geometry repair, -lossy field-name normalization, a parser for an unspecified A2ML dialect, -weakening a CI gate, or changing another repository. Those outcomes are not -accepted by this plan. +a new mandatory hub/serializer, a third WASM export, a geometry transform beyond +PDF-defined semantics, lossy field-name normalization, a parser for an +unspecified A2ML dialect, weakening a CI gate, or changing another repository. +Those outcomes are not accepted by this plan. == Source index @@ -738,6 +956,12 @@ accepted by this plan. https://github.com/hyperpolymath/standards/blob/3108c1b01186741eea80c00da3f4beda912286f9/docs/SEAMS-SPEC.adoc[SEAMS-SPEC]. * FFP producer marker, print path, and vectors: https://github.com/hyperpolymath/standards/tree/3108c1b01186741eea80c00da3f4beda912286f9/1-formats/sub-specs/form-fill-provenance[FFP 1.0.0]. +* Standards proof policy: + https://github.com/hyperpolymath/standards/blob/46ee18323305d5b36e2eef936e47dd54c39ea736/3-practice/PROOF-POLICY.adoc[PROOF-POLICY.adoc]. +* Mathesis proof architecture and Grade 3 definition: + https://github.com/hyperpolymath/mathesis/blob/50a012dba57fe90c9aca8d57241690c133ab117c/estate-mathematical-architecture[estate-mathematical-architecture], + https://github.com/hyperpolymath/mathesis/blob/50a012dba57fe90c9aca8d57241690c133ab117c/docs/status/PROOF-NEEDS.adoc[PROOF-NEEDS.adoc], and + https://github.com/hyperpolymath/mathesis/blob/50a012dba57fe90c9aca8d57241690c133ab117c/docs/status/PROOF-STATUS.adoc[PROOF-STATUS.adoc]. * A2ML/DEED status evidence: https://github.com/hyperpolymath/standards/blob/3108c1b01186741eea80c00da3f4beda912286f9/1-formats/a2ml/state/README.adoc[STATE supersession warning], https://github.com/hyperpolymath/standards/blob/3108c1b01186741eea80c00da3f4beda912286f9/1-formats/a2ml/meta/README.adoc[META supersession warning], and diff --git a/rust/pdftool_core/README.adoc b/rust/pdftool_core/README.adoc index d579ab1..b6805a5 100644 --- a/rust/pdftool_core/README.adoc +++ b/rust/pdftool_core/README.adoc @@ -5,16 +5,28 @@ Rust/WASM core for `blocky-writer`. == Implemented * `detect_blocks` parses PDF AcroForm widget annotations and returns their - labels and rectangles. It does not perform OCR or general-purpose visual - block detection. + labels and rectangles. PDF `/Rect` corner pairs may be supplied in either + order; chained indirect arrays and indirect coordinate values are resolved + with cycle protection; coordinates are returned in PDF user space as `f64`. + Missing, unresolved, + malformed, non-finite, or unrepresentable widget rectangles fail detection + with `BW_PDF_WIDGET_RECT_INVALID`. Null or dangling optional `/Annots` and + annotation entries are treated as absent; malformed non-null entries fail + with structured errors. This is not OCR or general-purpose visual block + detection, and no page-rotation or screen-coordinate transform is applied. * `fill_blocks` updates matching AcroForm field values and returns serialized PDF bytes. It supports the field types and validation covered by the unit tests; it is not a PDF renderer. * Synthetic-PDF unit tests in `src/lib.rs` cover representative widget detection, rectangle/annotation validation, and selected `Tx` / `Ch` / `Btn` writeback - paths. They are not general PDF conformance tests or real-world corpus - coverage. The detector additions in the current session branch still require - formatting, compilation, tests, and Clippy before they can be called verified. + paths. The 22 source tests include 6 fill/error tests, 14 detector tests, and + 2 direct numeric-geometry tests. Rust CI run `41326285595` at implementation + commit `9f9370e` passed formatting, tests, and Clippy. Chained indirect + references are cycle-protected. These are not general + PDF conformance tests or real-world corpus coverage. +* `pdftool_core` is version `0.2.0`; the `Block` numeric representation and + three new public `BW_*` error codes are versioned as a breaking change. + Compatibility review of the error contract remains part of PR #78. == Native development checks diff --git a/wiki/Architecture.md b/wiki/Architecture.md index 436b43f..812d1f7 100644 --- a/wiki/Architecture.md +++ b/wiki/Architecture.md @@ -49,7 +49,7 @@ SPDX-License-Identifier: MPL-2.0 | Layer | Reality | | --- | --- | -| Rust/WASM core | **Implemented and tested.** `rust/pdftool_core/src/lib.rs`, ~1030 lines, 6 unit tests. | +| Rust/WASM core | **Implemented and tested in CI.** `rust/pdftool_core/src/lib.rs` contains 22 unit-test functions (6 fill/error, 14 detector, 2 numeric-geometry); PR #78 revision `9f9370e` passed the Rust CI job in run `41326285595`. | | AffineScript surfaces | **Prototype.** `src/*.affine` sources exist; no compiler config, no bundle pipeline. | | Extension packaging | **Absent.** `public/manifest.json` and icons are present; nothing produces a loadable `.xpi`. | | Storage | **Prototype.** `src/core/Storage.affine` only. | @@ -65,10 +65,18 @@ functions. That boundary is the whole contract: -> Result ``` -* `detect_blocks` walks every page's `Annots`, keeps the ones that are widget - annotations with a usable `Rect`, and returns a `Block { label, x, y, width, - height }` per widget. Labels come from the field's `/T`, falling back to the - parent field's `/T`, falling back to `field__`. +* `detect_blocks` walks every page's `Annots` and returns a `Block { label, x, + y, width, height }` per widget with usable geometry. `/Rect` accepts its two + diagonal corners in either PDF-permitted order and resolves chained indirect + arrays and numeric coordinate objects with cycle protection; output + coordinates are PDF user-space `f64` + values, with no screen or page-rotation transform. Missing, unresolved, + malformed, non-finite, or unrepresentable widget rectangles fail detection + with `BW_PDF_WIDGET_RECT_INVALID` rather than being silently skipped. A null + or dangling optional `/Annots` entry is treated as absent; other malformed + non-null annotation objects fail with a structured error. Labels come from + the field's `/T`, falling back to the parent field's `/T`, falling back to + `field__`. * `fill_blocks` takes the original PDF bytes plus a `field name → value` map, writes the values into the AcroForm, and returns new PDF bytes. It is **name-driven, not coordinate-driven** — the `blocks` argument is parsed and @@ -87,8 +95,10 @@ with a stable machine code, a human message, and optional context: { "code": "BW_FILL_NO_MATCHING_FIELDS", "message": "…", "context": "Choice" } ``` -There are 24 unique `BW_*` codes in the current source (20 baseline codes plus -four detector-validation codes). Error codes are external API; repository -guidance treats additions and renames as breaking changes, so this expansion -needs compatibility/version review before merge. The four additions also remain -unverified until the current Rust diff passes formatting, tests, and Clippy. +There are 23 unique `BW_*` codes in the current source (20 baseline codes plus +three detector-validation codes). Error codes are external API; repository +guidance treats additions and renames as breaking changes. The core is bumped +to `0.2.0` for this contract change; owner compatibility review is still open. +Rust CI run `41326285595` passed formatting, all 22 tests, and Clippy at +implementation commit `9f9370e`. That run does not establish general PDF +conformance or a formal proof. diff --git a/wiki/CI-and-Gates.md b/wiki/CI-and-Gates.md index aae0534..7eebb62 100644 --- a/wiki/CI-and-Gates.md +++ b/wiki/CI-and-Gates.md @@ -63,17 +63,26 @@ only job in that workflow: ## Known problems — read before you trust a green -* **Current main CI state (2026-11-01).** Rust CI passed on `3fd5ba1` in run - `38818494415`, but Governance (`38818495000`) and Lock Sync Gate - (`38818494312`) failed at generated-lock verification. The workflow uses - `hyperpolymath/smtp-notify-action@v0.5.0`; the generated lock still records - v0.3.0. Do not hand-edit `actions.lock`; use the prescribed `gh actions-lock` - generator, then verify the result. The current Arena feature branch has not - been compiled or tested. -* **Other current failures.** Scheduled CodeQL run `41099036814` and Hypatia - run `41255373342` failed during job setup; mirror run `38818495057` had four - failing destinations. Logs were unavailable during the review, so causes are - unknown. See `docs/ci/CHECK-DETERMINATIONS.adoc` for step-level evidence. +* **Rust CI evidence (2026-11-01).** Main passed on `3fd5ba1` in run + `38818494415`. PR #78 implementation commit `9f9370e` passed formatting, all + 22 unit tests, and Clippy in run `41326285595`. This sandbox has no Rust + toolchain; these are remote CI receipts, not local runs. +* **Current PR #78 blockers.** Lock Sync Gate run `41326285660` fails + `Verify lockfile synchronisation`: workflow YAML uses + `hyperpolymath/smtp-notify-action@v0.5.0`, while generated `actions.lock` + records `v0.3.0`. Do not hand-edit the lock; use the prescribed generator. + CodeQL run `41326285683` and Hypatia run `41326285619` fail at `Set up job`; + causes are unknown. GitGuardian, SonarCloud and Semgrep passed, including + Semgrep scan `258901955`. CodeRabbit was skipped; the prior + `CHANGES_REQUESTED` review has not been refreshed. Governance and Secret + Scanner runs `41325016132` and `41325016140` previously failed before creating + jobs with a generic workflow-file issue; causes are unconfirmed. See + `docs/ci/CHECK-DETERMINATIONS.adoc` for exact evidence. +* **Other failures on current `main`.** Main Governance and Lock Sync Gate runs + `38818495000` and `38818494312` failed on the same visible lock drift; + scheduled CodeQL (`41099036814`), Hypatia (`41255373342`), and mirror + destinations (`38818495057`) also failed. Causes for setup/mirror failures + remain unknown. * **Two Pages workflows.** `pages.yml` and `casket-pages.yml` both build and deploy to the `github-pages` environment under the same `pages` concurrency group, so they cancel each other. `pages.yml` also looks for `README.md`, diff --git a/wiki/Contributing--Dev-Setup.md b/wiki/Contributing--Dev-Setup.md index 1b82242..e42f877 100644 --- a/wiki/Contributing--Dev-Setup.md +++ b/wiki/Contributing--Dev-Setup.md @@ -53,11 +53,11 @@ cargo test --manifest-path rust/pdftool_core/Cargo.toml --locked cargo clippy --manifest-path rust/pdftool_core/Cargo.toml --locked --all-targets -- -D warnings ``` -The current working source contains 17 test functions: six existing fill/error -tests and eleven direct detector tests added in this branch. Those additions -have not been compiled or run in this environment because the Rust toolchain is -absent. Keep this count synchronized with `TEST-NEEDS.adoc`; CI, not the source -count alone, determines whether they pass. +The current source contains 22 test functions: six fill/error tests, fourteen +detector tests and two numeric-geometry tests. PR #78 implementation commit +`9f9370e` passed formatting, tests and Clippy in run `41326285595`; this +sandbox still has no Rust toolchain. Keep this count synchronized with +`TEST-NEEDS.adoc`. Tests are not a formal proof or a real-world PDF corpus. ## Building the WASM package (optional) diff --git a/wiki/Glossary.md b/wiki/Glossary.md index c7769d2..db47082 100644 --- a/wiki/Glossary.md +++ b/wiki/Glossary.md @@ -39,8 +39,9 @@ SPDX-License-Identifier: MPL-2.0 [berrywiki](https://github.com/metadatastician/berrywiki). **Block** -: The struct blocky-writer exports: `{ label, x, y, width, height }`. One per - detected widget annotation. Coordinates are PDF user-space, `f32`. +: The struct blocky-writer returns for each detected widget annotation: + `{ label, x, y, width, height }`. Coordinates are PDF user-space `f64` values; + no screen or page-rotation transform is applied. **CLADE** : A taxonomy declaration in `.machine_readable/CLADE.a2ml`, registered with @@ -78,5 +79,8 @@ SPDX-License-Identifier: MPL-2.0 Only the run's web-page annotation distinguishes them. **Widget annotation** -: An annotation with `/Subtype /Widget`. `detect_blocks` keeps only these, and - only when they carry a usable `/Rect`. +: An annotation with `/Subtype /Widget`. `detect_blocks` requires a valid + `/Rect`; missing, unresolved, malformed, non-finite, or unrepresentable widget + geometry fails the detection call with `BW_PDF_WIDGET_RECT_INVALID`. Null or + dangling `/Annots` entries are treated as absent, while malformed non-null + annotations fail with a structured error. diff --git a/wiki/Home.md b/wiki/Home.md index e90eded..4403113 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -38,16 +38,17 @@ is versioned, diffable and reviewable like any other file. See ## Current status in one paragraph -The Rust core's baseline on `main` passed `cargo fmt`, `cargo test --locked`, -and `cargo clippy -D warnings` in run `38818494415` at `3fd5ba1`. The current -session branch adds detector validation and tests, but those edits have not been -compiled or run here because the Rust toolchain is absent. The -browser-extension frontend is a **prototype**: this checkout has no -AffineScript compiler configuration, no JavaScript package manifest and no -reproducible extension bundle pipeline, so there is nothing to build or release -yet. Do not read the completion percentages in `TOPOLOGY.adoc` as a statement -about the frontend — see [CI-and-Gates](CI-and-Gates) for the current honest -picture. +The Rust core's `main` baseline passed formatting, tests and Clippy in run +`38818494415` at `3fd5ba1`. PR #78 implementation commit `9f9370e` also passed +all three Rust checks in run `41326285595` (22 tests). Other PR gates remain +blocked: the generated Actions lock is out of sync, and CodeQL/Hypatia fail +before analysis starts. This sandbox still has no Rust toolchain, so the run +receipts are remote CI evidence. The browser-extension frontend is a +**prototype**: this checkout has no AffineScript compiler configuration, no +JavaScript package manifest and no reproducible extension bundle pipeline, so +there is nothing to build or release yet. Do not read the completion percentages +in `TOPOLOGY.adoc` as a statement about the frontend — see +[CI-and-Gates](CI-and-Gates) for the current honest picture. ## Quick commands diff --git a/wiki/Rust-Core.md b/wiki/Rust-Core.md index d8a87f3..474d964 100644 --- a/wiki/Rust-Core.md +++ b/wiki/Rust-Core.md @@ -25,8 +25,10 @@ SPDX-License-Identifier: MPL-2.0 | Crate types | `cdylib`, `rlib` | | Licence | MPL-2.0 | | `#![forbid(unsafe_code)]` | yes | -| Lines (`src/lib.rs`) | ~1030 | -| Unit tests | 6 | +| Lines (`src/lib.rs`) | See current source | +| Unit tests | 22 (6 fill/error, 14 detector, 2 numeric-geometry) | +| Current crate version | 0.2.0 | +| PR Rust CI | Run `41326285595` at `9f9370e` passed fmt, tests, and Clippy | ## Dependencies @@ -45,14 +47,28 @@ Nothing else. `Cargo.lock` is committed and CI runs with `--locked`. Two `#[wasm_bindgen]` exports, plus the `Block` struct they share: ```rust -pub struct Block { pub label: String, pub x: f32, pub y: f32, - pub width: f32, pub height: f32 } +pub struct Block { pub label: String, pub x: f64, pub y: f64, + pub width: f64, pub height: f64 } ``` Everything else in the crate is private. `resolve_object`, `object_to_number`, `rect_from_object`, `object_to_text`, `dict_text`, the field-descriptor walkers and `apply_field_value` are internal machinery. +## Geometry and error contract + +`/Rect` supplies two diagonal corners in PDF user space; PDF permits either +corner order. Detection maps them to `{x, y, width, height}` using the PDF-defined +interpretation and does not apply screen, rotation, or page-box transforms. +Chained indirect `/Rect` arrays and numeric elements are resolved with +reference-cycle protection. The output uses `f64` to preserve exactly +representable PDF integer coordinates through +binary64. Missing, unresolved, malformed, non-finite, or unrepresentable widget +geometry fails detection with `BW_PDF_WIDGET_RECT_INVALID`. Null or dangling +optional `/Annots` entries are treated as absent; malformed non-null entries +fail with structured codes. The PR's three new error codes and numeric surface +are included in crate version `0.2.0`. + ## What `fill_blocks` does, in order 1. Reject empty input (`BW_PDF_EMPTY`).