Skip to content

chore(pm): delete report-only check-widening-tells.mjs and its wiring (ruling 208) - #22002

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-21959-delete-check-widening-tells
Oct 7, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/issue-21959-delete-check-widening-tells

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21959
Clause-②: no

Deletes the report-only widening-tell instrument scripts/pm/check-widening-tells.mjs (6,528 lines) with all of its wiring, under ruling 208 (report-only instruments get no dev; their only in-flight work is deletion) and the maintainer's answer to decision batch 2 item 2, verbatim 「同意」. ⛔ Nothing replaces it, no gate is added, no workflow step is added.

What changed — the full list (12 hunks in 8 files; nothing outside them)

Line numbers are at base 6befe19c (origin/main when the worktree was cut).

# where (base) before after
1 scripts/pm/check-widening-tells.mjs the instrument, 6,528 lines deleted
2 package.json:85 the check:pm-widening-tells script removed
3 .github/workflows/lint.yml:1324-1336 the "Widening-tell gate self-test" step with its 10-line comment removed
4 .github/workflows/lint.yml:1693 "same split as the widening-tell and prior-ruling steps above" "same split as the prior-ruling step above" (bounded in-place fix, see below)
5 scripts/pm/measurement-claim-triage.mjs:57-62 scope docblock names two excluded files names only scripts/pm/dispatch-gates.mjs
6 scripts/pm/measurement-claim-triage.mjs:122-125 EXCLUDED row for the instrument removed
7 scripts/pm/check-half-states.mjs:1336-1339 docblock says the register is the instrument's SCHEMA_PROPERTY_FORMS the register is described on its own terms; no file or constant named
8 scripts/pm/check-prior-rulings.mjs:215-217 comment says the instrument takes the same import clause dropped
9 .claude/skills/pm-dispatch/references/instrument-discipline.md:7 放宽 tell(C5)由 scripts/pm/check-widening-tells.mjs 印 file:line,归达档复核裁。 放宽 tell(C5)不设仪器,Clause-② 的方向由达档复核裁。
10 .claude/skills/pm-dispatch/references/instrument-discipline.md:6 判意图的仪器(放宽 tell、半状态巡查)… 判意图的仪器(半状态巡查)… (bounded in-place fix, see below)
11 docs/audits/gate-census-2026-09.md:246 verdict retire retire · deleted 2026-10 (ruling 208; 「同意」) — row kept
12 docs/audits/gate-census-2026-09.md:353 retirement-list row same row, marked deleted 2026-10 — row kept

instrument-discipline.md stays at 12 lines (both edits are in place).

Bounded in-place fixes beyond the dispatch's list (rows 4 and 10). Both are the same defect class as the card (a live sentence naming the deleted instrument), mechanical, in a file this PR already edits, and in the same gate family. Row 4: after row 3 the comment pointed at a step that no longer exists. Row 10: line 6 listed 放宽 tell as an existing intent-judging instrument. Without this edit it would contradict the rewritten line 7 on the line below.

Premise checks (the card body is a lead, not a spec)

  • "check-half-states.mjs and check-prior-rulings.mjs import SCHEMA_PROPERTY_FORMS from it": false at base. Probe git grep -n -E "(import|from|require|import\()[^;]*check-widening-tells" 6befe19c returns exactly 1 hit, and that hit is a comment: check-prior-rulings.mjs:215. The control is the same shape on check-half-states. It returns 33 hits, real imports among them, check-prior-rulings.mjs:218 and the instrument's own :1484 included. Outside the instrument, SCHEMA_PROPERTY_FORMS appears only in the check-half-states.mjs:1336 docblock. The import direction is the reverse of the card's reading. The instrument imported from check-half-states.mjs, dispatch-gates.mjs and regen-artifacts.mjs, and nothing imported from it. The "move the constant" item therefore became rows 7–8, which rewrite the two comments. No code moved.
  • Pin, "a repo-wide grep returns only historical changelog lines": git grep -n -E "widening-tells|SCHEMA_PROPERTY_FORMS" over the whole tree at HEAD returns 4 lines, and none is in a CHANGELOG. No CHANGELOG ever named it. The 4 lines are: census :246 and :353 (marked deleted, kept per dispatch), census :398 (the historical drift paragraph, deliberately untouched) and scripts/pm/dispatch-gates.mjs:3895. That file is frozen by ruling 208: its own --self-test is green with the instrument gone (below), so the word stays.

Verification (HEAD 95c510eb, worktree objectstack-issue-21959)

  • node scripts/pm/dispatch-gates.mjs --self-test → ✓ dispatch-gates self-test: 1976 cases pass. EXIT=0 (nohup + tail --pid)
  • pnpm check:pm-dispatch-gates → ✓ check:pm-dispatch-gates --self-test: the exit contract holds in all three directions. · ✓ dispatch-gates self-test: 1976 cases pass. · the battery took 842.7s on this box. EXIT=0. It ran under nohup + tail --pid. A first attempt inside my sequential runner hit that runner's own 540 s per-command timeout, so it was re-run on its own, and that run is the one recorded here.
  • node scripts/pm/check-half-states.mjs --self-test → ✓ check-half-states self-test: 4912 cases pass. EXIT=0
  • node scripts/pm/check-prior-rulings.mjs --self-test → ✓ check-prior-rulings self-test: 155 cases pass EXIT=0
  • node scripts/check-self-test-wired.mjs EXIT=0 and node scripts/check-self-test-workflow-commands.mjs EXIT=0. The three wiring pieces went out together.
  • node scripts/check-scripts-symbol-anchors.mjs → 3722 anchors across 281 scripts resolve EXIT=0
  • pnpm check:pm-skill-ratchet, check:pm-skill-id-lint, check:pm-governed-prose, check:pm-governed-merges, check:pm-expected-skips, check:doc-authoring, check:nul-bytes, check:issue-citations → all EXIT=0
  • node scripts/pm/measurement-claim-triage.mjs --self-test → EXIT=1 identically at base 6befe19c and at HEAD. The failure in both is UNTRIAGED scripts/check-dts-references.mjs:74, which predates this PR and is unrelated to it. The report run's only difference base→HEAD is the dropped NOT SWEPT … check-widening-tells.mjs line; the population is unchanged (88 claim(s) over 49 file(s)). The tool is not wired into CI.
  • dispatch-gates --commands (no paths; change set from the merge base) derived 82 commands. I ran all 82, plus the six the dispatch named on top: 80 EXIT=0. NOT MEASURED (exit 3, PREREQUISITE NOT MET, no dist/ in a fresh worktree): check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure, check:sourcemap-no-sources-content, @objectstack/lint check:doc-formula-expressions. Declared narrowing: these read built workspace packages, this diff touches no workspace package, and CI runs them.
  • --ran reconciliation: ✓ dispatch-gates --ran: 82 derived famil(ies) accounted for — 77 run, 5 NOT-MEASURED (5 DERIVED from a recorded exit 3). 0 UNRUN.
  • Lint, run on the edited files only (a proved narrowing; the repo-wide pnpm lint belongs to CI). eslint --no-inline-config --format json on the 3 edited .mjs files: 3 files linted, 0 errors, 0 warnings, and none was reported ignored, so all 3 sit inside the config's population. Invariance: eslint.config.mjs never enables type-aware linting (no parserOptions.project, no typed rules). Nothing imported the deleted file. So this diff cannot move the verdict on any untouched file.

Landing

  • Governed Tier S (.claude/skills/pm-dispatch/references/instrument-discipline.md). The PR stays draft until the seat's contract-tier review.
  • Changed lines are 6,580 (+17 / −6,563), over the 5,000 human-merge threshold (AGENTS.md §7 class c). Almost all of it is the one deleted file.
  • skip-changeset: nothing here ships in any package's files[] (scripts/pm/**, .github/**, .claude/**, docs/audits/**, root package.json scripts).

Acceptance notes (observed, not filed)

  • scripts/pm/dispatch-gates.mjs:3895 still lists check-widening-tells among "five" live prose mentions; four remain. The file is frozen and its self-test is green, so the mention was left. Carrier: none.
  • measurement-claim-triage.mjs --self-test has been red on main since before this PR (scripts/check-dts-references.mjs:74 untriaged). The tool is report-only and unwired. Carrier: none.
  • instrument-discipline.md has no row in check-skill-line-ratchet.mjs CEILINGS, so no gate holds its line count. It is kept at 12 regardless. Carrier: none.
  • .claude/skills/pm-dispatch/references/triage-duties.md:64 still names 放宽 tell among report-only instruments whose fix cards are closed on first touch. The rule stays true for any stray card about the deleted file, so it was left. Carrier: none.

维护者速读(草稿)

改了什么:删掉一个只报告、不挡任何 PR 的 PM 内部检查脚本(放宽 tell 仪器,6528 行),连同它的 package.json 脚本行和 CI 里跑它自测的一步;再把仓里所有还点名它的地方改成不再指向一个不存在的文件(两处脚本注释、一处 CI 注释、一份工具的排除表、PM 技能的仪器纪律两行、门禁普查表两行标「已删」)。

为什么改:裁决 208 定了只报告的仪器在途工作只有删除;维护者在决策批 2 第 2 项答「同意」删它。它在普查窗口里零拦截,却每次 CI 都要跑自测、隔三岔五还要派人修。

风险与代价(含回滚):不替换、不新增门禁;Clause-② 方向的判断回到达档复核席位手里(原本也是席位裁,脚本只是印读数)。相关自测与门禁本地均绿。回滚 = revert 本 PR 一次即恢复脚本与 CI 步。

席位意见:

你要做的:本 PR 触受管面(Tier S)且改动行数超过 5000(几乎全是删一个文件),按规则需要一次授权 APPROVED 审阅后由席位落地;无需其它动作。

Synced: the parked red cleared (landing-operations §C, released)

This PR was parked as a draft behind PR #22016 (the OSV fix for #22013) with an expected-red list for Validate Package Dependencies. The release condition was met: PR #22016 merged into main as 289ff6d4. The seat then ran the one update-branch that list named: head fab444b4 is the merge of main 289ff6d4 into the reviewed head 95c510eb, with no file authored by anyone; the net diff against main is unchanged (8 files, +17 / −6563, every added and removed line byte-identical to the reviewed diff).

  • Checks on fab444b4: 37 check-runs completed, 0 failures, 0 cancelled; the 4 skips are all in the expected-skips roster (check-expected-skips --pr 22002 exit 0). Validate Package Dependencies is green on the fixed lockfile.
  • Contract review on this head: PASS, comment 6025638745 (successor to 6019414425 on 95c510eb).
  • What remains is the Tier H terminal: the diff is over the 5000-line human-merge threshold (check-governed-merges --pr 22002 exit 3), so this PR stays a draft until an authorized APPROVED from os-zhuang or hotlong; the seat then clears needs-user-decision, flips ready and arms auto-merge, and the PR lands through the queue.

Section written by the domain:skills seat 2 PM (session_0181E4ZeZmWyknawnauxD2CE); everything above it is the dev's.


Generated by Claude Code

…ring

Removes scripts/pm/check-widening-tells.mjs, its check:pm-widening-tells
package script and its lint.yml self-test step, and re-points every live
mention: the measurement-claim triage EXCLUDED row and scope docblock, two
source comments that cited the file as an importer, the stale step reference
in a neighbouring lint.yml comment, the two pm-dispatch instrument-discipline
lines that named it, and the census rows (kept, marked deleted).

Nothing replaces it; no gate is added.

Claude-Session: https://claude.ai/code/session_0181E4ZeZmWyknawnauxD2CE
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 95c510eb8abb1808ec2fb10d52f7b89d2f429b4c
Local-runs: none

Face reviewed: governed rule text — .claude/skills/pm-dispatch/references/instrument-discipline.md lines 6–7 (a Tier S path; the PR as a whole is Tier H by size: 6580 changed lines, over the 5000-line human-merge threshold per check-governed-merges --pr 22002). Inputs: card #21959, ruling 208 (#19491, comment 5755284987), PR #22002's body and file list, the net diff of 95c510eb against merge-base 6befe19c (+17 / −6563 across 8 files), and the report comment 6019127602. Check-runs on the head at 2026-10-06T15:16Z: 31 completed, 7 in progress, 1 failure (Validate Package Dependencies — OSV advisories on main's lockfile, not this PR's; anchor card #22013, see the landing-status comment). Nothing built, run or re-run here.

① Derived judgments

  • Accept set / public surface: none changed. The deletion is a PM-internal instrument (scripts/pm/**), its package.json script line and its CI self-test step; no package publishes anything from them; no schema, no error code. Right.
  • Governed line 7, 「放宽 tell(C5)不设仪器,Clause-② 的方向由达档复核裁。」: states what the card's Done-when asks (the direction of Clause-② is judged at the contract-review tier) and what ruling 208 implies once the instrument is gone. Right. Line 6, 「判意图的仪器(半状态巡查)只印读数…」: the example list no longer names an instrument that does not exist; without this edit lines 6 and 7 would contradict each other. Right — a bounded in-place fix, declared in the report and the body.
  • The deletion is pure (−6528, 0 added); package.json loses exactly the one script line; lint.yml loses the step and its comment block (13 lines) and one stale comment at :1693 is re-pointed; the two docblock / comment rewrites remove references to a file and a constant that no longer exist; the census rows are kept and marked deleted. Right.
  • The card's premise that two files import SCHEMA_PROPERTY_FORMS from the instrument was false on origin/main (zero import sites; both cited lines were comments) — the seat's pre-dispatch grep and the dev's probe (import / from / require shapes over the file name: 1 hit, a comment; control over check-half-states.mjs: 33 hits) agree; the comments were rewritten instead of a constant moved. Right; the Done-when is met in substance.
  • scripts/pm/dispatch-gates.mjs untouched (frozen under ruling 208); its docblock at :3895 still counts the instrument among "five" live mentions while four remain — a stale count in a frozen file; its --self-test is green with the file gone (1976 cases, per the report, to be confirmed by CI). Left. Right.
  • The two governed lines are 71 and 100 bytes; instrument-discipline.md has no CEILINGS row (the dev's reading, which corrects the seat's "12 / 12" claim-time reading — no ratchet gate reads this file); 12 lines before and after regardless. No issue number added. Right.

② Semver level

Clause-②: no on the claim (6018121757) and at PR body line 2 — right: no published contract moves. skip-changeset — right: scripts/pm/**, .github/**, .claude/**, docs/audits/** and a root package.json script line publish nothing. No existing changeset is edited.

③ Boundary flags

  • Two bounded in-place fixes beyond the dispatch list (lint.yml :1693 comment; instrument-discipline.md line 6): accepted; the claim's file surface is amended on the card in the same round.
  • The constant not moved (zero importers): accepted, evidence in the report and the PR body.
  • Base 6befe19c (one unrelated commit after dispatch): accepted.
  • The pin reading (after the deletion a repo-wide grep leaves the two census rows, the history paragraph and the frozen docblock): accepted as the card allows.
  • The model-free commit trailer pair: correct per the dev definition.
  • PR size over 5000 ⇒ Tier H: the dev read it right; the four-item terminal is the seat's and is on this thread.
  • Labeler-set labels left alone: right.
  • Acceptance notes (4, carrier: none): the frozen docblock's count; measurement-claim-triage --self-test red identically at base and head (UNTRIAGED scripts/check-dts-references.mjs:74) — a report-only tool outside CI, no card under ruling 208; no ratchet row for instrument-discipline.md; triage-duties.md :64 still names 放宽 tell among report-only instruments — a rider for the next PR on that file. Notes, no card.
  • open_questions: none. One red check on the head is not this PR's (OSV advisories on main's lockfile; anchor card [finding] main's lockfile matches two new OSV advisories (sharp 0.35.4 GHSA-wq5f-xc86-pv6w high, fixed in 0.35.5; shell-quote 1.10.0 GHSA-pqg4-j6r4-53mv critical, fixed in 1.11.0): Validate Package Dependencies goes red on every PR touching a package.json #22013); the landing waits for its fix and an update-branch.

Implemented-by: claude/issue-21959-delete-check-widening-tells
Reviewed-by: session_0181E4ZeZmWyknawnauxD2CE

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)

改了什么:删掉 PM 内部那个只报告、不挡任何 PR 的「放宽 tell」仪器 scripts/pm/check-widening-tells.mjs(6528 行),连同 package.json 里它的脚本行和 CI 里跑它自测的一步;仓里还点名它的地方全部改成不再指向一个不存在的文件(两处脚本注释、一处 CI 注释、一份工具的排除表、PM 技能仪器纪律两行、门禁普查表两行标「已删」)。不新增任何替代仪器、门禁或 workflow 步。

为什么改:裁决 208 定了只报告的仪器不配 dev、在途工作只有删除;您在分诊席决策批 2 第 2 项答「同意」删它。它在普查窗口里零拦截,却每次 CI 都跑自测、隔三岔五要人修(最近一次 10-01)。

风险与代价(含回滚):Clause-② 方向的判断回到达档复核席手里(原本也是席位裁,脚本只印读数)。卡上一条前提是假的——仓里没有任何文件从它 import SCHEMA_PROPERTY_FORMS,所以不搬常量、只改两处注释;冻结的 dispatch-gates.mjs 一个字没动,其自测在文件删除后仍绿(1976 例)。回滚 = revert 本 PR 一次。CI 现有一项红 Validate Package Dependencies,不是本 PR 的:OSV 扫出 sharp 0.35.4 与 shell-quote 1.10.0 两条今天下午新发布的 advisory,本 PR 一条依赖没动(只删了一行脚本,触发了按路径过滤的 workflow);已立锚卡 #22013 止血,修复落 main 后席位给本 PR 合一次 main 让它重跑。

席位意见:席位达档复核 PASS(记录在本 PR 评论 ⬆);改动是一次机械删除加七处改引用,范围与卡面一致,无扩写。建议批准。

你要做的:一个动作——在本 PR 上点 Approve。改动 6580 行超过 5000 行人工合并线,规则要求授权账户 APPROVED;批准后由席位在 OSV 红清除、CI 全绿时入队落地,不需要您合并。

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Landing status — Tier H by size; the one red check is not this PR's · session_0181E4ZeZmWyknawnauxD2CE (domain:skills#2) · 2026-10-06T15:21Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

CI note — one re-run of the cancelled type-check lane · session_0181E4ZeZmWyknawnauxD2CE (domain:skills#2) · 2026-10-06T15:39Z

TypeScript Type Check went red on 95c510eb only because its lane Type Check · source gates (job 112338519086, run 37483796876) was cancelled at the Checkout repository step after a ten-minute hang — every later step was skipped, so no test body ran; the aggregate "Verify every type-check lane succeeded" then read cancelled where it expects success. The four sibling lanes (workspace, debt ledger, consumer gates, Lint & Repo Gates) all passed on the same head. That is the one case the landing rules allow a re-run for (died before any test body ran, on the current head); the failed jobs of run 37483796876 were re-queued once at this reading. A second failure would be real and is read as such. Validate Package Dependencies stays red for the reason in the landing-status comment above (#22013, not this PR's) and is not re-run.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: fab444b4b9935b3c8199f8480e6164ec2a4c49aa
Local-runs: none

Successor record to 6019414425 (head 95c510eb). Head fab444b4 is the GitHub update-branch merge of main 289ff6d4 (the squash of PR #22016, the OSV fix this PR was parked behind) into the reviewed head 95c510eb; parents 95c510eb and 289ff6d4, no file authored by anyone. Face reviewed: the same governed rule text, .claude/skills/pm-dispatch/references/instrument-discipline.md lines 6–7 (a Tier S path; the PR as a whole stays Tier H by size, 6580 changed lines). Inputs: card #21959, ruling 208, PR #22002's body and file list, the net diff of fab444b4 against main 289ff6d4 (8 files, +17 / −6563) compared line by line with the diff reviewed in 6019414425 (95c510eb against merge-base 6befe19c): every added and removed line is byte-identical (6580 lines, equal hash), hunk headers and context lines identical; the only bytes that moved are the index blob ids of files main changed elsewhere (.github/workflows/lint.yml by PR #22033, outside this PR's hunks). Check-runs on the head at 2026-10-06T21:18Z: 18 completed, 16 in progress or queued, 0 failure(s); Validate Package Dependencies is running against the lockfile that now carries #22016's fix, and the landing waits for every check to complete green. Nothing built, run or re-run here.

① Derived judgments

  • Accept set / public surface: none changed. The deletion is a PM-internal instrument (scripts/pm/**), its package.json script line and its CI self-test step; no package publishes anything from them; no schema, no error code. Right, unchanged from 6019414425.
  • Governed line 7 (放宽 tell has no instrument; the direction of Clause-② is judged at the contract-review tier) states the card's Done-when and what ruling 208 implies once the instrument is gone; line 6 no longer lists an instrument that does not exist, so lines 6 and 7 agree. Right; the bounded in-place fix on line 6 was declared in the report and the body.
  • The deletion is pure (−6528, 0 added); package.json loses exactly the one script line; lint.yml loses the step with its comment block and has one stale comment re-pointed; the two docblock / comment rewrites drop references to a file and a constant that no longer exist; the census rows are kept and marked deleted. Right.
  • The card's premise that two files import SCHEMA_PROPERTY_FORMS from the instrument was false on origin/main (zero import sites; both cited lines were comments); comments were rewritten instead of a constant moved. Right; the Done-when is met in substance.
  • scripts/pm/dispatch-gates.mjs untouched (frozen under ruling 208); its stale "five" count at :3895 is left; its self-test is green with the file gone (1976 cases in the report; confirmed by the previous head's CI). Right.
  • The merge brings in nothing that touches this PR's eight files beyond lint.yml's unrelated regions; no conflict marker in the tree. Right.

② Semver level

Clause-②: no on the claim (6018121757) and at PR body line 2 — right: no published contract moves. skip-changeset — right: scripts/pm/**, .github/**, .claude/**, docs/audits/** and a root package.json script line publish nothing. No existing changeset is edited; the merge adds none.

③ Boundary flags

  • Two bounded in-place fixes beyond the dispatch list (lint.yml comment; instrument-discipline.md line 6): accepted, as in 6019414425.
  • The constant not moved (zero importers): accepted, evidence in the report and the PR body.
  • Head moved by a base sync only (the one update-branch named in the body's parked section, after its release condition — PR fix(deps): take the fixes for sharp and shell-quote that turn main's OSV scan red #22016 merged — was read on main and on the queue refs): accepted; the diff content is unchanged, so every judgment above carries.
  • PR size over 5000 ⇒ Tier H: the four-item terminal stands (this record, needs-user-decision on the PR, 速读 6019466267, review requested from os-zhuang and hotlong); landing after an authorized APPROVED and all checks green, by the seat, through the queue.
  • Acceptance notes (4, carrier: none): unchanged — the frozen docblock's count; measurement-claim-triage --self-test red identically at base and head; no ratchet row for instrument-discipline.md; triage-duties.md :64 still names 放宽 tell. Notes, no card.
  • open_questions: none.

Implemented-by: claude/issue-21959-delete-check-widening-tells
Reviewed-by: session_0181E4ZeZmWyknawnauxD2CE

VERDICT: PASS


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review October 7, 2026 04:26
@os-zhuang
os-zhuang enabled auto-merge October 7, 2026 04:27
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit 5bd8cb1 Oct 7, 2026
46 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-21959-delete-check-widening-tells branch October 7, 2026 05:02
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…to a checkout budget plus a body budget (objectstack-ai#22033)

Fixes objectstack-ai#22020
Clause-②: no

## Which route

The card's done-when offers two routes. This PR takes the first one, in
the split form triage asked for (`6022336293`). Every full-history
checkout under a fixed job wall gets its own step-level
`timeout-minutes: 20`. Each job wall becomes that checkout budget plus
the job's re-measured body budget. No gate, command, fetch depth, job id
or check name changes. The seven required contexts are untouched, and
the merge-base anchor of the authorable-surface deletion gate keeps its
full history (`fetch-depth: 0` is unchanged on every job).

| job | file | wall before | checkout step | body budget | wall after |
|---|---|---|---|---|---|
| Type Check · source gates (the card) | `lint.yml` | 10 | 20 | 10 |
**30** |
| Type Check · workspace (adjacent) | `lint.yml` | 30 | 20 | 35 | **55**
|
| Type Check · debt ledger (adjacent) | `lint.yml` | 15 | 20 | 15 |
**35** |
| Type Check · consumer gates (adjacent) | `lint.yml` | 20 | 20 | 25 |
**45** |
| Governed Surface Queue Guard (adjacent) | `governed-surface-guard.yml`
| 10 | 20 | 10 | **30** |

**In-scope adjacent fixes.** All four have the same defect class and get
the same mechanical treatment:
- The other three Type Check lanes, added by the PM after merge-group
run 37509811445. In that run, job 112427734781 ("Type Check · consumer
gates") spent 11m05s in the checkout and was cancelled at its 20-minute
wall. The aggregate failed and PR objectstack-ai#22016 was ejected from the queue.
- The Governed Surface Queue Guard, which the claim named (run
37508992265, job 112424902156).

The `lint` job is not touched. Open PR objectstack-ai#22002 edits its region.

## Measured

**Window.** I read the 300 most recent completed runs of `Lint & Type
Check` (2026-10-05T15:32Z to 2026-10-06T18:41Z) and of `Governed Surface
Guard` (2026-10-05T14:57Z to 2026-10-06T18:59Z). Every attempt was
included (`GET /actions/runs/{id}/jobs?filter=all`), using step
timestamps. The controls come from the 300 most recent `CI` runs
(2026-10-05T15:15Z to 2026-10-06T18:22Z).

**Checkout per job.** "Wall hits" counts the runs that the old wall
cancelled behind a slow checkout:

| job | n | p50 | p90 | p99 | max | wall hits |
|---|---|---|---|---|---|---|
| Type Check · source gates | 293 | 31 s | 122 s | 599 s | 600 s (cut by
the wall) | 8 |
| Type Check · workspace | 296 | 31 s | 100 s | 395 s | 421 s | 0 |
| Type Check · debt ledger | 293 | 31 s | 123 s | 599 s | 774 s | 2 |
| Type Check · consumer gates | 293 | 31 s | 151 s | 574 s | 735 s | 2
(both `merge_group`) |
| Governed Surface Queue Guard | 292 | 31 s | 156 s | 595 s | 599 s (cut
by the wall) | 4 |

**Pooled.** Adding the same fetch from `ci.yml`'s Test Core shards gives
n = 3,459: p50 31 s, p90 119 s, p99 553 s, max 895 s. Another 19 samples
were cut off by a wall and are lower bounds only.

**Control.** The shallow (`fetch-depth` 1) checkouts in `ci.yml` over
the same hours (n = 2,743) read p50 15 s, p99 63 s, max 107 s. None went
over 180 s. So the slow tail belongs to the full-history fetch, not to
the runner pool.

**Body budgets.** The body is everything after the checkout, on
successful runs:

| job | max | 2 × max | budget |
|---|---|---|---|
| source gates | 3.2 min | 6.4 | 10 (the floor) |
| workspace | 17.9 min | 35.8 | 35 |
| debt ledger | 7.4 min | 14.8 | 15 |
| consumer gates | 11.5 min | 23 | 25 |
| guard | 1.1 min | 2.2 | 10 (the floor) |

The rule is the `lint` job's: 2× the measured max, on a 5-minute grain,
never below 10.

**Checkout budget.** 20 minutes is 1.3× the pooled 895 s max. The rule's
2× would give 30, but the queue's window binds first: the workspace
lane's 35 plus 30 is 65, past the 55 cap the `lint` job argues, while 35
+ 20 = 55. It is one fetch, so every job gets the same budget.

## What the required aggregate reads

**Before.** The lane's job wall stopped any checkout that left the body
too little time. That covers slow checkouts that would have finished,
such as the 555 s and 588 s cases, as well as stuck ones. The lane read
`cancelled` and every gate was skipped. `TypeScript Type Check` printed
"concluded `cancelled` -- expected `success`" and failed the PR.

**After:**
- **A checkout that ends inside 20 minutes:** nothing happens. The body
keeps its full budget and the lane goes green. In this window, that
covers every checkout that completed.
- **A checkout still running at 20 minutes:** the step fails with "The
action 'Checkout repository' has timed out after 20 minutes." Later
steps are skipped, the lane reads `failure` and the aggregate goes red.
No layout can keep a truly hung checkout green, because the aggregate
correctly refuses a lane that ran no gate. This layout names the step
that hung, and it fires only beyond 1.3× anything measured.
- **A later step that hangs:** the job wall cancels the job, the lane
reads `cancelled`, and the aggregate goes red, as before but at the new
wall.

**Source check (premise 2).** In actions/runner `main` at `67f01c27`,
`StepsRunner.RunStepAsync` evaluates every step's `timeout-minutes`,
`uses:` steps included. When it expires, the runner sets
`TaskResult.Failed` with that message. A job-level cancellation sets
`TaskResult.Canceled` instead. No workflow in this repo had a step-level
timeout before this PR.

## Deviation from triage's direction

Triage wrote: "The gate steps keep the stall guard's intent through
their own step-level timeouts." I did not add those. The PM thread has
the reasoning:
- Actions has no timeout over a group of steps. Covering the gates would
take one step-level timeout per step, about 65 of them across the four
lanes, each sized from a single 27-hour window.
- Several of those steps are bimodal, for example a turbo cache hit in 4
s against a miss in 5 minutes. Timeouts sized that tightly would be a
new source of random reds, which is this card's own defect class.

The cost: after a fast checkout, a hung gate now runs up to the new wall
(30, 35, 45 or 55 minutes) instead of the old one. Every new wall stays
at or under the 55 cap, inside the queue's 60-minute window.

## This PR's own lane run

Read from the jobs API by the seat (step timestamps), run `37519814891`
(`Lint & Type Check`, head `f5060b251`) and run `37519814823` (`Governed
Surface Guard`):

| job | conclusion | checkout | job duration | new wall |
|---|---|---|---|---|
| Type Check · source gates | success | 34 s | 197 s (3.3 of 30 min) |
30 |
| Type Check · debt ledger | success | 147 s | 217 s | 35 |
| Type Check · workspace | success | 242 s | 280 s | 55 |
| Type Check · consumer gates | success | 33 s | 298 s | 45 |
| TypeScript Type Check (aggregate) | success | — | 3 s | — |
| Governed Surface Queue Guard | success | 38 s | 66 s | 30 |

_Section filled in by the `domain:devx` seat 2 PM from the run above,
because the body is written before its own run exists._

## Local gates (head `f5060b251`)

`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 44 commands from the actual diff.
Exit codes were written to disk before reading:
- 42 exit 0. This includes `check:required-contexts`,
`check:stall-guard-budget`, `check:stall-guard-headroom`,
`check:workflow-status-functions`, `check:workflow-step-name-quoting`,
`check:aggregator-roster` (+ `--self-test`) and `check:step-collectors`
(+ `--self-test`). The full reconciliation is in the report.
- I also ran the guard's own `check-governed-queue-guard.mjs
--self-test`: 296 cases passed, including the `fetch-depth: 0` pin.
- `check:type-check-debt` ran its `--self-test` half (exit 0). Its
`--re-measure` half needs the workspace build the debt lane runs first.
It is declared to this PR's CI rather than measured here, because the
diff touches no package and no ledger.

## Acceptance notes

- **Mechanism, measured locally and not changed here.** `fetch-depth: 0`
makes actions/checkout v7 fetch `+refs/heads/*` and `+refs/tags/*`:
1,236 branch heads and about 7,950 tags today. From this container:

  | fetch | time | pack |
  |---|---|---|
  | `main`'s full history alone | 29 s and 32 s | 397 MB |
  | the all-refs refspec CI uses | 473 s | 598 MB |
  | all heads without tags | 686 s | 597 MB |
  | depth 1 plus unshallowing `main` | 120 s and 169 s | not recorded |

The last variant kept the anchor exact on PR objectstack-ai#22002's merge ref and on
PR objectstack-ai#6356's old head. The cost is the ref set, not `main`'s depth.
Fetching less could make every one of these jobs faster, but the CI tail
of any narrower fetch is unmeasured (n = 0), and its semantics need a
per-gate audit across all five jobs. It is left as a question for the PM
in the report.
- **`ci.yml` Test Core shards**, same fetch, 45-minute wall: one shard
was cancelled at the wall behind a 508 s checkout in the window (run
37338337323, `Test Core (2/6)`). This is outside this PR's file surface,
and those steps run under the stall guard, whose budget gate reads that
wall. The report names it for the seat.
- **`Lint & Repo Gates`** is not exposed in the window: body max 34.5
min plus checkout max 730 s is 46.7, under its 55.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…OSV scan red (objectstack-ai#22016)

Fixes objectstack-ai#22013

Clause-②: no

## What this does

`Validate Package Dependencies` runs OSV-Scanner against
`pnpm-lock.yaml`. Two advisories published on 2026-10-06 match `main`'s
lockfile, so every PR that touches a `package.json` inherits a red that
is not its own (PR objectstack-ai#22002, run 37483796939, is the first measured), and
the next scheduled scan will be red too. Both advisories name a fixed
version, so this PR takes both fixes. There is no exemption:
`osv-scanner.toml` is untouched, because it is for advisories with no
fixed release.

PR objectstack-ai#22002 is not touched here. Its `update-branch` after this lands is
the PM's pointer to its holder.

## OSV reading: before and after

Measured locally with OSV-Scanner **v2.3.8**, the version
`validate-deps.yml` pins, in offline mode (`--offline-vulnerabilities
--download-offline-databases`). The OSV npm database was downloaded on
2026-10-06 at 16:36 UTC, after both advisories were published (13:40 and
13:43 UTC).

`main` at `803764a3` (exit 1). These are the two rows the card names:

```text
| https://osv.dev/GHSA-wq5f-xc86-pv6w | 8.9  | npm       | sharp       | 0.35.4  | 0.35.5        | pnpm-lock.yaml |
| https://osv.dev/GHSA-pqg4-j6r4-53mv | 9.2  | npm       | shell-quote | 1.10.0  | 1.11.0        | pnpm-lock.yaml |
```

This PR at `d263b701` (exit 0): `No issues found`. One vulnerability is
filtered, the standing `sprintf-js` exemption, unchanged. The scanner
names nothing beyond these two advisories, so no third one has appeared
since the card was filed.

## Changes

### `sharp`: the override target lifts from `^0.35.4` to `^0.35.5`

- **Advisory:** GHSA-wq5f-xc86-pv6w (8.9 high). It is a memory bug in
the librsvg that sharp's prebuilt binaries bundle. The range is
introduced 0, fixed 0.35.5, read from the scanner's offline database.
- **Edit:** the target only, `'sharp@>=0.34.0 <0.36.0': '^0.35.5'`. The
selector already sits at the 0.x caret boundary, so it does not change.
This is the same shape as the objectstack-ai#16999 lift on this entry.
- **Dedupe, not a forced upgrade:** `next@16.3.6` declares the optional
`sharp: ^0.35.4`, which already admits 0.35.5. 0.35.4 was the single
resolved copy.
- **What moves with it:** sharp pins its prebuilt `@img/sharp-*`
binaries exactly, so they move to 0.35.5, and the libvips binaries move
to 1.3.4.
- **Measured:** the installed sharp 0.35.5 loads on linux-x64. It
reports `rsvg 2.63.2`, the librsvg release the advisory names as fixed,
and it renders a PNG.

### `shell-quote`: a new override, `'shell-quote@>=1.8.4 <2.0.0':
'^1.11.0'`

- **Advisory:** GHSA-pqg4-j6r4-53mv (9.2 critical). In `quote()`, a line
terminator in a string after a `{ comment }` token ends the comment, and
the rest of that string runs as shell input. The range is introduced
1.8.4, fixed 1.11.0.
- **The one path:** `launch-editor@2.14.1`, then
`@changesets/cli@3.0.3`, then the root `package.json`'s
`devDependencies`. launch-editor declares `shell-quote: ^1.8.4`, which
admits the fix, so this is a dedupe onto the patched line. The copy sat
on 1.10.0 through lockfile inertia.
- **Selector shape:** the floor is the advisory's 1.8.4, and the bound
sits at the 2.0.0 major boundary, per the block header's rule. The bound
is never `<1.11.0`.
- **Resolution:** `^1.11.0` floats to **1.12.0**, the newest 1.x.
launch-editor calls shell-quote in one place, `parse()` on the editor
command (`guess.js`). 1.11.0 and 1.12.0 only teach `parse()` ANSI-C
quoting and more operators. Seven editor command strings (`code --wait`,
a quoted macOS path with `-w`, `emacsclient -t -a ''` and others) parse
identically under 1.10.0 and 1.12.0. The vulnerable `quote()` is never
called on this path. The fix is taken anyway, because the gate reads the
lockfile, not the call graph.
- **Note:** the entry carries a note in the block's style, at the foot
of `overrides:`.

### Why an override and not a `@changesets/cli` bump

No release on that path forces the fix (`npm view`, 2026-10-06):

- `@changesets/cli` 3.0.3 is npm `latest`, which is the version the root
already declares. Every 3.0.x declares `launch-editor: ^2.14.1`.
- launch-editor's latest, 2.14.2 (published today), declares
`shell-quote: ^1.10.0`, which still admits the flagged 1.10.0.

So the bump route does not exist, and a launch-editor bump would still
leave the floor to lockfile inertia. On the four axes:

- **Real need:** the measured need is a patched resolution and a green
gate. Only the override delivers both, because no upstream release
declares a range above 1.10.0.
- **Long-term soundness:** the entry follows the block header's selector
rule (bound at the major boundary), so a later advisory is a target-only
lift. It turns into a dedupe floor once launch-editor declares
`^1.11.0`. Its cost is one more ledgered entry.
- **Making AI mistakes harder:** a declared floor is audited by
`check:override-consistency`, and no re-lock can land below it. A bare
re-lock with no floor, like objectstack-ai#21951's `proxy-addr` step, leaves nothing
to stop a later resolution from drifting back.
- **Startup scope:** this is the smallest change. It moves no
devDependency and adds no gate.

## Lockfile diff

`pnpm-lock.yaml` is **+126/−125**, re-locked by `pnpm install`, never by
hand. Every changed line falls into one of four kinds:

- a `sharp`, `@img/sharp-*` or `shell-quote` package or snapshot key;
- a dependency edge onto one of those packages;
- the integrity line under one of those keys;
- one of the two `overrides:` header lines, which are the sharp target
and the new shell-quote entry.

Nothing else moves. This was measured by attributing every changed line:
after dropping the lines that name sharp or shell-quote, and the
integrity lines under those keys, zero lines remain.

`pnpm-workspace.yaml` is **+39/−1**: the sharp target, its dated note,
and the shell-quote entry with its note.

`pnpm why`, before and after:

- `sharp` 0.35.4 becomes 0.35.5, one version in both cases. The tree
shape is byte-identical with the version masked: through `next@16.3.6`
under `@objectstack/docs` (and the fumadocs packages), and through
better-auth's `next` peer under `@objectstack/plugin-auth`.
- `shell-quote` 1.10.0 becomes 1.12.0, one version in both cases:
`launch-editor@2.14.1`, then `@changesets/cli@3.0.3`, then the root's
devDependencies.

## Changeset

`skip-changeset`. The diff touches `pnpm-lock.yaml` and
`pnpm-workspace.yaml`, both repo-root configuration, and neither is in
any package's `files[]`. sharp resolves under two importers:

- `@objectstack/docs`, which is private.
- `@objectstack/plugin-auth`, which is published. Its `files[]` is
`dist`, `README.md` and `CHANGELOG.md`, and its `package.json` does not
change. It declares no sharp range at all. sharp reaches it only through
better-auth 1.7.3's optional `next` peer, which this workspace
auto-installs (`auto-install-peers=true`), and next's own optional
`sharp` dependency.

Overrides do not reach downstream installs, so nothing published
changes. shell-quote is dev-only, under the private root.

## Local verification, at `d263b701`

- `pnpm install --frozen-lockfile --prefer-offline`: exit 0.
- The gates come from `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack`, derived from the change set at this
head: 22 commands. The dispatch named 25 because it included
`package.json`, which this diff does not touch. The `--ran`
reconciliation is filled in below.

`--ran` reconciliation, with exit codes recorded before any pipe: **22
derived, 18 run, 4 NOT-MEASURED, 0 UNRUN**.

| Gate | Exit |
|---|---|
| `node scripts/check-changeset-fixed.mjs` | 0 |
| `node scripts/check-closing-keyword-parity.mjs` (+ `--self-test`) | 0
/ 0 |
| `node scripts/check-comment-mask-corpus.mjs` | 0 |
| `node scripts/check-dts-emitted.mjs --self-test` | 0 |
| `node scripts/check-osv-exemptions.mjs` (+ `--self-test`) | 0 / 0 |
| `node scripts/check-prerelease-pin-watch.mjs --self-test` ;
`--verbose` | 0 / 0 |
| `pnpm --filter @objectstack/spec run check:llms-txt` | 0 |
| `pnpm check:driver-memory-census` · `check:gitlink-declared` ·
`check:nul-bytes` · `check:override-consistency` ·
`check:refd-timer-probe` · `check:vendor-export-contract-resolve` ·
`check:watch-hint-literal` · `check:workspace-manifest-cycles` | 0 each
|
| `pnpm check:dts-closure` · `check:dual-build-cjs-loads` ·
`check:lean-entry-closure` · `check:sourcemap-no-sources-content` | **3,
PREREQUISITE NOT MET** |

- **`check:override-consistency`:** the census now counts 38 overrides,
up from 37. shell-quote appears in neither report: it has a consumer,
and its bound clears the target floor.
- **NOT MEASURED (four gates):** they read every package's built
`dist/`. This diff touches no package source, so the local scope builds
no package, and the build these four need is the full `pnpm build`. CI's
`Build Core` and `Lint & Repo Gates` measure them on the built tree.
This narrowing is declared here, and none of the four is counted as a
pass.
- **Not run locally, CI's:** the path-scheduled jobs that
`dispatch-gates` lists (`Test Core`, `Temporal Conformance`, `Dogfood
Regression Gate`, `Dogfood Verify CLI`, `Build Core`, `Build Docs`) and
the type-check lanes.

## Acceptance notes

- `pnpm install` prints `ioredis-mock 8.13.1: unmet peer ioredis@^5:
found 6.0.0`. That warning is already on `main` and is not from this
diff.
- The sharp selector's floor is 0.34.0, while the new advisory's range
starts at 0. No sharp copy below 0.34 resolves anywhere, and the card
rules the selector untouched, so the floor stays where it is.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 9, 2026
…rge base; main keeps the absolute daily scan; job timeout 15 min (objectstack-ai#22138)

Fixes objectstack-ai#22082

Clause-②: no

## What this does

Implements ruling A as recorded on the card (comment 6049727506). On
`pull_request`, the OSV verdict of `validate-deps.yml` is now
base-relative. On `schedule` and `workflow_dispatch`, `main` keeps the
absolute verdict from the same step as before. That step's only change
is `if: github.event_name != 'pull_request'` (its version comment is
also corrected, see below). The job gains `timeout-minutes: 15`, sized
from the measured p50 of 1.3 min and max of 1.9 min.

- **Lockfile unchanged.** The new stage step compares the pull request's
test merge with its merge base. If neither `pnpm-lock.yaml` nor
`osv-scanner.toml` differs, the step prints the notice "lockfile
unchanged — inherits main's verdict" and runs no scan. The ledger counts
as part of the input because a PR that deletes an exemption changes the
verdict without touching the lockfile (replay R3 below).
- **Otherwise, both lockfiles are scanned.** The same pinned action
scans the PR's lockfile and the merge base's. Each is judged under its
own `osv-scanner.toml`, so the merge base's verdict is what `main`'s
scan gives and the PR's verdict is what the old absolute scan gave.
`scripts/osv-base-relative.mjs` then sorts the advisories:
- An advisory matched by both is **inherited**. It gets a `notice`
annotation that names it, the package versions each side matched, and
the open finding card that names it, when one exists. It does not fail
the step.
- An advisory matched only by the PR's lockfile is **introduced**. It
gets an `error` annotation and fails the step, as before.
- **The merge base.** `actions/checkout` puts the PR's test merge
(`refs/pull/N/merge`) at depth 1. Its first parent is the base-branch
commit that GitHub merged the PR into, which makes it the merge base of
the scanned tree. The step reads that parent from the commit object and
fetches it with one depth-1 `git fetch` (never `fetch-depth: 0`). If
HEAD is not a two-parent merge, the step fails with an error annotation.
- **Fail-closed.** A side is read only when its result file parses and
its step outcome matches its finding count (success means zero findings,
failure means some). In every other case the step fails and names that
side.

This adds no new gate, changes nothing that `main`'s scan judges, and
adds no exemption. `osv-scanner.toml`,
`scripts/check-osv-exemptions.mjs` and the other gate scripts are
untouched, and they still run first and unchanged.

## Why a helper script, and why not upstream's `osv-reporter-action`

The upstream reporter was read at `google/osv-scanner` v2.5.0
(`cmd/osv-reporter/main.go`,
`internal/ci/vulnerability_result_diff.go`). Three things rule it out:

- **It fails open.** When the new-side result file is missing or does
not parse, it logs a warning, treats the side as having no findings and
exits 0. Upstream's own reusable PR workflow runs both scans with
`continue-on-error: true`, so a scan error on the PR side passes the
gate.
- **It needs both sides at the same source path.** The diff keys on
`source.path`, so the base lockfile would have to be swapped into the
workspace and restored mid-job.
- **It cannot name the inherited advisories**, and the ruling requires
that notice.

The helper is one definition of introduced versus inherited, with the
scan-error guard built in. Its `--self-test` (23 cases, floor pinned)
runs as the first line of the stage step, so `check:self-test-wired`
sees it. The helper is in the `paths:` filter, so a PR that edits it
runs this workflow.

## Measured mechanism (osv-scanner 2.5.0, the binary in the pinned
action's image)

- **The pin.** `f4cfcc01edc9c8b756a9b873b7a623ca674da51e` is an untagged
upstream commit between the v2.5.0 and v2.5.1 tags. Its `action.yml`
runs `ghcr.io/google/osv-scanner-action:v2.5.0`, and run 37483796939's
job steps include "Pull ghcr.io/google/osv-scanner-action:v2.5.0". The
`# v2.3.8` comment had been stale since Dependabot's sha bump (objectstack-ai#9209).
It is corrected, and all three `uses:` lines stay on that one sha.
- **The image's binary matches the release binary.** It is built from
the same `cmd/osv-scanner` with the same flags (goreleaser builds
`osv-scanner-action`). The release binary `osv-scanner_linux_amd64`
matched its published SHA256SUMS (`edcfc41d…`). The action's entrypoint
is upstream's `exit_code_redirect.sh`, which rewrites exit 128 (no
packages) to 0.
- **The ledger is found only in the lockfile's own directory.** With a
ledger one directory up, an ignored advisory was still reported. The
workflow therefore passes `--config` explicitly on both sides. This
settles dispatch assumption ②: without `--config`, a base lockfile
staged at another path is scanned with no ledger at all.
- **Exit codes, with `--config` and `--output-file`:**

| case | exit | result file |
|:--|:--|:--|
| no findings | 0 | written, `results: []` |
| findings | 1 | written |
| expired `ignoreUntil` | 1 | written; the advisory is reported again
(the ledger's expiry convention holds on both sides) |
| unknown key / broken TOML / missing `--config` file | 127 | **none** |
| missing lockfile | 127 | **none** |
| no packages | 128, rewritten to 0 by the action | **none** |

Because the outcome alone cannot tell findings from errors, the helper's
file-plus-outcome agreement check is what keeps the verdict closed.

## The pin

The body is written once, when the PR is created and before any run
exists, so the run ids are posted in the `os-dev-report` comment on
objectstack-ai#22082, keyed by the head shas named here.

**Real runs** (seat amendment, from the dev's report on objectstack-ai#22082). All are
`validate-deps.yml` on `pull_request`, against the merge base
`ef1fcb26a24f` (the `main` tip):

| Head | Run (job) | Result |
|---|---|---|
| `ce598b1ab7` | `37719042481` (`113122177520`) | success, with the
"lockfile unchanged" notice |
| `501569be49` (probe: `minimist` 1.2.5) | `37719190017`
(`113122655549`) | **failure**, one error: `GHSA-xvch-5gv4-984h`
(minimist@1.2.5) |
| `3576cdc334` (probe reverted) | `37719368398` (`113123226526`) |
success, with the "lockfile unchanged" notice |
| `5473cad514` (probe: `minimist` 1.2.8, no advisory, lockfile changed)
| `37719558844` (`113123838149`) | success, with **six inherited
notices** for `next@16.3.6` |
| `cc98faf47a` (final; same tree as `ce598b1ab7`) | `37719733046`
(`113124391631`) | success, with the "lockfile unchanged" notice |

`main` at `ef1fcb26a24f` carries those six advisories (filed as objectstack-ai#22148).
So:
- the inherited pin is a **real run**: `37719558844`;
- the `scripts`-only pin is `37719368398` / `37719733046`, against that
advisory-carrying `main`;
- the replays R1–R3 below are supplementary.

### "A lockfile that introduces a new advisory fails": real run

- **Head `501569be49`** is a TEMPORARY probe commit. It adds root
devDependency `minimist` 1.2.5 (GHSA-xvch-5gv4-984h, fixed in 1.2.6)
through `pnpm add -D -w`. The PR squashes, and `3576cdc334` reverts the
probe.
- **Expected run result:** the stage step reports `compare=true`, both
scans run, and the judge fails with exactly one `error` annotation, for
GHSA-xvch-5gv4-984h (minimist@1.2.5).
- **Pre-check run locally**, with the same steps against main
`8fc50b7647` and the 2026-10-06 database snapshot described under R1–R3:

```
::error title=OSV advisory introduced by this pull request::GHSA-xvch-5gv4-984h (minimist@1.2.5) is matched by this pull request's lockfile and not by the merge base's (8fc50b7). ...
OSV base-relative verdict against the merge base (8fc50b7): 1 introduced, 0 inherited, 0 resolved.
judge exit=1
```

### "Lockfile unchanged": real runs

- The heads are `ce598b1ab7` (the implementation, at PR open) and
`3576cdc334` (after the probe's revert). The two trees are identical:
both are `d477febb20`.
- This PR changes neither `pnpm-lock.yaml` nor `osv-scanner.toml`, and
`.github/workflows/validate-deps.yml` is in the `paths:` filter, so the
workflow runs on it.
- **Expected:** a pass with the notice "lockfile unchanged — inherits
main's verdict", and the three scan and judge steps skipped.
- This branch runs no scan, so `main`'s advisory state cannot reach it.
That is why a `scripts`-only PR against a `main` that carries an
advisory passes here. R1 replays exactly that case.

### "Inherited passes with a notice": real run `37719558844` (above);
the replays below are supplementary

`main` is clean today: scheduled run 37565270565 succeeded. A real PR
run here cannot have a merge base that carries an advisory unless `main`
is touched or a throwaway branch is made, and both are out of bounds.
Following the dispatch, the step logic was replayed on real historical
pairs instead. The replay worked like this:

- It built the test merge GitHub would build (`git merge-tree
--write-tree BASE PRHEAD`, then `git commit-tree -p BASE -p PRHEAD`) in
an isolated clone.
- It ran the stage step's `run:` text verbatim, as extracted from this
branch's workflow. The only change was the helper's path, because the
historical trees carry no copy of the helper.
- It ran each scan step's `scan-args` verbatim through upstream's own
`exit_code_redirect.sh` and the release binary, with outcome = exit 0 ?
success : failure.
- It ran the judge step's `run:` text verbatim.

**The gap, named:** this container's egress policy refuses
`api.osv.dev`. The scans therefore ran `--offline` against a local
snapshot of the OSV npm database. That snapshot is an `all.zip` of
230,017 records, with max `modified` 2026-10-06T16:30:04Z and sha256
`0bd50081c140…`. It was found already on this container's disk, and its
provenance is otherwise unknown. Its GHSA-wq5f-xc86-pv6w (sharp,
published 13:43Z) and GHSA-pqg4-j6r4-53mv (shell-quote, published
13:40Z) records are those finding card objectstack-ai#22013 measured. The anchor
lookup used the real repo-scoped listing.

**R1: PR objectstack-ai#22002 as run 37483796939 saw it (the card's specimen: a
`scripts`-only change).** Base is `f0022c46c1`, `main`'s tip when the
run was created (15:00:57Z). PR head is `95c510eb8a`.

```
osv-base-relative self-test: 23 case(s), all held (floor 23)
::notice title=OSV-Scanner verdict (base-relative)::lockfile unchanged — inherits main's verdict. pnpm-lock.yaml and osv-scanner.toml are identical to the merge base f0022c4, so this pull request introduces no advisory; main's daily scheduled scan judges that lockfile.
GITHUB_OUTPUT: base=f0022c46c10a142f5fb29e6891a4fc06ec05a296 / compare=false
-- for contrast, the old absolute step on the same test merge's lockfile:
| https://osv.dev/GHSA-wq5f-xc86-pv6w | 8.9  | npm | sharp       | 0.35.4 | 0.35.5 | pnpm-lock.yaml |
| https://osv.dev/GHSA-pqg4-j6r4-53mv | 9.2  | npm | shell-quote | 1.10.0 | 1.11.0 | pnpm-lock.yaml |
Exit code: 1
```

**R2: PR objectstack-ai#21951 (a real lockfile-changing PR against a `main` that
carries advisories).** Base is `4c49150e0c`, PR head is `539b0752cd`.
The constructed merge's `pnpm-lock.yaml`, `osv-scanner.toml` and
`pnpm-workspace.yaml` blobs equal those of the PR's real squash commit
`787104baa9`.

```
GITHUB_OUTPUT: base=4c49150e0cb27a493390c4ac24e0b93e13efe662 / compare=true
osv-scan-base: found 1372 packages; GHSA-hp3w-g68c-fv3c filtered (the base's ledger); Exit code: 1  -> outcome failure
osv-scan-head: found 1371 packages; GHSA-hp3w-g68c-fv3c filtered (the PR's ledger);  Exit code: 1  -> outcome failure
::notice title=OSV advisory inherited from the merge base::GHSA-pqg4-j6r4-53mv (here: shell-quote@1.10.0; at the merge base: shell-quote@1.10.0) is matched by the merge base's lockfile (4c49150) too, so it is main's to fix and does not fail this pull request; no open issue names it yet; main's daily scheduled scan is the signal that files its finding card.
::notice title=OSV advisory inherited from the merge base::GHSA-wq5f-xc86-pv6w (here: sharp@0.35.4; at the merge base: sharp@0.35.4) is matched by the merge base's lockfile (4c49150) too, so it is main's to fix and does not fail this pull request; no open issue names it yet; main's daily scheduled scan is the signal that files its finding card.
OSV base-relative verdict against the merge base (4c49150): 0 introduced, 2 inherited, 3 resolved.
  resolved    GHSA-238p-pmpm-9mq7  katex@0.16.47
  resolved    GHSA-68fv-2mgg-jv7q  source-map-js@1.2.1
  resolved    GHSA-jqcg-44mw-7w3h  proxy-addr@2.0.7
judge exit=0
-- the old absolute step on the same lockfile: sharp + shell-quote, Exit code: 1
```

The anchor clause reads "no open issue names it yet" because objectstack-ai#22013 is
closed today. A live probe of the same listing found the open issues
that name a given advisory id. It also turned up a seat board post
quoting one, which is why board posts (`pm:seat`) are skipped; the
self-test pins both the anchor case and the board-post skip.

**R3: a synthetic ledger-only PR** that deletes the sprintf-js exemption
from `787104baa9`'s `osv-scanner.toml` and touches no lockfile:

```
GITHUB_OUTPUT: base=787104baa9ecca77ff13a8e83a4b7349728feadc / compare=true   (no short-circuit: the ledger differs)
::error title=OSV advisory introduced by this pull request::GHSA-hp3w-g68c-fv3c (sprintf-js@1.1.3) is matched by this pull request's lockfile and not by the merge base's (787104b). ...
OSV base-relative verdict against the merge base (787104b): 1 introduced, 2 inherited, 0 resolved.
judge exit=1
```

## Helper ablations (each run with `scripts/ablation-replace.mjs`, which
restores the file and confirms the restored blob equals HEAD)

- **A1: `compare` ignores the base** (`if (base.has(id)) inherited.push`
becomes `if (false) inherited.push`). The self-test fails: 25 failed
expectations over 23 cases, exit 1.
- **A2: a missing result file read as "no findings"** (upstream's
fail-open). The self-test fails: 4 failed expectations, exit 1.
- **Restore:** the blob is back to `f35d31ee1072` = HEAD, and `git diff
HEAD` is empty.

## Local verification

These gates ran at `bf15ed3831`, plus `ce598b1ab7` for the entry-guard
fix. The final tree `d477febb20` is identical at `ce598b1ab7` and
`3576cdc334`.

- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands`
derived 54 commands over the actual diff, and all 54 exit 0.
- **One red caught and fixed:** `check:entry-guard` first went red
because the helper exported bindings and ran on import. The fix puts the
dispatch behind `isEntrypoint`, and the gate is now green.
- **`pnpm check:pm-dispatch-gates`:** 1976 cases pass, in 820.5s.
- **`--ran` reconciliation:** 54 derived, 54 run, 0 NOT-MEASURED.
- **eslint on the new file:** `npx eslint --no-inline-config --format
json scripts/osv-base-relative.mjs` reported 1 file, 0 errors and 0
warnings.
- The file is in eslint's population: a JSON result came back for it,
not an "ignored" warning.
- The config enables no type-aware linting (`eslint.config.mjs` has no
`parserOptions.project`), so this diff cannot change the verdict on any
file it does not touch.
  - The whole-repo `pnpm lint` belongs to CI.

## Acceptance notes

- **The unit is the advisory id,** following the ruling's wording ("an
advisory matched by both is reported as inherited"). Suppose a PR adds a
second vulnerable version of a package for an advisory `main` already
carries. That advisory reads as inherited, and the notice lists both
sides' package versions so the addition is visible. `main`'s daily scan
still judges it. Upstream's reporter counts occurrences instead.
- **Prose that drifts with this change, in files outside this PR's
allowed surface.** Both are out of bounds for this PR, and this PR has
no carrier for them:
- The `osv-scanner.toml` header says the workflow "blocks on any
advisory at any severity". That is still true of `main`'s scan, but a PR
now answers only for what it introduces.
- The `osv-scanner.toml` and `scripts/check-osv-exemptions.mjs` headers
cite measurements "against v2.3.8". The pinned image has been v2.5.0
since the Dependabot bump.
- **Remaining inherited-red path, by design:** an exemption that expires
on `main` still reddens every dependency-touching PR through the
unchanged "Verify OSV exemptions carry an expiry and a reason" step,
which the ruling keeps as is.
- **The job keeps `issues: write`,** which no step in it uses. The
judge's anchor lookup only reads. Permissions are unchanged here.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw)_

---------

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

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants