Skip to content
Closed
2 changes: 1 addition & 1 deletion .claude/skills/pm-dispatch/references/platform-readings.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@
- 本车道强制多是过宽的回忆:`dispatch-gates.mjs --tier PATH` 逐路径现推,路径线是下限非放行。
- 该脚本只答自己那棵树:姊妹仓路径回 absent from this tree,姊妹仓的档位与条款②只能手推。
- `os-verify-lock.sh` 只住 objectstack:objectui 无它,跑本仓副本读的是容器级锁,非该仓深度。
- required checks 的名单是每仓事实,objectstack 七个:
- required checks 的名单是每仓事实,objectstack 七个(`Console Pin Gate` 已登记,入 Settings 即第八):
`TypeScript Type Check` · `Lint & Repo Gates` · `Test Core` · `Dogfood Regression Gate` ·
`Build Core` · `Temporal Conformance (live PG + MySQL)` · `Governed Surface Queue Guard`。
- ⭐ 本表的 `mustName` 声明 `statesTheSet` ⇒ 必须逐字等于注册表,加注册行不跟改本表即红。
Expand Down
93 changes: 79 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2310,16 +2310,41 @@ jobs:
# under `packages/**`, and watching it here would rebuild the console on a large
# fraction of every PR. release.yml stays the only check for that direction.
#
# ⚠️ This gate is ADVISORY, and its name is no longer pinned. It was enrolled
# in `check:required-contexts` in #6865 on the strength of the 2026-08-09
# ruling approving it for the required set; the live ruleset read of
# 2026-08-18 (#9533) found it had never landed there, and the row was dropped
# rather than left claiming otherwise — see that file's TOMBSTONE for the full
# provenance. So a red here does NOT block a merge, and renaming this job no
# longer turns anything red. The name is still load-bearing PROSE that renaming
# this job silently falsifies — docs/releases-maintenance.md and
# packages/console/README.md both describe the pin flow by this name (#9793
# tracks the missing pin for job names that are not required contexts).
# ⭐ This job CONCLUDES ON EVERY RUN, and its name is pinned again (#17673,
# ruling #18900 item ⑤: back to required, in two steps). Step one is this
# file plus the REQUIRED_CONTEXTS row in scripts/check-required-contexts.mjs,
# so renaming this job reddens the required `Lint & Repo Gates` context.
# Step two — adding `Console Pin Gate` to Settings → Rulesets — is the
# maintainer's click AFTER step one lands. Until that click a red here still
# does NOT block a merge, and `--verify-required-set` reports the row as
# direction A (registered, not yet live); the row's `authorized` field and
# that file's TOMBSTONE carry the provenance, including the #9533 drop this
# reverses.
#
# Why the job carries NO filter term in its `if:` any more. A job-level `if:`
# that evaluates false concludes `skipped`: the question this gate asks was
# never asked, yet the check-run reads like a pass. Ruling ⑤ asks for a
# conclusion on every PR — a `success` on an unaffected one — so the filter
# term moved from the job's `if:` onto EVERY STEP's own `if:`, spelled
# exactly as it was at job level: `needs.filter.outputs.console != 'false'`.
# THE FILTER CONTRACT (#4928) is kept in both halves, one level down: only an
# EXACT 'false' skips a step, so a dead `filter` job (output '') still builds,
# and `!cancelled()` still displaces the implicit success() at the job level.
# On merge_group / schedule / workflow_dispatch the output is widened to
# 'true' and the full gate runs, exactly as before.
#
# ⛔ Not a step that computes the verdict once into a step output for the
# others to read. An `if:` is a declaration that GitHub, a reader and
# scripts/pm/dispatch-gates.mjs all read the same way — the derivation follows
# a filter term on a step `if:` when the job-level `if:` carries none (#17673)
# — whereas a verdict a shell step wrote is readable only by trusting the
# shell. With it, a pin-only diff stopped deriving check:console-sha and
# check:console-injection at all.
#
# An unaffected run MUST say so: its one running step writes a notice and a
# step summary stating that nothing was built. That `success` is the filter's
# verdict, never a measured build — read it as NOT BUILT, not as a pass of the
# pinned SHA.
#
# ⚠️ It used to need telling apart from a same-shaped sibling, `Console Pin
# Freshness`, which asked whether the pin was CURRENT. That gate is gone
Expand All @@ -2333,31 +2358,62 @@ jobs:
name: Console Pin Gate
needs: filter
# See THE FILTER CONTRACT on the filter job's outputs (#4928). `console`,
# not `core` — each job keeps its own filter output.
if: ${{ !cancelled() && needs.filter.outputs.console != 'false' }}
# not `core` — each job keeps its own filter output, read by each STEP's
# `if:` below rather than here (see the header: a job-level filter term
# concludes `skipped`, and this job must conclude on every run).
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read

steps:
# The one step an unaffected run executes, and its only output is the
# statement that nothing else ran. Its `if:` is the exact complement of
# every other step's: `== 'false'` here, `!= 'false'` on all the rest.
- name: Report NOT BUILT when the console filter said false
if: needs.filter.outputs.console == 'false'
env:
EVENT_NAME: ${{ github.event_name }}
run: |
set -euo pipefail
echo "::notice title=Console Pin Gate: NOT BUILT::The console filter said false for this diff, so nothing was built or checked here. This success is the filter's verdict, not a measured build of the pinned objectui SHA. The merge-queue build widens the filter and runs the full gate."
{
echo "### Console Pin Gate — NOT BUILT on this run"
echo
echo "The \`filter\` job's \`console\` output was \`false\` (event: \`${EVENT_NAME}\`): this diff moves none of the console build inputs listed in ci.yml's \`console\` filter."
echo
echo "- ⛔ No objectui checkout, no console build, no dist restore, no \`check:console-sha\` / \`check:console-injection\` ran."
echo "- This \`success\` is the **filter's verdict**, not a measured pass of the pinned SHA — read it as NOT BUILT."
echo "- On \`merge_group\` the filter is widened to \`'true'\` and this job runs the full gate on the merged tree."
} >> "$GITHUB_STEP_SUMMARY"

# Every step from here on carries the filter term in the contract's own
# spelling — skip only when EXPLICITLY 'false' — so a step added without
# it costs runner time on an unaffected run (and fails without a
# checkout), never a false green.
- name: Checkout repository
if: needs.filter.outputs.console != 'false'
uses: actions/checkout@v7

- name: Setup Node.js
if: needs.filter.outputs.console != 'false'
uses: actions/setup-node@v7
with:
node-version: '22'

- name: Setup pnpm
if: needs.filter.outputs.console != 'false'
uses: ./.github/actions/setup-pnpm

- name: Get pnpm store directory
if: needs.filter.outputs.console != 'false'
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

- name: Setup pnpm cache
if: needs.filter.outputs.console != 'false'
uses: actions/cache@v6
with:
path: ${{ env.STORE_PATH }}
Expand All @@ -2371,6 +2427,7 @@ jobs:
# builds a superset of the client closure needed here and is seeded from
# main (same reasoning as the Temporal Conformance job's fallback).
- name: Restore Turbo cache
if: needs.filter.outputs.console != 'false'
uses: actions/cache/restore@v6
with:
path: .turbo/cache
Expand All @@ -2382,6 +2439,7 @@ jobs:
${{ runner.os }}-turbo-build-core-

- name: Install dependencies
if: needs.filter.outputs.console != 'false'
run: pnpm install --frozen-lockfile

# build-console.sh bundles THIS tree's @objectstack/client into the console
Expand All @@ -2393,6 +2451,7 @@ jobs:
# script's existing callers (release.yml, showcase-smoke.yml) pre-build for
# the same reason.
- name: Build @objectstack/client and its dependencies
if: needs.filter.outputs.console != 'false'
run: pnpm exec turbo run build --filter=@objectstack/client... --concurrency=4

# Same key as release.yml's "Cache vendored Console dist" step. Actions
Expand All @@ -2409,6 +2468,7 @@ jobs:
# the same guarantees as a freshly built one.
- name: Restore vendored Console dist (keyed on the objectui pin)
id: console-dist
if: needs.filter.outputs.console != 'false'
uses: actions/cache/restore@v6
with:
path: packages/console/dist
Expand All @@ -2419,7 +2479,9 @@ jobs:
# packages/console/, and asserts the bundle canary — every failure mode a
# bad pin can carry, surfaced on the PR instead of at publish time.
- name: Build the Console SPA at the pinned objectui SHA
if: steps.console-dist.outputs.cache-hit != 'true'
# Both terms: on an unaffected run the restore above is skipped, its
# cache-hit output is '' and `'' != 'true'` alone would BUILD.
if: needs.filter.outputs.console != 'false' && steps.console-dist.outputs.cache-hit != 'true'
run: bash scripts/build-console.sh

# check:console-sha exits 0 — deliberately — when there is no dist at all
Expand All @@ -2430,6 +2492,7 @@ jobs:
# check with nothing to check and report success. Prove one is really in
# place before letting that check speak for the job.
- name: Assert a real Console dist is in place
if: needs.filter.outputs.console != 'false'
env:
CACHE_HIT: ${{ steps.console-dist.outputs.cache-hit }}
run: |
Expand All @@ -2446,6 +2509,7 @@ jobs:
echo "✓ Console dist present ($(du -sk "$dist" | awk '{print $1}') KB, via $origin) from objectui@$(cut -c1-12 "$dist/.objectui-sha")"

- name: Verify the Console dist stamp matches the pin
if: needs.filter.outputs.console != 'false'
run: pnpm check:console-sha

# The spec-injection half of the same question, and the reason it is a
Expand Down Expand Up @@ -2474,6 +2538,7 @@ jobs:
# whose post-step saves even on failure, under this same repo-scoped key — so
# a poisoned entry is reachable from here and must not pass quietly.
- name: Verify the restored Console dist bundles this tree's spec
if: needs.filter.outputs.console != 'false'
env:
CONSOLE_DIST_CACHE_KEY: ${{ runner.os }}-console-dist-${{ hashFiles('.objectui-sha', 'scripts/build-console.sh') }}
run: pnpm check:console-injection --require-stamp
Expand All @@ -2484,7 +2549,7 @@ jobs:
# key moves only when the pin does. A pin-bump PR writes one entry, and the
# rest of that PR's pushes reuse it instead of rebuilding the console again.
- name: Save vendored Console dist
if: steps.console-dist.outputs.cache-hit != 'true'
if: needs.filter.outputs.console != 'false' && steps.console-dist.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
with:
path: packages/console/dist
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,12 +513,12 @@ 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** — seven contexts block: `Lint & Repo Gates` (all `check:*` gates),
set** — seven block (eight once Settings adds `Console Pin Gate`): `Lint & Repo Gates` (all `check:*` gates),
`TypeScript Type Check`, `Test Core`, `Dogfood Regression Gate`, `Build Core`,
`Temporal Conformance (live PG + MySQL)` and `Governed Surface Queue Guard`. A check outside
those seven is advisory and rides through, and an advisory red that lands rides `main`'s
those eight 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 seven names as contract.
name, so a rename detaches its gate silently — treat those eight names as contract.

**Re-arm awareness** — none of these is a reason to avoid the queue; all are reasons to
confirm a PR is still *in* it: a red queue build **ejects** your entry and drops
Expand Down
Loading
Loading