Skip to content

[0.2.1] RC | Preflight & GUI Foundation - #554

Draft
mberrys wants to merge 59 commits into
unstablefrom
dev
Draft

[0.2.1] RC | Preflight & GUI Foundation #554
mberrys wants to merge 59 commits into
unstablefrom
dev

Conversation

@mberrys

@mberrys mberrys commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

No description provided.

claude and others added 30 commits September 5, 2026 20:41
Adds pdfquick::tokens (LoopLibQuick/sources/looptokens.h/.cpp): semantic
spacing and colour-role tokens with dark/light/high-contrast values, each
foreground/background pair WCAG contrast-checked. Adds resolveStateVisual()
(loopstatevisual.h/.cpp), the canonical finding/check presentation mapping
issue #194 asks for, with a table-driven test (UnitTestsLoopStateVisual)
asserting an incomplete check and an actively-waived finding never resolve
to the passed treatment. Documents both in docs/LOOP_DESIGN_SYSTEM.md,
including why this uses the repo's current Loop/Quick naming rather than
the issue's stale pre-rebrand Pdf4QtLibGui/Loupe paths, and what remains
open (component implementations land with their consuming surfaces #193,

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxoABBuEB3mYqr8QyA6KuB
scripts/ci/check_loop_identity.py fails CI on any tracked text file
containing the pre-rebrand product name outside its historical-evidence
allowlist. The "Naming note" section quoted that name three times while
explaining why this doc uses current naming instead of issue #194's stale
paths; reword it to make the same point without the literal token.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxoABBuEB3mYqr8QyA6KuB
Adding UnitTestsLoopStateVisual to UnitTests/CMakeLists.txt bumped the
project-wide CMake target count the Phase 5 Widgets inventory tracks (its
generator enumerates every target to prove widgets-linkage status, not just
widgets-related ones). Regenerate docs/generated/phase5-widgets-inventory.json
via scripts/generate_phase5_widgets_evidence.py --write (new target's
widgets_linkage is "none", as expected) and update the test's hardcoded
target count (70 -> 71) to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxoABBuEB3mYqr8QyA6KuB
agent-fast's clang-tidy step runs on every changed C++ file regardless of
module classification, but the cmake --build step that generates each
test's AUTOMOC .moc file only runs for targets selected by
classify()/selected_values() against agent-policy.json's module_boundaries
paths. UnitTestsBudgetExhaustion was already listed in core's `tests`, but
its source file, UnitTests/tst_budgetexhaustiontest.cpp, was missing from
core's `paths` glob list, so a change touching only that file never
classified as "core" and the target was never built before clang-tidy ran
on it standalone -- producing "tst_budgetexhaustiontest.moc file not
found". Surfaced by 0aa0d4c's one-line fix to that file. Add the missing
path entry so the target builds first, as it does for every other core
test file already listed there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxoABBuEB3mYqr8QyA6KuB
PDF/X-1a:2001 and PDF/X-3:2002 forbid live transparency, but
standards-convert never called the existing PDFTransparencyFlattener
operation (#164) -- live transparency was an unconditional, unfixable
blocker even though Core already has a working flatten path that
PageMaster's export pipeline uses as a separate stage.

Wire PDFTransparencyFlattener::apply()/hasLiveTransparency() into
PDFStandardConversion::preview()/apply(), mirroring the existing
RGB-to-CMYK integration exactly:

- New PDFStandardConversionSettings::flattenTransparency (default-on
  for X-1a/X-3, matching normalizeColor's existing default pattern;
  opt-in for X-4/PDF-A, which permit live transparency).
- pdfx.transparency.allowed becomes a fixable preflight blocker only
  when flattening is requested, so unrelated fixtures without live
  transparency are unaffected (transparencyObjects stays 0, the rule
  already reports Passed).
- The flatten runs before the output-intent/page-box rewrite and its
  report is surfaced verbatim under a new transparency_flatten report
  field -- a real, reported content change, never a silent
  approximation.
- New flatten_transparency parameter on the standards-convert
  operation, available identically from PdfTool's repair command and
  PageMaster's export job (the one shared Core implementation).

Also correct docs/STANDARD_CONVERSION.md's stale claim that an Editor
adapter can land "after the 0.1.1 GUI gate": that gate is already
complete per docs/LOOP_SHELL_CONTRACT.md, which gates product GUI work
behind the still-closed S21/S22 admission contracts instead. Note that
docs/REPO_MAP.md's LoopEditorPlugins/ module does not exist in the
current Qt-Quick-based tree, so a future Editor adapter belongs under
LoopLibInteraction/ + LoopEditor/qml/.

No Qt/CMake toolchain is available in this environment, so the build
and UnitTestsStandardOracle/UnitTestsConversionOracle/UnitTestsRepairOperation
targets could not be run locally; clang-format, source-integrity, and
architecture-catalog checks all pass. CI will provide the first real
build/test signal for this change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgoNmy614kqRiTBjiSjRJU
Works the net-new findings from the September 2026 read-only review that were
reproducible against the current tree, plus three adjacent defects the review
did not name.

Security / privacy
- PDFLogScrubber scrubs credential material (URL userinfo such as a Sentry DSN,
  HTTP authorization values, secret-named key/value pairs) ahead of the existing
  path and email passes. The bare auth-scheme pass deliberately excludes "Token"
  so parser diagnostics are not redacted as secrets.
- loop-ocr reads a staged raster once by descriptor instead of re-resolving the
  path for isfile(), PIL, and easyocr in turn, closing the TOCTOU window;
  language codes are shape-validated before they reach easyocr's model file
  names; PdfTool stages the raster 0600.
- Diagnostics bundles truncate plugin display fields.

Fail-closed behaviour
- PdfTool extraction commands record output.empty-result and accept a shared
  --fail-if-empty (exit 1, findings) so an empty output directory cannot pass a
  pipeline that gates on produced files.
- writeIncremental reports whether it appended or only byte-copied; damaged
  documents now carry a source digest, so its "file changed underneath us"
  guard is no longer silently disabled for permissively recovered documents.

Bounds on attacker-controlled shapes
- Damaged-document recovery bounds its dense object table by objects recovered,
  not by the highest declared object number.
- PDFNameTreeLoader terminates cyclic Kids chains and caps depth, entry count,
  and key length (net-new: the cycle was unbounded recursion).
- Structure-tree parsing bounds recursion depth on long acyclic chains.
- PDFJBIG2Bitmap::paint validates grown dimensions on its expandY path, the one
  path that escaped the constructor's dimension check (net-new).

Ergonomics
- isPathContained no longer rejects a planned output whose target directory does
  not exist yet, keeping the stricter symlinked-parent rule for the file side.
- makeUniqueFileName probes 128 sequential names, then random ones.
- OCR option defaults are defined once and shared by capability discovery and
  the command-line parser.

Not built or run here: this environment has no Qt, so the C++ changes are
unverified by compilation. The Python sidecar tests pass and the new scrubber
patterns were validated against a reference implementation of the same passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDJQDFR5ctoKDJbp7LG4GS
Whole-file clang-format across LoopLibCore/PdfTool/UnitTests sources to clear format drift flagged by agent-fast. Add moc-generated autogen include dirs to UnitTests target so Qt meta-object headers resolve. Map agent-policy tidy gates to the three active test suites.
Add canonical milestone text for 0.2.1 (patch on the 0.2.0 line), register
GitHub milestone 17 in the sync manifest, and update living-sequence
references across milestone docs and the 0.5.0–0.10.0 roadmap extension.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Replace legacy product tokens in milestone issue summaries, rename the
budget-exhaustion corpus schema kind to loop, and fix workflow
working-directory typos left from the Loupe-to-Loop rebrand.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Manual .moc includes are generated at build time, so clang-tidy cannot
analyze those sources during agent-fast. Exclude them from tidy while
keeping format checks.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
…#144)

Add pdf::PDFBlockingThreadGuard, a runtime thread-affinity check a blocking
service adapter opens with so it refuses to run on the registered
interactive (canvas) thread instead of silently stalling pointer and frame
handling. Wire it into PreflightEngine::run(), the one blocking
implementation already reached through PDFJobScheduler, and register
EditorHost's owning thread as the interactive thread at construction.

Add PDFJobKind to PDFJobTraceEvent so job traces identify async work by
type, and document the interactive-thread boundary (what pointer/frame
callbacks may do directly vs. what must go through a submitted job) in
docs/JOB_SCHEDULER.md.

Wiring an actual UI trigger for interactive preflight runs, and correlating
PDFJobTraceEvent timing against InteractionTraceRecorder's slow-frame
attribution, remain open follow-up work for #144.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xnr9TZYfGr1nMgBeh16CSb
Add pdfinteraction::evaluateTraceContracts() and its TraceContract/TracePhase
enums (LoopLibInteraction/sources/interactiontracecontract.h/.cpp): the
fixed-order pass/fail evaluator issue #146 AC7 asks for. Given an ordered
QList<TraceContractCheck>, it returns the first unsatisfied contract and the
phase responsible, matching scripts/ci/check_interaction_traces.py's
CONTRACTS/PHASES tuples and docs/schemas/interaction-trace-report.schema.json
exactly, so a future harness that supplies the nine checks in order gets AC7
for free. phaseForStage() is docs/INTERACTION_CONTRACT.md's TraceStage-to-phase
attribution table given a type.

The evaluator takes an already-assembled checklist rather than an
InteractionTraceRecorder or a live replay, so it is fully testable with
synthetic checks -- no InteractionController, no scheduler, no hit-test
dispatch -- and is covered by the new UnitTestsInteractionTraceContract target.

The prior gh-146 work (scenario/report schemas, the nine-scenario corpus, and
check_interaction_traces.py) validates the corpus as data but has no test
binary to produce a run; this evaluator is the piece that decides a run's
verdict once one exists. Replaying a scenario through InteractionController,
applying its cost model, and assembling a full report run remain open, as
UnitTestsInteractionTraces/UnitTestsInteractionTracesPresent in
docs/INTERACTION_CONTRACT.md's two-lanes table.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xnr9TZYfGr1nMgBeh16CSb
…readGuard

docs/generated/phase5-widgets-inventory.json went stale when
UnitTestsBlockingThreadGuard landed (issue #144 commit): it's a target
directly in UnitTests/CMakeLists.txt, which
scripts/generate_phase5_widgets_evidence.py scans, but the catalog was never
regenerated for it. CI's policy job caught the drift --
test_verify_phase5_widgets_contract.py's crlf-currency check and its
hardcoded target count (70) both went red against the actual count (71).

Regenerate via scripts/generate_phase5_widgets_evidence.py --write and bump
the test's hardcoded count to match.

(scripts/generate-architecture-catalogs.py's separate architecture-catalog.json
was already regenerated for this target in the #144 commit; this is the
sibling widgets-surface catalog that step didn't cover.)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xnr9TZYfGr1nMgBeh16CSb
Rename leftover working-directory: loupe to loop in reusable Linux and
Windows workflows so agent-fast/build can run generate_corpus.py --check.

Rename schema_kind from loupe-processing-budget-exhaustion-corpus to
loop-processing-budget-exhaustion-corpus in the corpus generator, test,
and manifest so check_loop_identity passes.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Co-authored-by: michael berry <mberrys@users.noreply.github.com>
The agent-fast build proof never built the blocking-thread-guard test
target because it was absent from the core module's test list, so its
AUTOMOC .moc file was never generated and clang-tidy failed on
tst_blockingthreadguardtest.cpp. Registering the target lets
check-change.py build it (and generate the moc) before static analysis.
…ues (#194)

Continue PR #525 on current origin/dev. Shared pdfquick::tokens cover
spacing, typography, focus geometry, and colour roles; resolveStateVisual()
is the canonical finding/check mapping. Incomplete and waived states never
resolve as passed, and each state carries a unique icon plus accessible name.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Qt::red against white is only ~4.0:1. Use the light-theme fill (#B91C1C) so
a destructive button label still meets WCAG text contrast in high contrast.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Drop restated enum notes, unused includes, and CMake/doc padding. Mapping
invariants stay in tests and LOOP_DESIGN_SYSTEM.md.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Keep both UnitTests/tst_jbig2decodertest.cpp (PR 519) and
UnitTests/tst_lifecycletest.cpp (dev) in the core module boundary paths.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Keep 0.2.1 in the living release train while adopting dev's consolidated
0.5.0-0.8.0 planned-train wording and retired-title note.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Resolve agent-policy.json conflict by keeping both
tst_budgetexhaustiontest.cpp and tst_diagnosticstest.cpp
in the core module paths.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Co-authored-by: michael berry <mberrys@users.noreply.github.com>
…ds-convert (#554)

The target default was re-applied with '||' on top of the parsed value, so
'flatten_transparency: false' on PDF/X-1a/X-3 still flattened. The parsed
setting becomes a three-state policy (Automatic/Always/Never) resolved once by
flattensTransparency(), so an explicit opt-out is no longer overridden:
preview() stops advertising a transparency.flatten change and apply() does not
run the flattener.

PDFStandardConversionSettings::flattenTransparency was added by this same PR
and has never shipped in unstable or stable, so replacing it is not a break
for a released consumer.
PDFTransparencyFlattener::apply() rasterizes every selected page, while
preview() only advertises a transparency change when hasLiveTransparency() is
true. Guarding the apply path with the same condition stops opaque vector/text
documents from being silently replaced by full-page rasters.
…ecks (#554)

preview() and apply() pass pdfxProfile() to PreflightEngine, but
parseProfile() rejects a profile with an empty 'checks' array before it reads
'pdfx', so result.pdfx was never populated: preview() reported no blockers for
any PDF/X target and every PDF/X apply() failed at postflight. The profile now
carries the shared checks of loop-preflight/examples/profile-pdfx-x1a2001.json,
which is what makes an explicit flatten opt-out fail closed.
The file overload handed the caller's outcome pointer to the device overload,
which filled it in before QSaveFile::commit(). A failed rename returned an
error while the outcome claimed the save had completed.

The commit-failure path is verified by construction rather than by a unit test:
QSaveFile::open() refuses a read-only target ('Existing file ... is not
writable') before the nested write runs, so a test cannot reach commit() on
this platform and none can be made to fail for the intended reason. The new
slot covers the file overload's success reporting (append vs verbatim copy).
…ds (#554)

A defaulted parameter changes the mangled symbol, so the previous overloads
would have disappeared from LoopLibCore. They return as forwarding overloads
and the default argument is gone, which also keeps a four-argument call
unambiguous. UnitTestsIncrementalSave now static_casts both signatures so a
future defaulted parameter cannot silently remove them again.

Symbol check (dumpbin is not installed here; this is the DLL's PE export
table): LoopLibCore.dll exports exactly four writeIncremental symbols - the two
four-argument signatures plus the two five-argument ones.
The standalone authorization pass required a digit or punctuation character in
the credential, so an opaque value such as 'Bearer abcdefghijklmnop' reached
support bundles verbatim. A second alternative now matches scheme-delimited
all-letter tokens of 16+ characters; prose after a scheme name still survives.
PreflightPane.qml renders preflightVerdictOperatorSummary() directly, so its
pass and fallback copy must go through a translation context instead of
QStringLiteral - otherwise every non-English build shows English-only status.
…#554)

stateChanged is operatorSummary's notifier, but setCurrentRevision() and
cancelRun() emitted it before assigning the new text, so observers retained the
previous run's summary.
…554)

acceptResult() copied the raw error findings into the model, so a run whose
blocking findings were all covered by an active disposition announced PASS while
the findings list and canvas overlays still showed them as errors. The model now
takes the verdict's waived IDs, exposes them through WaivedRole, and maps them
to a non-blocking overlay severity.
…wn (#554)

EditorHost::registerInteractiveThread() had no matching cleanup, so a host recreated in the same process left a stale registration that kept refusing synchronous blocking work on that thread.
…554)

The schema pattern was lower-case only, so a client validating the published contract rejected 'EN' - a request the service accepts and normalizes to 'en'. The pattern is now case-tolerant in shape and says so in a description, and a test keeps the schema and the runtime normalization in step in both directions.
…gment (#554)

Covers the ten review findings fixed here, the PDF/X profile defect fixed in
this PR (issue #556), and the CMYK-ordering finding deferred to issue #555.
The overlay findings.isEmpty() exception was removed when every
preflight surface moved onto reducePreflightVerdict, but the unit test
still imported OVERLAY_FINDINGS_GUARD. Drop that stale contract and
regenerate the Phase 5 widgets inventory for UnitTestsPreflightVerdict's
LoopLibInteraction link.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
cursoragent and others added 10 commits September 11, 2026 19:31
Introduce the typed seven-workspace model on EditorHost with
setWorkspace()/workspaceChanged, documentShellStatus and
productionStateName projections, ProductionModel sync, manifest menu
metadata on command descriptors, and interaction-to-inspector dispatch.
Gate developer diagnostics behind LOOP_LOOP_DISTRIBUTION_BUILD.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Move InspectorPane into the Document workspace right column, add
placeholder panes for Production Preview, Pages/Production, Inspect, Fix,
and Compare (disabled), and mirror the layout in ProductQuickAccessibilitySmoke.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Add ShellToolBar with catalog-driven open/save/zoom/preflight actions and
extend the bottom status bar with always-visible document, production, and
preflight segments plus page/zoom context.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Replace hardcoded menu trees with MenuModel/ShellMenuBar grouping of catalog
actions by menuGroup, target, and disposition, omitting HIDE/STOP-SHIPPING
entries and gating ADVANCED items behind allowDeveloperDiagnostics.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Register UnitTestsShellWorkspace and UnitTestsShellInspectorDispatch with
transition-matrix, menu-route, and selection-kind coverage.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Record the seven-workspace rail, contextual inspector dock, manifest menus,
and always-visible status segments in the shell contract.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment