Conversation
…ty matrix (#266) capabilitiesReportMatrixVersions is a drift guard, not a RED-first fix: the four published schema versions already match the compatibility matrix, so there was no failing assertion to write first. It passes before and after the source change.
…Core diagnostic (#266)
Implement Interaction-layer catalog, controller, steps model, and run submitter; wire EditorHost with validate/plan/confirm/execute workflow; replace the Fix placeholder with ActionListPane.qml; extend unit tests and accessibility smoke mirrors. Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Pinned invariant: no analyze, apply, or candidate-serialize path writes to PDFRepairTransactionOptions::sourcePath. The slot passed without a production change.
…239) Pinned invariant: PDFArtifactStore publishes an imported file at its digest-addressed path with write bits cleared, so the received input is a distinct, read-only identity.
) Pinned invariant: the slot passed without a production change. Its non-vacuity comes from three assertions inside it - the registry holds at least 7 operations, at least one of them declines the append path, and downsample-images declares full rewrite. A test-side mutation that inverted the loop's else-branch expectation failed on add-bleed, proving the branch is reached against a signed source.
document.redact now runs pdf::validateSaveRequest with a
fullRewrite("redaction removes prior content") requirement before it reads the
document, so the refusal does not depend on how much work the redaction would
have done, and it emits save-policy.refused with ProcessingFailure (4) instead
of silently overwriting the caller's input.
Also reformats PdfTool/pdftoolredact.cpp: the file was already clang-format
dirty at HEAD (exit 74), and the changed-file format gate checks touched files.
…ources Both files were already clang-format-dirty at the branch base, and the per-file format gate has no clean-at-base exemption.
Issue #589. Certified preflight state (#133/#237) is internal to Loop, so a shop has no production handoff it can give a customer, a press, or an auditor and have verified without Loop. Core (`pdf::PreflightEvidenceBundle`): - `buildPreflightEvidenceBundle()` produces a machine-readable manifest plus declared members from canonical state: the pathless preflight report, the retained certificate, the governed sign-off record, the operation-history slice, and digest-addressed rollback references. The manifest carries the document revision digest, the effective profile digest with its identity and resolution provenance, the coverage scope actually evaluated, the decisions and approvals with actor and timestamp, and a `sha256` plus `byte_count` for every member. Export fails closed when the report does not describe the supplied bytes, when the certificate or sign-off binds a different revision or profile, or when the supplied chain does not verify from sequence 1. - `writePreflightEvidenceBundle()` refuses a non-empty output directory and commits `manifest.json` last, so a directory carrying a manifest is a complete bundle. - `verifyPreflightEvidenceBundle()` is the offline verifier: member integrity against the manifest, the exact declared member set, path hygiene, identity agreement across manifest/report/certificate/sign-off, and the hash chain of the exported history slice. Every refusal is attributable, naming the member and the value it disagrees on. The verdict is refusal-driven (each finding clears it) rather than derived from an aggregate. Path hygiene: the exported report drops the source-path keys, free-form operator text has path-shaped substrings replaced with `<path-omitted>` (profile JSON pointers are deliberately kept), rollback references keep the content digest instead of the store path, and artifact identities drop the storage token. The exported chain is therefore re-hashed over the redacted copy: the manifest says `chain_mode: "path-redacted"`, records the canonical chain digest so an auditor with the sidecar can bind the slice, and the certificate's issuance event is bound by id rather than by position. The bundle is explicitly not a second source of truth. PdfTool: - `export-evidence-bundle <document.pdf> --report <file> --output <dir> [--certificate] [--sign-off] [--artifact]` and `verify-evidence-bundle <dir>`, both JSON-first with the four registration points (capability, descriptors, parser, options) and the non-JSON rejection guard its siblings use. Evidence: - `UnitTestsPreflightVerdict` gains three slots: identity binding plus offline verification, per-member tamper/missing/undeclared/short-circuit detection with attributable codes, and the no-raw-paths assertion (non-vacuity checked: the fixture inputs carry the path, and disabling the source-path drop or the string redaction fails the slot). - `UnitTestsPdfToolContract` gains the CLI export/verify pair end to end on a certified fixture, including the off-line assertion after the document, its sidecar, and the supplied certificate are deleted, plus the non-JSON rejection. - `scripts/qualification/run_independent_validators.py --evidence-bundle` consumes a bundle without Loop and fails the lane closed on any mismatch, covered by six new unit tests. Docs: `docs/PREFLIGHT_EVIDENCE_BUNDLE.md` (export/verify pair, bundle layout, finding codes, and the two properties a reader must not over-read), plus cross-references from `CERTIFIED_PREFLIGHT.md`, `PROVENANCE_EVENT_CHAIN.md`, `INDEPENDENT_VALIDATION.md` and `PDFTOOL_CLI_CONTRACT.md`.
…ndle Covers the two scope bullets the first commit asserted only structurally: the corrected-output identity and the publication sign-off bound to exact accepted identities. The slot fails closed on a sign-off record that names a different source, profile, or published artifact, and proves the exported approval keeps its actor, timestamp, and path-redacted rationale. Non-vacuity: disabling the sign-off source check fails the slot.
… report The exporter's fail-closed binding check had no coverage: a report edited after certification must be refused with an explanatory error instead of exported with a binding that cannot be re-derived.
…he coverage backlog (#165) Issue #165 asks for a published check catalog and a GWG / PDF-X coverage matrix. I measured all four of its scope items against the tree before writing anything: the coverage matrix (item 2) and the in-product coverage scope (item 3) already shipped, so this change closes the two that had not, and does not re-do the rest. Check rows. Every one of the 21 registered checks now carries, in `docs/preflight-check-catalog-overlay.json` and in the generated catalog: - `parameters`, every profile-side value the check reads, as `{id, type, default, range, meaning}`. Ids come from the engine's own profile parser, and defaults from the `PreflightCheckConfig` member initialisers and the parser's per-check fallbacks. - `severity`, the finding types the check emits and the condition for each, mirroring how the engine sets `PreflightFinding::severity`. - `evidence`, the report fields the check's findings carry, named as `findingToJson` emits them, with a check's own evidence keys written `evidence.<key>`. - `fixups`, the registered preflight fixup ids that remediate the check's findings, `[]` when none does. Coverage backlog. `docs/generated/preflight-coverage-backlog.json` is a prioritised gap register derived from the same matrix: 26 rows, each with the gap, the process families it affects, a priority from a stated rule, a state from a stated rule, and either a verified GitHub issue, the landed check id that now covers the class, or the literal `unfiled`. The generator records the issue snapshot it verified and fails closed on an unverified reference, on state or issue drift, on an orphan `not_covered` class, and on a registered preflight fixup that no row claims. Both artifacts are held by the existing seam rather than a second document. The generator already failed closed when the registry and the overlay disagreed; it now requires the new row fields and validates the backlog the same way, so the published catalog cannot drift from the checks without reddening `scripts/generate-architecture-catalogs.py --check`. `scripts/ci/test_preflight_check_catalog.py` pins each of those refusals, mirroring the sibling correction-operation catalog test, and runs in the `source_integrity` job beside the existing catalog check. Verification at this commit, all exit 0: `generate-architecture-catalogs.py --write` then `--check`; `check_source_integrity.py`; `test_correction_ operation_catalog.py` (9 tests); `check_phase5_residue.py`; `check_unmanaged_ async.py`; `test_preflight_check_catalog.py` (18 tests); `test_workflow_ contracts` (27 tests). Non-vacuity: removing `severity` from one overlay row fails with `catalog entry 'ink-coverage' missing severity`, and deleting a backlog row reddens `--check` as staleness; both were restored byte-exactly. Sourcing review, done against the tree rather than accepted from the generator: all 56 parameter ids resolve to a key the engine reads, and a declared-type sweep against the engine's members found one real mismatch, `min_dpi` typed as `number` while the parser reads it with `toInt` (`preflightengine.cpp:7107`), now `integer`. One association is not engine-derived and is labelled in the docs: `image-resolution -> downsample-images`, which comes from the audited correction-operation catalog's `evidence_impact.findings`, because the engine advertises that fixup on profile declaration plus a high-DPI image candidate, never keyed on a check id.
A hard exit inside one output's staging window is the crash the atomic write exists for, and no seam reached it: the job's manifestPersist kill fires around the manifest write, not inside the output publish. PDFPageMasterExportJob::beforeOutputCommit is called for one output after its bytes are handed to the atomic writer and before the commit that makes them visible at the final path, keyed on the output's path because the manifest goes through the same writer helper. Behaviour is unchanged with an empty seam: PDFSafeFileWriter::writeData is a one-line wrapper over writeDevice with a producer that writes the payload and returns written == size (pdfsafefilewriter.cpp:37-46), and the staged producer mirrors that check before calling the seam.
… gaps Nine slots on the existing target, no new target: a five-output batch whose third destination directory was never created marks only that output failed while outputs 1-2 still reopen as valid PDFs; a child killed inside the third output's staging window leaves a parsing manifest, byte-valid outputs 1-2, no file at any unpublished final path, and a batch that resumes to five written outputs under the same batch_id; resume retries a failed entry, re-runs a written entry whose file is gone, and treats a missing manifest as a fresh batch; manifest integrity covers a path that cannot be read as a manifest, an empty outputs array, and two batches sharing the default name in one directory, asserted as the observed aliasing rather than as a lock; the success path pins the default location, schema_version 3 and manifest retention. The kill harness exits 92 or 93 when its seam was armed and never fired, so the scenario cannot pass by measuring nothing.
The code drifted from the accepted ADR, so the ADR is amended rather than the tests bent to it: schema_version 3 and the fields it actually carries, exactly pending|written|failed with no skipped status, QSaveFile open/producer/commit instead of a <finalPath>.<pid>.partial file plus QFile::rename, a manifest retained after a successful batch with no completion marker, and resume's real rules. Each drifted claim is listed as deliberately superseded, and Last-verified moves to this commit.
The deleted PDFRecoveryManager held the policy clamp and the source-identity inspection that docs/EDITOR_RECOVERY.md specifies, and both are pure value logic with no Widgets or event-loop dependency, so they belong in LoopLibCore on their own. Bodies are restored from 2a19e2c^ rather than rewritten: the clamp, the fingerprint reads, the normalized-path hash, inspectSource and classifySource keep their deleted text, with the namespace, the export macro and the four signature lines changed to a Core free-function surface.
UnitTests/tst_recoverytest.cpp included a header that existed nowhere and was registered in no target, so the only executable specification of this contract did not compile. It now builds as UnitTestsRecovery, app-less and linked to Core and Test only. The two slots keep every assertion and bound they had; only their call sites move from the deleted manager's statics to the restored free functions, and slot 2's fixture calls the clamp directly. Registering the target also fixes the classification: agent-policy.json gains the target in core.tests and the test file in core.paths, so a test-only edit classifies as core rather than unclassified, and the two generated catalogs are regenerated to match.
The page opened by stating the service does not exist in this tree, which stops being true once its value surface is registered. It now names what exists, what has no coverage yet, and keeps everything below as the contract to restore. One contract bullet also had to change: it prescribed QtConcurrent for serialization and hashing, which CI pins at zero remaining launches in scripts/ci/check_unmanaged_async.py, so the sanctioned mechanism is recorded as the job scheduler instead.
…ed issues (#165) The register left twelve rows unfiled. Two of those are P1, meaning no registered check inspects the class at all and a clean run is silent about it, so they are now filed and the rows reference them: GWG 2022/2024 sheetfed and packaging conformance certificates as #664, and PDF/X-5 and PDF/A-3 validation and output as #665. The generator verifies every reference against the recorded GitHub state, so the entries carry the verbatim OPEN state and the row states still agree with it. The remaining ten rows stay unfiled on purpose: they are P2 and P3, where a check does inspect the class and its named limitation is what can suppress a finding, so the register is the right place for them until triage.
The backlog validator already rejected unknown families, but check overlay rows could drop or corrupt their families field without failing --check. Co-authored-by: michael berry <mberrys@users.noreply.github.com>
The per-file format gate has no clean-at-base exemption and this file carried five clang-format violations at dbb1c32 (the `checks` braced list in preflightRestrictedAuditBindsEffectiveScope and the std::find_if lambda below it). Formatting it is a prerequisite for any change that touches it. Whitespace-only: `git show HEAD:UnitTests/tst_pdftoolcontract.cpp | tr -d '[:space:]'` and the same over the formatted file hash identically (sha256 6619d4d18bf37a92...), and `clang-format --dry-run --Werror` over the result reports zero violations.
Issue #589's first acceptance criterion is a documented export/verify pair whose input the CLI produces. The portable evidence bundle requires a preflight report file, but `preflight` only ever emitted the report inside its stdout envelope, so the documented workflow required a hand-extracted report. `repair` already takes `--report-file`; preflight now matches it. The file is the report in its retained form (`redactSensitiveJson` over `preflightAuditReportSummary`): exactly the payload the operation-history chain stores and `issuePreflightCertificate` hashes, so a consumer can bind the file to the certificate's `report_digest` without re-running the document. The JSON envelope keeps reporting the run's own view, and the written file is registered as an extra output alongside it. Failing first: with `--report-file` unimplemented, the new slot `PdfToolContractTest::preflightWritesTheCanonicalReport` fails with `run.exitCode` 2 plus `cli.invalid-arguments` / "Unknown option 'report-file'." and no report file on disk. After the change the same slot passes, asserts the written report's canonical JSON equals the accepted PreflightRun event's retained summary, and `UnitTestsPdfToolContract` is 29 passed / 0 failed. The touched test source carried five clang-format violations at dbb1c32 (the per-file gate has no clean-at-base exemption), which the preceding `style:` commit fixes whitespace-only.
…preflight Validate preflight output paths against the input document and each other before running checks, preventing --report-file from overwriting the PDF. Register --report-file only on the preflight command via PreflightReportFile so add-bleed and action-list no longer advertise an unused option. Repair keeps its existing separately registered --report-file. Add contract tests for both behaviors. Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Co-authored-by: michael berry <mberrys@users.noreply.github.com>
… gaps (#44) (#659) ## What changed A mid-batch failure or a hard kill during a PageMaster export cannot silently leave a torn PDF, and until now that promise rested on tests that did not reach the window it protects. The kill the suite exercised fired around the manifest write, not inside one output's publish, so the case atomic writes exist for was measured at the wrong instant. This closes the four measured gaps and amends ADR-004 to the code as built. The next maintainer inherits a suite where the crash case cannot pass by measuring nothing, and an ADR whose claims match the tree instead of contradicting it. `Refs #44`. ## Scope, measured before writing anything The issue asks for five test cases. I checked each against `UnitTests/tst_pagemasterexporttest.cpp` at `dbb1c324`, which already held 36 slots. | Issue case | State at base | This PR | | --- | --- | --- | | 1. Mid-batch operation failure | Only a write-stage failure | Adds an operation-stage failure at index 3 of 5, with outputs 1-2 still reopening as valid PDFs | | 2. Kill mid-write | Killed after commit | Adds the kill inside the staging window, the window atomic writes exist for | | 3. Resume | Six slots, three behaviours missing | Adds retry of a `failed` entry, re-run of a `written` entry whose file was deleted, and a missing manifest treated as a fresh batch | | 4. Manifest integrity | Corrupt JSON only | Adds a path unreadable as a manifest, an empty `outputs` array, and two batches sharing the default name in one directory | | 5. Success path | Partial | Pins the default location, `schema_version == 3`, and that the manifest survives | ## The one seam, and why an empty seam cannot change production `PDFPageMasterExportJob` gains `beforeOutputCommit`, a `std::function<void(const QString&)>` beside the existing `manifestPersist` seam, empty by default. It is called for one output after that output's bytes are handed to the atomic writer and before the commit that makes them visible at the final path, keyed on the output's path, because the manifest goes through the same writer helper and an ordinal write counter is not the output index. The write site now stages through `PDFSafeFileWriter::writeDevice` with a producer that mirrors the payload write. I checked the substitution at the source rather than assuming it: `writeData` is a one-line wrapper over `writeDevice` whose producer writes the payload and returns `written == size` (`pdfsafefilewriter.cpp:37-46`). The staged producer keeps that short-write check, so a disk-full or quota short write is still reported as failure rather than as a truncated output. With an empty seam the behaviour is identical. ## ADR-004 amended to the code The code drifted from the accepted ADR, and the issue's rule is that the ADR wins or is amended, not the test. The amendment records `schema_version` 3 with the fields the manifest really carries, exactly `pending|written|failed` with no `skipped` status, `QSaveFile` open, producer and commit instead of a `<finalPath>.<pid>.partial` file plus `QFile::rename`, a manifest retained after a successful batch with no completion marker, and resume's real rules. Every drifted claim is listed as deliberately superseded, `Amended:` is the field ADR-003 already uses, and `Last-verified` moves to this commit. ## Honest gaps - The literal open-failure branch of `loadExistingManifest` stays unexercised. On Windows a path that `exists()` reports true while `open()` fails is a directory, a sharing violation, or an ACL denial, and the directory case is caught one step earlier by the planned-output conflict check. The function's parse-failure branch is covered, and the slot says this in the code rather than leaving it implicit. - A hard exit inside the staging window leaves Qt's own staging file behind, named `<finalPath>.XXXXXX` with six random alphanumerics and zero bytes in every observed case. It never appears at a final output path, which is the promise the ADR makes and the test asserts, and it is reported as a diagnostic only. Nothing ever removes that residue, so it is a candidate for its own issue. ## Proof `python scripts/agent/check-change.py --base dbb1c32 --head-branch test/0.3.0-44-pagemaster-manifest-regression --build-dir build-local` on `080dfa17`: ```json { "status": "pass", "risk": "high", "modules": ["core", "documentation", "pagemaster"], "protected_paths": ["LoopLibCore/sources/pdfpagemasterexport.cpp", "LoopLibCore/sources/pdfpagemasterexport.h"], "targets": ["LoopLibCore", "PdfTool"], "tests": 46, "checks": "62/62 pass" } ``` `risk: high` is the protected-path signal, disclosed below rather than buried. 62 checks pass, including 46 test targets built and run. Suite on my own run, after `ninja: no work to do` confirmed the build was current: `Totals: 47 passed, 0 failed, 0 skipped, 0 blacklisted, 16274ms`, up from the 38-row baseline. Non-vacuity, measured rather than assumed. The kill harness exits 91 for the in-window kill, 92 for a bad mode, and 93 when the seam was armed and never fired, and the slot asserts 91 exactly. I reproduced 93 myself by occupying the third output's final path with a directory, so a run that measures nothing cannot read as a pass. The retry slot self-checks its discriminator, asserting that a rewrite cannot survive because a read-only destination rejects the Win32 replace Qt uses. ## Contracts touched - `LoopLibCore/sources/pdfpagemasterexport.h` and `.cpp` are protected paths, so this is named for review. The change is additive, default-empty, leaves behaviour identical when unset, and follows the `manifestPersist` seam already in that header. No caller outside the library and this test includes the header. If you would rather the kill point lived elsewhere, the alternative I rejected was a compiled-out environment kill switch inside the shared library, which trades an optional member for a build-configuration promise shipped in `LoopLibCore`. - `docs/adr/adr-004-pagemaster-batch-manifest.md` is amended, with front-matter that `scripts/generate-architecture-catalogs.py --check` still validates. ## Open decision for review Should the `QSaveFile` staging residue get its own issue? It is harmless for the ADR's promise and untidy on disk after a crash. My recommendation is yes, as a cleanup item, and I will file it with the reproduction if you agree. <!-- codesmith:footer --> --- <a href="https://app.blacksmith.sh/studio-berry/codesmith/loop/pr/659?ref=codesmith_pr_footer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img alt="View with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a> <a href="https://backend.blacksmith.sh/track/enable-autofix?expires=1792490197&installation_model_id=435800&pr_number=659&ref=codesmith_pr_footer&repository=studio-berry%2Floop&return_to=https%3A%2F%2Fgithub.com%2Fstudio-berry%2Floop%2Fpull%2F659&signature=92aebbfaf6b583a2e948a2ce5e61382cfc002e876e400e2f227e086d5877bc12"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img alt="Autofix with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a> <sup>Need help on this PR? Tag <code>@codesmith-bot</code> with what you need. Autofix is disabled.</sup> <!-- codesmith:autofix:disabled --> <!-- /codesmith:footer -->
… orphaned spec (#575) (#660) ## What changed The Editor's crash recovery has a starting point again, and the tree's only executable specification of it runs for the first time. `UnitTests/tst_recoverytest.cpp` used to include a header that existed nowhere and was registered in no CMake target, so the two slots that define source-identity classification and policy clamping were dead text. This restores the value-level surface those slots exercise and makes them run. The full service, the per-session lock, the checkpoint rotation, the retention sweep and the Editor wiring are still absent, and the page that documents the contract now says so instead of claiming nothing exists. `Refs #575`. This is slice 0 of the restoration and does not close the issue. ## Why the port rather than a redesign I ran two competing designs. One restored the deleted implementation nearly verbatim, the other redesigned from the contract. The port won on a falsifiable claim: the deleted pair contains no Widgets reference, and the ported code compiles against today's Core with no API drift. | Measure | Result | | --- | --- | | Deleted `.cpp` | 951 lines, of which 97 of the 114 ported lines are byte-identical to `2a19e2c1^` | | Deleted header value region | 46 lines, of which 39 are byte-identical | | The 7 header exceptions | Exactly the contract-forced grafts below | | Abandon trigger | Did not fire. The first cold compile and link of the ported code was clean, so this is a restoration and not a rewrite wearing one | The redesign's strongest argument was that the orphan suite is `QTEST_APPLESS_MAIN` and can never exercise a Core object that owns timers. That is true and it does not decide this slice, because both slots are pure. One fingerprints source identity, the other clamps a policy, and neither needs an event loop. Confirmed by measurement: the ported translation unit references no timer, the target links no Gui, and the app-less run passes. ## Four grafts, all contract-forced - The dead `cleanlySaved` field is gone. It was written false and read, and nothing acted on it. - The candidate carries `sessionId` and `sourceFileName` and no path, because `docs/EDITOR_RECOVERY.md` allows the raw path only inside the private recovery store. The deleted `sessionDirectory` and `recoveryFile` are not restored. - The document revision is a `QString` from the revision identity rather than a `quint64`, and diagnostics travel as a bounded code the host translates. - The export macro is the Core one. `scripts/ci/check_loop_identity.py` matches the retired product token case-insensitively, so the deleted macro cannot come back. ## The specification was not edited to fit the code The two orphan slots keep every assertion and every bound they had, `>= 1`, `>= 0`, `> 0`, the `isValid` check, and the three `RecoverySourceStatus` comparisons. Only their call sites move, from the deleted manager's statics to the restored free functions, and slot 2's fixture now calls the clamp directly instead of round-tripping through a manager that does not exist yet. The stale `NOT COMPILED` comment above the includes is removed because it is no longer true. ## One contract sentence had to change The page prescribed `QtConcurrent` for serialization and hashing. `scripts/ci/check_unmanaged_async.py` pins remaining unmanaged launches at zero and `LoopLibCore` does not link `Qt6::Concurrent`, so the bullet now records the job scheduler as the sanctioned mechanism. Left as written, the next slice would have implemented the contract and failed CI. ## Proof `python scripts/agent/check-change.py --base dbb1c32 --head-branch feat/0.3.0-575-editor-recovery-restore --build-dir build-local` on `df40df81`: ```json { "status": "pass", "risk": "high", "modules": [ "build_policy", "core", "documentation" ], "protected_paths": [ "LoopLibCore/sources/pdfrecoverymanager.cpp", "LoopLibCore/sources/pdfrecoverymanager.h", "UnitTests/CMakeLists.txt" ], "targets": [ "LoopLibCore", "PdfTool", "UnitTests" ], "tests": 46, "checks": "63/63 pass" } ``` Suite on my own run: `Totals: 4 passed, 0 failed, 0 skipped, 0 blacklisted, 6ms`, with `PASS : RecoveryTest::sourceIdentityDetectsReplacement()` and `PASS : RecoveryTest::policyClampsUnsafeValues()`. Checks run by hand: `generate-architecture-catalogs.py --check`, `check_source_integrity.py`, `check_phase5_residue.py`, `check_unmanaged_async.py` (`Known legacy unmanaged launches: 0`) and `check_loop_identity.py` all exit 0, and `clang-format --dry-run -Werror` is clean on all three C++ files. One pre-existing failure is environmental and unrelated. `scripts/ci/test_verify_phase5_widgets_contract.py` fails because this machine holds gitignored sibling worktrees under `.worktrees/` whose forms appear in its recursive scan, and the assertion diff is entirely `.worktrees/...` paths. No path in my diff appears in it. ## Contracts touched `LoopLibCore/sources/**` and `UnitTests/CMakeLists.txt` are protected paths, so both are named here. The Core change adds new files rather than altering an existing interface, and the new symbols are the free functions and the candidate type. Registering the target also required `agent-policy.json` to place `UnitTestsRecovery` in `core.tests` and the test file in `core.paths`, without which a test-only edit classifies as unclassified. ## What remains, as its own slice The manager, the per-session `QLockFile` claim with stale-lock reclamation, the checkpoint write with payload validation and generation rotation, the scheduler submission, the retention sweep, the Editor wiring at the capture point, and the process-kill GUI coverage. The capture point is the open question I would settle first, since nothing in product code calls the modification path the contract names. <!-- codesmith:footer --> --- <a href="https://app.blacksmith.sh/studio-berry/codesmith/loop/pr/660?ref=codesmith_pr_footer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img alt="View with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a> <a href="https://backend.blacksmith.sh/track/enable-autofix?expires=1792491224&installation_model_id=435800&pr_number=660&ref=codesmith_pr_footer&repository=studio-berry%2Floop&return_to=https%3A%2F%2Fgithub.com%2Fstudio-berry%2Floop%2Fpull%2F660&signature=41fab0bf8cc332a939df49f5e8f0de3846287c4098d5d74c80f36360b518d0f4"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img alt="Autofix with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a> <sup>Need help on this PR? Tag <code>@codesmith-bot</code> with what you need. Autofix is disabled.</sup> <!-- codesmith:autofix:disabled --> <!-- /codesmith:footer -->
…he coverage backlog (#165) (#658) ## What changed Anyone who has to answer "is this profile adequate for this job" can now read what each preflight check actually measures, what it reads from the profile and with what range, which severities it can emit and when, what evidence its findings carry, and which fixups can remediate it. That answer previously required reading `preflightengine.cpp`. The same change publishes the register of what Loop does **not** detect: `docs/generated/preflight-coverage-backlog.json`, 26 rows, each naming the gap, the GWG families it affects, a priority from a stated rule, a state from a stated rule, and either a verified GitHub issue, the landed check that now covers the class, or an explicit `unfiled`. For the maintainer: both artifacts are generated from one seam that already fails closed. The registry and the overlay could not disagree before; now a row missing a required field, an unverified issue reference, state or issue drift, an orphan `not_covered` class, or a registered preflight fixup that no row claims all redden `scripts/generate-architecture-catalogs.py --check`. `Closes #165`. `dev` is not the default branch, so `.github/workflows/issue-promotion.yml` closes it on promotion. ## Scope, measured before writing anything The issue's four scope items, audited against the tree at `dbb1c324` rather than trusted from the issue body. Two were already satisfied, so this PR does not re-do them. | Item | State | Evidence | | --- | --- | --- | | 1. Published catalog from the registry with parameters and ranges, severity model, evidence, fixups, limitations | **Partial, closed here** | All 21 checks had rows carrying only `measures`, `limitations`, `coverage`, `families` | | 2. Coverage matrix against GWG families and PDF/X | Already shipped | `docs/PREFLIGHT_COVERAGE_MATRIX.md`, `docs/PDFX_POLICY_MATRIX.md`, 5 families, 3 PDF/X targets, 5 named not-covered classes | | 3. Surface the coverage scope in the product | Already shipped | `report.coverage_scope` carries `claim`, `matrix_id`, `enabled_checks`, asserted by five suites | | 4. Prioritised backlog cross-referenced to filed checks | **Missing, added here** | No register existed; the checks the issue names are largely closed and landed | The issue's headline numbers are stale. It says eight checks are registered and lists seven open issues; the registry holds 21, and `#12`, `#124`, `#130`, `#131` are closed with their checks in the registry. ## Sourcing rules, so the rows cannot drift into prose `parameters` ids are fields the engine's own profile parser reads, not fields a schema merely lists. Defaults come from the `PreflightCheckConfig` member initialisers and the parser's per-check fallbacks. The shared check envelope is excluded from `parameters` and described by the `severity` block instead. `evidence` names follow `findingToJson`, with a check's own evidence keys written `evidence.<key>`. A `fixups` id must be a registered repair operation whose `isPreflightFixup()` is true, every registered preflight fixup must be claimed by at least one row, and a fixup is listed only where the engine, or the audited correction-operation catalog, ties it to that check. ## Review findings on the generated data I reviewed the data rather than accepting the generator's output, with two throwaway sweeps kept outside the repository. - Every one of the 56 parameter ids resolves to a key the engine reads. - A declared-type sweep against the engine's members found one real mismatch: `min_dpi` was typed `number` while the parser reads it with `toInt` (`preflightengine.cpp:7107`). Fixed to `integer` with default `0`. The other two sweep hits are artifacts of my own regex, since the engine declares `QStringList allowedColorModes` and two siblings, never a `bool allowed`. - One association is **not** engine-derived and is labelled as such in the matrix doc: `image-resolution -> downsample-images`, taken from `docs/correction-operation-catalog-overlay.json` `evidence_impact.findings`, because the engine advertises that fixup on profile declaration plus a high-DPI image candidate and never keys it on a check id. ## Proof `python scripts/agent/check-change.py --base dbb1c32 --head-branch docs/0.3.0-165-preflight-catalog-coverage-backlog --build-dir build-local` on `df354868`: ```json { "status": "pass", "risk": "standard", "modules": ["documentation"], "protected_paths": [], "targets": [], "tests": [], "checks": "9/9 pass" } ``` Nine checks pass, including the four whole-tree guards that ran because they run on every change set: `source_integrity`, `preflight_truth_source`, `qml_mirror_parity`, `qt_test_runtime`. No build is required, since no CMake target changed. Non-vacuity, each mutation reverted and restored byte-exactly: | Mutation | Observed failure | | --- | --- | | removed `severity` from the `ink-coverage` overlay row | `error: cannot generate architecture catalog: catalog entry 'ink-coverage' missing severity`, exit 1 | | deleted the `bleed-raster-strip-depth` backlog row | `generated preflight coverage backlog is stale:` with the missing row in the diff, exit 1 | Guard suites, all exit 0: `scripts/ci/test_preflight_check_catalog.py` (18 tests, new), `scripts/ci/test_correction_operation_catalog.py` (9), `python3 -m unittest scripts.ci.test_workflow_contracts` (27), `check_source_integrity.py`, `check_phase5_residue.py`, `check_unmanaged_async.py`. ## Contracts touched `.github/workflows/ci.yml` gains one `source_integrity` step that runs the new guard, immediately after the existing catalog check. A guard that never runs is not a guard, and adding it beside its sibling is the smallest place to put it. ## Open decisions for review The register left twelve rows `unfiled`. The two P1 rows are now filed and referenced: GWG 2022/2024 sheetfed and packaging conformance certificates as #664, and PDF/X-5 and PDF/A-3 validation and output as #665, both where no registered check inspects the class at all and a clean run is silent about it. The remaining ten stay unfiled on purpose, eight P2 and two P3, because a registered check does inspect those classes and its named limitation is what can suppress or misclassify a finding, so the register is the right place for them until triage. Every reference is verified against the recorded GitHub state, so the generator fails when a row and its issue disagree. Both P1 gaps were filed in the same push that repointed their rows, so the register and the tracker agree at this head. No decision is left open on this PR.
…#589) (#657) ## What changed Loop can certify a preflight run internally (#133/#237), but has no production handoff: a shop cannot give a customer, a press, or an auditor a self-contained artifact that can be verified without Loop. This adds the export/verify pair from issue #589 — `PdfTool export-evidence-bundle` / `PdfTool verify-evidence-bundle` over a new Core surface, `pdf::PreflightEvidenceBundle`. The exporter writes a manifest plus declared members (`report.json`, `certificate.json`, `signoff.json`, `history.json`, `rollback-references.json`) from canonical state, refuses to build when the report, certificate, sign-off, or chain disagree with the supplied revision, and refuses a non-empty output directory. `manifest.json` is committed last, carries a `sha256` and `byte_count` for every member, and carries the document revision digest, the effective profile digest with its identity and resolution provenance, the coverage scope actually evaluated, the decisions and approvals with actor and timestamp, and digest-addressed rollback references. The verifier reads only the bundle directory and reports attributable findings per member and per chain link; its verdict is refusal-driven rather than derived from an aggregate. Bundle hygiene: the exported report drops source-path keys, free-form operator text is path-redacted to `<path-omitted>` (profile JSON pointers are deliberately kept), rollback references keep the content digest instead of the store path, and artifact identities drop the storage token. Because the canonical chain hashes a payload that names the source path, the exported history slice is re-hashed over the redacted copy: the manifest states `chain_mode: "path-redacted"`, records a digest over the canonical chain so an auditor holding the sidecar can bind the slice, and binds the certificate's issuance event by id. The bundle is explicitly not a second source of truth, and `verify-certificate` remains the check against live Loop state. Issue closure: `dev` is not the default branch, so `.github/workflows/issue-promotion.yml` closes #589 on promotion. ## Release changelog Topic PR: the fragment is `changes/feat-0.3.0-589-preflight-evidence-bundle.md` (Category `added`, Audience `operators, integrators, auditors`, Breaking-Change `no`). ## Proof - [x] `python scripts/agent/check-change.py --base dbb1c32 --head-branch feat/0.3.0-589-preflight-evidence-bundle --build-dir build-local` — report attached in the comments of this PR - [x] One `changes/feat-0.3.0-589-preflight-evidence-bundle.md` fragment added - [x] Changed behaviour has a test that fails without the change (mutation-checked below) - [x] Protected paths touched: `LoopLibCore/sources/**`, `docs/schemas/**`, `UnitTests/CMakeLists.txt` is **not** touched (no new target added, an existing mapped target was extended) ## Internal logic (touched behavior-bearing code) - [x] Guard clauses handle invalid, stale, cancelled, absent, unauthorized, and terminal cases before the happy path: the exporter refuses a missing report, a report describing other bytes, a certificate or sign-off binding a different revision or profile, an absent operation-history sidecar, and a chain that does not verify from sequence 1; the verifier refuses an absent/unsupported manifest, a missing or undeclared member, a member whose bytes disagree with the manifest, and every identity disagreement - [x] Untrusted input is parsed once at the boundary into trusted typed or domain state: member bytes are hashed and compared against the manifest in one pass; the exported history is parsed into `PDFOperationHistoryEvent` once and then chain-verified field by field - [x] Invalid state stops before partial mutation or publication: `writePreflightEvidenceBundle` refuses a non-empty directory and publishes `manifest.json` last, so a directory carrying a manifest is a complete bundle - [x] Names carry the domain intent; comments explain rationale (why the chain is re-hashed, why JSON pointers survive redaction, why the certificate head is bound by id rather than by position) ## Anti-slop pass - [x] Redundant or explanatory comments that do not match the file's style removed - [x] Abnormal defensive checks removed: the verifier does not re-derive what the manifest already declares; it checks agreement instead of re-parsing - [x] No cast added to suppress a type error - [x] Python imports stay at file scope - [x] Generated boilerplate and needless wrappers removed (the export/verify pair shares one `.cpp`; the bundle format constants are functions rather than a duplicated literal set) Anti-slop summary: the exporter is one build function plus one writer, and the verifier is one pass that records a refusal instead of accumulating a verdict from an aggregate — the semantic-trust source guard rejects aggregate verdict derivation, and the first draft of `verification.valid = verification.findings.isEmpty()` was replaced with a refusal-driven state rather than renamed around it. ## Security and rollback - [x] Untrusted input validated at the trust boundary: bundle members are untrusted input to the verifier, so the member name is restricted to a plain bundle-local name, the declared member set is compared against the directory contents, and no member content is executed or dereferenced - [x] Rollback: revert the two commits (`0316d4f2`, `e63c0b66`); the surface is additive (`export-evidence-bundle`, `verify-evidence-bundle`, one Core header) and no existing command, schema kind, or persistence format changes ## Docs - [x] `docs/PREFLIGHT_EVIDENCE_BUNDLE.md` (new: export/verify pair, bundle layout, finding codes, the two properties a reader must not over-read), cross-referenced from `docs/CERTIFIED_PREFLIGHT.md`, `docs/PROVENANCE_EVENT_CHAIN.md`, `docs/INDEPENDENT_VALIDATION.md` and `docs/PDFTOOL_CLI_CONTRACT.md`; the independent-validation evidence schema gained the `bundle` block ## Self-review (BSP-002 §4.3) - [ ] Reviewed in the diff view, not the editor, at least 30 minutes after the final commit; overnight if the change touches security-sensitive code, data handling, or public API surface ### Non-vacuity (mutation) evidence Each guard was broken, rebuilt, and observed failing, then the source was restored byte-exactly (`cmp` clean): | Mutation | Observed failure | | --- | --- | | stop dropping the source-path keys | `bundle.source-path-present [report.json] The exported report still carries the source path key 'pdf'.` and all three bundle slots red | | stop redacting path-shaped free-form text | `bundle member 'report.json' carries 'C:/…/artwork.pdf'` | | stop comparing member digests against the manifest | `member.digest-mismatch` no longer reported for a same-size edit | ### Local results at the commit - `UnitTestsPreflightVerdict`: 43 passed, 0 failed (3 new slots) - `UnitTestsPdfToolContract`: 30 passed, 0 failed (2 new slots) - `python -m unittest scripts.qualification.test_run_independent_validators`: 12 passed - `scripts/ci/check_trust_contract_sources.py`, `check_source_integrity.py`, `check_loop_identity.py`, `validate_product_surface.py`, `generate-architecture-catalogs.py --check`, `test_correction_operation_catalog.py`: pass - `clang-format --dry-run --Werror`: 0 violations on every changed C++ source (the two test files were already dirty at base, so their reformat is a separate `style:` commit, verified whitespace-only by comparing `tr -d '[:space:]'` against the base blobs) <!-- codesmith:footer --> --- <a href="https://app.blacksmith.sh/studio-berry/codesmith/loop/pr/657?ref=codesmith_pr_footer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img alt="View with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a> <a href="https://backend.blacksmith.sh/track/enable-autofix?expires=1792484222&installation_model_id=435800&pr_number=657&ref=codesmith_pr_footer&repository=studio-berry%2Floop&return_to=https%3A%2F%2Fgithub.com%2Fstudio-berry%2Floop%2Fpull%2F657&signature=38aaee7efc7cc5405aa64405088f0b1ccd63e7c60bd6a7c968106721bb450223"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img alt="Autofix with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a> <sup>Need help on this PR? Tag <code>@codesmith-bot</code> with what you need. Autofix is disabled.</sup> <!-- codesmith:autofix:disabled --> <!-- /codesmith:footer -->
# Conflicts: # PdfTool/pdftoolabstractapplication.h
…#669) <!-- Berry Studio BSP-002 §4.2, adopted for this repository. Loop's own gates live in AGENTS.md. --> ## What changed Issue [#589](#589) asks for a portable proof-of-preflight handoff, and its first acceptance criterion is that a *documented export/verify pair* produces and validates the artifact with the CLI. The exporter that landed on [#657](#657) (`PdfTool export-evidence-bundle`) requires a preflight report file, but nothing on the CLI could write one: `preflight` emitted the report only inside its stdout envelope, so the documented workflow needed a hand-extracted report — the CLI test in #657 does exactly that extraction in C++. This PR gives `preflight` the missing producer, matching the `--report-file` that `repair` has had since #370: ```text PdfTool preflight --profile profile.json --report-file preflight-report.json --certify certificate.json document.pdf PdfTool export-evidence-bundle document.pdf --report preflight-report.json --output handoff-bundle PdfTool verify-evidence-bundle handoff-bundle ``` `--report-file` writes the report in its **retained form** — `redactSensitiveJson` over `preflightAuditReportSummary`, i.e. byte-for-byte the payload `PDFOperationHistoryStore` stores and `issuePreflightCertificate` hashes — so a consumer can bind the file to a certificate's `report_digest` without re-running the document. Writing the envelope's own rendering instead would hand out a report that no certificate digest covers. The JSON envelope still reports the run's own view; the file is registered as an extra envelope output (`outputs[]`, `{"kind": "file", "role": "report", "state": "written"}`). Two commits: `aa2a29ed` (whitespace-only clang-format of the touched test source, which carried five violations at `dbb1c324` — the per-file format gate has no clean-at-base exemption) and `4392027a` (the feature, its test, the docs and the changelog fragment). `Refs #589`. `dev` is not the default branch, so this PR does not close the issue: the promotion workflow closes it once the bundle work (#657) and this producer are on a promoted branch. No issue is closed by hand here. ## Release changelog Topic PR: fragment is `changes/feat-0.3.0-589-preflight-report-file.md` (Category `added`, Audience `operators, integrators`, Breaking-Change `no`). No promotion changelog in this PR. ## Proof - [x] `python scripts/agent/check-change.py --base origin/dev --head-branch feat/0.3.0-589-preflight-report-file --build-dir build-local` — `pass`: ```json { "status": "pass", "risk": "standard", "checks": "20/20 pass", "modules": ["documentation", "pagemaster", "pdftool"], "targets": ["PdfTool"], "tests": ["UnitTestsOcrCli", "UnitTestsPageMasterExport", "UnitTestsPdfToolContract"], "protected_paths": [], "comparison_base_sha": "dbb1c324…", "head_sha": "4392027a…" } ``` The 20 checks are `changelog`, `source_integrity`, `architecture_catalog`, `policy_adapters`, `preflight_truth_source`, `qml_mirror_parity`, `search_budget_gate`, `independent_validation_gate`, `qt_test_runtime`, four `format:*`, two `clang_tidy:*`, `build:PdfTool` plus the three test targets, and `focused_tests` (`100% tests passed, 0 tests failed out of 3`) - [x] One `changes/feat-0.3.0-589-preflight-report-file.md` fragment added (Category, Audience, Breaking-Change, Summary) - [x] Changed behaviour has a test that fails without the change (evidence below) - [x] Protected paths: **none touched.** `PdfTool/**`, `UnitTests/tst_pdftoolcontract.cpp`, `docs/CERTIFIED_PREFLIGHT.md` and `changes/**` are all outside `agent-policy.json`'s `protected_paths` (which lists `LoopLibCore/sources/**`, `UnitTests/CMakeLists.txt`, `docs/schemas/**`, `loop-preflight/schemas/**`, the root `CMakeLists.txt` and the vcpkg files). No contract or schema changes. ### Failing first (RED) ```text FAIL! : PdfToolContractTest::preflightWritesTheCanonicalReport() Compared values are not the same Actual (run.exitCode) : 2 Expected (expectedExitCode): 0 tst_pdftoolcontract.cpp(89) : failure location FAIL! : PdfToolContractTest::preflightWritesTheCanonicalReport() 'QFile::exists(reportPath)' returned FALSE. Totals: 2 passed, 1 failed, 0 skipped, 0 blacklisted, 250ms ``` and the invocation that produced it, straight from `PdfTool`: ```json {"status": "invalid-invocation", "exit_code": 2, "diagnostics": [{"code": "cli.invalid-arguments", "message": "Unknown option 'report-file'.", "severity": "error"}]} ``` ### Passing after (GREEN) ```text UnitTestsPdfToolContract: Totals: 29 passed, 0 failed, 0 skipped, 0 blacklisted ``` The slot is not satisfied by "a file appeared": it asserts the written report's `document_revision_digest` equals the SHA-256 of the document, that the effective-profile digest and coverage scope are present, that the verdict matches the envelope's, and that `pdf::canonicalJson(file)` equals `pdf::canonicalJson(<accepted PreflightRun event>.resultSummary)` from the operation-history sidecar — i.e. the file is the retained payload, not a second rendering of the run. ### Probe against the built binary (Windows/MSVC, Qt 6.11.1, `build-local`) `PdfTool preflight artwork.pdf --profile profile.json --report-file preflight-report.json --certify certificate.json --console-format json`: ```text preflight exit: 0, envelope status: success outputs: [{"kind": "file", "path": "…\\preflight-report.json", "role": "report", "state": "written"}] report bytes: 3806 report document_revision_digest == certificate document_revision_digest: True report effective_profile_digest == certificate effective_profile_digest: True report pdf entry: …\loop-589-probe\artwork.pdf capabilities reports report-file: {"names": ["--report-file"], "value_name": "file", "value_type": "path", "required": false, "sensitive": false} ``` The two `True` lines are the binding the change exists for: the file a run writes is the payload the certificate issued in that same run covers. The `pdf` line is the caveat the docs now state. ## Internal logic (touched behavior-bearing code) - [x] Guard clauses before the happy path: the write runs only when `--report-file` is non-empty, and a failed open, short write or failed commit returns `ProcessingFailure` with `output.write-failed` and the OS error text instead of reporting success - [x] Untrusted input parsed once at the boundary: the report object already exists (`auditSummary`); the change neither re-parses nor re-derives it, it only re-inserts it as a file - [x] Invalid state stops before partial publication: the write uses `QSaveFile`, so a failure leaves no partial file at the operator's path (`cancelWriting`), and it precedes the `--certify` block so certification never runs after a report-write failure. One ordering consequence is deliberate and stated in the commit body: the operation-history event is appended before this write, so a failed write leaves a correct chain and no report file — the fail-closed direction - [x] Names carry domain intent (`preflightReportPath`, `retainedReport`) and the comment explains *why* the redacted summary is written rather than restating the code ## Anti-slop pass - [x] No explanatory comments added beyond the one rationale comment (why the retained form, not the envelope view) - [x] No abnormal defensive checks: the new guard is a real boundary (filesystem write), and no check duplicates `QSaveFile`'s own failure reporting - [x] No cast added to suppress a type error - [x] Python imports: none added (the edit scripts used to keep CRLF byte-exact live outside the repository) - [x] No needless wrapper or boilerplate: the option reuses the existing `PreflightProfile` flag group and the existing `PDFToolOptions`/descriptor machinery rather than introducing a new flag group Anti-slop summary: the change is one option registration plus one guarded write; nothing was abstracted to make it look smaller, and the write reuses Core's own `redactSensitiveJson` rather than reimplementing redaction in `PdfTool`. Kept on purpose: the explicit `#include "pdfartifactidentity.h"` (the function is used directly here and was previously only reachable transitively). ## Security and rollback - [x] Untrusted input validated at the trust boundary: `--report-file` is an operator-supplied path passed through `QSaveFile`; the option is registered with `PDFToolValueType::Path` in the capability descriptor so `PdfTool capabilities` publishes it as a path, and no new unsafe construct is introduced - [x] Data handling: the file reproduces what the local history sidecar already stores for this document. It is not path-stripped — the retained report identifies the revision by digest and its `pdf` entry records the path the run was invoked with, which `docs/CERTIFIED_PREFLIGHT.md` now states — so operators who hand the report to a third party derive the same disclosure they already had from the sidecar. The evidence bundle exporter (#657) drops that key when it exports - [x] Rollback: revert `4392027a` (the feature) and `aa2a29ed` (the whitespace-only format commit); the surface is additive — one option, one diagnostic path, one extra output record. No existing command, schema kind or persistence format changes ## Docs - [x] `docs/CERTIFIED_PREFLIGHT.md`: the certificate example now includes `--report-file`, with the retained-form property and the `pdf` caveat stated - [x] `changes/feat-0.3.0-589-preflight-report-file.md`: the fragment - [x] `docs/PDFTOOL_CLI_CONTRACT.md`: none needed — it specifies the envelope, exit codes, diagnostics and `data` payloads, and does not enumerate a command's options; `PdfTool capabilities` publishes this one from the descriptor registry at runtime - [x] `docs/PREFLIGHT_EVIDENCE_BUNDLE.md`: none possible in this PR — that file arrives with #657, so this PR deliberately does not link to it (a link to a file that does not exist on `dev` would be a dead link). The cross-reference belongs to whichever of the two PRs lands second ## Relationship to PR #657 (same issue) #657 is open and green with the bundle feature; this PR is the producer its documented pair needs. One defect found while reviewing #657 cannot be fixed here: `docs/PREFLIGHT_EVIDENCE_BUNDLE.md:144` names `run_independent_validators.py --bundle`, while the script's flag is `--evidence-bundle`. That file does not exist on `dev`, so the one-line fix belongs on #657's branch, which this PR does not touch (its review clock stays intact). Flagged here so it is not lost. ## Self-review (BSP-002 §4.3) - [ ] Reviewed in the diff view, not the editor, at least 30 minutes after the final commit; overnight if the change touches security-sensitive code, data handling, or public API surface <!-- codesmith:footer --> --- <a href="https://app.blacksmith.sh/studio-berry/codesmith/loop/pr/669?ref=codesmith_pr_footer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img alt="View with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a> <a href="https://backend.blacksmith.sh/track/enable-autofix?expires=1792556695&installation_model_id=435800&pr_number=669&ref=codesmith_pr_footer&repository=studio-berry%2Floop&return_to=https%3A%2F%2Fgithub.com%2Fstudio-berry%2Floop%2Fpull%2F669&signature=40d8ae97edb63ba8c8125636adec167560836ae68434d43a10d92c2b2384b5e7"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img alt="Autofix with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a> <sup>Need help on this PR? Tag <code>@codesmith-bot</code> with what you need. Autofix is disabled.</sup> <!-- codesmith:autofix:disabled --> <!-- /codesmith:footer -->
Summary
Promotes dev into unstable as the current integration candidate.
This PR aggregates 162 commits across 375 files and advances the 0.3.0
architecture across preflight execution, governed repair, provenance,
schema evolution, evidence generation, operator workflows, fuzzing,
recovery, and CI qualification.
Technical scope
Preflight architecture
* Extends PreflightEngine, verdict reduction, audit generation, profile
resolution, and finding production.
* Adds versioned preflight report/schema fixtures and generated
preflight check catalogs.
* Adds restriction-aware execution across page, region, layer,
object-class, and page-box scopes.
* Adds revalidation semantics for post-repair state transitions.
* Expands certified-preflight and provenance-linked evidence generation.
Governed correction pipeline
* Expands repair operation definitions, parameter schemas, operation
history, and impact analysis.
* Adds object-selection primitives and governed execution boundaries.
* Adds generated correction-operation catalog validation.
* Extends bleed repair, production geometry, page-master export,
recovery, and repair-diff infrastructure.
* Adds action-list execution across core, interaction, editor, and CLI
layers.
Evidence and provenance
* Adds portable preflight evidence bundles with manifest-based member
integrity verification.
* Adds candidate-revision binding via SHA-256 digest checks.
* Rejects undeclared, missing, tampered, or revision-mismatched bundle
members.
* Extends provenance-event, evidence-graph, and operation-result schema
coverage.
* Strengthens source-integrity and trust-contract validation.
Incremental save and signature preservation
* Adds signed PDF fixtures for incremental-save regression testing.
* Adds independent qpdf structural validation and pdfsig signature
validation.
* Adds CI evidence generation for signed incremental-save preservation.
* Verifies that appended revisions preserve the original signed byte
range.
CLI and schema surface
* Extends PdfTool commands for preflight, repair, action lists, evidence
bundles, certificate verification, and schema inspection.
* Adds schema-version and compatibility handling.
* Adds versioned fixtures for reports, certificates, action lists,
operation plans/results, provenance events, package manifests, and
capability discovery.
* Expands CLI contract and acceptance coverage.
Editor and interaction layer
* Extends preflight, inspection, action-list, pages/production, and
production-preview surfaces.
* Adds finding navigation and overlay integration.
* Adds state visualization and preflight profile draft handling.
* Extends operator-facing governed repair and inspection workflows.
* Adds recovery/restore integration.
Fuzzing and malformed-input coverage
* Adds seeded corpora for:
* PDF parser
* content-stream processor
* stream-filter decoders
* Adds malformed fixtures covering:
* truncated xref tables
* cyclic page trees
* generation mismatches
* malformed object streams
* unsupported encryption filters
* non-PDF input
* Enforces corpus manifest ownership, checksums, and per-harness seed
coverage.
* Marks Fuzz/corpus/**/*.bin as binary to prevent line-ending
transformation.
CI and qualification
* Adds CI gates for correction-operation catalogs and preflight coverage
catalogs.
* Adds Linux independent-validation qualification for signed incremental
saves.
* Adds Windows Qt runtime validation and explicit runtime-path setup for
test executables.
* Adds governed-parity, source-integrity, trust-contract, and
workflow-contract checks.
* Adds issue-promotion automation for branch-stage tracking.
* Extends generated architecture artifact validation.
Verification focus
For this promotion, the primary review surface is integration
correctness rather than isolated feature review.
Validate:
* Linux and Windows qualification pipelines remain green.
* Generated catalogs are consistent with their source implementations.
* Preflight verdicts, provenance, and evidence outputs remain
deterministic.
* Repair operations trigger the expected revalidation scope.
* Evidence bundles fail closed on integrity or revision mismatch.
* Signed incremental saves preserve existing signatures.
* Versioned schema contracts remain backward-compatible where required.
* CLI and editor paths produce equivalent governed execution semantics.
* Fuzz and malformed-input paths terminate safely without hangs or
uncontrolled resource use.
* No branch-specific or development-only state is introduced into
unstable.
Add-LoopQtRuntimeToPath is Windows-only; Linux Release Gate already exports Qt via LD_LIBRARY_PATH. Unblocks run-quick-backend-smoke.ps1. Co-authored-by: michael berry <mberrys@users.noreply.github.com>
UnitTestsBleedFixup used QTEST_APPLESS_MAIN, so PDFBleedFixup::apply aborted in the raster path on the Windows widgets-absent release build. QTEST_MAIN supplies a QGuiApplication without linking Widgets. Drop the unused storage local that MSVC reported as C4189. Co-authored-by: michael berry <mberrys@users.noreply.github.com>
loop-release surface verification rejects capability commands that are absent from docs/product-surface.json. Register export-evidence-bundle, verify-evidence-bundle, schema, verify-certificate, and the preflight-profile export, fork, and validate commands. Co-authored-by: michael berry <mberrys@users.noreply.github.com>
windows-latest configures with the Visual Studio generator, which omits CMAKE_CXX_COMPILER. The guard treated that cache as a non-Windows build and failed Release Gate after the Windows tests had already passed. Co-authored-by: michael berry <mberrys@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Release changelog
Proof
python scripts/agent/check-change.py --base origin/dev --build-dir build-localreportspass, or the report and the failing check are quoted herechanges/<sanitized-head-branch>.mdfragment added (Category, Audience, Breaking-Change, Summary)Internal logic (touched behavior-bearing code)
Anti-slop pass
anyor equivalent cast added only to suppress a type errorAnti-slop summary (1-3 sentences):
Security and rollback
Docs
Self-review (BSP-002 §4.3)
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.