Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/skills/pm-dispatch/references/platform-readings.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,9 +387,9 @@
- 本车道强制多是过宽的回忆:`dispatch-gates.mjs --tier PATH` 逐路径现推,路径线是下限非放行。
- 该脚本只答自己那棵树:姊妹仓路径回 absent from this tree,姊妹仓的档位与条款②只能手推。
- `os-verify-lock.sh` 只住 objectstack:objectui 无它,跑本仓副本读的是容器级锁,非该仓深度。
- required checks 的名单是每仓事实,objectstack 八个:
- required checks 的名单是每仓事实,objectstack 九个:
`TypeScript Type Check` · `Lint & Repo Gates` · `Test Core` · `Build Core` · `Lint UI` ·
`Governed Surface Queue Guard` · `Type Check UI` · `Build & E2E UI`。
`Governed Surface Queue Guard` · `Type Check UI` · `Test UI` · `Build & E2E UI`。
- ⭐ 本表的 `mustName` 声明 `statesTheSet` ⇒ 必须逐字等于注册表,加注册行不跟改本表即红。
- ⇒ 门绿即名单逐字齐全的读数;⛔ 但数量词未钉:实测 七→六 照绿,只能手跟改。
- ⛔ 聚合命令同样不作判定:`check:type-check-debt` 可在包级 typecheck 绿时红。
Expand Down
68 changes: 68 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3722,6 +3722,74 @@ jobs:
pnpm --dir ui run test --shard=${{ matrix.shard }}/16 --reporter=default --reporter=github-actions
fi

test-ui-gate:
# Stable required-check name for the sharded Test UI matrix (#26460, ruling
# A, comment 6435318516), in test-gate's shape and for test-gate's reason
# (#3622): a matrix publishes only `Test UI (k/16)`, so the bare `Test UI`
# context the ruleset requires can come from nothing but this job, and the
# shard count stays free to move without a settings edit. Until this job,
# none of the 16 shards was required and the queue landed PRs whose Test UI
# was red (#26419 → #26421).
#
# `name:` and `if: always()` are both load-bearing, as on test-gate. The
# name IS the required context (`check:required-contexts` pins the
# literal), and a gate that skips because a dependency failed publishes no
# context at all, which leaves the PR permanently pending (#3622). ⛔ Never
# `if: !cancelled()` here.
#
# The verdict reads the matrix rollup `needs.test-ui.result`, and only
# `success` passes, with one exception: `skipped`, when `filter` concluded
# `success` AND said the diff reaches no ui path (the shards' own `if:`).
# That is #4928's skipped-only-when-filter-succeeded guard, and it makes a
# queue build or PR that touches no ui path read green here the way Lint
# UI and Build & E2E UI read `skipped` there. Everything else is red:
# `failure`; `cancelled`, which is also what a shard killed by its own
# `timeout-minutes` reads (on a superseded push this paints the OLD sha
# red, which no required check reads); a `skipped` that `filter` does not
# explain; and any lifecycle value such as the `abandoned` test-gate met
# (run 31120902911). Unlike test-gate it counts no shard credentials: the
# Test UI shards publish none, and adding them is a change to `test-ui`,
# which is the ui lane's job.
name: Test UI
needs: [test-ui, filter]
# ── ROSTER DECLARATION (#10490) ───────────────────────────────────────
# `check:aggregator-roster` holds this equal to `needs:` in both
# directions. `filter` is the #4928 input, not a lane.
env:
OS_AGGREGATOR_MEMBERS: test-ui
OS_AGGREGATOR_NON_MEMBERS: filter
if: always()
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Verify Test UI shard results
env:
OS_TEST_UI_RESULT: ${{ needs['test-ui'].result }}
OS_FILTER_RESULT: ${{ needs.filter.result }}
OS_UI_TREE: ${{ needs.filter.outputs.ui_tree }}
OS_UI: ${{ needs.filter.outputs.ui }}
run: |
echo "Test UI (k/16) rollup: '$OS_TEST_UI_RESULT'; filter: '$OS_FILTER_RESULT' (ui_tree='$OS_UI_TREE', ui='$OS_UI')."
case "$OS_TEST_UI_RESULT" in
success)
echo "✓ Test UI: every shard concluded success."
;;
skipped)
if [ "$OS_FILTER_RESULT" = 'success' ] && { [ "$OS_UI_TREE" = 'false' ] || [ "$OS_UI" = 'false' ]; }; then
echo "✓ Test UI: the shards were skipped because filter found no ui path in this diff."
else
echo "::error title=Test UI::the Test UI shards were skipped, but filter (result '$OS_FILTER_RESULT', ui_tree='$OS_UI_TREE', ui='$OS_UI') does not explain the skip. A skip passes only when filter succeeded and said the diff reaches no ui path (#4928)."
exit 1
fi
;;
*)
echo "::error title=Test UI::the Test UI shards concluded '$OS_TEST_UI_RESULT', not success. Read the red 'Test UI (k/16)' check-runs on this sha for the failing test; a shard killed by its timeout reads 'cancelled'."
exit 1
;;
esac

# ── Lint UI and Build & E2E UI: the ceilings (#25869 row F13) ──────────
# objectui's rule (its lint.yml `Lint` ceiling): the smallest round number
# that is both >= 3x the slowest successful run and >= that run + 15 min, so
Expand Down
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -520,11 +520,11 @@ Even inside your own worktree, operate defensively:
yet"; `in_progress` is not a pass. Arming a red PR does not queue it, it hides it:
every poll then misreads "not on `main` yet" as "queued". Always read *two* things:
the queue branch **and** `origin/main`. And **the queue enforces only the required
set** — eight contexts block: `Lint & Repo Gates` (all `check:*` gates), `TypeScript Type Check`,
`Test Core`, `Build Core`, `Governed Surface Queue Guard`, `Lint UI`, `Type Check UI` and
`Build & E2E UI`. A check outside those eight is advisory and rides through, and an advisory red
set** — nine contexts block: `Lint & Repo Gates` (all `check:*` gates), `TypeScript Type Check`,
`Test Core`, `Build Core`, `Governed Surface Queue Guard`, `Lint UI`, `Type Check UI`, `Test UI`
and `Build & E2E UI`. A check outside those nine is advisory and rides through, and an advisory red
that lands rides `main`'s merge ref into every later PR until stanched. A required context is
matched by check-run name, so a rename detaches its gate silently — treat those eight names as contract.
matched by check-run name, so a rename detaches its gate silently — treat those nine names as contract.
**Two speeds** (ruling record 6120820653): PR and queue runs carry the fast set, 10 minutes of
wall per queue entry; `main`'s scheduled (4-hourly) and push runs carry the full battery — dogfood, e2e,
`Temporal Conformance (live PG + MySQL)` — read by the publish lane before
Expand Down
11 changes: 8 additions & 3 deletions scripts/check-aggregator-roster.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
*
* ## The gap this closes
*
* Four of this repo's required contexts are aggregator jobs standing in for a
* Five of this repo's required contexts are aggregator jobs standing in for a
* set of real jobs, each deciding its verdict from what it lists in `needs:`:
* `test-gate` / `Test Core` and `dogfood-gate` / `Dogfood Regression Gate` in
* ci.yml, and `typecheck` / `TypeScript Type Check` and, since #22991,
* `lint` / `Lint & Repo Gates` in lint.yml.
* `lint` / `Lint & Repo Gates` in lint.yml, and, since #26460, ci.yml's
* `test-ui-gate` / `Test UI`.
*
* Nothing held those rosters to anything. The failure mode is one forgotten
* line:
Expand Down Expand Up @@ -68,7 +69,7 @@
*
* Discovery is by declaration, so a NEW aggregator that adds the two keys is
* covered without editing this file. `REQUIRED_AGGREGATORS` is the floor
* underneath that: the four jobs that carry a branch-protection-required
* underneath that: the five jobs that carry a branch-protection-required
* context must each be found AND declared, so deleting the declaration to make
* a red go away is itself red.
*
Expand Down Expand Up @@ -110,6 +111,8 @@ export const REQUIRED_AGGREGATORS = [
{ workflow: 'lint.yml', job: 'typecheck', context: 'TypeScript Type Check' },
// #22991: the single 193-step job became an aggregate over eight lanes.
{ workflow: 'lint.yml', job: 'lint', context: 'Lint & Repo Gates' },
// #26460: the `Test UI` aggregate over the 16 `test-ui` shards.
{ workflow: 'ci.yml', job: 'test-ui-gate', context: 'Test UI' },
];

/** A legal GitHub Actions job id. A comma or a stray quote fails here. */
Expand Down Expand Up @@ -507,6 +510,7 @@ async function selfTest() {
from: ' needs: [dogfood, dogfood-verify, filter]\n',
to: ' needs: [dogfood, filter]\n',
},
{ label: 'test-ui-gate loses its matrix from needs:', file: 'ci.yml', member: 'test-ui', from: ' needs: [test-ui, filter]\n', to: ' needs: [filter]\n' },
];
for (const scenario of dropped) {
const verdict = fixture(scenario.label, scenario.file, (s) => s.replace(scenario.from, scenario.to));
Expand Down Expand Up @@ -536,6 +540,7 @@ async function selfTest() {
to: ' needs: [dogfood, dogfood-verify, filter, dogfood-ghost]\n',
ghost: 'dogfood-ghost',
},
{ label: 'test-ui-gate needs a job that was deleted', file: 'ci.yml', from: ' needs: [test-ui, filter]\n', to: ' needs: [test-ui, filter, test-ui-ghost]\n', ghost: 'test-ui-ghost' },
];
for (const scenario of phantom) {
const verdict = fixture(scenario.label, scenario.file, (s) => s.replace(scenario.from, scenario.to));
Expand Down
42 changes: 37 additions & 5 deletions scripts/check-required-contexts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,15 @@ export const REQUIRED_CONTEXTS = [
'after #25856 (fc2aaaec52) closed bill item 49 with the job green on the main push run 40253800516',
carries: 'the type-check + vitest-setup type-check family of the `ui/` workspace',
},
{
workflow: 'ci.yml',
job: 'test-ui-gate',
context: 'Test UI',
authorized:
'#26460 ruling letter A (staged), maintainer 「批 #408 同意」 2026-10-27T06:24Z, recorded in comment 6435318516 — ' +
'after the ui flake reading 6436579765 (0/50); the ruleset entry is the maintainer\'s, in the same sitting as the merge (#9325)',
carries: 'the sharded `ui/` vitest matrix, `Test UI (k/16)`, read through the aggregate job',
},
];

/**
Expand Down Expand Up @@ -585,10 +594,10 @@ export const INSTRUCTION_SURFACES = [
// with every gate green, which is what put the exclusivity rule below in
// the judge. Then 7→5 by #22998 (two-speed CI: the dogfood and temporal
// contexts moved to the full battery). Then 5→7 by #25853 (the `ui/`
// import's `Lint UI` and `Build & E2E UI`) and 7→8 with `Type Check UI`
// once bill item 49 closed (#25856). The full set is pinned here so the
// corrected sentence cannot rot back — a rename in ANY of the eight
// reddens this gate instead.
// import's `Lint UI` and `Build & E2E UI`), 7→8 with `Type Check UI`
// once bill item 49 closed (#25856), and 8→9 with the `Test UI` aggregate
// (#26460). The full set is pinned here so the corrected sentence cannot
// rot back — a rename in ANY of the nine reddens this gate instead.
file: 'AGENTS.md',
statesTheSet: true,
mustName: [
Expand All @@ -600,6 +609,7 @@ export const INSTRUCTION_SURFACES = [
'Lint UI',
'Build & E2E UI',
'Type Check UI',
'Test UI',
],
},
{
Expand Down Expand Up @@ -631,6 +641,7 @@ export const INSTRUCTION_SURFACES = [
'Lint UI',
'Build & E2E UI',
'Type Check UI',
'Test UI',
],
},
{
Expand All @@ -652,7 +663,7 @@ export const INSTRUCTION_SURFACES = [
* of the six surfaces have one; `AGENTS.md` does not, because a repo-root FILE
* has no separator to be found by — so an AGENTS.md card derived this gate not
* at all, while that surface is the one carrying `mustName` for every
* required context (widened 2→6 by #9677, 6→7 by #17798, 7→5 by #22998, 5→7 by #25853, 7→8). Editing the merge-queue
* required context (widened 2→6 by #9677, 6→7 by #17798, 7→5 by #22998, 5→7 by #25853, 7→8, 8→9 by #26460). Editing the merge-queue
* paragraph there is precisely how this gate goes red, and it was reachable
* only by judgment.
*
Expand Down Expand Up @@ -1936,6 +1947,13 @@ async function selfTest() {
renamedTestCore.problems.some((p) => p.includes("job 'test-gate'") && p.includes("'Test Core'")),
'growing a suffix on the bare `Test Core` name ⇒ red — the exact literal is the contract, not decoration',
);
// The same property on the second matrix aggregate (#26460): `Test UI` is the
// bare name of `test-ui-gate`, and its shards are `Test UI (k/16)`.
const renamedTestUi = fixture('rename Test UI', 'ci.yml', (s) => s.replace(' name: Test UI\n', ' name: Test UI (aggregate)\n'));
assert(
renamedTestUi.problems.some((p) => p.includes("job 'test-ui-gate'") && p.includes("'Test UI'")),
'growing a suffix on the bare `Test UI` name ⇒ red, naming the aggregate job and the required context',
);

// ── (2) the job disappearing entirely ─────────────────────────────────────
// Re-pointed from `console-pin` to `temporal-conformance` when #9533 dropped
Expand Down Expand Up @@ -2087,6 +2105,20 @@ async function selfTest() {
baseline.problems.length === 0 && sources['ci.yml'].includes(TEST_CORE_SHARD_NAME),
'the real suffixed shard name coexists with the bare gate name (assertion 9 compares whole names, not prefixes)',
);
// The second live specimen (#26460): the `test-ui` shards and their `Test UI`
// aggregate, the same shape with the matrix width read the same way.
const TEST_UI_SHARD_NAME = `name: Test UI (\${{ matrix.shard }}/${shardTotal(sources['ci.yml'], 'test-ui')})`;
const collidedUi = fixture('collide the Test UI shard name with its gate name', 'ci.yml', (s) =>
s.replace(TEST_UI_SHARD_NAME, 'name: Test UI'),
);
assert(
collidedUi.problems.some((p) => p.includes("job 'test-ui'") && p.includes('published by ci.yml:test-ui-gate')),
'a Test UI shard wearing the bare `Test UI` context name ⇒ red',
);
assert(
sources['ci.yml'].includes(TEST_UI_SHARD_NAME),
'the real suffixed Test UI shard name coexists with the bare `Test UI` gate name',
);

// ── (8) a registry that lists one context twice ──────────────────────────
battery('(8) a registry that lists one context twice');
Expand Down
Loading