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 632b7c6..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 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 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. @@ -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 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` 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 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 }`; 20 `BW_*` codes. +| `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 761b2a3..1b78a3d 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,25 @@ 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 |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 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 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 beb35f1..07618c1 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 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% detect_blocks + fill_blocks, 6 tests green + 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 ██████████ 100% 20 stable BW_* codes + 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% 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% 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% Rust core ready; 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 ca3fee7..beeb321 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,156 @@ 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` and PR #78 implementation revision `9f9370e`* +| 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. 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` -| *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` and PR #78* +| 2026-11-01 +| The fix for #67 was valid at the time, but the lock drifted again after #74. + 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. +|=== + +== Current `main` run snapshot (2026-11-01) + +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"] +|=== +| Check / workflow | Run | Result | Evidence and current disposition + +| Rust core (`CI`) +| `38818494415` +| Passed +| Push on `3fd5ba1`, actor `hyperpolymath`; formatting, tests, and Clippy all + succeeded. This is the main baseline; PR #78's separate Rust receipt is + `41326285595` at `9f9370e`. + +| 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`. |=== -== Also red on `main`? +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 -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..7d3f957 --- /dev/null +++ b/docs/plans/2026-10-10-ultraplan-formatrix-ffp-geometry.adoc @@ -0,0 +1,986 @@ +// 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 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; 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. +. 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. Mathesis may catalogue proof receipts; the +original repository remains the owner of its proofs and engineering contracts. + +== Current implementation status (2026-11-01) + +[cols="1,3",options="header"] +|=== +| Work area | Current state on `arena/d32cfaf0-blocky-writer` + +| Detector geometry +| 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 + 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); 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`, `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, SEAMS-SPEC gates, and Mathesis/Standards proof-policy + coordination remain open as recorded below. +|=== + +== Recon snapshot and limits + +[cols="1,3",options="header"] +|=== +| Item | Snapshot used + +| Blocky Writer recon base +| Fork point `c2b073eaf5e204f466adc0797484478ce94ef95b`; this is the pre-change + base, not the implementation revision. + +| 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). + +| Formatrix Docs +| `hyperpolymath/formatrix-docs` at + `85e6692aaa544c0b2b7b41f156c11f3de25d95fc` (2026-10-09). + +| Presswerk +| `hyperpolymath/presswerk` at + `f17620192b69ccffb9454cd49aac8d5ec9454c48` (2026-10-05). +|=== + +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 + +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 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. 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 + `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. 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. + +| 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. 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`; 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 + 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 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` +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) 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 + 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. +. 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 + 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. + +=== 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 + 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 + +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 + +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 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 + 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 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 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. + +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 + +`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 + +* 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 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 + +=== 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. + +=== 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 +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. + +=== 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"] +|=== +| 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/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 + 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 + +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 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, 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 + +* 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]. +* 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 + 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/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/README.adoc b/rust/pdftool_core/README.adoc index 1d33ae9..b6805a5 100644 --- a/rust/pdftool_core/README.adoc +++ b/rust/pdftool_core/README.adoc @@ -5,13 +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. -* 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. 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/rust/pdftool_core/src/lib.rs b/rust/pdftool_core/src/lib.rs index c6ceba9..f99e7ca 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,15 +53,74 @@ 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), +fn resolve_object_chain(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(); + } + 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, } } -fn rect_from_object(obj: &Object) -> Option<(f32, f32, f32, f32)> { +/// 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() + .all(|coordinate| coordinate.is_finite()) + { + return None; + } + + // 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; + } + + 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; }; @@ -70,16 +129,12 @@ fn rect_from_object(obj: &Object) -> Option<(f32, f32, f32, f32)> { 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])?; - - 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)) + 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 { @@ -578,6 +633,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")); @@ -615,17 +678,37 @@ fn detect_blocks_impl(pdf_data: &[u8]) -> CoreResult> { let annots = match resolve_object(&doc, annots_obj) { Ok(Object::Array(arr)) => arr, - Ok(_) | Err(_) => continue, + // 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", + Some(format!("page {}", page_number)), + )); + } }; for (annot_index, annot_ref) in annots.iter().enumerate() { let annot_obj = match resolve_object(&doc, annot_ref) { + Ok(Object::Null) | Err(_) => continue, Ok(obj) => obj, - Err(_) => continue, }; let widget = match annot_obj { Object::Dictionary(dict) => dict, - _ => continue, + Object::Null => 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 +716,39 @@ 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_chain(&doc, rect_obj).ok_or_else(|| { + core_error_with_context( + "BW_PDF_WIDGET_RECT_INVALID", + "could not resolve widget Rect or encountered an indirect-reference cycle", + Some(format!( + "page {} annotation {}", + page_number, + annot_index + 1 + )), + ) + })?; + 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 finite, exactly representable numbers with finite extents", + 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 +906,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 +915,239 @@ 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_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 + .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 + .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 +1289,241 @@ 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 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] + 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())); + 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_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 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_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] + 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 object_to_number_preserves_f64_integer_boundaries() { + let mut 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 + ); + + 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); + } + + #[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_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", + ); + + 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] + 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::Null, Object::Reference(missing_id)]), + ); + assert!(detect_blocks_impl(&pdf) + .expect("null and unresolved annotation entries are absent") + .is_empty()); + } + + #[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..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,5 +95,10 @@ 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 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 08f599c..7eebb62 100644 --- a/wiki/CI-and-Gates.md +++ b/wiki/CI-and-Gates.md @@ -63,6 +63,26 @@ only job in that workflow: ## Known problems — read before you trust a green +* **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 3ca9620..e42f877 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 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 4ee86e8..4403113 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -38,14 +38,17 @@ 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 -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`).