Repository navigation
Commit 5dda600
Fixes #22991
Clause-②: no
Executes ruling 6120820653 (letter D) on the card: `Lint & Repo Gates`
becomes an aggregator over eight parallel `Lint · *` lanes, and `pnpm
check:pm-dispatch-gates` runs its fast tier on `pull_request` /
`merge_group` while `schedule` and `push` keep both tiers.
## What changed
- **`.github/workflows/lint.yml`**: the 193-step `lint` job is split.
Job id `lint` and `name: Lint & Repo Gates` are kept byte-identical (the
required context is matched by check-run name). The job now runs one
step: a verdict over `toJSON(needs)`, copied from the `typecheck`
aggregate. It carries `OS_AGGREGATOR_MEMBERS` (equal to `needs:`), `if:
always()` and `timeout-minutes: 10`. Every gate step moved verbatim into
one of eight lanes: same `run:`, same `if:`, same comment block, same
relative order. Each lane repeats the six setup steps, the gate-family
selector when it carries a scoped step, and the unmeasured-tail reporter
pair. Lane walls use the Type Check lanes' scheme: a checkout budget of
20 on the checkout step plus a body budget (2× the measured body
maximum, 5-minute grain, floor 10). That gives 30 for seven lanes and 50
for the PM lane, whose body budget still has to cover both tiers on
`schedule` / `push`.
- **`scripts/pm/check-dispatch-gates.mjs`**: under
`GITHUB_ACTIONS=true`, `GITHUB_EVENT_NAME` decides the tier.
`pull_request` and `merge_group` get `--self-test --fast`. Every other
event gets both tiers: `schedule`, `push`, `workflow_dispatch`, and any
trigger nobody listed. `--slow` stays the manual override everywhere,
and the chosen tier and its reason are printed before the spawn. Its own
self-test covers each event. It also blanks the runner's own
`GITHUB_EVENT_NAME`, which would otherwise leak into the cases on CI.
- **`scripts/check-aggregator-roster.mjs`**: `lint` / `Lint & Repo
Gates` is registered as a required aggregate, so deleting its roster is
a refusal. It is added to both per-aggregator positive controls. The two
`typecheck` refusal fixtures are anchored on typecheck's own roster: the
unanchored pattern hit the first `OS_AGGREGATOR_MEMBERS` in lint.yml,
which is now `lint`'s, and that is how the gate first spoke on this
diff.
- **`scripts/check-required-contexts.mjs`**: the pin must now run in a
job the `Lint & Repo Gates` context carries. That is `lint` itself, or a
lane it both declares and lists in `needs:`. The step may not carry an
`if:`, and neither may the lane hosting it. New fixtures cover: declared
and needed counts as wired; declared-only does not; needed-only does
not; a lane-level `if:` is seen.
## Lanes (predicted)
How the prediction was made: each lane's wall is rebuilt per run from
the per-step timestamps of the 70 successful `Lint & Repo Gates` runs,
2026-10-12T04:40Z to 08:45Z (`pull_request` and `merge_group`, jobs
API). Each rebuild uses that run's own setup steps plus the lane's steps
as they ran there. The measured walls of this PR's own CI run go into
the `os-dev-report` comment on the card. This body is written once,
before that run exists.
| lane | name | steps | p50 min | max min | timeout |
|:--|:--|--:|--:|--:|--:|
| `lint-eslint` | Lint · ESLint | 2 | 3.3 | 4.0 | 30 |
| `lint-repo-tooling` | Lint · repo & tooling gates | 60 | 4.2 | 4.7 |
30 |
| `lint-docs-code` | Lint · docs & code guards | 55 | 4.1 | 4.6 | 30 |
| `lint-ci-engine` | Lint · CI, release & engine gates | 58 | 3.9 | 4.4
| 30 |
| `lint-scoped-erasure` | Lint · scoped · query-options erasure | 2 |
3.5 | 5.0 | 30 |
| `lint-scoped-slot-corpus` | Lint · scoped · slot lookup & corpus | 3 |
3.5 | 4.9 | 30 |
| `lint-scoped-tooling` | Lint · scoped · tooling self-tests | 3 | 1.1 |
4.4 | 30 |
| `lint-pm-dispatch-gates` | Lint · PM dispatch-gates | 1 | 1.1 | 5.1
(fast tier, estimated) | 50 |
**Critical path.** Before: p50 17.8, p90 35.9, max 38.5 minutes. After
(predicted): p50 4.2, p90 4.7, max 5.1 minutes on `pull_request` /
`merge_group`, plus the aggregate's own seconds. On `schedule` / `push`
the PM lane runs both tiers, about 15–16 minutes.
How the cut differs from the PM's H2:
- The ESLint lane and the three always-on lanes match H2.
- H2's single scoped lane would be p50 6.4 and max 11.5 minutes in the
same rebuild, over the 5-minute budget. So it is split three ways and
the PM self-test stays alone.
- `query-options erasure` (160 s) and `slot-lookup` (84 s) run together
in 53 of 70 runs. Together they would put one lane over budget at
median, so they sit in different lanes.
## Mechanism hypotheses, checked
- **H1 holds.** The only inter-step reads in the old job were
`steps.gate-families.outputs.*`: nine `if:` lines and one step `id:`. No
`run:` in the job builds, writes `GITHUB_PATH`, or reads `RUNNER_TEMP`
across steps. The one `GITHUB_ENV` write (`STORE_PATH`) is a setup step
every lane repeats. Of the 196 scripts the job invokes, I scanned every
write call site that has no temp-dir use. None writes a repo file that a
later step reads; they are ledger `--write` modes or fixture strings.
- **H2 refined**, as described in the cut above.
- **H3 holds, with one deliberate difference.** The fast tier is a
closed list (`pull_request`, `merge_group`) rather than "fast unless
`schedule` or `push`". The ruling moved the slow tier off those two
events only, so `workflow_dispatch` keeps both tiers. An event nobody
listed also lands on the full battery rather than the shorter one.
- **H4: the selector runs once per scoped lane.** It cost 0 s at the
median and 1 s at p90 over the 70 runs. A separate `families` job would
put a second full-history checkout and a runner pickup on the critical
path of every scoped lane, and it would change all nine `if:` spellings,
which `check:select-gate-families` pins.
- **H5:** each lane's timeout is derived from its measured body, as in
the table. The aggregate takes 10, which is lower than the old 55, as
the ruling allows.
- **H6:** two pins spoke, `check-aggregator-roster --self-test` and
`check:required-contexts`. Both are updated, and neither is loosened
(see "What changed"). The other five pins named in H6 stayed green
unchanged.
## Proof the move is pure
A one-off verifier parsed the old and the new lint.yml (the `yaml`
package; PyYAML agrees on the result). It checked:
- each of the 184 gate steps appears in exactly one lane, deep-equal to
its old parse, in its old relative order;
- the six setup steps are identical in every lane except the added
checkout `timeout-minutes: 20`;
- the selector is present, identical and ahead of the first scoped step
in exactly the lanes that carry one;
- the tail pair is last in every lane;
- every other job, and the workflow's `name`, `on` and `concurrency`,
are unchanged.
Result: 0 problems. Comment edits only name the step a moved neighbour
referred to ("the step above" became the step's name and its lane). They
also note the per-lane order rule, the per-lane selector and the PM tier
change where those notes live.
## Ablations (one-time, each restored and confirmed byte-identical to
HEAD by `scripts/ablation-replace.mjs`)
- `FAST_TIER_EVENTS` emptied: the first run **exited 0**, because the
self-test looped over the constant under test. The fix names the two
events literally; rerun red (2 cases).
- The event branch forced to always-fast: red (5 cases).
- `lint-ci-engine` deleted from the aggregate's `needs:`:
`check:required-contexts` red, naming the unwired pin.
- The `needs:` read in `jobsCarriedByLint` deleted: red on the
declared-only fixture.
- `lint` dropped from `REQUIRED_AGGREGATORS`: `check-aggregator-roster
--self-test` red on the lint refusal fixture.
## Gates run locally at `79b0fd6d` (this head, after merging
`origin/main`)
Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, which found 54 commands; I ran all of them.
- 52 ran in the foreground, all exit 0. Among them:
`check-aggregator-roster` (and its `--self-test`),
`check:required-contexts`, `check-ci-filter-parity`,
`scripts/ci/scheduled-full-run.mjs --self-test`,
`check-self-test-workflow-commands`, `check-step-collectors`,
`check-self-test-wired`, `check:select-gate-families`,
`check:workflow-step-name-quoting`, `check:workflow-status-functions`
and `check:nul-bytes`.
- `pnpm check:pm-dispatch-gates` ran detached, exit 0: the fast tier,
1828 cases, 264.9 s on this box.
- `pnpm check:type-check-debt` is **NOT MEASURED**. Its `--self-test`
passed. Its `--re-measure` half runs tsc over the ledgered package and
was cut at 500 s (exit 124). It needs the build that CI's `Type Check ·
debt ledger` lane runs first, and this diff touches no TypeScript
program.
`--ran` reconciliation: 54 derived, 53 run, 1 NOT-MEASURED, 0 unrun.
`actionlint` is not installed in this container; two YAML parsers agree.
## Acceptance notes
- Every lane keeps `fetch-depth: 0`. I did not re-derive which of the
~190 gates read history. A gate that loses its history degrades to "not
verified" rather than red, so a shallow checkout per lane (about 20 s
each) is left to a later card.
- A lane carrying a scoped step without its own selector would fail
OPEN: the step would run on every PR. The one-off verifier above checks
for this, but no checked-in pin does. `check:select-gate-families` reads
lint.yml as text and does not pair each scoped `if:` with a selector in
the same job.
- The `typecheck` aggregate's inline verdict script still says, in a JS
comment inside its `run:` text, that the roster gate runs "in the lint
job". I left that `run:` text untouched; the YAML comment beside it is
updated.
- `scripts/pm/dispatch-gates.self-test.mjs` line 6 says the both-tier
run is "the battery CI runs". That is still true on `schedule` / `push`,
so it is left alone.
- The PM lane's fast-tier time on a runner is an estimate: 253–281 s on
this 4-core container, where CPU-bound gates run about 1.1–1.3× slower
than on CI. This PR's run gives the real number. If the PM lane is over
5 minutes at median, the remedy is splitting the battery, not skipping
it.
- Eight lanes plus the aggregate replace one job: about seven more
runner pickups and about seven more minutes of setup per run, in
exchange for the wall.
## 维护者速读(草稿)
**改了什么**:把 `Lint & Repo Gates` 这个必过检查从"一台机器串行跑 193 步"拆成 8
条并行车道,原名字保留做汇总(合并队列按名字认,所以不受影响)。另外,PM 派发工具的自测在 PR
和合并队列上只跑快速档,完整档改在每小时定时跑和合入 main 后跑。
**为什么改**:这个检查中位 17.8 分钟、最长 38.5 分钟,一半的合并队列都在等它。按 70 次真实运行重算,拆分后预计中位约 4.2
分钟、最长约 5.1 分钟(PR 和合并队列上)。
**风险与代价(含回滚)**:每次运行多占约 7 台机器、多约 7 分钟的安装时间(机器分钟换等待时间)。PM 工具的慢速档用例不再在 PR
上跑,有问题会在 main 上的定时运行里第一次变红(这是裁决接受的代价)。回滚就是 revert 这个 PR,必过检查名字没变,设置里不用动。
**席位意见**:
**你要做的**:看本 PR 自己那次 CI 的各车道耗时(dev 报告评论里有),确认 8 条车道和汇总都是绿的,`Lint & Repo
Gates` 仍出现在检查列表里;然后批准合入。
---
_Generated by [Claude
Code](https://claude.ai/code/session_01LqNgdPZB2Jnr5HpGbaqLeB)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5187e57 commit 5dda600
4 files changed
Lines changed: 1658 additions & 661 deletions
File tree
- .github/workflows
- scripts
- pm
0 commit comments