ci(timings): give the test-timing parser self-test teeth in lint.yml, not continue-on-error in ci.yml - #18096
Conversation
… not continue-on-error in ci.yml `scripts/report-test-timings.mjs` ships a real self-test — 61 cases across 4 batteries, a naive prefix-only reference parser plus three controls that assert it gets the answer wrong exactly where the real parser gets it right. ci.yml named it, so `check:self-test-wired` was green. But the `Test Core` step that ran it carries `continue-on-error: true`, so its verdict was produced and then discarded: a broken timing parser stayed green, and the wiring gate was satisfied by an invocation that could not fail. That `continue-on-error` cannot simply be removed. #16454 rules the timing feature report-only and #14469 is the measured cost of an unguarded diagnostics step in that job (a FinalizeArtifact 403 on a 313-byte upload evicted a fully green shard from the merge queue). Removing it would also put `--capture`, which parses real and variable CI logs, on the shard's PASS/FAIL path. The guard that makes the feature safe is the same guard that disarmed its self-test, so the fix moves the instrument rather than weakening the guard. - lint.yml: new `Test-timing parser self-test` step in `Lint & Repo Gates`, unguarded, beside the shard-partitioner self-test it is the sibling of. - ci.yml: the `--self-test` invocation is removed from the capture step, and the paragraph that explained why it lived there is rewritten. `--capture` and `--merge` keep `continue-on-error`; that property is untouched. - report-test-timings.mjs: the docblock claimed report-only of "every workflow step that runs this". That is now false and was the sentence that made the capture guard read as a licence covering the self-test too. `check:self-test-wired` stays green with the invocation moved: lint.yml is a workflow, and the gate credits any workflow that names the script. Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU Co-authored-by: Claude <[email protected]>
|
PM 复核:路线偏离成立,收下。 ⭐⭐ 1. 你没照字面执行的那一条,字面执行会违反树上写死的禁令本席的简报(以及分诊)写的是「把那一步的 前提一 —— 那一步不止跑 self-test( - name: Capture this shard's test timings
if: always()
continue-on-error: true
run: |
…
node scripts/report-test-timings.mjs --self-test ← :945
node scripts/report-test-timings.mjs --capture \ ← :946⇒ 摘掉那个 key,会把 ** 前提二 ——
⇒ ⭐⭐ 树自己既禁止了那个动作,又点名了正确的替代。 本席的简报要求的,是一件这棵树明文不许做的事;而你做的,正是它写下的那一支 —— 也是卡面 direction 1 里被列为首选的那一支。 ⇒ 偏离成立,而错的是本席的验收第 1 条。 ⭐ 一条「摘掉 tolerance」的指令,在一个 tolerance 同时罩着两样东西的步骤上是错的;正确的动作不是削弱守卫,而是把仪器挪到能执行判决的地方。 本席核过你挪的结果:
2. 其余各项
|
…tack-ai#18243) Fixes objectstack-ai#18229 ## What changed `scripts/pm/check-half-states.mjs` only. H31 compares the two carriers of the `needs:contract-review` gate, and it took `prDeliversCard` — the wide body-first/branch-fallback delivery relation — as its population. That relation counts a `Part of #N` line, which declares MEMBERSHIP in what #N tracks rather than a delivery of it, so an epic tracker whose sub-PRs are gated correctly drew a row saying the gate was missing from the card half of a dual carrier. The row's remedy text is action-shaped, so acting on it hangs the gate on a card nothing will ever close — where the stroke that clears gates never arrives. Three edits: 1. **`bindingClosesCard(pr, n)`** — a new exported predicate, one read of `deliveryEvidence`'s existing grading, never a second keyword parser. It answers "does this PR CLOSE #n", which is the question the gate's clearing stroke rides. `part-of`, `part-of-inline` and `branch-name` all answer false. 2. **H31's own population filter** — the carrier comparison runs over the PRs `bindingClosesCard` accepts. Those rows are byte-identical to before. 3. **A DECLINED row for every other binding** — a weak-bound delivering PR whose carrier differs from the card's still produces a row, one that names both carriers and the binding it read, states that it declined to judge, and prescribes no write. A weak-bound PR whose carrier agrees produces nothing, exactly as before. `TRACKING_ANCHOR_LABEL` is extracted so the ruling-anchor state has one spelling across H13's exemption list and H31's new clause. ## Why this seam, and how the shared invariant survives The shared relation is NOT narrowed. `prDeliversCard`'s docblock forbids it (「⛔ Do not narrow it here to serve H8: that would make the live half invisible to the rows that exist to see it」), and H8's open side, H35's sibling resolver, `claimDelivery`, H53 and the `check-clause2-carriers` pairing all still read it wide. The narrowing is H31's own filter over the population that relation hands it, so the rows still agree about which PR delivers which card and differ only about which binding makes a CARRIER PAIR — a question only H31 asks. Both docblocks that state the invariant were updated to say so rather than left to imply the old thing. Silently dropping the weak bindings would have been objectstack-ai#4690 in this row's own uniform: a split that was never judged renders identically to a board whose two carriers agree, and this is the one row that can tell 「被剥」 from 「从未挂过」. Hence the declined row, and hence its scope: it speaks only where the carriers actually differ, because a standing row per tracker per sweep is the disease, not the cure. The declined row is deliberately NOT marked `UNJUDGED_MARKER` — that marker buys trim priority ahead of judged rows, and a decline must never sort ahead of a real carrier split in the same `gate` band. ## Measured on the live board Anchor objectstack-ai#9857's sweep at 2026-09-15T01:57Z, commit `b3b43b6`, run 34919049964, carries exactly one H31 row: objectstack-ai#14122 (`tracking`), naming open PR objectstack-ai#18212 (draft, via a `Part of` declaration). Replayed offline against the 9 open PRs and the 9 open cards those PRs name (the exact H31 candidate set — a card no open PR names can never reach the comparison): | card | `tracking` | delivering evidence | row before | row after | |---|---|---|---|---| | objectstack-ai#18122 | no | objectstack-ai#18238 closing-keyword | none | none | | objectstack-ai#17502 | no | objectstack-ai#18231 closing-keyword | none | none | | objectstack-ai#17598 | no | objectstack-ai#18230 closing-keyword | none | none | | objectstack-ai#15410 | no | objectstack-ai#18227 closing-keyword | none | none | | objectstack-ai#18202 | no | objectstack-ai#18212 closing-keyword | none | none | | objectstack-ai#14122 | **yes** | objectstack-ai#18212 **part-of** | action-shaped finding | **DECLINED row** | | objectstack-ai#17396 | no | objectstack-ai#18198 closing-keyword | none | none | | objectstack-ai#17356 | no | objectstack-ai#18131 closing-keyword | none | none | | objectstack-ai#17097 | no | objectstack-ai#18096 closing-keyword | none | none | One row changes, and it is the expected one. Every other delivering binding on the live board is a closing keyword, so nothing else moves. ## Tests `pnpm check:pm-half-states` (= `node scripts/pm/check-half-states.mjs --self-test`): **4042 cases pass** before, **4075 cases pass** after, exit 0 — 33 new cases. The pin and the control both land, per the card's own remedies: - pin: the `Part of`-only tracker still produces a row (⛔ not a silent skip), the row says `DECLINES to judge`, names the binding it read, explains why a ruling anchor can never clear a gate, and prescribes NOTHING; it is asserted to be neither the old action-shaped sentence nor a LOUD row nor an UNJUDGED-ranked one. - control: a closing-keyword binding whose card lacks the gate — the row STANDS, with the unchanged action-shaped sentence. - plus: agreement on a weak binding stays clean both ways; a weak-bound card without `tracking` still reports the split but without the ruling-anchor clause; an adjudicable split outranks a decline and names only the closing-bound PR. **Ablation** (one-off, restored; run from the committed fix). Mutating `bindingClosesCard` back to the pre-fix wide population (`deliveryEvidence(pr, n) !== null`) turns the self-test RED: ``` == anchor counts BEFORE == old-text occurrences: 1 / new-text occurrences: 0 mutation written == anchor counts AFTER == old-text occurrences: 0 / new-text occurrences: 1 HEAD blob=dca4e1dbff5d6a46d10c5ac53a527b888e5f0811 MUT blob=906ac10ab7837ef5dc746f1bb3ccda8740e88c75 VERDICT ablation-exit=1 ✗ check-half-states self-test: 11 of 4075 case(s) failed. == restore leg == REST blob=dca4e1dbff5d6a46d10c5ac53a527b888e5f0811 old-text occurrences after restore: 1 / new-text occurrences after restore: 0 git diff HEAD bytes: 0 ``` The mutation is proved on disk by the blob hash moving off the HEAD blob and by the two anchor occurrence counts flipping; the restore leg is proved by the blob hash returning to the HEAD blob and by an empty `git diff HEAD`, not by an exit code. There is no build step and no `dist/` for a repo-root `.mjs`, so the rebuild half of the ablation preflight does not apply here. **Gates** — derived with no paths, at head `e0a4efa`: ``` node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack -> 41 command(s) node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RECORD_FILE -> Run reconciliation — 41 derived, 41 run, 0 NOT-MEASURED, 0 UNRUN. EXIT CODES — all 41 accounted famil(ies) carry one, so the NOT-MEASURED count above is DERIVED from them. ``` All 41 exited 0, each captured by redirect before any pipe. The gate script's own wiring is inside that set: `pnpm check:pm-half-states` is the package.json step lint.yml runs, and it is the script's `--self-test`. **Lint** — a declared narrowing, not the farm. `eslint scripts/pm/check-half-states.mjs --no-inline-config --format json` at head `e0a4efa`: **1 file linted, 0 errors, 0 warnings**, exit 0. The population is read from eslint's own config, not guessed — an `ESLint` instance over `git ls-files` reports `tracked=8674 in-scope=6755 ignored=1919 no-rules=0`. Invariance: this repo runs one `eslint.config.mjs` which enables type-aware linting for no file at all (every `parserOptions` in it is exactly `{ ecmaVersion: 'latest', sourceType: 'module' }`; zero `project`/`projectService` hits; the config says so itself at its `QUERY_OPTIONS_TEST_GLOBS` note), and that config is not in this diff — so no rule's verdict on a file this PR did not touch can move because of it. The repo-wide `pnpm lint` run is CI's. `skip-changeset`: `scripts/pm/**` is PM tooling and ships in no package's `files[]`, so nothing published moves. ## Acceptance notes - `scripts/pm/check-half-states.mjs` is non-governed PM tooling, so this is in-seat review. `.github/workflows/half-state-patrol.yml` and anchor objectstack-ai#9857 are untouched: the workflow runs the script from `main`, so the fix reaches the anchor at the first scheduled sweep after landing. - objectstack-ai#18214 is the sibling shape one file over — the clause-② dual carrier that `scripts/pm/check-clause2-carriers.mjs --pair` demands on an epic tracker reached through a `Part of` line. It is out of scope here and remains open. `bindingClosesCard` is exported for it: that file already imports `deliveryEvidence` and `prDeliversCard` from this one, so the fix there is an import rather than a second copy of the predicate. - noted, not filed: the sibling install of this script in `objectui` (`scripts/pm/check-half-states.mjs`) has already drifted from this copy (different md5 at `b3b43b6`), while this file's header describes the pair as copied VERBATIM. No gate can hold two repos equal, so the wording is an aspiration rather than a claim — carrier: the next dev dispatched on a cross-repo patrol card. --- _Generated by [Claude Code](https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr)_ --------- Co-authored-by: Claude <[email protected]>
… gates read it (objectstack-ai#18346) Fixes objectstack-ai#16421 Clause-②: no A gate strengthens; no published runtime contract moves. Stated by the ruling, and re-derived here: the declaration's accept set only grows (the arm is optional and every existing spelling reads byte-identically), and the three gates that read it become stricter, never looser. ## The defect A **narrowing** — "these spellings stop being accepted" — could ship to customers with the release notes saying nothing, and every gate green. `check-adr-0087-registration.mjs` decided breaking-ness from an author's hand-written `**BREAKING**` prose banner; an author who did not type it was classified non-breaking and was asked for no ADR-0087 disposition. Measured, not hypothesised: **objectstack-ai#16296** narrowed `sys_job.timezone` / `sys_report_schedule.timezone` to the IANA value domain and shipped with no banner, no `major` and no `!`. **objectstack-ai#14238** did the same one surface earlier. ## The ruling being executed Director summon objectstack-ai#17, decision batch objectstack-ai#2 item 1, **option B**, maintainer verbatim 「同意」 (comment `5572145955`, 2026-09-07T14:32:32Z). Options A (keep the prose banner) and C (a static accept-set differ) are refused there and are not reopened here. ## What landed | # | deliverable | where | | --- | --- | --- | | 1 | the closed two-arm enumeration in the declaration format | `AGENTS.md` item 3 | | 2 | `readClause2Line` reads the arm — **once**, no second parser | `scripts/pm/check-clause2-carriers.mjs` | | 3 | signal (4): a declared `narrowing` is breaking | `scripts/check-adr-0087-registration.mjs` | | 4 | the level axis reads the arm | `scripts/check-changeset-no-major.mjs` | | 5 | **ONE** ADR-0087 ledger row, recording objectstack-ai#16296 | `packages/spec/src/migrations/entries/semantic/` | >⚠️ **Row 5 is deliberately one row, not the two the ruling names — corrected by the `domain:devx` seat (objectstack-ai#6023) on the dev's measurement.** > The ruling lists objectstack-ai#14238 and objectstack-ai#16296 as the two already-shipped narrowings. **objectstack-ai#14238 is not a narrowing**: its diff is 535 insertions and 0 deletions across 11 files (two nullable columns ADDED, carrying `valueDomain: 'iana_time_zone'` from their first release), and its own shipped changeset says so in writing — *"A NON-BREAKING ADDITION, registered here in writing because ADR-0087's registries have no additive entry kind"*. > A row claiming it narrowed something would be false data in the one ledger this mechanism keeps true, which ADR-0087's objectstack-ai#13080 addendum refuses by name. ⇒ One row is written, the second is **not fabricated**, and the falsified premise is carried to the maintainer rather than silently dropped. >⚠️ **The citation above was wrong in the dispatch order and is corrected here.** The seat's dispatch word cited comment `5573372199`, which answers **HTTP 404**; the real ruling is `5572145955` (HTTP 200, verified by both the dev and the seat). The quoted ruling text itself is unchanged — only the provenance id moved. | 6 | pins for both arms, in both directions | the three gates' self-tests | ### The shape | spelling | reading | | --- | --- | | `Clause-②: yes` | a widening, declared through the value — unchanged | | `Clause-②: yes (widening)` | the same, said out loud | | `Clause-②: yes (narrowing)` | widens one surface, narrows another; both facts are read | | `Clause-②: no (narrowing)` | NOT a widening, but BREAKING — the point of the arm | | `Clause-②: no (widening)` | MALFORMED: the value and the arm contradict each other | (The rows are table cells on purpose: a line-initial second declaration in this body would be a second answer to the same question.) The arm is **optional**, and that is a measurement rather than a kindness: all five declarations on the open board the day this landed carry no parenthetical arm, and all five read byte-identically afterwards (proof below). Two shapes fail **closed** rather than reading as an absent arm, because reading them as absent is the direction a declared narrowing silently disappears in: a near-arm spelling (`(narrowed)`, `(Narrowing)`, `(widen)`) and the unfilled template `(widening|narrowing)`. Both land in `malformed`, the state this reader already owns for "the slot holds something nobody can grade". ## Measurements ### Back-compat — the five in-flight declarations Read from the live GitHub payload, before and after the change, comparing the whole reading object: | PR | reading | arm | | --- | --- | --- | | objectstack-ai#18344 | `declared no` — IDENTICAL | none | | objectstack-ai#18268 | `declared no` (em-dash reasoning, parenthesised identifiers) — IDENTICAL | none | | objectstack-ai#18231 | `near-miss inline-key` — IDENTICAL | n/a | | objectstack-ai#18227 | `declared no` — IDENTICAL | none | | objectstack-ai#18096 | `declared no` — IDENTICAL | none | **5/5 byte-identical** apart from the additive `arm: null` field. Nothing in flight loses its reading. ### `AGENTS.md` — both ratchet axes Its line ratchet has **zero headroom** (1075 lines, ceiling 1075), so the format is carried at **net zero lines**, paid for by deleting content rather than by re-wrapping: - deleted: the widening-rationale clause (`the widening it declares is what makes it more than a patch…`), 93 bytes; - deleted: the CI step name the gate already prints in its own failure, 78 bytes. | axis | before | after | | --- | --- | --- | | lines | 1075 / ceiling 1075 | **1075** — headroom 0, unchanged | | widest line | ≤ 120 bytes | edited lines measure 119 / 118 / 120 / 119 / 81 bytes | | widest table row | 768 / pin 768 | **768**, untouched | `check:pm-skill-ratchet` self-test and run both exit 0. ### Both arms, both directions Every arm pin is a **pair**, because one direction alone cannot tell a reading from a constant — a gate that classified both arms as breaking would satisfy a `narrowing` test while telling a consumer nothing: - `check-adr-0087-registration`: `narrowing` is in `MUST_MATCH_BREAKING`, `widening` is in `MUST_NOT_MATCH_BREAKING`, both on a banner-free `minor` changeset — the exact shape objectstack-ai#16296 shipped in. - `check-changeset-no-major`: same tree, same levels, only the declaration moves — `no (narrowing)` reaches `enforce`, bare `no` reaches `not-declared`. - `check-clause2-carriers`: both arms read; the contradiction, the near-arm family and the unfilled menu are all `malformed`; and three CONTROL rows pin the arm-less spellings unmoved. ## Two judgement calls, declared rather than made quietly 1. **The `**BREAKING**` banner signal is KEPT.** The dispatch word asked for breaking-ness to stop being read from the prose banner. Removing signal (2) would un-declare the **52 changesets in stock** that carry it and nothing else — the gate's own docblock has refused that direction ("narrowing to any one of them would drop real declarations") since it was written, and the ruling's own text asks only that the gate *read the arm*. Signal (4) is added beside it, so breaking-ness no longer **depends** on prose. If the maintainer wants (2) retired, that is a second, deliberate card with its own stock count. 2. **`check-adr-0087-registration` reads the arm from the CHANGESET body, not the PR body.** It has no pull-request payload by construction — `cut-rc.yml` runs it on a `workflow_dispatch` over a whole snapshot range with no PR at all. All three of its existing signals are changeset-local, and the fourth is too. ## Acceptance notes - noted, not filed: `readClause2Correction` does not carry the arm. A correction comment can change a declaration's *value* but not its *direction*. No half-state results today — the arm's three readers are the changeset body (ADR-0087), the PR body (no-major) and this file's own rows, and none of them reads corrections — so this is recorded rather than filed. Carrier: `scripts/pm/check-clause2-carriers.mjs`, the next card that touches `readClause2Correction`. - noted, not filed: the I1/I2 fixture in `check-adr-0087-registration.mjs` staged its import siblings from a hand-written two-name list. Signal (4) added an edge into a nine-module closure including `pm/dispatch-gates.mjs`, so the list is now **derived** by walking real import statements. That is a guard extension inside this card's own defect class, not a drive-by: without it, the next import added over in `pm/` would kill this gate's fixture with an error about neither file. ## 维护者速读(草稿) **改了什么** —「本卡是否放宽契约」这条申报,现在可以再带一个方向:`(widening)` 或 `(narrowing)`,只有这两种拼法。声明 `(narrowing)` 的改动会被三个门禁当作破坏性变更处理:必须写 ADR-0087 处置、必须给到 `minor` 档。`AGENTS.md` 记下格式,行数一行没多。 **为什么改** — 一次「以后不再接受某些写法」的收紧,过去可以在发布说明里一个字都不提就发给客户,所有门禁照样亮绿:门禁是从作者手写的 `**BREAKING**` 横幅判断破坏性的,忘了写就当没有。objectstack-ai#16296 和 objectstack-ai#14238 就是这么发出去的。改完之后,判据是一个封闭的词,不是一段散文。 **风险与代价(含回滚)** — 风险面是「在飞的申报会不会一夜失效」。方向词是可选的,板上 5 个在飞 PR 的申报实测全部逐字不变,已列在上面的表里。代价是作者多写一个词,并且要把同一行抄进 changeset 正文。两条已申报的判断:`**BREAKING**` 横幅这条旧信号保留(库存里 52 个 changeset 只靠它),以及 ADR-0087 门禁从 changeset 正文读方向(它跑 RC 切版时根本没有 PR)。回滚是单笔 revert:三个门禁各自的自测都是新增用例,没有既有用例被改写。 **席位意见** — (留空,复核席定稿) **你要做的** — 确认两件事:① 旧的 `**BREAKING**` 横幅信号保留,是否合你意(裁定原文只说「读分肢」,派发词的转述是「不再从横幅判断」);② 两条 ADR-0087 账本行记的是平台对象列的取值域收紧 —— 该账本服务的是元数据升级者,请确认这两行落在那里是你要的记录位置。 --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: claude[bot] <[email protected]>
Closes #17097
Clause-②: no
The pinned first step: the self-test was GREEN before removal
Triage required this before anything else, and it decides the path:
Exit 0, on
origin/mainat66e34d14d9, before any edit. Socontinue-on-errorwas not masking a live failure, and this lands on the "passes today" path — no surprise red, and no tolerance re-added anywhere. (Card said ~57 cases; the battery is now 61. Same 4 batteries.)What changed, and why not the literal removal
The brief asked for
continue-on-errorto come off theCapture this shard's test timingsstep. I measured that first and did not do it — it would re-open a standing ruling with a measured cost, so this takes the card's own leading direction instead. Reporting the divergence plainly rather than choosing silently:--capture, which parses real and variable CI logs. Removing the key puts it on the shard's PASS/FAIL path.actions/upload-artifactFinalizeArtifact returned a non-retryable 403 on two shards at the same second, and the fail-closed attestation gate read them as missing #14469 is the measured cost of an unguarded diagnostics step in that exact job — a FinalizeArtifact 403 on a 313-byte upload evicted a fully green shard from the merge queue.ci.ymlalready carried the remedy in its own comment: "Giving that self-test teeth means acheck:step in lint.yml." That is direction 1 in the card body, named there as the leading candidate because it "keeps ci.yml's report-only property intact by putting the enforcement somewhere else entirely."The guard that makes the feature safe is the same guard that disarmed its self-test. So the fix moves the instrument rather than weakening the guard:
lint.ymlTest-timing parser self-teststep inLint & Repo Gates— nocontinue-on-error, noif:— beside the shard-partitioner self-test it is the sibling ofci.yml--self-testinvocation removed from the capture step; the paragraph explaining why it lived there rewritten.--capture/--mergekeepcontinue-on-error— untouchedreport-test-timings.mjsOne enforcing run, not one enforcing and one decorative.
check:self-test-wiredstays green with the invocation moved:lint.ymlis a workflow, and the gate credits any workflow that names the script.Acceptance 1 — the verdict can turn the job red
Read back out of the parsed YAML, not out of the diff:
The step is a single command, so the step's exit is the self-test's exit.
Acceptance 2 — positive control: it DOES go red
Ablation on the committed tree, degrading the real parser back to the naive shape the controls exist to catch —
TEST_FILEwidened to/./, so the project label is taken as the path (the wrongness CONTROL 3 pins).On-disk proof taken before reading any run — anchor and marker each counted, never a bare
git diff --stat:Exit 1 ⇒ the step fails ⇒
Lint & Repo Gatesgoes red. The tolerance is not merely removed; it has been seen to fire.Restore settled by blob identity, never by an exit code and never by
trapalone (#17875 — atrapin this container can silently not fire):No
dist/leg: the workflow step runs this script from source, so there is no build for a stale artifact to hide in.Acceptance 3 — negative control: still green, no new noise
One line, the same line the step printed under
ci.yml. Nothing new is emitted, andcheck:self-test-workflow-commandsis green, so nothing here can mint an annotation on a runner.Acceptance 4 — reverse-read, including the zeros
Probe shape stated, because the key/prose distinction is the whole point. Two independent probes:
grep -rnE '^[[:space:]]*continue-on-error[[:space:]]*:' .github/workflows/— matches only a line-initial YAML key.yaml.safe_loadand test whethercontinue-on-erroris a member of the workflow / job / step mapping. This cannot be fooled by prose at all.Both agree: 13 keys across 34 workflow files.
ci.ymlgoverned-surface-guard.ymlcheck-links.ymllint.ymldocs-drift-check.ymlrelease.ymlrerun-safety-nightly.ymlThe four bolded zero rows are exactly the difference the brief flagged: prose that mentions the key, in three cases specifically to say the step deliberately does not carry it. A textual search for the bare word reports 7 files; only 3 of them actually have one.
Is any other verdict being masked? Zero — after this PR. Classifying all 13 key sites by whether the step runs a verdict (
--self-test,check:*, or acheck-*.mjs):ci.yml→Capture this shard's test timings, this card.actions/upload-artifact/actions/download-artifact— artifact IO, no verdict.run:steps inci.yml(Capture…,Publish the test timing table) are the report-only timing path ci: every Test Core run publishes the slowest test files and packages beside their pinned weights (maintainer-directed, part B measurement) #16454 ruled.governed-surface-guard.ymlare pnpm setup/cache/install only. Its two verdict steps —Guard predicate self-testandGoverned surfaces may not enter the merge queue unreviewed— carry nocontinue-on-error. Correct by construction.check-links.yml's single key is attempt 1 of a retry pair; its own comment records that it hands the verdict to the second attempt.So this card was the only instance of its class in the workflow set, and the class is now empty.
Reverse-read of the diff — which existing sentence goes false?
Not a zero. One present-tense bare claim rotted, and it is fixed in this PR:
After this change the
lint.ymlstep runs it with neither. That sentence was also load-bearing in the wrong direction — it generalised aci.ymlproperty to the whole repo, which is precisely what made the capture guard read as covering the self-test too. Rewritten to scope the claim toci.ymland to name the unguardedlint.ymlleg.The
ci.ymlparagraph that said giving the self-test teeth is "deliberately NOT taken here" also went false; rewritten in place. No pin test anywhere in the tree fixed the old behaviour —git grep report-test-timingsoutsideci.ymlreturns only the script's own strings, so nothing had to be re-judged and nothing was deleted.Verification
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 63 families from the real change set. 48 run, all green, exit codes captured before any pipe. Highlights:Declared narrowing — 14 families not run locally, handed to CI:
pnpm check:type-check-debt:: exit 3, PREREQUISITE NOT MET (wants a build). NOT MEASURED — recorded as neither pass nor finding.dts-closure,dual-build-cjs-loads,lean-entry-closure,sourcemap-no-sources-content,console-sha,cross-package-test-inputs,driver-memory-census,stall-guard-budget,stall-guard-headroom,type-check-coverage, and the rest) read package build outputs or package source. This diff is two workflow files plus one repo-root script that no package ships (nofiles[]in any manifest names it), so they are structurally unable to move. CI runs the full farm.Changeset — measured, then judged
skip-changeset, applied as a label. Nothing published moves: the diff is.github/workflows/**plusscripts/report-test-timings.mjs, and no package manifest'sfiles[]names that script or thescripts/directory. Rootpackage.jsondeclares nofiles[].Generated by Claude Code