Skip to content

feat(ffp): classify and record machine-filled form provenance in the print path (D189) #122

Description

@hyperpolymath

Why

Ruling D189 (2026-09-30, scope: repo, applied-unless-struck) answers presswerk#118:

Yes: record machine-filled provenance as metadata in the print path so audit/routing can distinguish it.

A filled application form is a print job, and today the print path cannot tell one that arrived already filled by a machine from one being printed blank to be completed by hand. Nothing in the queue or in the audit trail records which it saw, so neither routing nor audit can act on it afterwards.

The cross-repository contract is now written down as FFP v1.0.0 (Form-Fill Provenance), standards PR #1142:

  • spec: 1-formats/sub-specs/form-fill-provenance/ once merged; branch-readable at arena/01a1046c-standards
  • the part that binds this repo: FFP/3 — The print path (obligations P1–P7)
  • the detection rules: FFP/2 — Detection and classification
  • the acceptance oracle: spec/conformance/ — 14 vectors + a reference probe + a runner

What to build

Classify every document that enters the print path and record the result on the job and in the audit trail. No rendering. No mutation of the document bytes.

Acceptance criteria

  1. Detector — presswerk-document::provenance::classify(&[u8]) -> FormProvenance implements FFP/2, never panics, never mutates the input.
  2. Conformance — FFP_DETECTOR=<binary> bash 1-formats/sub-specs/form-fill-provenance/spec/conformance/run-conformance.sh from a standards checkout: 14/14, including the negative controls (blank-form-need-appearances → blank-form; viewer-filled → filled-unknown, not "hand"; unreadable → unreadable, never blank-form).
  3. Recorded with the job — the JSON record (FFP/2 §The record) persists in the job's own metadata, so a later reader can distinguish machine-filled from machine-filled-suspected from blank-form without re-parsing the document (filled_fields/total_fields and the evidence codes included).
  4. Recorded in the audit trail — the submission audit entry carries the classification (and evidence), so the trail can answer "did we already know it was machine-filled?" after the print.
  5. Surfaced (P4) — the jobs list/detail distinguishes blank-form / machine-filled / machine-filled-suspected without opening the document.
  6. Blank-print hazard (P5) — when appearances == incomplete and filled_fields > 0, the job records the hazard and the detail surface shows it. This is a real failure mode: NeedAppearances is deprecated by ISO 32000-2 (PDF 2.0), and viewers/rasterisers that ignore it print those fields empty.
  7. Network intake — jobs received by the embedded IPP server (and any other inbound path) are classified at receipt, not only local file picks.
  8. No silent normalisation (P3) — nothing in this change generates appearances, rewrites /V, or strips NeedAppearances. A repair, if ever offered, is explicit and user-visible.

Integration checklist (this repo)

File Change
crates/presswerk-core/src/provenance.rs new — the FFP record types + serde wire vocabulary (ffp = "1.0")
crates/presswerk-core/src/lib.rs pub mod provenance; + re-export
crates/presswerk-document/src/provenance.rs new — the detector (FFP/2)
crates/presswerk-document/src/lib.rs pub mod provenance;
crates/presswerk-print/src/queue.rs new form_provenance TEXT column + migration (the existing MIGRATE_* style), insert + all three SELECTs + row_to_print_job
crates/presswerk-app/src/services/app_services.rs classify in print_document (PDF only) before insert; put the record in the audit details
crates/presswerk-app/src/pages/jobs.rs badge/label from FormProvenance::label() + hazard marker
crates/presswerk-print/src/ipp_server.rs same classification at receipt for inbound jobs
crates/presswerk-document/examples/ffp-classify.rs (or a --bin) the thin CLI the conformance runner drives: arg → path → prints the canonical line

A draft of the core half

Posted as a follow-up comment: presswerk-core types + the presswerk-document detector with 8 unit tests covering the vector shapes. It has not been compiled — this was written on a machine with no Rust toolchain and no crates.io access, verified only against lopdf 0.40's source (fetched from the pinned tag) and the idioms already in presswerk-document/src/pdf/reader.rs. Treat it as a draft to make compile and review, not as merge-ready. The parts that matter and are verified are the semantics: the 14 vectors.

Notes for the reviewer

  • machine-filled-suspected is a first-class outcome, not a fallback: blocky-writer's current output (values + NeedAppearances, no appearance streams) is exactly that shape, and calling it plain machine-filled would be a claim the bytes do not support.
  • The detector deliberately has no "hand-filled" output. /AP appearances can be written by a program as easily as by an editor, so the honest answer for that shape is filled-unknown (FFP/2 §honesty rule).
  • Downstream: blocky-writer gets the producer-side marker as a separate issue; until it lands, its output classifies as machine-filled-suspected.

Ref: hyperpolymath/standards#1142, #118

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew capability or improvement to existing behaviour

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions