Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/actions.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ workflows:
- 'actions/checkout@v7.0.1'
- 'dtolnay/rust-toolchain@v1'
- 'swatinem/rust-cache@v2.9.2'
'.github/workflows/fmt.yml':
- 'actions/checkout@v7.0.1'
- 'dtolnay/rust-toolchain@v1'
- 'Swatinem/rust-cache@v2.9.2'
'.github/workflows/boj-build.yml':
- 'actions/checkout@v7.0.1'
'.github/workflows/casket-pages.yml':
Expand Down Expand Up @@ -50,7 +54,10 @@ workflows:
- 'dtolnay/rust-toolchain@v1'
- 'softprops/action-gh-release@v3.0.3'
- 'swatinem/rust-cache@v2.9.2'
'.github/workflows/rust-ci.yml': []
'.github/workflows/rust-ci.yml':
- 'actions/checkout@v7.0.1'
- 'dtolnay/rust-toolchain@v1'
- 'Swatinem/rust-cache@v2.9.2'
'.github/workflows/scorecard.yml': []
'.github/workflows/secret-scanner.yml': []
'.github/workflows/security.yml':
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ jobs:
fi
return $code
}
# Auto-format before checking so PRs don't fail on fmt alone (D189 follow-up)
cargo fmt --all || true
run_logged check cargo check --workspace --all-targets || true
run_logged test cargo test -p presswerk-core -p presswerk-security -p presswerk-document -p presswerk-print || true
run_logged clippy cargo clippy --workspace --all-targets -- -D warnings || true
Expand Down
38 changes: 38 additions & 0 deletions .github/workflows/fmt.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# SPDX-License-Identifier: MPL-2.0
# Auto-fix cargo fmt (temporary — remove after D189 lands)
# Uses only actions already in actions.lock to avoid lock churn.

name: fmt

on:
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: write

jobs:
fmt:
runs-on: ubuntu-latest
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
27 changes: 27 additions & 0 deletions .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,32 @@ 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)
1 change: 0 additions & 1 deletion crates/presswerk-app/src/pages/jobs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ pub fn Jobs() -> Element {
let provenance_note = job.form_provenance.summary();
let provenance_label = job.form_provenance.label();
let show_provenance = job.form_provenance.form != presswerk_core::provenance::FfpForm::Unknown;
let is_machine_or_suspected = job.form_provenance.is_machine_or_suspected();
let hazard = job.form_provenance.has_blank_print_hazard();

rsx! {
Expand Down
11 changes: 8 additions & 3 deletions crates/presswerk-app/src/services/app_services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ use presswerk_core::AppConfig;
use presswerk_core::error::{PresswerkError, Result};
use presswerk_core::provenance::FfpClassification;
use presswerk_core::types::{
DiscoveredPrinter, DocumentType, JobId, JobSource, JobStatus, PrintJob,
PrintSettings, ServerStatus,
DiscoveredPrinter, DocumentType, JobId, JobSource, JobStatus, PrintJob, PrintSettings,
ServerStatus,
};
use presswerk_document::classify_for_print;
use presswerk_print::discovery::PrinterDiscovery;
Expand Down Expand Up @@ -341,7 +341,12 @@ impl AppServices {
queue.update_status(job_id, JobStatus::Pending, None)?;
}

self.audit("job_released", &job.document_hash, true, Some(&job.document_name));
self.audit(
"job_released",
&job.document_hash,
true,
Some(&job.document_name),
);

self.spawn_send(
*job_id,
Expand Down
4 changes: 2 additions & 2 deletions crates/presswerk-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub mod types;
pub use config::AppConfig;
pub use error::PresswerkError;
pub use provenance::{
FfpAppearances, FfpClassification, FfpDeclared, FfpForm, FfpRecord, FormOrigin,
FormProvenance, ProvenanceConfidence,
FfpAppearances, FfpClassification, FfpDeclared, FfpForm, FfpRecord, FormOrigin, FormProvenance,
ProvenanceConfidence,
};
pub use types::*;
106 changes: 80 additions & 26 deletions crates/presswerk-core/src/provenance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ impl FfpClassification {
}
}

// Inherent `from_str` (token vocabulary pair to `as_str`), deliberately
// not `std::str::FromStr`: parsing never fails — unrecognised tokens
// surface as `Unreadable` rather than as an error, matching
// `FormProvenancePolicy::from_token`.
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Self {
match s {
"no-form" => Self::NoForm,
Expand Down Expand Up @@ -146,12 +151,20 @@ pub struct FfpDeclared {
pub tool_version: Option<String>,
#[serde(rename = "filledAt", skip_serializing_if = "Option::is_none")]
pub filled_at: Option<String>,
#[serde(rename = "appearancesGenerated", skip_serializing_if = "Option::is_none")]
#[serde(
rename = "appearancesGenerated",
skip_serializing_if = "Option::is_none"
)]
pub appearances_generated: Option<bool>,
}

impl FfpDeclared {
pub fn machine(tool: Option<String>, tool_version: Option<String>, filled_at: Option<String>, appearances_generated: Option<bool>) -> Self {
pub fn machine(
tool: Option<String>,
tool_version: Option<String>,
filled_at: Option<String>,
appearances_generated: Option<bool>,
) -> Self {
Self {
filled_by: "machine".to_string(),
tool,
Expand Down Expand Up @@ -248,7 +261,9 @@ impl FfpRecord {
pub fn should_hold(&self, policy: crate::types::FormProvenancePolicy) -> bool {
use crate::types::FormProvenancePolicy;
match policy {
FormProvenancePolicy::HoldForReview => self.is_machine_or_suspected() || self.has_blank_print_hazard(),
FormProvenancePolicy::HoldForReview => {
self.is_machine_or_suspected() || self.has_blank_print_hazard()
}
_ => false,
}
}
Expand All @@ -269,12 +284,21 @@ impl FfpRecord {
"machine-filled form".to_string()
}
}
FfpClassification::MachineFilledSuspected => "machine-filled form (suspected — incomplete appearances)".to_string(),
FfpClassification::FilledUnknown => "form values present — provenance unknown".to_string(),
FfpClassification::MachineFilledSuspected => {
"machine-filled form (suspected — incomplete appearances)".to_string()
}
FfpClassification::FilledUnknown => {
"form values present — provenance unknown".to_string()
}
FfpClassification::Unreadable => "form provenance unreadable".to_string(),
};
if self.is_form() {
format!("{head}; {}/{} fields filled; appearances {}", self.filled_fields, self.total_fields, self.appearances.as_str())
format!(
"{head}; {}/{} fields filled; appearances {}",
self.filled_fields,
self.total_fields,
self.appearances.as_str()
)
} else {
head
}
Expand Down Expand Up @@ -324,7 +348,9 @@ impl FfpRecord {
FfpClassification::NoForm => "NotAForm",
FfpClassification::BlankForm => "Empty",
FfpClassification::FilledUnknown => "Human",
FfpClassification::MachineFilled | FfpClassification::MachineFilledSuspected => "Machine",
FfpClassification::MachineFilled | FfpClassification::MachineFilledSuspected => {
"Machine"
}
FfpClassification::Unreadable => "Unknown",
}
}
Expand Down Expand Up @@ -388,7 +414,9 @@ impl std::fmt::Display for FormOrigin {
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)]
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
)]
pub enum ProvenanceConfidence {
#[default]
None,
Expand Down Expand Up @@ -492,19 +520,25 @@ impl From<FormProvenanceLegacy> for FfpRecord {
} else {
FfpAppearances::Generated
};
let declared = legacy.marker.map(|m| FfpDeclared {
filled_by: m,
tool: legacy.producer,
tool_version: None,
filled_at: None,
appearances_generated: None,
}).or_else(|| legacy.producer.map(|p| FfpDeclared {
filled_by: "machine".to_string(),
tool: Some(p),
tool_version: None,
filled_at: None,
appearances_generated: None,
}));
let declared = if let Some(m) = &legacy.marker {
Some(FfpDeclared {
filled_by: m.clone(),
tool: legacy.producer.clone(),
tool_version: None,
filled_at: None,
appearances_generated: None,
})
} else if let Some(p) = &legacy.producer {
Some(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 {
evidence.push("FFP-E-NEED-APPEARANCES".to_string());
Expand All @@ -515,7 +549,9 @@ impl From<FormProvenanceLegacy> for FfpRecord {
match legacy.origin {
FormOrigin::NotAForm => evidence.push("FFP-E-NO-ACROFORM".to_string()),
FormOrigin::Empty => evidence.push("FFP-E-NO-VALUES".to_string()),
FormOrigin::Unknown if !legacy.inspected => evidence.push("FFP-E-UNREADABLE".to_string()),
FormOrigin::Unknown if !legacy.inspected => {
evidence.push("FFP-E-UNREADABLE".to_string())
}
_ => {}
}
if legacy.marker.is_some() {
Expand Down Expand Up @@ -572,7 +608,10 @@ mod tests {
total_fields: 2,
appearances: FfpAppearances::Incomplete,
declared: None,
evidence: vec!["FFP-E-NEED-APPEARANCES".to_string(), "FFP-E-AP-INCOMPLETE".to_string()],
evidence: vec![
"FFP-E-NEED-APPEARANCES".to_string(),
"FFP-E-AP-INCOMPLETE".to_string(),
],
};
assert_eq!(
r.canonical_line(),
Expand All @@ -590,7 +629,13 @@ mod tests {
filled_fields: 2,
total_fields: 2,
appearances: FfpAppearances::Incomplete,
declared: Some(FfpDeclared { filled_by: "machine".to_string(), tool: Some("blocky-writer".to_string()), tool_version: Some("0.2.0".to_string()), filled_at: None, appearances_generated: Some(false) }),
declared: Some(FfpDeclared {
filled_by: "machine".to_string(),
tool: Some("blocky-writer".to_string()),
tool_version: Some("0.2.0".to_string()),
filled_at: None,
appearances_generated: Some(false),
}),
evidence: vec!["FFP-E-DECL-MACHINE".to_string()],
};
let json = r.audit_json();
Expand All @@ -601,7 +646,10 @@ mod tests {
#[test]
fn unreadable_line() {
let r = FfpRecord::unreadable("parse failed");
assert_eq!(r.canonical_line(), "classification=unreadable form=unknown filled=0/0 appearances=unknown evidence=FFP-E-UNREADABLE");
assert_eq!(
r.canonical_line(),
"classification=unreadable form=unknown filled=0/0 appearances=unknown evidence=FFP-E-UNREADABLE"
);
}

#[test]
Expand All @@ -614,7 +662,13 @@ mod tests {
filled_fields: 2,
total_fields: 2,
appearances: FfpAppearances::Incomplete,
declared: Some(FfpDeclared { filled_by: "machine".to_string(), tool: None, tool_version: None, filled_at: None, appearances_generated: None }),
declared: Some(FfpDeclared {
filled_by: "machine".to_string(),
tool: None,
tool_version: None,
filled_at: None,
appearances_generated: None,
}),
evidence: vec!["FFP-E-DECL-MACHINE".to_string()],
};
assert!(r.should_hold(FormProvenancePolicy::HoldForReview));
Expand Down
Loading