Skip to content

fix(pm): the half-state patrol reads a seat post's NEWEST comment page, and H44 knows the current marker spelling - #18322

Merged
claude[bot] merged 2 commits into
mainfrom
claude/issue-18312-h44-seat-leg-newest-page
Sep 15, 2026
Merged

claude[bot] merged 2 commits into
mainfrom
claude/issue-18312-h44-seat-leg-newest-page

Conversation

@claude

@claude claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #18312

The defect, measured

commentRowsFor asks /issues/{number}/comments?per_page=100 with no page, and the
per-issue comments endpoint serves oldest-first (direction=desc / sort=created are
silently ignored there). That is the right window for a card — a claim is early in the thread,
and H50's walk completes a full first page — and the wrong one for a seat post, which is an
append-only log running to hundreds of comments.

On the triage seat post (#6015, 818 comments) the page-less request returns
2026-08-06T16:01Z → 2026-08-10T17:29Z: five weeks of archive, holding none of the current
round's artefacts. H44, H56 and H64 all read that page; H65 (landed earlier, PR #18311) had to
buy its own newest page for exactly this reason.

Second half: H44's round-open leg matched nothing live. H44_ROUND_OPEN_MARKER expects
R+N open; the triage seat writes
**Round-open marker** · triage seat · ... · **R+236** · fire 2026-09-15T14:51Z. Probe on the
base tree: H44_ROUND_OPEN_MARKER.test(body of 5682407231) reads false.

What this changes

One shared seat-post window. seatPostRowsFor is the file's single declaration of which
page is a seat post's window
: the last page, ceil(comments / H50_COMMENTS_PAGE_SIZE) located
from the carrier's own count through H65's two exported helpers, memoised — one request per
seat post per run
, whichever row asks first. H44's seat leg lands it in commentCache before
the judging loop, so H56 and H64 inherit it by construction; H65 consumes the same memo instead
of buying a second copy.

  • An ordinary card keeps its window — exactly one page-less comment read exists in the
    file and it is the card one (pinned by a source audit).
  • A single-page seat post costs no new request: page 1 is the newest page, so the cached
    rows are reused unfetched.
  • A seat post with no readable comments count cannot have its newest page located, so the
    window falls back to page 1 and is counted apart from newest-page coverage.
  • A seat post's cached thread is now a page of the tail, not a prefix, so the walk memo
    records it incomplete and H52 keeps treating those posts as UNJUDGED rather than clean.

One declaration of the marker spellings. h44RoundOpenShapes() reads the round-open entries
back out of H65_ROUND_ARTEFACT_SHAPES and tests them against the comment's headline
(H65's measured false-positive control: a comment that merely quotes a marker deeper down is not
one). H44_ROUND_OPEN_MARKER is unchanged and still matched body-wide, so no carrier this row
already reports falls out. Neither row carries a private copy of the other's spelling.

Design choice (b) of the two the card offered — the shared bought page — rather than (a), a
seat-aware cache window: (a) leaves two rows deciding the same question in two places, which is
the dialect the file spends its length refusing.

Live before → after (report-only run on this board, 2026-09-15T16:51Z → 17:00Z)

Rows that moved — H44 on the three multi-page held seat posts, each now naming an artefact
from the current page instead of an August one:

carrier comments before after
#6021 127 5249069333 — 12 cards 5622000371 — 551 commits
#6023 225 5224786202 — a36db28 5593116616 — 4013 PRs
#6367 137 5239794336 — 33 PR 5537619920 — 97bcd99e

#6026 (48) and #7623 (63) fit in one page and are byte-identical before and after, which is the
single-page leg working. Row counts are unchanged: H44 26, H56 6, H64 10, H65 1.

Coverage clauses:

  • H44: 1184 comment(s) across 185 thread(s) → 958 across 185, seat leg 5 of 5 both times,
    and the new sentence reads 5 of those post(s) had that page LOCATED. The corpus shrinks
    because a last page is shorter than a full first page — that is the trade this card bought:
    fewer comments read, and the ones read are the live ones.
  • H56: 1184 judged → 958; ambiguous held-out lines 14 → 18.
  • H64: texts 1702 → 1476, signed 872 → 748; its census (102 / 22 / 80) and its
    filed rows are unchanged.
  • H65: 11 round artefact(s) ... 18 comment(s) ... 1 carrier(s) filed — identical. Its counters
    keep their meaning; only the clause's prose changes, to say the page is shared rather than
    never cached.

Not measurable on this board today, and stated rather than implied: #6015 is 🔴 空缺, and
H44's seat leg gate is pm:seat + seatIsHeld, which excludes a vacant or 🟢 Routine seat by
name — so H44/H56/H64 read nothing on the triage post before or after. That gate is out of this
card's surface; it is written up in the report.

Reverse verification

Three ablation legs from the committed state, each mutated on disk, proved on disk, read, and
restored to the HEAD blob (git diff HEAD empty, git hash-object back to
b42680717dff2cd6aada4440b978e3905dcf87b4):

  1. the located page becomes a page-less read again → 3 cases red by name, including
    seat window: exactly ONE page-less comment read exists in this file (got 2, want 1);
  2. the seat leg abandons the memo → seat window: ...taken through the memo by more than one row (got 1, want 2);
  3. the round-open headline leg never matches → 3 cases red, among them
    H44 marker: ...under the marker's own name rather than the carrier's (got "a seat-post section", want "a round-open marker") — which is precisely what the base tree reported.

Gates

All 41 commands derived by node scripts/pm/dispatch-gates.mjs --commands scripts/pm/check-half-states.mjs were run and reconciled through --ran: every one exit 0.
The battery pnpm check:pm-dispatch-gates took 453.5s (1730 self-test cases). The file's own
--self-test goes 4196 → 4235 cases, all passing. ESLint on the file: exit 0.
node scripts/check-self-test-wired.mjs: exit 0.

skip-changeset: nothing published moves — scripts/pm/** ships in no package's files[].


Generated by Claude Code

…e, and H44 knows the current marker spelling

`commentRowsFor` asks the comments endpoint for `?per_page=100` with no `page`,
and GitHub serves issue comments OLDEST-FIRST (`direction=desc` / `sort=created`
are ignored there). That is the right window for a card and the wrong one for a
seat post: on the triage seat post (816 comments) it returns 2026-08-06 →
2026-08-10, five weeks of archive holding none of the round then running. H44,
H56 and H64 were all reporting on that page; H65 had to buy its own.

The seat-post window is now `seatPostRowsFor` — the last page, located from the
carrier's own `comments` count through H65's two helpers, memoised, ONE request
per seat post per run. H44's seat leg lands it in the comment cache before the
judging loop, so H56 and H64 inherit it by construction, and H65 consumes the
same memo instead of a second purchase. An ordinary card keeps its first-page
window; a single-page seat post costs no new request; a post whose count cannot
be read falls back to page 1 and is counted apart from newest-page coverage.
A seat post's cached thread is a page of the tail rather than a prefix, so the
walk memo records it incomplete and H52 keeps treating those posts as unjudged.

H44's round-open leg also matched nothing live: its regex expects `R+<n> open`
and the triage seat writes `**Round-open marker** · … · **R+236** · fire …`.
It now reads the round-open entries of `H65_ROUND_ARTEFACT_SHAPES` — the file's
one declaration of those spellings — against the comment's headline, H65's
measured false-positive control. `H44_ROUND_OPEN_MARKER` is unchanged and still
matched body-wide, so no carrier this row already reports falls out.

Self-test 4196 → 4235 cases.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 15, 2026
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d625971c1160add8f38265c0e6c490c2af088e11

Reviewing seat: domain:skills, in-seat review at the contract-review tier — get_session read at 2026-09-15T07:15Z: configured_model = session_context.model = last_served_model, the constant's tier, no fallback notice this round. Card #18312, claim 5684219721, report 5684847182 (posted through the tool; read back footer-appended); Clause-②: no on the claim; default-tier build, seat review at tier.

① Derived judgments

  • Accept set / public surface: none moves — scripts/pm/check-half-states.mjs (+388/−56, the only file; the head is a merge of origin/main 863c7c44 whose four spec-liveness files are outside this PR's diff; the patrol file's blob b4268071 is identical on both heads) against merge-base 863c7c44; nothing published; skip-changeset read back. Non-governed ⇒ in-seat landing.
  • The change, read in full on a detached worktree of the first head (blob-identical): design (b) — ONE shared memo seatPostRowsFor declares which page is a seat post's window (the LAST page, located from the carrier's own comments count through H65's two exported helpers); H44's seat leg takes it and lands it in commentCache before the judging loop, so H56 and H64 inherit it by construction; H65 consumes the same memo at the foot instead of a second purchase; a single-page thread reuses the rows already in hand; an unlocatable count falls back to page 1 and is counted separately (readingSeatNewest); a multi-page seat post is marked an INCOMPLETE walk so H52 stays unjudged. H44 recognises the current marker spelling through H65's declared shapes filtered to ROUND_OPEN_ARTEFACT_KIND (headline-only, H65's false-positive control), its owned regex kept body-wide and unchanged; seatWindowAudit reads the file's own source and pins ONE page-less comment read (the card window), ONE purchase site, two readers. The seat took the pure readings on the head: the two live markers 5682407231 / 5681229485 → h44IsRoundOpenMarker true, shape 「a round-open marker」 (the owned regex still false on both; origin/main exports no such predicate); the audit reads 1 / 1 / 2 / 4. Self-test 4196 / main → 4235 / head (+39; one H65 case rewritten because its pinned phrase 「never cached」 became false — split into two pins on the new statement); ESLint 0/0; check-self-test-wired OK; --pair 18322 0; the battery 1730 pass on the seat's detached run (exit 0).
  • Gates 41 / 41 / 0 / 0 by --ran on the merge head (the union and the battery run twice, 453.5 s / 460.0 s). Reverse verification by the dev: three ablation legs from the committed state with blob hashes (the located page back to page-less → 3 red by name; the seat leg back to the card window → 1 red; the headline leg disabled → 3 red = the base tree's reading; restored, git diff HEAD empty; one non-unique anchor refused and re-anchored rather than retried blind); the live patrol before/after (base 10a7b9fa vs 09026edf, both exit 0): three H44 rows on multi-page HELD seat posts moved from August artefacts to September ones ([PM seat] domain:services — 🟢 os-warren · session_01CeRCvvvjxbxu2iYJAc5Cce #6021, [PM seat] domain:devx @ objectstack — 🟢 os-warren · session_012yK1ddAdAqfzwigRHZq3r2 #6023, [PM seat] domain:engine — ⏳ vacant #6367), the two single-page posts byte-identical, row counts unchanged (H44 26 / H56 6 / H64 10 / H65 1), the corpus SMALLER (1184 → 958 comments — a tail page is shorter than a full first page, the trade the card bought) and the clause now says which window it is.

② Semver level

Not applicable — nothing published moves; skip-changeset measured.

③ Boundary flags

open_questions: none. One seat premise refuted, owned here: 「H44's seat leg reaches the triage post」 — true as a header claim, false live: the leg's gate needs seatIsHeld, which excludes a 🟢 Routine or vacant seat, and #6015 read 🔴 空缺 during both live runs, so the before/after on the triage post itself could not be taken — filed below. Four deviations answered: origin/main merged in (never rebased) with the union and the battery re-run on the merge commit — accepted; the PR body not re-sent after the merge (every number unchanged) — accepted; the live before/after taken on the three multi-page held seat posts rather than #6015 — accepted, stated; one H65 case rewritten, not deleted — accepted. Out of scope, FILED by the seat as three cards: (1) H44_READING_TIMESTAMP rejects a minute-precision ISO stamp because the preceding T is a word character (the ruled spelling itself reads false); (2) H44's seat-leg population excludes a Routine / vacant triage seat, so the window fix is inert on #6015 until a session holds it; (3) H32 / H38 still read the seat post's OLDEST page for the idle clock and T_seat. Noted, not filed: an artefact between the first and the last page is read by neither window — visible in the clause every run. Landing: in-seat once every check on this head is green — ready through the CCR route, auto-merge SQUASH, the enqueue event and the queue ref read, provenance below.

Implemented-by: claude/issue-18312-h44-seat-leg-newest-page
Reviewed-by: session_01HZfg2AwVX191qCizp88gQr

VERDICT: PASS


Generated by Claude Code

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Landing provenance — skills seat, session session_01HZfg2AwVX191qCizp88gQr, 2026-09-15T17:35Z. Non-governed landing (scripts/pm/check-half-states.mjs only): review of record 5684896900 PASS on head d625971c1160add8f38265c0e6c490c2af088e11, ACCEPT 5684897383 on #18312; every check on that head read green at 2026-09-15T17:34Z (25 success / 8 skipped / running none / failed none; combined status success). The seat flipped the PR ready through POST …/pulls/18322/ccr/ready_for_review and armed auto-merge SQUASH through PUT …/pulls/18322/ccr/auto_merge; the ready_for_review event reads 2026-09-15T17:34Z and the added_to_merge_queue event one second later, both by claude[bot]; the queue ref refs/heads/gh-readonly-queue/main/pr-18322-82b3032c… read 1 twenty-three seconds after the enqueue (a lone entry, chained on 82b3032c). Landing is confirmed by the three readings (merged_at + git log origin/main carrying (#18322) single-parent + the queue ref gone) at the next wake; then #18312's residue is stripped and the landing record posted; #18323 / #18324 / #18325 (same file) stay serial behind this PR until then.


Generated by Claude Code

Merged via the queue into main with commit a46cd8c Sep 15, 2026
35 checks passed
@claude
claude Bot deleted the claude/issue-18312-h44-seat-leg-newest-page branch September 15, 2026 17:55
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…s a T — the ruled minute spelling no longer reads as untimestamped (objectstack-ai#18326)

Fixes objectstack-ai#18323

`H44_READING_TIMESTAMP` is the leg that decides a paragraph already
carries its 取数时刻. It anchored the hour with a bare word boundary, which
needs a NON-word character in front of it — and the `T` of an ISO
instant is a word character. So the leg read **false** on the very
spelling the 2026-09-02 ruling gives, and H44 reported readings that DO
carry their time as untimestamped: a false positive on the row's own
subject. A seconds stamp passed only by accident, the boundary holding
before its `mm:ssZ` tail, so the match began at the MINUTES.

```js
// before
/\b\d{2}:\d{2}(?::\d{2})?Z\b/
// after — the left anchor is a word boundary OR a lookbehind for the ISO T
/(?:\b|(?<=T))\d{2}:\d{2}(?::\d{2})?Z\b/
```

## Reverse verification — probes on the module's own export

A `file://` import of the script (a pure export; the module runs nothing
on import and makes no request), against the unpatched file at
`a46cd8c4` and against this branch:

| probe | before | after |
|---|---|---|
| `2026-09-15T14:51Z` — the live R+236 marker's fire stamp | false |
**true** |
| `2026-03T00:21Z` — the spelling the ruling itself gives | false |
**true** |
| `2026-09-15T14:51:42Z` | true | true |
| `14:51Z` — a bare clock | true | true |
| `19:38` — a bare clock with no zone | false | false |
| `12314:51Z` — a clock inside a longer digit run | false | false |

The seconds probe is the one that shows the difference between the fix
and the accident it replaces: `'2026-09-15T14:51:42Z'.match(...)[0]` was
`51:42Z` before and is `14:51:42Z` now — the match starts at the HOUR,
and that is pinned as a case rather than the bare `true` that held
either way.

## The one design choice, on the four axes

The choice is between **naming the left anchor** (start / whitespace /
punctuation, which the word boundary already spells, plus the ISO `T`)
and **dropping the left boundary altogether**. This PR names it, and
looks behind rather than consuming, so a match is still exactly the
stamp.

- **实际业务需求** — measured, not supposed: the only predecessor that occurs
on this board is the ISO `T`, in the R+236 round-open marker (`fire
2026-09-15T14:51Z`) and in the ruling's own `2026-03T00:21Z`. No
artefact was measured writing a stamp behind any other letter. Dropping
the boundary would serve a case nobody has: `12314:51Z` is a digit run
that happens to contain a clock, never a reading's time.
- **项目长远合理性** — the boundary is doing real work and the defect was its
SCOPE, not its presence; keeping it and naming the one exception is the
contract-shaped repair, where dropping it would be a workaround that
trades one class of wrong answer for another. The row's banner already
claimed that a paragraph carrying `HH:MM[:SS]Z` is clean — this makes
the regex true to a statement the file has been making all along, rather
than rewriting the statement down to the implementation.
- **防 AI 写代码犯错** — a report-only patrol that fires on a well-formed
artefact teaches the seat writing it that the ruled spelling is wrong,
and the cheapest way to silence a false row is to write a MORE precise
stamp than the ruling asks for (which is exactly what this file's own
marker case had been reduced to doing). Removing the false positive
removes that pressure. Looking behind rather than consuming keeps the
match equal to the stamp, so a future reader that echoes it gets no
stray designator. Admitting any letter — the lenient direction — would
make the leg quieter in ways nobody declared; the three negatives are
pinned as cases so the widening cannot creep.
- **创业阶段不扩散** — exactly one predecessor is admitted, and only in the
uppercase the ruling spells. RFC 3339's lowercase `t` is legal and
absent from this corpus; it is pinned as NOT admitted, so taking it
later is a measurement rather than a guess.

## Self-test

`node scripts/pm/check-half-states.mjs --self-test`: **4235 → 4243 cases
pass**, exit 0. Eight cases added, one rewritten:

- the two ISO spellings that read false (`2026-09-15T14:51Z`, and the
ruling's own `2026-03T00:21Z`);
- where the seconds stamp's match STARTS;
- three declared negatives — the digit run, any other letter, and the
lowercase `t`;
- an ISO instant with no zone (`2026-09-15T14:51`) still reads false,
exactly as a bare clock does;
- the H44 marker case re-pinned to MINUTES, with the SECONDS spelling of
the same reading kept beside it so neither can regress alone. The
comment above it said this file spelled that stamp to seconds because a
minute stamp could not clean a paragraph — a statement that is now
false, so it is rewritten (not deleted) to say the limit is gone.

**Firing control (ablation):** with the one regex line reverted and
every case kept, the self-test goes red on exactly four cases by name —
the two ISO spellings, the match-start pin, and the marker case, which
reports `{shape: 'a round-open marker', kind: 'tree', fragment:
'827cacbf'}` where `null` is wanted. That row IS the false positive this
PR removes. Restoring reproduced the committed blob hash exactly, and
`git diff HEAD` and `git status --porcelain` are both empty.

## Scope

`scripts/pm/check-half-states.mjs` only, and inside it only the constant
at the H44 grammar, its doc comment, and its self-test cases. H44's
population, its seat gate, its marker shapes (`h44RoundOpenShapes` /
`h44IsRoundOpenMarker`), the fragment grammar and every other H row are
untouched. `:9791` is the constant's only consumer (`grep` count: one
declaration, one consumer, the self-test cases) and it calls `.test`, so
nothing reads the match text today.

The two sibling cards dispatched on this same file — objectstack-ai#18324 and objectstack-ai#18325 —
work at other regions (`h44NeedsSeatComments`, the `seatMarkers`
capture, `latestSeatMarker`) and also add self-test cases at the tail;
whichever lands second merges `main` and re-runs the self-test. PR
objectstack-ai#18322 is the predecessor that landed the marker spelling this card's
measurement came from.

## Acceptance notes

- `PROTOCOL_STAMP_RE` (H56's stamp shape) carries the same `\b` opener
and is NOT affected: its boundary sits in front of the four-digit YEAR,
where a non-word character really does precede it. Checked, not assumed.
- Noted, not filed: the leg validates the SHAPE of a clock and not its
ranges, so `1234-56:78Z` reads as a stamp. It read that way before this
change and still does; the direction is toward MORE paragraphs clean, so
it can only make the row quieter, and no artefact on the board writes
it.

`skip-changeset`: nothing published moves. The diff is one file under
`scripts/pm/**`, which no package's `files[]` ships.

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

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

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
… skip is not a failure, and a check reports the skips outside the declared roster (objectstack-ai#18357)

Fixes objectstack-ai#18308

## What

`.claude/skills/pm-dispatch/SKILL.md` :629 read 「入队资格 = PR 上每一个 check
全绿,⛔ 不是 required 子集;required 集是队列强制的地板。」 — a bar no landing on this
repository can satisfy, because `skipped` is the ordinary conclusion of
a path-filtered job and the charter defined no state for it. Measured
over the ten most recent landed heads (below), every head carried 8–19
skipped check-runs beside its successes; the seat's landings read
「success = green, skipped = not failed」 by an unstated convention.

Two changes, both inside the claimed file surface:

1. **SKILL.md :629, re-keyed in place** (112 B → 119 B, ≤120 B; 812 →
812 lines; no issue number):

> `- 入队资格 = 每个 check 为 success 或预期 skip(名单:check-expected-skips.mjs),⛔
不是 required 子集。`

The bar now names the two states a check may be in — `success`, or an
expected skip — and points at the one machine-readable roster of
expected skips. The third clause of the old line (「required 集是队列强制的地板」)
did not fit the byte ceiling and is carried by AGENTS.md §7 (「the queue
enforces only the required set」); the ⛔ clause is kept verbatim.

2. **`scripts/pm/check-expected-skips.mjs`** (new; `package.json` gains
`check:pm-expected-skips` = its `--self-test`): given `--pr N` or
`--head SHA` (or a pre-fetched payload via `--check-runs-json FILE|-`),
it reads the head's check-runs and judges every `skipped` run against a
roster declared once, in the file, as data with a one-line reason per
row. Exit register: **0** every skip is in the roster · **4** a skip is
outside it (each named and classified: a filter miss, or a dependency
skip when the same check suite holds a failed run; a raw `matrix`
template in the name is read as "skipped before matrix expansion, i.e. a
job-level gate — never a workflow-level `paths:` filter, which creates
no check-run at all") · **3** NOT MEASURED (unresolvable sha, 404,
network, no check-runs on the head, or a check-run still running — the
skip set is not final). Report-only; the self-test pins structurally
that the file carries no `method:` key and imports no writer.

The roster is **tied to the workflows, not remembered**: `--self-test`
parses each row's workflow with the `yaml` package and asserts the job
exists, carries the row's name, carries an `if:`, that the `if:` spells
the declared gate (`needs.filter.outputs.X != 'false'`, the
`github.event.action` exclusion, or the label literal), and — for ci.yml
rows — that the `filter` job's output keeps its `|| 'true'` widening,
which is what makes "the merge-queue build runs it" true. The audit is
driven red in the self-test on a deleted, renamed, un-gated and re-gated
job, a lost widening and an unreadable workflow.

## The roster (11 names), measured over ten landed heads

| name | workflow › job | mechanism | over the ten heads |
|---|---|---|---|
| `Build Core` | ci.yml › build-core | `filter` output `core` said
false; REQUIRED context, judged on the queue build | skipped 10/10 |
| `Temporal Conformance (live PG + MySQL)` | ci.yml ›
temporal-conformance | same, REQUIRED context | skipped 10/10 |
| `Dogfood Regression Gate (${{ matrix.shard }}/3)` | ci.yml › dogfood |
same; raw matrix template = pre-expansion name (the aggregate `Dogfood
Regression Gate` runs `if: always()`, never skips) | skipped 10/10 |
| `Dogfood Verify CLI` | ci.yml › dogfood-verify | same | skipped 10/10
|
| `Test Core (${{ matrix.shard }}/6)` | ci.yml › test | `core` OR
`crosspkg` both false (scripts/** is in `crosspkg`, so scripts/pm heads
RUN it) | skipped 3/10 — only the .md-only heads |
| `Build Docs` | ci.yml › build-docs | `filter` output `docs` | skipped
10/10 |
| `Console Pin Gate` | ci.yml › console-pin | `filter` output `console`
| skipped 10/10 |
| `Check PR Size` | pr-automation.yml › pr-size | `if:` excludes
`labeled` / `unlabeled` / `edited` events; each event is its own run on
the same head | skipped 9/10, success beside it 10/10 |
| `Auto Label` | pr-automation.yml › auto-label | same | skipped 9/10,
success beside it 10/10 |
| `Check Changeset` | pr-automation.yml › changeset-check | `if:` skips
a PR carrying `skip-changeset` | skipped 10/10 (every head carried the
label), success beside it 9/10 (the run before the label) |
| `Packed-tarball smoke (opt-in)` | pack-smoke-optin.yml › pack-smoke |
opt-in by `needs:pack-smoke` | skipped 10/10 |

Never skipped on any of the ten heads (and carrying no `if:`): `Lint &
Repo Gates`, the four `Type Check ·` lanes, `TypeScript Type Check`,
`Test Core` and `Dogfood Regression Gate` (the aggregates), `Governed
Surface Queue Guard`, `filter`, the four claim/keyword guards, `Check
Documentation Links`, `Close issues referenced in other repositories`.
Workflows with a workflow-level `paths:` filter
(`half-state-patrol.yml`, `board-snapshot.yml`) produce no check-run at
all on a non-matching head — they are absent on 6 of the ten heads,
never `skipped` — which is the measured basis for the "a skipped
check-run is never a `paths:` filter" reading.

## Reverse verification (all at `7a1f99ea`)

| leg | result |
|---|---|
| `--head` on the ten landed heads objectstack-ai#18298 · objectstack-ai#18307 · objectstack-ai#18311 · objectstack-ai#18315 ·
objectstack-ai#18316 · objectstack-ai#18322 · objectstack-ai#18326 · objectstack-ai#18327 · objectstack-ai#18328 · objectstack-ai#18332 | **exit 0 on every
one**; accepted skips per head: 11 · 12 · 11 · 19 · 12 · 8 · 11 · 18 ·
18 · 11, every name in the roster; e.g. objectstack-ai#18322 (the 8-skip head): `Build
Core`, `Build Docs`, `Check Changeset`, `Console Pin Gate`, `Dogfood
Regression Gate (…/3)`, `Dogfood Verify CLI`, `Packed-tarball smoke
(opt-in)`, `Temporal Conformance` |
| constructed fixture: the real objectstack-ai#18322 payload with `Lint & Repo Gates`
mutated to `skipped` | **exit 4**, naming `Lint & Repo Gates (check
suite 94780297729)` and classifying it `filter-miss` |
| garbage sha `--head deadbeef…deadbeef` | **exit 3** — `NOT MEASURED —
HTTP 422 — the API cannot resolve that sha` |
| `--pr 18315` (the head is looked up through the proxy) | exit 0, `19
skipped check-run(s), every one in the roster`; `--pr 18308` (an issue
number, not a PR) → exit 3 (HTTP 404) |
| `--self-test` | 99 cases pass, offline (the roster's truth on the live
workflows and its audit driven red six ways; the judge on the measured
39-run objectstack-ai#18315 head and on fixtures for 0 / 4 / 3; read classification;
argv; the real CLI on payload files incl. `--json`; the structural pins)
|
| SKILL.md ratchet | `wc -l` 812 → 812; :629 112 B → 119 B;
`check-skill-line-ratchet: SKILL.md is 812 lines (ceiling 812; headroom
0)` |

## Gates (local, at `7a1f99ea`)

`node scripts/pm/dispatch-gates.mjs --commands
.claude/skills/pm-dispatch/SKILL.md scripts/pm/check-expected-skips.mjs
package.json` derived 45 commands; all 45 were run with the exit
captured by redirect, and `--ran` reconciles: `✓ dispatch-gates --ran:
45 derived famil(ies) accounted for — 40 run, 5 NOT-MEASURED (5 DERIVED
from a recorded exit 3)`. The five NOT MEASURED are the `dist/`-reading
families on an unbuilt tree (`check:dts-closure`,
`check:dual-build-cjs-loads`, `check:lean-entry-closure`,
`check:sourcemap-no-sources-content`, `@objectstack/lint
check:doc-formula-expressions` — each prints `PREREQUISITE NOT MET`);
this diff touches no package, so no build closure is owed locally and CI
runs them built. The `pnpm check:pm-dispatch-gates` battery was not
derived, so it was not run.

Named gates, verdict lines quoted: `check-skill-line-ratchet: SKILL.md
is 812 lines (ceiling 812; headroom 0)` · `check-skill-id-lint: 27
file(s) clean` · `check-skill-frame-sync: the one declared copy of the
decision frame is internally coherent` · `check-self-test-wired: every
one of the 212 script(s) CI runs that ship a --self-test has that
self-test run by CI` (the new script is not in that population — see
Acceptance notes) · `check-nul-bytes: OK (scanned 8707 text file(s))` ·
`check-governed-prose: 2 instruction surface(s) name all 5 registered
governed surfaces` · ESLint (`--no-inline-config`) on the new file: exit
0 · `check-governed-merges.mjs --test
.claude/skills/pm-dispatch/SKILL.md`: **GOVERNED** (`.claude/**` ×1),
exit 3 as designed. `check-clause2-carriers.mjs --pair` is run once this
PR exists and its reading goes in the report comment.

## The one design choice, on the four axes: a roster declared in the
check vs. deriving expectedness live from the workflows' `paths` filters

- **实际业务需求** — the measured need is name-level: 31 landings this shift
and the ten heads above were judged by "is this skipped name one that
always skips?", and zero of them needed a diff-level answer. The
diff-level question ("should `Build Core` have run on THIS diff?") is
already answered for the required family by the platform: on
`merge_group` ci.yml's `filter` widens every output to `'true'` (the `||
'true'` half of the filter contract, now pinned by this check's
self-test), so the family runs on the merged tree before `main` moves. A
live derivation would answer a question nobody measured a need for, at
the cost below.
- **项目长远合理性** — a roster is a declaration that can rot; a live
derivation is a second evaluator of the platform's own semantics
(dorny/paths-filter's picomatch dialect, GitHub's expression language,
matrix name templates, per-event runs) that can drift from the real
evaluator. Both are drift; the roster's drift is made LOUD here (every
row is pinned to its live job, name, `if:` and gate spelling — a rename
or re-gate reddens CI), while an evaluator's drift is silent by
construction (a wrong glob yields a confident "expected").
Contract-first: the workflow file is the contract, and the roster is a
checked reading of it, not a copy of its path lists.
- **防 AI 写代码犯错** — the roster makes the wrong move structurally hard: a
new gated job's first skip is exit 4 until someone adds a row WITH its
mechanism, and a row that names a job the tree does not gate is red. A
live evaluator is where an AI would quietly mis-implement glob semantics
and produce the false green this tree refuses everywhere else (the
"could not read" ≠ "clean" class). The declared-vs-delivered line is
kept: the check advertises the name question only, and says so in its
header and report.
- **创业阶段不扩散需求** — the roster is ~11 rows of data and one audit; live
derivation is a YAML-expression evaluator with parity tests against
GitHub. No pull exists for the latter; if a rostered required job is
ever found skipped on a diff inside its filter, that measurement is the
card that would justify it.

**Recommendation: the roster in the check (implemented).** Should the
seat prefer live derivation, nothing here blocks it — the roster rows
already carry `workflow`, `job` and the gate's outputs, which is the
input a derivation would start from.

## Acceptance notes

- **Self-test wiring.** `check:pm-expected-skips` exists in
`package.json` (mirroring the report-only siblings), but no workflow
names it and lint.yml was outside this card's file surface, so
`check-self-test-wired` (correctly) does not count it and CI does not
run its 99 cases. The completion is one lint.yml step beside the other
`check:pm-*` steps (`run: pnpm check:pm-expected-skips`); left to the
seat — 承接者:the skills seat, on this PR or a sibling. Noted, not filed.
- **:629's floor clause dropped for the byte ceiling** (「required
集是队列强制的地板」); AGENTS.md §7 carries the fact. Noted, not filed.
- **Exit 4 judges skips only.** Other conclusions on the head
(`failure`, `cancelled`, `neutral`, …) are printed loudly under `other
conclusions` and do not move this check's exit; the bar's success half
is read from the same listing. A malformed `--head` (non-hex) is a usage
error (exit 2), a well-formed sha the API cannot resolve is exit 3.
Noted, not filed.
- **The card's five-name family was a subset.** The measured recurring
family is eleven names (six ci.yml `filter`-gated jobs the card did not
list, including two REQUIRED contexts); the card's citation of a
"platform-readings discipline (a skip is not a pass)" has no verbatim
carrier — the nearest lines are AGENTS.md §7 (「Green means the
gate-carrying jobs' conclusion is success」) and
`references/review-checklist.md:43`. Recorded in the report, no card.
- objectstack-ai#18349 is not addressed here; it holds :513 / :523 of the same file
(region-level parallel). `origin/main` did not move under this branch
after cut (`ceb6b5fb`).

## 维护者速读(草稿)

**改了什么**:入队资格这一行改成「每个 check 为 success 或预期 skip」,并新增一个只读的检查脚本
`scripts/pm/check-expected-skips.mjs`:给它一个 PR 号或提交 SHA,它读出该提交上所有
check,凡是 `skipped` 的都对照脚本内声明的「预期 skip 名单」(11 个名字,每个带一句为什么会 skip
的机制),名单外的 skip 会被点名并退出码 4;读不到就退出码 3,绝不当作通过。

**为什么改**:原来的「每一个 check 全绿」在本仓库任何一个 PR 上都做不到——路径过滤的 job 本来就以 `skipped`
结束,实测最近十次落地每次都有 8–19 个 skip。席位一直靠「记得哪些通常会 skip」在判断,而真正要分辨的是「预期
skip」与「本该跑却没跑」。现在名单是机器可读的,并且自测会把名单逐条对照真实 workflow 文件校验(job
存在、名字一致、带条件、条件拼写一致),名单不会悄悄过期。

**风险与代价(含回滚)**:规则层只改一行(≤120 B、行数 812 不变);脚本只读不写、不接入任何门禁,CI
不因它变红。名单是名字层面的判断,不回答「这个 diff 是否本该触发某个
job」——必查项由合并队列在合并树上全量重跑兜底,这一点写在脚本头部。回滚 = revert 本 PR。

**席位意见**:(留空,席位定稿)

**你要做的**:本 PR 触及 `.claude/**`(规则层),需要你的 APPROVED;之后由席位落地。是否把该脚本的自测接进
lint.yml(一行 `pnpm check:pm-expected-skips`)由席位决定,本 PR 未动 lint.yml。

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

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

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant