Conversation
Introduce unstable as the qualification branch in the promotion chain (dev -> unstable -> stable) and move the required agent-fast / build status check there. dev remains the topic-integration branch but no longer carries branch-rule status requirements.
## Summary Introduces `unstable` as the qualification branch in the promotion chain and moves the required `agent-fast / build` status check there. **Promotion flow:** topic branch → `dev` → `unstable` → `stable` **Protection changes:** - `unstable` now carries the `agent-fast / build` requirement formerly on `dev` - `stable` still requires `release_ok` - `dev` remains the topic integration line but is no longer a protected branch with status-check requirements ## Changes - Canonical policy in `agent-policy.json` adds `qualification: unstable` and `promotion_chain` - `docs/BRANCH_POLICY.md`, `docs/CI.md`, `docs/REPO_MAP.md` updated with the new machine-readable declarations and prose - CI workflow triggers extended to `unstable` (`ci.yml`, `codeql.yml`, `documentation.yml`) - `scripts/ci/check_branch_policy.py` validates live protection on `unstable` instead of `dev`, and rejects status-check requirements left on `dev` - `scripts/agent/generate-adapters.py`, `scripts/generate-architecture-catalogs.py`, `scripts/agent/check-change.py` updated to know about the qualification branch - Generated adapters (`AGENTS.md`, `.claude/policy-brief.md`, `.cursor/agent-policy.md`, `docs/branch-policy.json`) and the architecture catalog regenerated - `scripts/hooks/pre-push.sh` protects direct pushes to `unstable` - Unit tests updated in `scripts/agent/test_check_change.py` and `scripts/ci/test_check_branch_policy.py` ## Operator follow-up GitHub branch protection rules still need to be applied on the remote: 1. Create the `unstable` branch (if it does not exist yet) 2. Move `agent-fast / build` required status from `dev` to `unstable` 3. Remove status-check requirements from `dev` 4. Keep `release_ok` on `stable` ## Test plan - [x] `python3 scripts/agent/check-change.py --base origin/dev` — pass (changelog, source_integrity, architecture_catalog, policy_adapters) - [x] `python3 -m unittest scripts.agent.test_check_change scripts.ci.test_check_branch_policy` — 34 tests pass - [x] `python3 scripts/ci/check_branch_policy.py` — pass --- _Generated by [Claude Code](https://claude.ai/code/session_017ieMToD9xqH6GWUZjpbqxT)_
Moves changes/work.md to changes/codex-fix-issues-from-codex-review.md so check-change finds the expected fragment.
…arch tab - add LoupeLibCore/pdfdocumentsearch for text extraction and search, keep revision fencing in Core - make QuickDocumentModel use the Core search API instead of local parsing - make Find select the Document Search tab and focus its field - addresses Codex P1 comments on PR 491
- Main.qml: keep maximized state when presentation changes, only leave FullScreen if we are in it - DocumentPane.qml: make Previous/Next observe commandEpoch so they update when search results change - DocumentPane.qml: use TreeView for outline so nested bookmarks are reachable - editorhost.cpp: handle unset cursor for backward search - rewrite changelog summary in plain language Fixes Codex P2 comments on PR 491.
* test: add resource envelope fixture matrix runner * test: strengthen resource envelope qualification * fix: validate envelope evidence identity and resolve paths - check both commit and fixture digest against candidate SHA and input digest before accepting a result - resolve pdf_tool and fixture paths to absolute before stat and before launching child with cwd=ROOT - pin rasterizers to fixed default and document why - rewrite changelog summary in plain language Fixes Codex P1/P2 on PR 493.
* Fix Linux cache and Windows MSI Qt packaging * fix: unslop changelog summary Rewrite in plain language without the redundant phrasing. * fix: trim trailing blank line from changelog fragment
…ew' into cdx/quick-pdf4qt-core-parity
Issue #146 needs traces that replay identically on every runner. This adds the data half of that, ahead of the C++ harness, so a malformed scenario fails in seconds rather than after a compile. A scenario is a manifest that embeds an InteractionTrace rather than an extension of it. InteractionTrace is a shipping type whose privacy contract forbids geometry and target identity, and a scenario needs exactly those to declare its fixture and expected selection. Embedding reuses the tested round-trip with no change to interactiontrace.cpp, and lets a recorded field trace drop straight in as the 'trace' member. The report schema pins the rule that missing telemetry is reported as available:false with null percentiles, never as zero, and that a failing run names both the contract it broke and the phase responsible. The checker enforces both, plus that every corpus scenario produced a run -- a scenario that silently stops running is otherwise invisible. Corpus digests are pinned to LF in .gitattributes and normalized in the checker. The repository checks text out as CRLF, so without this the gate would pass on the machine that wrote the manifest and fail on every fresh checkout. Refs #146, #139. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PWGFQCBSNwStAPYUCAZzpN
Six more scenarios covering the direct-manipulation half of issue #146's list: hover on a dense page, drag with and without snapping, cursor-anchored zoom, rapid zoom reversal with page switching, and a page whose overlay exceeds its bounds. hover-dense asserts the index candidate count rather than latency. A spatial index that stopped narrowing and a page that simply got heavier look the same in a frame-time percentile; only the candidate count separates them, which is the regression issue #145 can actually suffer. zoom-reversal is the corpus port of InteractionControllerTest::rapidZoomReversalAndPageSwitchSettleWithinTraceBudget. The C++ case stays where it is as the unit-level guard. A manifest entry may now carry blocked_on with a blocked_reason. drag-snap needs DragSnapper, which is still in PR #488, so nothing can run it yet. Marking it explicitly is what keeps the report coverage check strict for the other eight: without it the check would have to be relaxed for the whole corpus, and a scenario that silently stopped running would become invisible. Also documents the two lanes, the contract evaluation order, and the trace stage to phase mapping in INTERACTION_CONTRACT.md, and records Q-05 in the 0.2.0 closeout matrix as partial -- no verified latency measurement exists for this candidate, and the matrix must not imply one does. Refs #146, #139. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PWGFQCBSNwStAPYUCAZzpN
…ge boundary (#500) Repair the remaining Session 07 hosted packaging failures after #494: - copy the built Loupe.Quick module into the staged install tree on Windows so scrubbed --quick-smoke can resolve the packaged shell - stage Loupe.Quick under usr/lib/qml on Linux and remove optional Qt SQL drivers before linuxdeployqt probes missing vendor libraries Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Seven small, deterministic, synthetic adversarial PDF fixtures (generated by scripts/resource_envelope/budget_exhaustion_corpus.py), one per budget dimension named in the issue. New UnitTestsBudgetCorpus reads each fixture through PDFDocumentReader/PreflightEngine like a real upload and asserts the run terminates within a bounded time and fails closed with the exact exceeded budget attributed, never a hang, an OOM kill, or a silent clean result. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014oPh587Ls4UZTLwpxxQtpt
… target Adding UnitTestsBudgetCorpus to UnitTests/CMakeLists.txt changed the Widgets-free build's target count from 67 to 68; docs/generated/ phase5-widgets-inventory.json and its pinned test expectation were stale. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014oPh587Ls4UZTLwpxxQtpt
UnitTestsBudgetCorpus was only reachable via the coarse build_policy module (triggered by the UnitTests/CMakeLists.txt edit), so the CI agent-fast lane never built it before running clang-tidy on tst_budgetcorpustest.cpp, which failed with "tst_budgetcorpustest.moc file not found" -- the moc output only exists once its own target is built. List the new test file and target alongside the other individually-enumerated core-relevant UnitTests/tst_*.cpp files and targets (tst_bleedfixuptest.cpp, UnitTestsBudgetExhaustion, ...) so the fast lane builds and runs it like its siblings. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014oPh587Ls4UZTLwpxxQtpt
CI's first real build of the new target failed: 'const class QJsonArray' has no member named 'constFirst'; did you mean 'contains'? QJsonArray (unlike QList) doesn't have constFirst()/first(); use at(0), which is always available. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014oPh587Ls4UZTLwpxxQtpt
qt_add_qml_module writes to CMAKE_CURRENT_BINARY_DIR/Loupe/Quick under LoupeEditor, not the top-level build tree. Unblocks Linux_AppImage and Windows_MSI Deploy Qt steps. Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: michael berry <mberrys@users.noreply.github.com>
PDFObject has no isValid() member; use PDFForm::parse to determine whether the catalog exposes an AcroForm or XFA form. Unblocks Linux_AppImage builds. Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: michael berry <mberrys@users.noreply.github.com>
* test: add concurrent revision-authority stress scenario Issue #236 requires that DocumentContext be the single revision authority for caches and asynchronous results, and holds the issue open until the concurrent stress scenario passes. The fence, the identity separation, and the revision-keyed session and job caches already landed; the acceptance scenario itself was the one criterion with no coverage. tst_documentsessiontest.cpp proves the fence in one orchestrated round, with every producer released after the mutation. This adds the acceptance scenario instead: render, preflight, thumbnail, and repair-plan jobs in flight together while the document is mutated at points the producers do not observe, and asserts the four correctness properties named in the issue - zero stale findings applied, zero stale tiles presented past an invalidation boundary, deterministic cancellation, and no cache serving a result for the wrong revision. A stress test that quietly stops exercising the fence is worse than none, so the non-vacuity checks are deterministic rather than timing-based: a final phase holds one producer per job kind inside its work function, mutates the document underneath all of them, and only then releases them, asserting both the consumer-side rejection and the scheduler-side Stale outcome. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MaL7wNZgrcZibFiBZenb6t * chore: refresh Phase 5 widgets evidence for the new test target The source_integrity job regenerates docs/generated/phase5-widgets-inventory.json and compares it against the tree; adding UnitTestsRevisionStress made the tracked copy stale (67 -> 68 targets). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MaL7wNZgrcZibFiBZenb6t * chore: expect 68 Phase 5 targets after adding UnitTestsRevisionStress The Phase 5 widgets contract test pins the generated target count, so a new test executable has to move it with the regenerated inventory. Verified by running the full scripts/ci unittest discovery (219 tests) locally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MaL7wNZgrcZibFiBZenb6t --------- Co-authored-by: Claude <noreply@anthropic.com>
## Summary Closes #243. - Adds `scripts/resource_envelope/budget_exhaustion_corpus.py`, which deterministically generates seven small (a few KB), synthetic adversarial PDF fixtures — one per `pdf::PDFBudgetKind` dimension named in the issue: a decompression bomb, cumulative decoded bytes across many streams, a deep Form-XObject `Do` chain, an operator-heavy content stream, an oversized declared page extent probed by the `thin-parts` check, a deeply nested object-array literal, and a pathological indirect-object count. Fixtures and `UnitTests/testdata/budget_exhaustion/manifest.json` are committed, not hand-written. - Adds `UnitTestsBudgetCorpus` (`UnitTests/tst_budgetcorpustest.cpp`), a manifest-driven Qt test that reads each fixture through `PDFDocumentReader`/`PreflightEngine` the way a real upload would be handled, and asserts: - the run terminates within a bounded time (never a hang or unbounded allocation); - the *exact* expected budget dimension is exceeded, never a different one; - for the five fixtures checked through an already-parsed document, the structured `checks[].budget.{kind,pool,limit,attempted}` fields are populated and `reducePreflightVerdict()` yields `Incomplete` (never a clean pass over unexamined content); - for the two that are PDF-object-graph properties tripped inside `PDFDocumentReader` itself (before a document exists), the read fails outright and its error message names the same kind, limit, and attempted value. - This is the adversarial complement to the existing `tst_budgetexhaustiontest.cpp` (direct-API coverage of every budget kind) and to #64's memory-safety fuzz corpus — not "does it crash" but "does it fail closed with an attributable reason," per the issue. - Documents the corpus in `docs/RESOURCE_BUDGETS.md` and adds `changes/cc-nice-noether-u5ie9s.md`. ### Design notes - Every fixture is legitimately hostile-shaped but tiny; each test case tightens exactly one `PDFProcessingLimits` field (or, for the raster-probe fixture, the `thin-parts` check's own `max_raster_pixels` parameter) to a small value so the trip is deterministic without needing multi-GB files, matching the existing guidance in `docs/RESOURCE_BUDGETS.md`. - I traced the exact production code paths for each dimension against the current `dev` tree before writing the fixtures (content-stream decode threading a session's budget through `PDFDocument::getDecodedStream`, `PDFPageContentProcessor::processContent`'s `DepthScope`/`chargeRenderOperation` charging, the `catch (const PDFBudgetExceededException&) { throw; }` re-throw guards that keep the exception from being swallowed as an ordinary render error, `PDFDocumentReader::processReferenceTableEntries`'s per-object `chargeObject()`, and `pdfthinpartprobe.cpp`'s pre-allocation pixel-budget guard) so the design is grounded in what the code actually does, not assumed. - I independently verified the generated fixtures' byte structure in Python (xref offsets point at the right `N 0 obj`, the decompression bomb's ratio and the cumulative-bytes fixture's total both exceed their own tightened caps, the nested-array fixture's bracket run exceeds its tightened depth) — see `scripts/resource_envelope/test_budget_exhaustion_corpus.py`, which passes. ### What I could not verify here This sandbox has no configured Qt6/vcpkg toolchain or CMake build directory, so I could not actually compile or run `UnitTestsBudgetCorpus`, `ctest`, or `clang-tidy` — `python3 scripts/agent/check-change.py --base origin/dev` accordingly reports the `build:*`/`focused_tests` checks as failing for lack of a build directory (not a code error) and `clang_tidy` as incomplete. `changelog`, `source_integrity`, `architecture_catalog`, `policy_adapters`, and every `format:*` check pass. CI (`reusable-linux.yml`, which does have the toolchain) will build the new target and run `ctest --output-on-failure`; please treat the C++ side as compile-checked by careful manual review rather than by a local build, and let me know if CI turns up anything so I can fix it. ## Test plan - [x] `python3 -m unittest scripts.resource_envelope.test_budget_exhaustion_corpus -v` (10/10 pass) - [x] `python3 scripts/generate-architecture-catalogs.py --write` (registers `UnitTestsBudgetCorpus`) - [x] `python3 scripts/agent/check-change.py --base origin/dev` — passes changelog/source_integrity/architecture_catalog/policy_adapters/format; build/test/clang-tidy checks fail only for lack of a local toolchain (see above) - [ ] CI: `ctest --output-on-failure` builds and runs `UnitTestsBudgetCorpus` 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_014oPh587Ls4UZTLwpxxQtpt --- _Generated by [Claude Code](https://claude.ai/code/session_014oPh587Ls4UZTLwpxxQtpt)_
Resolve Loop/Loupe rename conflicts from the 0.2.1 promotion branch, regenerate Phase 5 widgets inventory (70 targets), refresh interaction-trace manifest digests after schema_kind renames, and align policy/workflow contracts with Loop naming. 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>
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Summary Resolves merge conflicts blocking [PR 490](#490) (`dev` → `unstable`) and fixes the failing `source_integrity` / `policy` gates on `dev`. ## Changes - Merged `origin/unstable` into `dev` workstream, resolving legacy-to-Loop rename conflicts across policy adapters, docs, workflows, packaging, and CMake. - Regenerated `docs/generated/phase5-widgets-inventory.json` (70 targets; stale `counts.targets` was 68 while the array held 69). - Renamed interaction/resource envelope schema kinds and corpus digests from legacy `loupe-*` to `loop-*` so `check_loop_identity.py` passes. - Updated dev-added Quick/document search files to Loop naming (`Loop.Quick`, `LOOPLIBCORESHARED_EXPORT`). - Preserved dev promotion policy (`agent-fast` gate on `unstable`) and Session 07 packaging improvements (QML staging, windeployqt, linuxdeployqt evidence). ## Verification - `python3 -m unittest discover -s scripts/ci -p 'test_*.py'` — 223 tests OK (1 skipped) - `python3 scripts/ci/check_source_integrity.py` — passed - `python3 scripts/generate_phase5_widgets_evidence.py --check` — passed - `python3 scripts/ci/check_loop_identity.py` — passed - `git merge-tree` against `origin/unstable` — no conflict markers predicted - CI on this PR: `source_integrity` ✅, `policy` ✅ ## Files resolved (conflicts) `.claude/policy-brief.md`, `.cursor/agent-policy.md`, `AGENTS.md`, `.github/workflows/LinuxInstall.yml`, `.github/workflows/WindowsInstall.yml`, `UnitTests/phase4-tests.cmake`, `WixInstaller/Product.wxs.in`, `docs/0.2.0-closeout-matrix.md`, `docs/CI.md`, `docs/JOB_SCHEDULER.md`, `docs/LOOP_SHELL_CONTRACT.md`, `docs/LOOP_WORKSPACES.md`, `docs/PLATFORM_SUPPORT.md`, `docs/REPO_MAP.md`, `docs/SEMANTIC_TRUST_ENGINE_ACCEPTANCE.md`, `docs/generated/phase5-widgets-inventory.json`, `scripts/agent/generate-adapters.py`, `scripts/ci/check_phase5_residue.py`, plus file-location conflicts for `LoopEditor/qml/DocumentPane.qml`, `LoopEditor/quickdocumentmodel.{cpp,h}`, and `LoopLibCore/sources/pdfdocumentsearch.{cpp,h}`. **Next step:** merge this PR into `dev` to unblock [PR 490](#490). <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-02eb1171-d4b3-4400-89db-df87f38c23e1?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-02eb1171-d4b3-4400-89db-df87f38c23e1&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2ab848b725
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- LoopLibCore: pass PDFProcessingBudget to PDFTextLayoutGenerator during searchDocumentText to enforce hostile-workload limits - LoopEditor QuickOutlineModel: expose page role from PDFOutlineItem destination and route via implemented goToPage/goToOutlinePage - LoopEditor DocumentPane/Host: make searchPanelVisible one-shot via acknowledgeSearchPanel to prevent repeated reveal on presentationChanged - UnitTests: bump catalog implemented count 16 -> 25 to match shell-implemented find/layout commands added in unstable
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6c628b41e4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Promotes
unstableintostablefor the 0.2.1 release line.Merge commit:
2ab848b7253714a735640067031eaf45f29a7d65This is the Session 07 package-workflow SHA after PR #490 landed on
unstable.