Skip to content

feat: async interactive-thread boundary (#144) + interaction-trace contract evaluator (#146) - #517

Draft
mberrys wants to merge 3 commits into
devfrom
0.2.2
Draft

feat: async interactive-thread boundary (#144) + interaction-trace contract evaluator (#146)#517
mberrys wants to merge 3 commits into
devfrom
0.2.2

Conversation

@mberrys

@mberrys mberrys commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

#144 — async interactive-thread boundary

Reading dev, the job-scheduler side of #144's contract (revision + cancellation token per job, stale-result discard, non-blocking delivery back through a relay, deterministic cancel/close) was already built and tested via PDFJobScheduler/IJobSubmitter. The one acceptance criterion with no runtime enforcement was the thread-affinity assertion itself (AC5) — nothing stopped a blocking implementation from being called on the interactive thread, it just happened not to be.

  • pdf::PDFBlockingThreadGuard (LoopLibCore/sources/pdfblockingthreadguard.h/.cpp): a small, opt-in, global thread-affinity check. A host with an interactive canvas registers its owning thread once (registerInteractiveThread()); a blocking service adapter opens with assertOffInteractiveThread(name) and folds a false return into its own typed error instead of doing the blocking work.
  • Wired into PreflightEngine::run() and into EditorHost's constructor.
  • PDFJobTraceEvent now carries PDFJobKind (issue AC7, first half).
  • docs/JOB_SCHEDULER.md gets a new "Interactive-thread boundary" section (issue AC8).
  • Tests: UnitTestsBlockingThreadGuard (new), a UnitTestsPreflightEngine case, and an extended UnitTestsJobScheduler assertion.

Still open on #144, called out so reviewers don't read this as a full close: no real UI trigger yet for an interactive preflight run (PreflightController::beginRun() is only called from tests), and AC7's second half (correlating PDFJobTraceEvent timing against InteractionTraceRecorder's slow-frame attribution) is a real design decision left as follow-up. OCR/AI adapters remain out of scope per docs/JOB_SCHEDULER.md's own migration table.

#146 — interaction-trace contract evaluator

Prior work already on dev (scenario/report JSON schemas, a nine-scenario corpus under UnitTests/testdata/interaction-traces/, and scripts/ci/check_interaction_traces.py) validates the corpus as data but has no C++ test binary to produce a run — UnitTestsInteractionTraces/UnitTestsInteractionTracesPresent, named in docs/INTERACTION_CONTRACT.md's two-lanes table, don't exist yet.

This adds the piece of that harness a future replay engine can be built around without risking a large, unverifiable change in this sandboxed environment (no Qt/CMake toolchain to build against):

  • pdfinteraction::evaluateTraceContracts() and its TraceContract/TracePhase enums (LoopLibInteraction/sources/interactiontracecontract.h/.cpp) — the fixed-order pass/fail evaluator issue [0.2.0] Automate interaction-performance regression traces #146 AC7 asks for. Given an ordered QList<TraceContractCheck>, it returns the first unsatisfied contract and the responsible phase, matching scripts/ci/check_interaction_traces.py's CONTRACTS/PHASES tuples and docs/schemas/interaction-trace-report.schema.json exactly.
  • phaseForStage() gives docs/INTERACTION_CONTRACT.md's TraceStage-to-phase attribution table a type.
  • The evaluator takes an already-assembled checklist rather than a live replay, so it's independently testable — no InteractionController, no scheduler, no hit-test dispatch — via the new UnitTestsInteractionTraceContract target.
  • docs/INTERACTION_CONTRACT.md documents the module and is explicit that the actual scenario-replay/report-assembly harness remains open.

Still open on #146: replaying a corpus scenario through InteractionController, applying its cost model, counting hit-test candidates and async-job overlap, and assembling a full report run (of which this evaluator decides only the passed/first_violated_contract/responsible_phase/failure_excerpt fields).

Proof

This session has no Qt/CMake toolchain, so scripts/agent/check-change.py --base origin/dev --head-branch 0.2.2 could only run the source-level checks locally:

  • changelog, source_integrity, architecture_catalog, policy_adapters — pass
  • clang-format --dry-run --Werror on every changed file — pass
  • build:* / clang_tidy / focused_tests — incomplete locally (no build/ directory); CI supplies these.

Regenerated docs/generated/architecture-catalog.json (scripts/generate-architecture-catalogs.py --write) and agent-policy.json's derived adapters (scripts/agent/generate-adapters.py --write) to pick up the new test targets.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Xnr9TZYfGr1nMgBeh16CSb

…#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
@mberrys mberrys changed the title feat: enforce async interactive-thread boundary for blocking services (#144) feat: async interactive-thread boundary (#144) + interaction-trace contract evaluator (#146) Sep 2, 2026
…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

mberrys commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

CI status on 76645699 / 3e274d9

Three checks failed on the merge commit for 76645699 (the #146 commit): source_integrity, policy (two runs), and agent-fast / build. Triaged:

Not this PR's — reproduces identically on origin/dev HEAD (1c0481b, checked in a clean worktree with no PR changes applied):

  • source_integrity / policy: test_check_loop_identity.py::test_repository_contract_passes fails with 5 pre-existing "legacy product token" findings — two of them are a real bug: .github/workflows/reusable-linux.yml:79 and reusable-windows.yml:92 both set working-directory: loupe (leftover from before the repo's rename to loop), which is also what breaks agent-fast / buildgenerate_corpus.py --check runs from a directory that doesn't exist in the checkout. The other three are stale "loupe-processing-budget-exhaustion-corpus" schema-kind strings in UnitTests/testdata/budget-exhaustion/manifest.json, UnitTests/tst_budgetexhaustiontest.cpp, and scripts/budget_exhaustion/generate_corpus.py. No fix for this exists yet on dev that I can port in, and it's unrelated to [0.2.0] Enforce async boundaries for preflight, OCR, AI, and file I/O #144/[0.2.0] Automate interaction-performance regression traces #146, so I'm not touching it in this PR — I've filed a follow-up task instead (details below). Proposed patch: rename loupeloop in the two workflow working-directory lines (functional fix) and either rename the schema-kind strings or add them to LEGACY_TOKEN_ALLOWLIST in scripts/ci/check_loop_identity.py (the schema-kind strings are cosmetic — I'd defer to whoever owns that corpus on which).
  • policy: test_verify_phase5_widgets_contract.py::test_check_treats_crlf_checkout_as_current was flagging the same staleness as the fix below, before it landed.

Was this PR's, now fixed (pushed as 3e274d9): test_verify_phase5_widgets_contract.py::test_current_evidence_is_valid_and_complete failed because docs/generated/phase5-widgets-inventory.json went stale when UnitTestsBlockingThreadGuard landed in the #144 commit — it's a target directly in UnitTests/CMakeLists.txt, which scripts/generate_phase5_widgets_evidence.py scans (unlike docs/generated/architecture-catalog.json, which I did regenerate for it at the time). Regenerated the catalog and bumped the test's hardcoded target count from 70 to 71.

Watching for the next CI run on 3e274d9.


Generated by Claude Code

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