Skip to content

Feat/0.3.0 239 save policy strength - #582

Merged
cursor[bot] merged 27 commits into
devfrom
feat/0.3.0-239-save-policy-strength
Sep 17, 2026
Merged

cursor[bot] merged 27 commits into
devfrom
feat/0.3.0-239-save-policy-strength

Conversation

@mberrys

@mberrys mberrys commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

mberrys and others added 27 commits September 13, 2026 23:50
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.
…yk (#239)

add-bleed and rgb-to-cmyk wrote their candidate to the path the caller named
with no policy guard, so `add-bleed received.pdf --output received.pdf
--overwrite` rewrote the trusted input even though both operations declare
saveAsNewArtifact. Move the request building and the refusal into one shared
PDFToolAbstractApplication::validateOperationSaveRequest, route redact's inline
block through it, and call it from add-bleed and rgb-to-cmyk with the policy
their registry entry declares. A missing registry entry keeps the command
working through the explicitly-undeclared policy. The guard runs before the
document is read and before validateDestructiveOutput, so the refusal is not
conditional on --overwrite and also covers --dry-run and --report.

PdfTool/pdftoolrgbtocmyk.cpp was already dirty at HEAD for clang-format
(case-labels, brace lists, namespace comments); formatted in this commit, no
semantic change.
Cover add-bleed and rgb-to-cmyk the way redact is covered: the input path passed
as its own output is refused with exit 4 and `save-policy.refused`, the Core
message is the pinned trusted-input text, no output is recorded and the input
stays byte-identical. The positive control in each slot re-runs the same
invocation against a distinct output path and requires exit 0, so the guard
cannot be satisfied by refusing the command outright.

Both slots fail when the add-bleed guard is deleted (verified by mutation).
The reusable-linux.yml job now installs qpdf and poppler-utils, runs
UnitTestsIncrementalSave with LOOP_SAVE_POLICY_EVIDENCE_DIR set so the
signed incremental artifact is emitted, runs
scripts/qualification/run_independent_validators.py with --claim structural
and --claim signature, asserts inline that the run is passed and that every
per-claim status is passed, and uploads the evidence JSON as the
save-policy-independent-validation workflow artifact.

The passing run is produced by the reusable-linux.yml job (step "Prove
signed incremental save with independent validators") and is pending the
first dispatch; CI cannot commit the evidence itself. This commit therefore
records the honest local run at
docs/evidence/session-15-save-policy/independent-validation-local.json
(status incomplete, reason_code validator-not-installed for both claims),
which never counts as a pass per docs/INDEPENDENT_VALIDATION.md.

docs/INCREMENTAL_SAVE.md no longer claims the repository carries no signing
fixture: the fixture, its generator, the byte-range and cost slots, and the
CI-owned structural/signature claims are named.
Pinned invariant, not a defect fix: nothing in the tree sets an approval kind
or a rollback approved-output flag today, so the new
OperationHistoryTest::noSavePathProducesAnApprovedOutputRecord slot passes on
first run and guards the 0.3.0-B approval workflow (LOUPE-53). Before this
commit the suite had 16 slots and none of them named the invariant.

Two plan snippets were wrong against the code and are corrected here (the
assertions are stronger than the plan's, not weaker):

- PDFOperationHistoryEvent has no operationId member (it belongs to
  PDFOperationHistoryExecution, pdfoperationhistory.h:100), so that line is
  dropped; the operation identity stays pinned on
  PDFRollbackPoint::operationId, which does exist.
- QVERIFY(!approval.isValid()) is false:
  PDFApprovalRecord::isValid() means "well formed", and
  PDFOperationHistoryStore::appendEvent refuses any event whose approval is
  invalid (pdfoperationhistorystore.cpp:381), so the default record must be
  valid. The pin is therefore kind == None plus every approval field empty,
  plus a positive control that a record claiming a Human decision without
  actor, decision or decision time is invalid. The gate that keeps a default
  record out of the rollback path is isValid() && kind != None
  (pdfoperationhistorystore.cpp:790).

Mutation evidence for non-vacuity: flipping PDFRollbackPoint::approvedOutput's
default to true fails exactly this slot ('!point.approvedOutput' returned
FALSE, 16 passed / 1 failed) and nothing else; reverted, the suite is
17 passed, 0 failed, 0 skipped.
docs/EDITOR_RECOVERY.md described a recovery service that is not in the tree.
It now states that 2a19e2c deleted LoupeLibGui/pdfrecoverymanager.{h,cpp}; that
the rest of the document is the contract to restore, not shipped behaviour;
that UnitTests/tst_recoverytest.cpp compiles against a header that does not
exist and is registered in no CMake target, so it neither compiles nor runs;
that the approval half of the 0.3.0-A criterion is pinned by
UnitTestsOperationHistory::noSavePathProducesAnApprovedOutputRecord; and that
the restore half is tracked by #575.

UnitTests/tst_recoverytest.cpp gains a // NOT COMPILED: header comment under the
license block so the orphan cannot be mistaken for a running test. The file is
kept as the specification and is deliberately still not added to a CMake target.

python scripts/ci/check_source_integrity.py: "Source integrity policy passed."
(exit 0).
Merge origin/dev into feat/0.3.0-239-save-policy-strength to unblock PR #582.
Keep both save-policy test mappings (UnitTestsRedactVerifier from #582) and
dev additions (UnitTestsRepairDiff, UnitTestsRepairOperatorAcceptance,
UnitTestsPageBoxCorpus). Auto-merged dev release 0.2.1-alpha and page-box
corpus work without touching save-policy write-boundary code.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Regenerate docs/generated/phase5-widgets-inventory.json so the Supply Chain
Policy check passes after merging origin/dev (CMake target graph drift).

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
UnitTestsBleedStress parsed stdout as a flat preflight report, but PdfTool
now emits schema_version 1 envelopes with the report under data.report.
Align runPreflight() with OperatorAcceptance and PreflightCorpus helpers
so failBleedPreflight sees check_id bleed again. No save-policy change.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Co-authored-by: michael berry <mberrys@users.noreply.github.com>
@cursor
cursor Bot merged commit 137edee into dev Sep 17, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants