Skip to content

L04-01: reconcile the operation registry and canonical plan contract (#33) - #208

Merged
mberrys merged 4 commits into
devfrom
l04-01-registry-plan-contract
Oct 5, 2026
Merged

mberrys merged 4 commits into
devfrom
l04-01-registry-plan-contract

Conversation

@mberrys

@mberrys mberrys commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

L04-01 (#33), the first slice of the L04 Governed Operation Engine stack (8 slices, one per issue; refs #5). It makes PDFRepairRegistry the single registration authority, binds a versioned registry digest into the canonical operation-plan digest, and closes the four failure gates before candidate computation: duplicate registration, unknown operation, ambiguous parameter, stale revision.

Stack position

Slice 1 of 8. Base: dev. Slices 2-8 (l04-02 ... l04-08) are stacked on this branch; land bottom-up so each child merges cleanly.

Per-criterion: already satisfied vs built here

Acceptance criterion State Proof
One unambiguous registration resolution already satisfied; enforcement built here registerOperation refused silently replacing (insert_or_assign); now refuses null/empty/duplicate, first wins; both registration TUs assert. duplicateRegistration_isRefused
Stable cross-platform plan digest partially satisfied; built here PDFRepairRegistry::digest() + registry_digest in the operation-plan envelope; pinned golden vector UnitTests/testdata/canonical-json/operation-plan-envelope.json + expected-plan-digests.json; planDigest_matchesPinnedGoldenVector
Failure: duplicate registration built here duplicateRegistration_isRefused
Failure: unknown operation behavior already satisfied; mapped regression added unknownOperation_isRefusedBeforeCandidate
Failure: ambiguous parameter partially satisfied (action list only); built here shared validateJsonSchemaFragment used by PDFRepairTransaction::add(); repeated CLI --param refused. ambiguousParameters_areRefusedBeforeAnalyze, repairRefusesRepeatedParameterAssignment
Failure: stale revision built here PDFRepairTransactionOptions::expectedSourceSha256 checked at the top of analyze(); staleRevision_isRefusedBeforeAnalyze
Migration decision recorded legacy addbleed/rgbtocmyk stay registry-metadata consumers; convergence deferred to L04-05 (#37). docs/GOVERNED_EXECUTION.md

Required proof

  • python scripts/agent/check-change.py --base e1a14184 --head cbad5614 --head-branch l04-01-registry-plan-contract --build-dir <loop-build-l04> -> status: pass, 83/83 checks (re-run after the third commit and after the stack was rebased onto the current dev) (builds LoopLibCore/PdfTool/loop-pdf-worker, 52 mapped suites, clang-tidy on all changed sources, format, changelog, architecture contracts).
  • Flake note, measured not argued: the first run failed focused_tests on UnitTestsPdfWorkerIsolation only (208.8 s, vs 145-159 s when green). Standalone reruns of that suite at the same head: 2/2 pass; the same suite passed in the concurrent batch proof at the base tree. A timing-sensitive fault-injection suite (135-150 s windows), measured flake rate 1 in 4 today; the re-run proof is fully green.
  • Focused suites run during implementation: UnitTestsRepairOperation / UnitTestsGovernedExecution / UnitTestsPdfToolContract / UnitTestsActionList / UnitTestsEditorHost / UnitTestsProductOperatorLoop -> 5/5 pass.
  • clang-format --dry-run --Werror: clean on every touched C++ file.
  • Architecture: check-architecture.py (static + --base 35ff26fc diff mode), test_architecture_contracts.py, test_correction_operation_catalog.py, generate-architecture-catalogs.py --check -> all green.
  • Pinned values confirmed by committed tests: registry_digest c05cab19c20096d2208463e4159884c1d350f9c8accc5e9144086ac1f5371315; plan/envelope digest d712302a4682021ff3d7a1c9d214cdc0a7a4041b4651138be670c51e3846e711.
  • Skipped locally: packaging linux/windows lanes (hosted CI); the authoritative lane is CI agent-fast on this head.

Notes

  • Closes #33 (issue closes on the promotion merge; this PR lands on dev).
  • The second commit is a review finding, confirmed in source: the lifted validator recorded required/additionalProperties violations without failing, so the Action List planner accepted unknown keys. It now fails on any recorded violation, pinned by validateJsonSchemaFragment_reportsStructuralViolations and rejectsUnknownAndMissingStepParameters.
  • The third commit is an independent-audit finding, confirmed in source and falsified as a claim: valuesEqual serialised both sides through QJsonValue::toObject(), which yields an empty object for any non-object value, so every pair of scalars compared equal and the enum branch accepted any correctly-typed value (an add-bleed mode of stretch passed an enum of mirror/pixel-repeat). The earlier "unreachable because every enum schema carries a type check" justification was wrong: the type check only rejects wrong-typed values. It now compares typed values, pinned by validateJsonSchemaFragment_rejectsValuesOutsideTheAllowedSet (measured red on the pre-fix validator, green after).
  • Breaking-change: no.

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

…into the plan digest (#33)

Make PDFRepairRegistry the single registration authority: registerOperation now
returns PDFOperationResult and refuses a null operation, an empty id, and a
duplicate id (first registration wins). The two registration units capture the
result and Q_ASSERT it so a debug build fails loudly. A public default
constructor is the isolated-registry test seam; instance() stays the production
singleton.

Bind registry identity into the plan contract: PDFRepairRegistry::digest() is
the SHA-256 of the canonical {id, version} set, and computeOperationPlanDigest
adds it as registry_digest to the operation-plan envelope, so a registry change
moves every plan digest and invalidates bound approvals. Envelope
schema_version stays "1.0": additions are non-breaking.

Close the four failure gates before candidate computation:
- duplicate registration is refused instead of silently replaced;
- unknown operation stays refused at find()/add(nullptr);
- ambiguous parameters are refused by PDFRepairTransaction::add() through the
  shared validateJsonSchemaFragment() (lifted from pdfactionlist.cpp, which now
  delegates to it), and PdfTool repair refuses a repeated --param key;
- PDFRepairTransactionOptions::expectedSourceSha256 refuses a stale source
  revision at the top of analyze() before any operation runs.

Tests: UnitTestsRepairOperation gains duplicate-registration,
unknown-operation, ambiguous-parameter and stale-revision slots;
UnitTestsGovernedExecution gains registry-digest format/stability and a plan
digest pinned to the committed operation-plan golden vector (envelope test
updated for registry_digest); UnitTestsPdfToolContract gains the repeated
--param refusal with a positive control. Fixtures: canonical-json/
operation-plan-envelope.json plus expected-plan-digests.json pin the
cross-platform plan-envelope bytes.

Verified locally (MSVC + Qt 6.11.1, Ninja, C:/.dev/repos/loop-build-l04):
- cmake --build --target LoopLibCore PdfTool UnitTestsRepairOperation
  UnitTestsGovernedExecution UnitTestsPdfToolContract UnitTestsActionList: ok
- ctest -R '^(UnitTestsRepairOperation|UnitTestsGovernedExecution|UnitTestsPdfToolContract|UnitTestsActionList)$':
  4/4 passed (75.7 s)
- clang-format --dry-run --Werror on every touched C++ file: clean
- python scripts/agent/check-architecture.py (static): ok
- python scripts/agent/test_architecture_contracts.py: OK (1 skipped)
- python scripts/ci/test_correction_operation_catalog.py: OK
- python scripts/generate-architecture-catalogs.py --check: ok

Skipped lanes: packaging:linux-build / packaging:windows-build are hosted CI;
the mapped check-change proof runs at the coordinator. UnitTestsActionList ran
as a manual regression check (target not mapped in agent-policy).

Migration decision (recorded in docs/GOVERNED_EXECUTION.md): legacy pdftool
addbleed/rgbtocmyk stay registry-metadata consumers; convergence is L04-05
(#37) scope.
…33)

validateJsonSchemaFragment recorded required/additionalProperties violations
without failing, so a caller that consumes only the bool (the Action List
planner) accepted an unknown key or a missing required parameter. The lifted
behaviour is preserved everywhere else; the structural branches now fail the
result, and PDFRepairTransaction::add() refuses on that single contract again.

Tests: a validator-contract slot in UnitTestsRepairOperation (missing required,
unknown key, nullptr error sink, valid control) and an Action List slot in
UnitTestsActionList (unknown key + missing required refused on a recipe).
UnitTestsRepairOperation / UnitTestsActionList / UnitTestsGovernedExecution /
UnitTestsEditorHost / UnitTestsProductOperatorLoop: 5/5 passed.
@mberrys
mberrys force-pushed the l04-01-registry-plan-contract branch from 3d7649f to 3effd51 Compare October 5, 2026 07:14
…heck (#33)

valuesEqual serialised both sides through QJsonValue::toObject(), which yields an empty
object for any non-object value, so every pair of scalars compared equal and the enum
branch accepted any correctly-typed value (an add-bleed mode of "stretch" passed an enum
of mirror/pixel-repeat). Compare the typed values instead, and pin the refusal with
validateJsonSchemaFragment_rejectsValuesOutsideTheAllowedSet.

Measured: the new slot fails on the pre-fix validator and passes with the fix;
UnitTestsRepairOperation 34/34, UnitTestsActionList cli-parity/dry-run slots pass.
@mberrys
mberrys force-pushed the l04-01-registry-plan-contract branch from bd9d12d to cbad561 Compare October 5, 2026 13:00
@mberrys
mberrys merged commit f2e494d into dev Oct 5, 2026
17 checks passed
@mberrys
mberrys deleted the l04-01-registry-plan-contract branch October 5, 2026 19:50
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.

1 participant