Skip to content

feat(print): record machine-filled form provenance in the print path (#118) - #123

Merged
hyperpolymath merged 2 commits into
mainfrom
arena/01a1046d-presswerk
Oct 4, 2026
Merged

hyperpolymath merged 2 commits into
mainfrom
arena/01a1046d-presswerk

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

What this closes

Closes #118 — blocky-writer: a filled application form is a print job — should it be distinguishable?

Ruling D189 (2026-09-30): record machine-filled provenance as metadata in the print path so audit/routing can distinguish it. This implements the ruling on both print paths, and records Presswerk's position in the ecosystem docs.

Position

Yes — and it is the print path's job to record it. Presswerk sits at the last step where the document is still a document: it keeps the audit trail and it routes jobs. So it records provenance on entry, whether or not upstream said anything.

Three commitments the implementation follows:

  1. Recording is unconditional — every PDF entering either print path is classified.
  2. A determination is never a guess presented as a fact — every result carries a confidence and its evidence; anything that stops inspection is recorded as inspected: false, never as "hand-filled".
  3. A determination alone never blocks a printout — the default policy (Record) changes nothing for the user. Only an explicit opt-in policy holds a job, and only on a positive machine determination.

And the part the issue cannot answer from here: this repository is the right place for the consuming half, not the authoritative half. Only blocky-writer knows it filled the form. Until it emits a marker, everything downstream is inference, and the code says so via ProvenanceConfidence rather than pretending otherwise. The ask of upstream is in the docs: two /Fill* entries in /Info, no change to /V, /DV or /AS, turns every downstream determination from Probable into Explicit.

What changed

Determination — new crates/presswerk-document/src/pdf/form.rs:

Signal Confidence
Explicit /FillOrigin marker (/Info or fillOrigin in raw bytes / uncompressed XMP) Explicit
/Producer or /Creator names a known form-filling tool Strong
Values present + /NeedAppearances set Probable
Values present with no /AP in the field's subtree Probable
Values present alongside /DV rewrites Speculative
Values + appearances + no signal Probable (human)
/AcroForm with no values / no /AcroForm Strong (empty / not a form)

Bounded: 8 MiB inspection budget, 100k-node and depth-32 field-tree walk, /Off treated as unselected.

Domain types — FormOrigin, ProvenanceConfidence, FormProvenance, FormProvenancePolicy in presswerk-core; AppConfig::form_provenance_policy (#[serde(default)], so pre-existing config.json files still load).

Persistence — form_origin (token, filterable in SQL) and form_provenance (full JSON) columns on the job queue, migrated in place, plus JobQueue::get_jobs_with_form_origin.

Both print paths — AppServices::print_document (local) and handle_print_job (network IPP). The server previously had no access to the audit trail at all, so start_with_audit was added; start delegates to it unchanged.

Routing — HoldForReview parks a machine-filled job in Held (locally and over IPP); the jobs page gains a Release action backed by AppServices::release_job, which keeps the job's identity and provenance. A held job's bytes are persisted so it survives until release; documents that print immediately are still never written to disk.

UI — provenance badge on the jobs page, plain-language summary for form_provenance audit entries, policy selector in Settings.

Bug fixed along the way — a Print-Job response hard-coded job-state = 3 (pending); it now reports the job's real state, so a held job is reported as 4 (held).

Architecture note

presswerk-print now depends on presswerk-document — a deliberate change to the documented crate graph (README, 0-AI-MANIFEST.a2ml). A job received over IPP never passes through the application layer, so the server has to classify it itself. No cycle: presswerk-document depends only on presswerk-core.

Verification — please read

I could not run cargo test, cargo clippy or cargo fmt --check in the sandbox I worked in. There is no Rust toolchain there and none is installable: static.rust-lang.org and crates.io are both unreachable (verified with curl), so neither rustup nor a dependency fetch can complete. This needs a CI run before merge.

What I was able to check mechanically:

  • Rust syntax — all 65 .rs files parsed with the tree-sitter Rust grammar: HEAD=0 WORKTREE=0 errors. (The grammar handles this codebase's let-chains and rsx! macros cleanly at HEAD, so the zero is meaningful rather than vacuous.)
  • The SQL — extracted CREATE_TABLE_SQL, MIGRATE_FORM_PROVENANCE_SQL, the 19-column INSERT and all three SELECTs from queue.rs and ran them against a real SQLite engine: fresh schema has 19 columns, round-trips both new ones, and a legacy 17-column database with an existing row migrates to 19 columns and reads back as 'Unknown' / '{}' — i.e. "not inspected", which is what the new tests assert. Re-running the migration raises duplicate column name, which the migration loop swallows by design.
  • The lopdf 0.40.0 API — fetched the crate source from GitHub and checked every symbol I used against it: pub trailer: Dictionary, Dictionary::{get,get_mut,set,iter,new}, set<K: Into<Vec<u8>>, V: Into<Object>>, Document::{with_version,add_object,get_object,get_object_mut,catalog,save_to,load_mem}, Stream::{new,dict,content}, StringFormat::Literal, and the Object variant shapes.
  • Manifests — all three changed Cargo.toml files parse as TOML; presswerk-document is a workspace dependency.
  • Struct literals — every PrintJob, SharedState and FormProvenance literal in the tree carries the new fields.

50 #[test] functions were added (188 → 238 in the workspace, static count), covering the classifier against real serialised PDFs built with lopdf, the queue round-trip and migration, and the IPP path end-to-end including the audit entries and the hold policy. They have never been executed.

Unrelated pre-existing drift I did not touch: 0-AI-MANIFEST.a2ml says "68 tests expected" for the four library crates; the static #[test] count there was already 187 before this PR.

A filled application form is a print job more often than it is anything
else, and blocky-writer's fill_blocks emits ordinary PDF bytes with no
marker saying who wrote the values (#118). Ruling D189 settled it:
record machine-filled provenance as metadata in the print path so audit
and routing can distinguish it. This does that, on both print paths.

Determination (presswerk-document::pdf::form)
  Reads /AcroForm field values, /NeedAppearances, values with no /AP in
  their subtree, /DV rewrites, /Producer and /Creator, and an explicit
  /FillOrigin marker in the document /Info dictionary (or a fillOrigin
  attribute in an uncompressed XMP packet). Every result carries a
  confidence and the evidence for it; anything that stops inspection is
  recorded as "not inspected" rather than guessed at.

Recording
  FormProvenance is stored on every PrintJob, persisted in two new job
  queue columns (migrated in place for existing databases) and written
  to the audit trail as form_provenance. Network-received IPP jobs are
  audited too: the server now takes an audit log at start-up, which it
  previously had no access to.

Routing
  FormProvenancePolicy defaults to Record, which changes nothing for the
  user. HoldForReview additionally parks a positively machine-filled job
  in Held — locally and over IPP — and the jobs page gains a Release
  action that sends it on, keeping the job's identity and provenance.
  An undetermined document is never held.

Also: a Print-Job response now reports the job's real job-state, so a
held job is no longer reported to the client as pending.

presswerk-print now depends on presswerk-document. This is a deliberate
change to the documented crate graph: a job received over IPP never
passes through the application layer, so the server must classify it
itself. There is no cycle.

Position, marker convention and known limitations are recorded in
docs/ecosystem/FORM-PROVENANCE.adoc, including the ask of upstream: two
/Fill* entries in /Info turn every downstream determination from
Probable into Explicit.

Closes #118

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 5e1261cc-7355-4ddd-928a-4646e78e0e00
📥 Commits

Reviewing files that changed from the base of the PR and between 5c1f008 and d29bf2c.

📒 Files selected for processing (21)
  • .machine_readable/6a2/ECOSYSTEM.a2ml
  • 0-AI-MANIFEST.a2ml
  • CHANGELOG.adoc
  • README.adoc
  • crates/presswerk-app/src/pages/audit.rs
  • crates/presswerk-app/src/pages/jobs.rs
  • crates/presswerk-app/src/pages/settings.rs
  • crates/presswerk-app/src/services/app_services.rs
  • crates/presswerk-core/src/config.rs
  • crates/presswerk-core/src/error.rs
  • crates/presswerk-core/src/human_errors.rs
  • crates/presswerk-core/src/types.rs
  • crates/presswerk-document/Cargo.toml
  • crates/presswerk-document/src/lib.rs
  • crates/presswerk-document/src/pdf/form.rs
  • crates/presswerk-document/src/pdf/mod.rs
  • crates/presswerk-document/src/pdf/reader.rs
  • crates/presswerk-print/Cargo.toml
  • crates/presswerk-print/src/ipp_server.rs
  • crates/presswerk-print/src/queue.rs
  • docs/ecosystem/FORM-PROVENANCE.adoc
 __________________________________________________
< I ran the tests. They filed a restraining order. >
 --------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

`dtolnay/rust-toolchain@v1` takes a rustup toolchain specification
(channel or version). It was being passed `v1`, which is not one, so the
step failed on every run — on main as well as on every branch — and
Check, Test, Clippy and Format check were all skipped. This repository's
CI has not compiled the workspace since that input was written.

Separate commit from the feature work so it can be reverted on its own.
The action references are unchanged, so actions.lock still matches.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
@hyperpolymath
hyperpolymath enabled auto-merge (squash) October 4, 2026 01:37
@hyperpolymath
hyperpolymath disabled auto-merge October 4, 2026 01:38
@hyperpolymath
hyperpolymath merged commit 1f49179 into main Oct 4, 2026
34 of 37 checks passed
@hyperpolymath
hyperpolymath deleted the arena/01a1046d-presswerk branch October 4, 2026 01:38
@coderabbitai

coderabbitai Bot commented Oct 4, 2026

Copy link
Copy Markdown

Autopilot could not be updated. Open Coding to check access and billing.

hyperpolymath added a commit that referenced this pull request Oct 4, 2026
… then fix the check failure (#124)

## Follow-up to #123: make CI actually report the compile error, then
fix it

#123 was merged, and merging it exposed two things that were previously
invisible:

1. `.github/workflows/ci.yml` asked `dtolnay/rust-toolchain@v1` for a
toolchain
literally named `v1`. The step failed on **every** run — including
`main` —
before a single `cargo` command ran, so `cargo check`, `cargo test`,
`clippy`
and `cargo fmt --check` never executed anywhere. Fixed to `stable`
(carried in
   from #123).
2. With the toolchain actually installing, `cargo check --workspace` now
**fails
with exit code 101**. That is a real compile error in the code merged
from
#123, and the sandbox this work was produced in has no Rust toolchain
and no
   crates.io access, so CI is the only compiler available.

GitHub Actions logs are not readable from that sandbox (the log and blob
hosts are
unreachable), so the one commit on this branch temporarily replaces the
four cargo
steps with a single step that tees each command's output and republishes
the first
error block of each as an `::error` **annotation** — the one channel
that is
readable.

That commit is explicitly temporary and will be reverted here once it
has done its
job. The follow-up commits on this branch are the actual compile fixes.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

blocky-writer: a filled application form is a print job — should it be distinguishable?

1 participant