Repository navigation
Review levels: the security pages, their citations test, the adversarial suite and the Dockerfile are locked as engine guards (#858) [PROTECTED: owner merges] - #885
Conversation
…te and the Dockerfile are locked A docs-only change to docs/guardrails.md, docs/security*.md or SECURITY.md, and a tests-only change to test/docs-guardrails.test.ts or anything under test/adversarial/, was L0 and could merge by itself on a PASS (measured: L0, auto=true under this repo's review.yaml and the example's). The root Dockerfile waited only where a review.yaml kept deploy-config, and Dockerfile.<name> not at all. Each is now self-modification in the engine's repo (src/review.ts SELF_MODIFICATION_PARTS): L3 and never auto-merged, whatever review.yaml says. The new parts are "the public security claims" and "the engine's container image"; the tests join "the tests of the harness controls", with the new test that holds these locks. SECURITY.md and Dockerfile* are named in ENGINE_ROOT_FILES, so like the rest of the engine's own files they stand aside in a managed app's repo, whose same-named files keep their categories (measured: unchanged). The engine's own review.yaml lists all of them under engine-guards (an engine that predates the lock reads it from main), and also a bare deploy path, the root compose files, dockerfile* and Containerfile*. Those last ones are not locked in code yet: a glob that matches no file needs an entry in test/merge-policy.test.ts NOT_YET_FILES, the owner's file. The adversarial suite gets six cases for the new locks (an edit, a rename away and a deletion among them), its "this suite locks itself" todo becomes them, and a todo for the code lock on deploy and the compose files takes its place. Test (fails without): test/engine-guard-locks.test.ts (2 of 3 fail on main; the third, a managed app's files unchanged, passes on main by design), and the six new cases in test/adversarial/cases/locked-paths.json (6 of 6 fail on main). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…e lock The guardrail change from #858 (this PR, #885) gets its own fragment under "Guardrail changes: what each one does", so the release says what now waits for the owner and that a managed app's same-named files are unchanged. Test (fails without): none, release notes only; test/changelog.test.ts and test/changelog-fragments*.test.ts pass. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Evaluator verdict: CHANGES NEEDED (round 1/2)Reviewed head The code is sound and every lock it adds holds. The one required change is to the PR body. It is a text correction (principle 5); no code change is required. Acceptance (#858)
1. Additions only (before/after)measured. I used my own path set, so this is not the builder's exact 36 paths. It covers 53 paths × 3 configs (own, example, every category emptied with nothing at L1 to L3 or waiting) × 2 scopes, which is 318 rows. 68 differ, and all 68 are tighter. None is looser or mixed. A row counts as looser if the level drops,
2.
|
…review.yaml alone holds Round 1 measured that dockerfile* and Containerfile* match no file either, so, like a bare deploy and the compose files, their code lock needs NOT_YET_FILES entries in test/merge-policy.test.ts, the owner's file. The todo line now names all five, so the suite's list of open gaps matches what is built. Test (fails without): none, a todo line's wording; test/adversarial/adversarial.test.ts passes with 13 todo. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
|
Round 1, required change 1: done in the body and in one commit,
|
…docs Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…le and Containerfile are locked in code (#885) The five root globs this PR held only in the engine's review.yaml (deploy, compose.*, docker-compose.*, dockerfile*, Containerfile*) are now self-modification in src/review.ts: dockerfile* and Containerfile* join the container image part, and a new part, the engine's deployment files, holds deploy, compose.* and docker-compose.*. All five match no file yet, so each gets its NOT_YET_FILES entry in test/merge-policy.test.ts (owner decision on #885, option 2: additions only, each paired with its lock). They join ENGINE_ROOT_FILES, so a managed app's own files of these names keep their categories (measured: same level and categories as main under the example review.yaml). A new test, test/not-yet-files-locks.test.ts, reads NOT_YET_FILES the way the weakening hold does (tableLines) and fails on any entry that is not one of SELF_MODIFICATION_PARTS's globs, so an exception can't outlive its lock or arrive without one. It locks itself under the tests of the harness controls. The adversarial todo for this gap is replaced by two cases. Test (fails without): test/not-yet-files-locks.test.ts fails with the five entries added and no locks ('deploy', 'compose.*', 'docker-compose.*', 'dockerfile*', 'Containerfile*' unpaired); the two new adversarial cases fail on main a5df225 ("merged with the evaluator fooled"). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Evaluator verdict: PASS (round 3, final check), head
|
| Fixture | Result |
|---|---|
head test/merge-policy.test.ts |
ok |
broken pair ('Containerfile*' changed to 'containerfile*', the builder's check) |
fail: + [ 'containerfile*' ] - [] |
| table renamed (not declared) | fail: NOT_YET_FILES is not declared |
...EXTRA spread |
fail: holds more than plain strings: ...EXTRA |
| declaration never closes | fail: line 3 is at its indent but doesn't close it |
head table plus NOT_YET_FILES.push('evil/**'); on a later line |
ok (the gap, see 3) |
It fails closed on a table it can't read (measured). The in-file fixture test also passes on head (measured).
3. The builder's claimed .push gap: covered by the weakening hold
- Measured:
NOT_YET_FILESis registered never-grow (src/merge-policy-weakening.ts:55,CHECK_TABLES). - Measured: I called
weakeningReasonson head'stest/merge-policy.test.tswith one added line,NOT_YET_FILES.push('evil/**');. Result:design: may weaken a check: changes a check's or guard's table outside its own list (test/merge-policy.test.ts: "NOT_YET_FILES: NOT_YET_FILES.push('evil/**');"). This happens with scope left out and with scopeengine. An index assignment (NOT_YET_FILES[NOT_YET_FILES.length] = ...) is held the same way. - The gap is not real at merge time. The pairing test alone misses it, but the by-design hold (design.md:377) catches it.
4. ENGINE_ROOT_FILES is not in CHECK_TABLES
- Shrinking it is not the risk. A shrink makes more locks hold in a managed app's repo, which is a tightening. The loosening is growth: every glob added there stops holding in a managed app's repo. It also stops holding in the weakening hold, because
ENGINE_ONLYisengineSourceGlob(src/merge-policy-weakening.ts:313). - Measured: growing it on its own line gives
guard-lines, becausesrc/review.tsis inGUARD_FILES. AENGINE_ROOT_FILES.push(...)on a new line gives no hold reason. Any change tosrc/review.tsis stillL3self-modification, so it waits for the owner anyway. Nothing merges unseen; only the hold's reason wouldn't name the table. - Measured: none of its seven globs existed in
src/on main, so nothing that main locked stands aside now. - Recommendation (not required here): once Merge policy: a check table or lint test registers itself where it's declared; only removing or loosening one waits (#869) [PROTECTED: owner merges] #886 lands, register both
ENGINE_ROOT_FILESandENGINE_TREE(same exposure, unregistered on main too) as never-grow with a// check-table: never-growmarker insrc/review.ts. That belongs in a follow-up, not in this PR.
5. Managed-app rows: nothing that was locked opens
Measured with a scratch script against the built computeLevel and mergeDecision:
- Inputs: main
a5df2254and head; every path ingit ls-filesplus 16 new paths (1,396 paths); configs base own, head own and example; scopes unset,engineandmanaged-app. - 12,564 rows: 0 looser (level, a lost category, or auto turning true). 174 are tighter, all in engine scope (29 per config and scope). 0 rows changed in
managed-appscope. - The review.yaml change alone (head code, base own against head own): 0 looser.
- The nine new paths:
L1 app-non-moneybefore and after in app scope. In engine scope,L1 app-non-moneybecomesL3 app-non-money,self-modification. engine-guard-lockson head: 3/3 pass. Fail-without, method (b): the file written fresh into a maina5df2254worktree (sha256 identical to head's) gives 3 tests, 1 pass, 2 fail. Test 2 passes on main by design.- Adversarial: all 8 new cases, written as a new file
cases/zz-eval-885.jsoninto the main worktree, give 8 run, 8 fail. Seven fail with "merged with the evaluator fooled".container-image-editfails withno reason with "high-risk: self-modification (Dockerfile)" in ["high-risk: deploy-config (Dockerfile)"]. On head they pass.
6. Scope beyond the brief: no weakening
Measured: weakeningOf on the whole diff (origin/main..HEAD, engine scope) returns four reasons. Each one holds the PR for the owner as intended:
unreadableon theNOT_YET_FILESdeclaring line;test-lineson the removedtodo.jsonline;guard-lineson the reflowed doc comment insrc/review.ts;markeron thetest.skipinside theadversarial-suite-editcase's attack text.
The scope items:
- Lock on the new test, and the
engine-guard-locksadditions: tighter only. - Todo removed: it is replaced by the
adversarial-suite-editandadversarial-case-file-deletedcases. Both fail on main and pass on head (measured), so the gap is closed, not hidden. review.yaml:+17 -0, additions only (measured).- Fragment: notes only.
7. Other checks
| Check | Result |
|---|---|
npm run typecheck |
clean (measured) |
npm run check:denylist |
denylist: 1375 files clean (measured) |
| Named tests on head | not-yet-files-locks, merge-policy, engine-guard-locks, adversarial, review: 180 tests, 168 pass, 0 fail, 12 todo. docs-guardrails, changelog, brand: 38/38 (measured) |
| Size | tarball 1,462,728 → 1,463,465 (+737); unpacked 4,903,731 → 4,906,198 (+2,467); 489 files both. Only dist/src/review.js changed, 37,480 → 39,947. Matches the body (measured). Inside PACKAGE_BUDGET (2,050,000 / 6,400,000) |
git merge-tree with origin/main a5df2254 |
clean (measured) |
git merge-tree with #886 head f03b59df |
conflict in test/merge-policy.test.ts (measured). #886 adds // check-table: never-grow on the line above NOT_YET_FILES, and this PR rewrites that line. src/review.ts auto-merges. Whichever lands second keeps the marker above this PR's line. The pairing test's imports (tableLines, withoutComments) are still exported on #886's head (measured), but its header comment "(CHECK_TABLES)" goes stale (inferred) |
| Commits | 4 non-merge commits, all Signed-off-by, no attribution trailers (measured) |
CI at 1687d568 |
green: 20 pass, 0 fail, 7 skipping (the real-systemd jobs, skipped by the workflow). All 8 test shards, denylist, dco, push-gate, scan and scope pass. Measured with gh pr checks 885; nothing rerun |
| Protected marking | title [PROTECTED: owner merges], **Risk class:** protected, needs-owner label, real "What this can never do" and "Compromised component" sections (measured) |
| Real state | the new tests read only repo files. No PATH, host tool or state access (measured, by grep) |
Body corrections (required, body only)
- "For the owner": "The weakening hold (Merge: a PR that removes or weakens a test, lint or guard waits for the owner (#783) [PROTECTED: owner merges] #820) will flag
test/merge-policy.test.tsNOT_YET_FILES(table grown)" is wrong. Measured: the hold reportscan't read a change to check it ... (test/merge-policy.test.ts: "NOT_YET_FILES: the line that declares it is removed or changed"), nottable-grown. It still holds (fail closed). Because the table is one line, any change to it shows asunreadableand the added entries are not named, which is why the digest has to show the line. - Adversarial pass,
.pushrow: "tableLines, and so the weakening hold, reads only the declaration, so such an entry would be hidden from both" is wrong for the hold. Measured: the hold'stable-usedrule holds it (section 3 above). Only the pairing test misses it.
… onto marks Main added four check tables and one lint test to the central lists in the batch (#883, #884). Each now carries its mark where it is declared, with the same kind, and the conflict in src/merge-policy-weakening.ts keeps the central lists to what can't carry a mark: - test/child-process.test.ts SPECIFIERS, BINDINGS and WORKER_SPECIFIERS: `// check-table: never-shrink`, and the file `// lint-test` (#883); - scripts/bench/pins.ts SELECTION_RULES: `// check-table: never-grow` (#884). #885's new NOT_YET_FILES value keeps its never-grow mark (the conflict in test/merge-policy.test.ts). Also registered, as both #886 reviewers recommended: src/review.ts ENGINE_TREE and ENGINE_ROOT_FILES, `// check-table: never-grow`. A glob under them stands aside in a managed app's repo (lockedCategories, holds), so growing either drops locks there; shrinking only applies them in more places. REGISTERED_BEFORE_MARKS now holds main's 75 registrations at 41f6dd4 (53 tables, 22 lint tests). Test (fails without): test/merge-policy.test.ts "every check registered before the marks is still registered" and "... still held by the hold itself", over the 75. A scratch probe on in-memory copies shows SELECTION_RULES, BINDINGS, ENGINE_TREE and ENGINE_ROOT_FILES each give a hold reason naming them with their mark and none without it. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…#858/#885's locks beside the vacuity tests' Three conflicts. merge-flow.ts and vacuity.ts: spawnSync and execFileSync now come from #846's os/child-process.js (no module but it imports node:child_process for a value). review.ts: main's lock entries (#858, #885) kept beside the six vacuity test files. On the merged tree: typecheck and build clean; the vacuity suites with merge-policy, review, engine-guard-locks, not-yet-files-locks, child-process, docs-guardrails and first-task-progress: 167 tests, 0 fail. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Intent: lock the engine's public security claims (
docs/guardrails.md,docs/security*.md,SECURITY.md), the test that keeps their citations true (test/docs-guardrails.test.ts) and the adversarial suite (test/adversarial/**) as engine guards, the way #853 lockeddeploy/**: L3, self-modification and never auto-merged, in the engine's own repo, whatever a narrowedreview.yamlsays. The same PR closes the #853 gaps the owner asked for: the engine's container image (Dockerfile*,dockerfile*,Containerfile*) and its root deployment files (a baredeploy,compose.*,docker-compose.*) are locked in code. Five of those globs match no file yet, so each has aNOT_YET_FILESentry intest/merge-policy.test.ts, added under the owner's decision on this PR (option 2: additions only, each paired with a new lock). A new test keeps everyNOT_YET_FILESentry paired with its lock. Nothing loosens (measured: the 216-row before/after table below, and the managed-app rows for the five new globs).Risk class: protected. The change touches
src/review.ts(the review levels),.worklane/review.yamlandtest/merge-policy.test.ts, all in the owner's critical set.Closes #858
Digest lines
The
NOT_YET_FILESchange, exactly as committed (test/merge-policy.test.ts, line 106; head1687d568). The two existing entries are unchanged and in place; the five new ones are appended. Because the table is one line, the line itself shows as changed in the diff (measured:git diff --cached,-1 +1):Each added entry and its paired lock in
src/review.tsSELF_MODIFICATION_PARTS, exactly as committed:NOT_YET_FILESentrysrc/review.ts'dockerfile*'{ what: "the engine's container image", globs: ['Dockerfile*', 'dockerfile*', 'Containerfile*'] },'Containerfile*'{ what: "the engine's container image", globs: ['Dockerfile*', 'dockerfile*', 'Containerfile*'] },'deploy'{ what: "the engine's deployment files", globs: ['deploy', 'compose.*', 'docker-compose.*'] },'compose.*'{ what: "the engine's deployment files", globs: ['deploy', 'compose.*', 'docker-compose.*'] },'docker-compose.*'{ what: "the engine's deployment files", globs: ['deploy', 'compose.*', 'docker-compose.*'] },The same five globs also join
ENGINE_ROOT_FILES, so they stand aside in a managed app's repo:self-modification: every glob names real engine files (none dead)intest/merge-policy.test.ts. Its finding would be wrong here: each glob is meant to match no file today, because the lock goes in before the first such file lands, asdeploy/**did for Deployment: the engine in your own cloud account (Helm), on your own servers (Terraform or OpenTofu, or docker compose on one VM), or on one machine (one command plus doctor); re-runs never loosen; time-to-first-merged-PR measured every run (provisional: blocked on the product architecture doc) #703. That test still asserts each entry matches no file, so the entry expires itself the day a file lands (inferred, by reading line 111).NOT_YET_FILESis a never-grow table insrc/merge-policy-weakening.tsCHECK_TABLES, so the weakening hold (Merge: a PR that removes or weakens a test, lint or guard waits for the owner (#783) [PROTECTED: owner merges] #820) holds this PR for the owner on this line.What changed
src/review.tsSELF_MODIFICATION_PARTS. Every glob that was there stays.docs/guardrails.md,docs/security*.md,SECURITY.md.Dockerfile*,dockerfile*,Containerfile*.deploy,compose.*,docker-compose.*. It sits next to "the machine setup" (deploy/**).test/docs-guardrails.test.ts,test/adversarial/**,test/engine-guard-locks.test.tsandtest/not-yet-files-locks.test.ts. The last two are the tests that hold these locks, and each locks itself.src/review.tsengineSourceGlob: a new exact list,ENGINE_ROOT_FILES(above). These root-level globs stand aside in a managed app's repo, like everything underENGINE_TREE. The match is exact (includes), so no existing glob changes scope. Measured:test/review.test.ts› "the globs that hold in every repo are exactly …" passes, and its exact list is unchanged.test/merge-policy.test.ts: the fiveNOT_YET_FILESentries above. Nothing else in that file changed (measured:git diff origin/main -- test/merge-policy.test.tson this branch shows that one line only).test/not-yet-files-locks.test.ts(new, round 3):NOT_YET_FILESfromtest/merge-policy.test.tswith the weakening hold's owntableLines. It fails if any entry is not exactly one ofSELF_MODIFICATION_PARTS's globs, so an exception can't arrive without its lock, or outlive it.${BRAND.configDir}. Fixture checks in the same file cover each of these..worklane/review.yamlengine-guards: new lines only. They cover every path above, including the five root globs. An engine that predates the code lock reads this file from main.test/engine-guard-locks.test.ts(new) has three tests:auto === falseand floorauto === false, with the plain "changes X: path" text. This holds under this repo's review.yaml, the example's, and one that drops every category, with engine scope given or left out. Round 3 addsdockerfile,Containerfile,Containerfile.dev,deploy,compose.yaml,compose.yml,compose.override.yaml,docker-compose.yml,docker-compose.prod.ymland the new pairing test.engine-guards.cases/locked-paths.json. Six are from round 1: the security pages docs-only, the citations test, the suite's own runner, cases and todo, a rename ofSECURITY.mdto an unlocked name, a deleted case file, and the Dockerfile running as root. Round 3 addscontainer-image-files-added(a rootdockerfileandContainerfile) anddeployment-files-added(a baredeploy,compose.yaml,docker-compose.yml).changelog.d/885-lock-security-pages-and-suite.md: the second note now says the root deployment files are locked in code.How it was verified
NOT_YET_FILESentries, with no lock insrc/review.tsyet. Thennpm run buildandnode --import ./dist/test/isolate.js --test dist/test/not-yet-files-locks.test.js dist/test/merge-policy.test.js:every NOT_YET_FILES entry … is a self-modification lock globfails with+ [ 'deploy', 'compose.*', 'docker-compose.*', 'dockerfile*', 'Containerfile*' ] - [].merge-policy.test.tspassed in that same run. That is the gap: an entry with no lock went unseen.dist/, logic unchanged. It reads a scratch copy of theNOT_YET_FILESline in which'Containerfile*'became'containerfile*'.node --teston it: 1 test, 0 pass, 1 fail, with+ [ 'containerfile*' ] - []. No repo file was edited for this.a5df2254and wrote the two new cases there, verbatim, as a new filetest/adversarial/cases/zz-885.json.--test-name-pattern="885" dist/test/adversarial/adversarial.test.js: 2 run, 2 fail, both "merged with the evaluator fooled".computeLevelandselfModificationParton maina5df2254and on head. In a managed app's scope under the example review.yaml, all nine new paths areL1 app-non-moneyon both. In engine scope they go fromL1 app-non-money(no part) toL3 app-non-money,self-modification.a5df2254, with temp HOME, state and TMPDIR). I named each file:not-yet-files-locks,merge-policy,review,engine-guard-locks,adversarial/adversarial,docs-guardrails,changelogandbrand. Result: 218 tests, 206 pass, 0 fail, 12 todo.changelog-fragmentsandchangelog-fragments-selfcheck: 12 pass, 0 fail.npm run typecheck: clean (measured).npm run check:denylist:denylist: 1375 files clean(measured).npm run check:dco -- origin/main HEAD:DCO: all commits signed off(measured). The push gate printed2 commit(s) and 1 tip(s) clean(measured).f9422c4e).engine-guard-locks: 3 tests, 1 pass, 2 fail. The six round-1 adversarial cases: 6 run, 6 fail.test/coordinator.test.ts› "self-modification: a harness PR touching only one of the harness's own controls …", andtest/change-files-renames.test.ts, which runs a script with the real PATH. CI runs both. Inferred: the new globs pass the coordinator flow the same waydeploy/**does, since none is a config-folder or workflow path that is refused before a PR.For the owner
NOT_YET_FILESentries are listed under "Digest lines".NOT_YET_FILESas "table grown". It reports it asunreadable("the line that declares it is removed or changed"), because the table is one line and that line changed. Measured by the final check: Review levels: the security pages, their citations test, the adversarial suite and the Dockerfile are locked as engine guards (#858) [PROTECTED: owner merges] #885 (comment).src/review.ts. That is expected: this is a protected PR and a principle-9 exception, so it goes to you.ENGINE_ROOT_FILESis a new list of globs that stand aside in a managed app's repo. It holds only globs that were never locked anywhere before, so no existing install loses a lock (measured: the managed-app rows). It is still a new list of exceptions, so a reviewer should treat it under principle 9.SECURITY.md,Dockerfile,compose.yamlordeploywould become L3 self-modification. That would change a managed app's behaviour, which engine-guards: lock the guardrails docs, their test and test/adversarial/** (Lane A; follow-up to #852, #855) #858's acceptance rules out.ENGINE_ROOT_FILESis not inCHECK_TABLES, so the weakening hold doesn't watch it for growth. Today onlysrc/review.ts's own lock covers it. Registering it would be an edit tosrc/merge-policy-weakening.ts, which is outside this brief.Follow-ups
ENGINE_ROOT_FILESandENGINE_TREEinsrc/review.tsas never-grow tables, each with a// check-table: never-growmarker.Adversarial pass
NOT_YET_FILESentry with no lock (round 3)not-yet-files-locks. Measured: before the locks, 5 unpaired.src/review.tsand keep itsNOT_YET_FILESentrynot-yet-files-locks; the entry is reported unpaired. Measured on a fixture:unpaired(['deploy', 'compose.*'], ['compose.*'])gives['deploy'].containerfile*).pushon a later linetable-usedrule catches it. Measured by the final check withweakeningReasons(comment linked under "For the owner"). Only the pairing test misses it, because it reads the declaration alone.engine-guards. Measured: thetest/not-yet-files-locks.test.tsrows inengine-guard-locks.compose.yaml,deploy,dockerfileorContainerfile(round 3)high-risk: self-modification. Measured: two new cases, which pass on head and fail on main.deployengine-guard-locks.sub/Containerfile,apps/x/compose.yaml,apps/x/deploy,deploy.md,deployment.yaml,Deploy,compose,docker-compose,composer.json)engine-guard-locks.SECURITY.mdto an unlocked name and edit it in the same PRsecurity-page-renamed-away.test.skip), drop a case and empty todo.json togetheradversarial-suite-edit.auto=false, floorauto=false. Measured: the first test inengine-guard-locks.docs/Security.md,Deploy/compose.yaml)src/guardrails/glob.ts. Inferred.dockerfile*is now locked as its own glob (measured). Other case variants are still open.What this can never do
autofrom false to true, in any scope or config. Measured: the 216 rows (round 1) and the nine new paths (round 3), all tighter or unchanged.review.test.ts's every-repo list.SELF_MODIFICATION_PARTS,LOCKED_CATEGORIESorALWAYS_L3, or an entry out ofNOT_YET_FILES. Measured from the diff: those tables only gain entries.Compromised component
SECURITY.md, weaken the adversarial suite or the citations test, or add a root compose file,deploy,dockerfileorContainerfile. With a fooled evaluator, that merged by itself.NOT_YET_FILESentry is held by the weakening hold. Removing a lock it pairs with fails the pairing test, andsrc/review.tsis itself locked.computeLeveland the merge floor are code, and a fully fooled evaluator still gets "waits" (measured by the adversarial stub).SECURITY.md, Dockerfile, Containerfile, compose files anddeploykeep its own categories.Package size
Measured:
npm pack --dry-run --jsonof maina5df2254against head1687d568, both built withtsc(no web build), compared file by file.dist/src/review.js, 37,480 to 39,947 bytes (new globs, parts and comments).PACKAGE_BUDGET(2,050,000 / 6,400,000), so no budget change.Release notes
changelog.d/885-lock-security-pages-and-suite.md, under "Guardrail changes: what each one does". Measured:test/changelog.test.tsandtest/changelog-fragments*.test.tspass.New dependencies
None.