Skip to content

0.2.1 promotion: move agent-fast build gate to unstable and update policies - #490

Merged
mberrys merged 41 commits into
unstablefrom
dev
Sep 2, 2026
Merged

0.2.1 promotion: move agent-fast build gate to unstable and update policies#490
mberrys merged 41 commits into
unstablefrom
dev

Conversation

@mberrys

@mberrys mberrys commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

No description provided.

claude and others added 18 commits August 31, 2026 22:02
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.
@mberrys mberrys changed the title 0.2.1 promotion / move agent-fast/build gate to unstable and update policies 0.2.1 promotion: move agent-fast build gate to unstable and update policies Sep 1, 2026
mberrys and others added 11 commits September 1, 2026 00:57
* 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
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
claude and others added 10 commits September 1, 2026 23:17
… 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>
cursoragent and others added 2 commits September 2, 2026 03:01
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>&nbsp;<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>&nbsp;</div>
@mberrys
mberrys merged commit 2ab848b into unstable Sep 2, 2026
18 of 20 checks passed
mberrys added a commit that referenced this pull request Sep 2, 2026
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
Promotes `unstable` into `stable` for the 0.2.1 release line.

**Merge commit:** `2ab848b7253714a735640067031eaf45f29a7d65`

This is the Session 07 package-workflow SHA after PR #490 landed on
`unstable`.
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-add9255c-ab75-4ce7-a6ef-326374bfe342?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>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-add9255c-ab75-4ce7-a6ef-326374bfe342&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>&nbsp;</div>
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.

3 participants