From b5d21ca564922210f84c77f1932fa650abf2d0ca Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 4 Oct 2026 22:25:56 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(ci):=20restore=20green=20build=20?= =?UTF-8?q?=E2=80=94=20printpdf=200.9=20migration,=20clippy,=20lockfile=20?= =?UTF-8?q?drift?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #127 (which resolved #122): the first real CI compile of the merged FFP stack surfaced three blocking problems. 1. presswerk-document failed to compile: writer.rs still used the printpdf 0.8 ops `SetFontSizeBuiltinFont` / `WriteTextBuiltinFont`, removed upstream when printpdf was bumped to 0.9.1 (#20, May 2026). The breakage was latent for months because presswerk-core failed to compile first; fixing core in #125/#127 unmasked it. Migrate to the 1:1 PDF operators `Op::SetFont` (Tf) / `Op::ShowText` (Tj) with `PdfFontHandle::Builtin`, the replacement documented in printpdf 0.9. 2. clippy `manual_map` (deny-warnings) in the FFP legacy→v1 bridge in presswerk-core/src/provenance.rs — rewrite the `else if let Some(p) = &legacy.producer { Some(...) } else { None }` chain as `legacy.producer.as_ref().map(...)` per clippy's suggestion. Preserves the E0382 fix (borrow + clone, no moves). 3. CI plumbing: - rust-ci.yml: drop the fmt job added by #126 and restore the wrapper-only form; its non-empty actions.lock section caused startup_failure (0 jobs) on every run since. Auto-formatting stays covered by the standalone fmt.yml workflow, which works. - actions.lock: reconcile drifted pins so workflow refs and the lockfile agree — swatinem/rust-cache canonical lowercase key in fmt.yml; github/codeql-action v4.38.0 → v4.38.2 (workflow was bumped by dependabot in #121 without a relock, killing CodeQL at startup); haskell-actions/setup v2.12.0 → v2.12.1 (casket-pages drift). rust-ci.yml section restored to [] per estate convention for reusable-call wrappers (cf. governance.yml). Note: the Governance "Actions lockfile verify" failure predates this work (failing on every main push since mid-September); this fixes the drifts that are identifiable without the gh-actions-lock tool. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- .github/workflows/actions.lock | 23 ++++++++---------- .github/workflows/rust-ci.yml | 27 --------------------- crates/presswerk-core/src/provenance.rs | 6 ++--- crates/presswerk-document/src/pdf/writer.rs | 21 ++++++++-------- 4 files changed, 23 insertions(+), 54 deletions(-) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 8cbc1f7..07eecef 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -10,7 +10,7 @@ workflows: '.github/workflows/fmt.yml': - 'actions/checkout@v7.0.1' - 'dtolnay/rust-toolchain@v1' - - 'Swatinem/rust-cache@v2.9.2' + - 'swatinem/rust-cache@v2.9.2' '.github/workflows/boj-build.yml': - 'actions/checkout@v7.0.1' '.github/workflows/casket-pages.yml': @@ -19,14 +19,14 @@ workflows: - 'actions/configure-pages@v6.0.0' - 'actions/deploy-pages@v5.0.1' - 'actions/upload-pages-artifact@v5.0.0' - - 'haskell-actions/setup@v2.12.0' + - 'haskell-actions/setup@v2.12.1' '.github/workflows/ci.yml': - 'actions/checkout@v7.0.1' - 'dtolnay/rust-toolchain@v1' - 'swatinem/rust-cache@v2.9.2' '.github/workflows/codeql.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - - 'github/codeql-action@v4.38.0' + - 'github/codeql-action@v4.38.2' '.github/workflows/dependabot-automerge.yml': - 'dependabot/fetch-metadata@v3.1.0' '.github/workflows/dogfood-gate.yml': @@ -54,10 +54,7 @@ workflows: - 'dtolnay/rust-toolchain@v1' - 'softprops/action-gh-release@v3.0.3' - 'swatinem/rust-cache@v2.9.2' - '.github/workflows/rust-ci.yml': - - 'actions/checkout@v7.0.1' - - 'dtolnay/rust-toolchain@v1' - - 'Swatinem/rust-cache@v2.9.2' + '.github/workflows/rust-ci.yml': [] '.github/workflows/scorecard.yml': [] '.github/workflows/secret-scanner.yml': [] '.github/workflows/security.yml': @@ -134,14 +131,14 @@ dependencies: commit: 'sha1-02cb101ec7c40f2c49e1d9714d64511d8e1b74de' owner_id: 1940490 repo_id: 260749683 - 'github/codeql-action@v4.38.0': - ref: 'v4.38.0' - commit: 'sha1-b96794f015dfd88f77b49b1c93e0fa7110f94c63' + 'github/codeql-action@v4.38.2': + ref: 'v4.38.2' + commit: 'sha1-2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2' owner_id: 9919 repo_id: 259445878 - 'haskell-actions/setup@v2.12.0': - ref: 'v2.12.0' - commit: 'sha1-6037f33647c3f17758a2356c80fc4a53d7e0685d' + 'haskell-actions/setup@v2.12.1': + ref: 'v2.12.1' + commit: 'sha1-0f8e8c99d88aeb3fbfd523f1ef2c6f762d10d64d' owner_id: 75048950 repo_id: 623796603 'hyperpolymath/deed-ecosystem@main': diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 6cf6ab0..bb07f89 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -17,32 +17,5 @@ permissions: contents: read jobs: - # Auto-format before the reusable Rust CI so PRs don't fail on fmt alone. - fmt: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v7.0.1 - with: - ref: ${{ github.head_ref }} - token: ${{ secrets.GITHUB_TOKEN }} - - uses: dtolnay/rust-toolchain@v1 - with: - toolchain: stable - components: rustfmt - - uses: Swatinem/rust-cache@v2.9.2 - - run: cargo fmt --all - - run: | - git config user.name "github-actions" - git config user.email "github-actions@github.com" - if ! git diff --quiet; then - git add -A - git commit -m "fmt: cargo fmt --all" - git push - else - echo "no fmt changes" - fi rust-ci: - needs: fmt uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@84355587cb2a1f86e6882de83514a32db2646e7a # re-pinned 2026-07-01: toolchain-required-input fix (standards#439) diff --git a/crates/presswerk-core/src/provenance.rs b/crates/presswerk-core/src/provenance.rs index 1a9815a..f1a2ff4 100644 --- a/crates/presswerk-core/src/provenance.rs +++ b/crates/presswerk-core/src/provenance.rs @@ -528,16 +528,14 @@ impl From for FfpRecord { filled_at: None, appearances_generated: None, }) - } else if let Some(p) = &legacy.producer { - Some(FfpDeclared { + } else { + legacy.producer.as_ref().map(|p| FfpDeclared { filled_by: "machine".to_string(), tool: Some(p.clone()), tool_version: None, filled_at: None, appearances_generated: None, }) - } else { - None }; let mut evidence = Vec::new(); if legacy.need_appearances { diff --git a/crates/presswerk-document/src/pdf/writer.rs b/crates/presswerk-document/src/pdf/writer.rs index 460de8e..f88e636 100644 --- a/crates/presswerk-document/src/pdf/writer.rs +++ b/crates/presswerk-document/src/pdf/writer.rs @@ -1,25 +1,27 @@ // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) // -// PDF writer — create new PDF documents from text or images using `printpdf` 0.8. +// PDF writer — create new PDF documents from text or images using `printpdf` 0.9. // -// printpdf 0.8 uses a data-oriented API: documents are built by constructing +// printpdf 0.9 uses a data-oriented API: documents are built by constructing // `PdfPage` structs containing `Vec` operation lists, then serialised via -// `PdfDocument::save()`. +// `PdfDocument::save()`. Text is emitted with the 1:1 PDF operators +// `Op::SetFont` (`Tf`) and `Op::ShowText` (`Tj`/`TJ`); the 0.8-era +// `SetFontSizeBuiltinFont` / `WriteTextBuiltinFont` ops were removed upstream. use std::path::Path; use presswerk_core::PaperSize; use presswerk_core::error::PresswerkError; use printpdf::{ - BuiltinFont, Mm, Op, PdfDocument, PdfPage, PdfSaveOptions, PdfWarnMsg, Point, Pt, RawImage, - RawImageData, RawImageFormat, TextItem, XObjectTransform, + BuiltinFont, Mm, Op, PdfDocument, PdfFontHandle, PdfPage, PdfSaveOptions, PdfWarnMsg, Point, + Pt, RawImage, RawImageData, RawImageFormat, TextItem, XObjectTransform, }; use tracing::{debug, info, instrument}; /// Creates new PDF documents from text content or raster images. /// -/// Uses `printpdf` 0.8 for generation, producing standards-compliant PDF output +/// Uses `printpdf` 0.9 for generation, producing standards-compliant PDF output /// suitable for printing. pub struct PdfWriter { /// Paper size for page creation. @@ -119,13 +121,12 @@ impl PdfWriter { y: Pt(y_pt), }, }); - ops.push(Op::SetFontSizeBuiltinFont { + ops.push(Op::SetFont { + font: PdfFontHandle::Builtin(BuiltinFont::Helvetica), size: Pt(font_size_pt), - font: BuiltinFont::Helvetica, }); - ops.push(Op::WriteTextBuiltinFont { + ops.push(Op::ShowText { items: vec![TextItem::Text(line.clone())], - font: BuiltinFont::Helvetica, }); ops.push(Op::EndTextSection); From a0d952e8b1c45c7c965de6f45a27e63ad5da63ac Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 4 Oct 2026 22:34:04 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20next=20compile=20layer=20=E2=80=94?= =?UTF-8?q?=20rusqlite=20u64=20FromSql,=20clippy=20collapsible=5Fif?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI (b5d21ca) advanced past presswerk-document and surfaced the next blocking layer; this fixes it. - presswerk-security/audit.rs `AuditLog::count`: rusqlite 0.39 has no `FromSql` impl for `u64` (SQLite integers are signed i64), so `row.get(0)` into `Result` failed E0277. Read as `i64` and widen to `u64` (COUNT(*) is never negative). - presswerk-document/provenance.rs: two clippy `collapsible_if` sites (deny-warnings) in the FFP detector — collapse nested `if let` chains into edition-2024 let-chains (`if let Ok(catalog) && let Ok(meta_obj)`, and the /Ch array-walk triple `if let ... && let ... && is_meaningful`). Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- crates/presswerk-document/src/provenance.rs | 98 ++++++++++----------- crates/presswerk-security/src/audit.rs | 7 +- 2 files changed, 55 insertions(+), 50 deletions(-) diff --git a/crates/presswerk-document/src/provenance.rs b/crates/presswerk-document/src/provenance.rs index 4af79a5..f084f24 100644 --- a/crates/presswerk-document/src/provenance.rs +++ b/crates/presswerk-document/src/provenance.rs @@ -81,54 +81,54 @@ pub fn classify_document(document: &Document, raw: &[u8]) -> FfpRecord { let mut declared_filled_by: Option = None; // Step 2 — declared marker (XMP in catalog Metadata) - if let Ok(catalog) = document.catalog() { - if let Ok(meta_obj) = catalog.get(b"Metadata") { - let payload = get_metadata_payload(document, meta_obj); - if payload.is_empty() { - evidence.insert("FFP-E-XMP-UNREADABLE".to_string()); - } else if contains_bytes(&payload, FFP_NS) { - // Namespace present — extract properties - let filled_by = xmp_extract(&payload, "filledBy"); - let tool = xmp_extract(&payload, "tool"); - let tool_version = xmp_extract(&payload, "toolVersion"); - let filled_at = xmp_extract(&payload, "filledAt"); - let ap_gen_str = xmp_extract(&payload, "appearancesGenerated"); - let ap_gen = ap_gen_str - .as_deref() - .map(|s| s.eq_ignore_ascii_case("true")); + if let Ok(catalog) = document.catalog() + && let Ok(meta_obj) = catalog.get(b"Metadata") + { + let payload = get_metadata_payload(document, meta_obj); + if payload.is_empty() { + evidence.insert("FFP-E-XMP-UNREADABLE".to_string()); + } else if contains_bytes(&payload, FFP_NS) { + // Namespace present — extract properties + let filled_by = xmp_extract(&payload, "filledBy"); + let tool = xmp_extract(&payload, "tool"); + let tool_version = xmp_extract(&payload, "toolVersion"); + let filled_at = xmp_extract(&payload, "filledAt"); + let ap_gen_str = xmp_extract(&payload, "appearancesGenerated"); + let ap_gen = ap_gen_str + .as_deref() + .map(|s| s.eq_ignore_ascii_case("true")); - if let Some(fb) = filled_by { - let fb_trim = fb.trim().to_string(); - if fb_trim == "machine" { - evidence.insert("FFP-E-DECL-MACHINE".to_string()); - declared_filled_by = Some(fb_trim.clone()); - declared = Some(FfpDeclared { - filled_by: fb_trim, - tool: tool.filter(|s| !s.trim().is_empty()), - tool_version: tool_version.filter(|s| !s.trim().is_empty()), - filled_at: filled_at.filter(|s| !s.trim().is_empty()), - appearances_generated: ap_gen, - }); - } else { - evidence.insert("FFP-E-DECL-UNRECOGNISED".to_string()); - // Still record declared verbatim per spec - declared = Some(FfpDeclared { - filled_by: fb_trim.clone(), - tool: tool.filter(|s| !s.trim().is_empty()), - tool_version: tool_version.filter(|s| !s.trim().is_empty()), - filled_at: filled_at.filter(|s| !s.trim().is_empty()), - appearances_generated: ap_gen, - }); - declared_filled_by = Some(fb_trim); - // For classification, unrecognised does not count as machine - } + if let Some(fb) = filled_by { + let fb_trim = fb.trim().to_string(); + if fb_trim == "machine" { + evidence.insert("FFP-E-DECL-MACHINE".to_string()); + declared_filled_by = Some(fb_trim.clone()); + declared = Some(FfpDeclared { + filled_by: fb_trim, + tool: tool.filter(|s| !s.trim().is_empty()), + tool_version: tool_version.filter(|s| !s.trim().is_empty()), + filled_at: filled_at.filter(|s| !s.trim().is_empty()), + appearances_generated: ap_gen, + }); } else { evidence.insert("FFP-E-DECL-UNRECOGNISED".to_string()); - // Namespace present but filledBy absent + // Still record declared verbatim per spec + declared = Some(FfpDeclared { + filled_by: fb_trim.clone(), + tool: tool.filter(|s| !s.trim().is_empty()), + tool_version: tool_version.filter(|s| !s.trim().is_empty()), + filled_at: filled_at.filter(|s| !s.trim().is_empty()), + appearances_generated: ap_gen, + }); + declared_filled_by = Some(fb_trim); + // For classification, unrecognised does not count as machine } } else { - // Metadata present but no FFP namespace — no declaration evidence + evidence.insert("FFP-E-DECL-UNRECOGNISED".to_string()); + // Namespace present but filledBy absent } + } else { + // Metadata present but no FFP namespace — no declaration evidence } } // If Metadata not present, no XMP evidence. @@ -580,7 +580,8 @@ fn is_meaningful(document: &Document, ft: &str, v_obj: Option<&Object>, v_raw: & return !name.eq_ignore_ascii_case(b"Off") && !name.is_empty(); } Object::String(bytes, _) => { - return !bytes.eq_ignore_ascii_case(b"Off") && !bytes.is_empty(); + return !bytes.eq_ignore_ascii_case(b"Off") + && !bytes.is_empty(); } _ => return false, } @@ -613,12 +614,11 @@ fn is_meaningful(document: &Document, ft: &str, v_obj: Option<&Object>, v_raw: & if is_string_object_meaningful(document, item) { return true; } - if let Object::Reference(id) = item { - if let Ok(res) = document.get_object(*id) { - if is_string_object_meaningful(document, res) { - return true; - } - } + if let Object::Reference(id) = item + && let Ok(res) = document.get_object(*id) + && is_string_object_meaningful(document, res) + { + return true; } } return false; diff --git a/crates/presswerk-security/src/audit.rs b/crates/presswerk-security/src/audit.rs index 791bd68..16519a4 100644 --- a/crates/presswerk-security/src/audit.rs +++ b/crates/presswerk-security/src/audit.rs @@ -199,9 +199,14 @@ impl AuditLog { /// Return the total number of entries in the audit log. pub fn count(&self) -> Result { + // SQLite integers are signed 64-bit; rusqlite has no `FromSql` impl + // for `u64`, so read as `i64` and widen (COUNT(*) is never negative). self.conn - .query_row("SELECT COUNT(*) FROM audit_log", [], |row| row.get(0)) + .query_row("SELECT COUNT(*) FROM audit_log", [], |row| { + row.get::<_, i64>(0) + }) .map_err(db_err) + .map(|n| n as u64) } }