Skip to content

fix(pm): H44's timestamp leg accepts an ISO instant whose hour follows a T — the ruled minute spelling no longer reads as untimestamped - #18326

Merged
claude[bot] merged 1 commit into
mainfrom
claude/issue-18323-h44-timestamp-regex
Sep 15, 2026
Merged

claude[bot] merged 1 commit into
mainfrom
claude/issue-18323-h44-timestamp-regex

Conversation

@claude

@claude claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #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.

// 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 — #18324 and #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 #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


Generated by Claude Code

…s a T

`H44_READING_TIMESTAMP` anchored the hour with a bare word boundary, which
needs a NON-word character in front of it. The `T` of an ISO instant is a word
character, so `2026-09-15T14:51Z` read false — and so did `2026-03T00:21Z`, the
spelling the 2026-09-02 ruling itself gives. A reading that DID carry its
取数时刻 in the ruled minute form was reported 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.

The left anchor now names one further predecessor — the ISO `T`, looked BEHIND
rather than consumed, so a match is still exactly the stamp. Nothing else is
admitted: the boundary is kept (a clock inside a longer digit run is still not
a stamp), no other letter opens one, and only the uppercase `T` the ruling
spells. Strictly more paragraphs read clean, so the row can only get quieter.

Self-test 4235 -> 4243 cases: the two ISO spellings that read false, a pin on
where the seconds match STARTS (the fix, not the accident), three declared
negatives, and the H44 marker case re-pinned to MINUTES with the seconds
spelling kept beside it — the comment saying that limit existed is rewritten,
because the limit is gone.

Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr
Co-authored-by: Claude <noreply@anthropic.com>
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 87fa4c8932fe02ea96707612900f245fe05e4cdd

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 #18323, claim 5685351787, report 5685737302 (posted through the tool; read back clean); 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 (+50/−6, the only file) against base a46cd8c4; nothing published; skip-changeset read back (size/s + skip-changeset). Non-governed ⇒ in-seat landing.
  • The change, read whole on a detached worktree of the head: ONE regex line — H44_READING_TIMESTAMP's left anchor becomes an alternation of the word boundary and a non-consuming look-behind for the uppercase ISO T (:9666), with a 26-line comment stating the defect, the one admitted predecessor and the three declared negatives; eight cases added (the ISO minute form, the ruling's own spelling 2026-03T00:21Z, the match now STARTING at the hour, a digit run, any other letter, the lowercase t, a zone-less ISO instant, the seconds spelling of the marker) and the marker case re-pinned to the ruled MINUTE spelling with its comment rewritten rather than deleted. The seat's probes on both trees agree with the report byte for byte: main reads false / false / true[51:42Z] / true / false / false, the head true[14:51Z] / true[00:21Z] / true[14:51:42Z] / true / false / false, lowercase t and x14:51Z false on both. Self-test 4235 / main → 4243 / head; ESLint 0/0; check-self-test-wired OK; --pair 18326 0; the battery on the seat's worktree — see the provenance for its exit.
  • Gates 41 / 41 / 0 / 0 by --ran on the head; the battery 483 s, 1730 pass (dev). Reverse verification: the ablation reverted the one regex line with every case kept — 4 of 4243 red by name (the three new grammar cases and the re-pinned marker case, which IS the false positive the card is about), restored blob 5bcfa9e7 = HEAD, git diff HEAD empty. The look-behind's own hazard (a regex fragment mangled by the write APIs) was handled as instructed: the literal is named by line, never pasted.

② Semver level

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

③ Boundary flags

open_questions: none. One seat premise refuted, owned here: 「adjust the floor to the measured count」 — this file carries NO case-count floor (the two cases.length sites are the verdict printer), so there was nothing to adjust; the dev measured it instead of inventing one. Five deviations answered: the design spelled twice before commit (a consuming T branch failed the match-start case; the look-behind landed) — accepted, the transcript carries both; no floor — accepted; ESLint run repo-wide, not narrowed — accepted; the PR body carries two footers (the platform appended its block on a rule-less footer — register :347, not a defect, not re-sent) — accepted; measure-self-test-floor.mjs read once and found refusing on main — accepted. Out of scope: one card FILED by the seat, #18329 (the census refusal; not this lane's by SUBJECT, left bare for triage); noted, not filed: range-less clock shapes (1234-56:78Z) read as stamps — quieter only; the double-footer reading is already register :347 / :409, so no card. 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. #18324 / #18325 (same file, other regions) fold next; whichever PR lands second merges origin/main and re-runs the self-test before its ACCEPT.

Implemented-by: claude/issue-18323-h44-timestamp-regex
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-15T18:37Z. Non-governed landing (scripts/pm/check-half-states.mjs only): review of record 5685923189 PASS on head 87fa4c8932fe02ea96707612900f245fe05e4cdd, ACCEPT 5685923716 on #18323; every check on that head read green at 2026-09-15T18:35Z (26 success / 11 skipped / running none / failed none); the seat's own battery on a detached worktree of the head: 1730 cases pass, 462 s, exit 0. The seat flipped the PR ready through POST …/pulls/18326/ccr/ready_for_review and armed auto-merge SQUASH through PUT …/pulls/18326/ccr/auto_merge (echo {"enabled":true,"merge_method":"squash"} — the echo is not the reading); the ready_for_review event reads 2026-09-15T18:35Z and the added_to_merge_queue event one second later, both by claude[bot]; the queue ref refs/heads/gh-readonly-queue/main/pr-18326-a46cd8c4… read 1 twenty seconds after the enqueue (a lone entry, chained on a46cd8c4). Landing is confirmed by the three readings (merged_at + git log origin/main carrying (#18326) single-parent + the queue ref gone) at the next wake; then #18323's residue is stripped and the landing record posted. #18324 / #18325 (same file, other regions) fold behind PR #18322's landing and this one; the fold's dev merges origin/main before its ACCEPT.


Generated by Claude Code

Merged via the queue into main with commit de9c319 Sep 15, 2026
39 checks passed
@claude
claude Bot deleted the claude/issue-18323-h44-timestamp-regex branch September 15, 2026 18:56
os-zhuang pushed a commit that referenced this pull request Sep 15, 2026
Takes in PR #18326 (the H44_READING_TIMESTAMP region and its cases), which
landed on main while this branch was in flight. Different regions of the same
file; no conflict.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…he newest page — H44 reaches a vacant or Routine seat, H32 / H38 take their marker from the last page (objectstack-ai#18332)

Two defects in one file, `scripts/pm/check-half-states.mjs`, both of the
same shape: a seat
leg reading a seat post through the wrong population or the wrong window
while the file
already declares the right one. Each is fixed by pointing the leg at
that declaration.

## 1 — H44's seat leg could not reach the triage post it was written for

`h44NeedsSeatComments` was `pm:seat` + `seatIsHeld`, and `seatIsHeld`
excludes a
`🟢 Routine` holder by name and every non-🟢 status. The triage seat runs
as a Routine or
sits vacant most of the time — it read `[PM seat] triage
(objectstack-wide) — 🔴 空缺 …`
throughout this work — so H44, H56 and H64 read NOTHING on the one post
H44's own header
names as its worked example (「carrier of the round-open marker that is
this card's worked
example」), while H65 read it every run through `h65IsTriageSeatPost`,
which carries no
held gate.

The gate now admits the triage post through that same predicate,
**beside** the held leg
rather than instead of it, so the file keeps ONE reading of 「is this the
triage post」.

**Measured cost: zero additional requests.** H65 already buys exactly
that page every run
through the shared `seatPostRowsFor` memo, so the widening only moves
which row asks first.

**⛔ No vacant-seat posture is owed, and that is measured rather than
assumed.** All three
rows accuse a COMMENT and name its author; none names the post's current
holder, so
「nobody is on the clock」 changes nothing about whether an artefact on
the thread states a
reading with no time on it. On the vacant triage post's newest page (18
comments,
2026-09-10T23:26:55Z → 2026-09-15T15:54:40Z) H44 files one carrier row
naming 10 offending
comments, H56 files 0 and H64 files 0 — confirmed offline before the
change and reproduced
exactly by the live run after it.

### The design choice, on the four axes

Option A was to widen the gate to the triage post; option B was to keep
the held gate and
rewrite the header to say the seat leg reaches only HELD posts. **A, on
all four axes** —
the dispatching seat's reading is confirmed, not refuted.

- **实际业务需求** — measured, not inferred: the triage post carries 10
comments H44's own
grammar flags, 0 of them visible to the row today, on the busiest
artefact producer on
the board (818 comments, 18 of them in the last five days). B leaves a
report-only row
  structurally unable to observe its own subject, which is dead letter.
- **项目长远合理性** — the file already declares both halves this needs
(`h65IsTriageSeatPost`
for the population, `seatPostRowsFor` for the window). A hands the leg
those declarations;
B is the workaround shape, correcting prose to match behaviour nobody
wants.
- **防 AI 写代码犯错** — these three rows are exactly the ones that catch
AI-written artefacts
stating a reading with no time on it or naming no session, and the
triage post is where AI
seats write most. Declaring a purpose in the header that the runtime
does not honour is the
anti-pattern this axis names. The held gate is an argument about
ACCUSING A NAMED HOLDER of
idleness — H32's subject, not this row's; inheriting it was the category
error.
- **创业阶段不扩散需求** — not a capability expansion: no new row, no new rule,
no new request,
one new finding on the live board. It makes an existing declared row
reach the population
its own header already claims, in the narrow spelling already in the
file. 声明而未兑现是
实现缺口,补实现或退役 — the implementation is supplied. Immediate, no staged
window.

## 2 — H32 and H38 read a seat post's OLDEST comment page

The `seatMarkers` gather captured `commentRowsFor`'s page-less FIRST
page, and GitHub serves
issue comments oldest-first, so `latestSeatMarker` returned the newest
row of page 1. That row
is H32's idle clock and carries H32's wait exemption, and it is H38's
`T_seat`.

Measured on the live board:

| seat post | comments | page-1 newest | real newest | stale by |
|---|---|---|---|---|
| objectstack-ai#6023 | 225 | 2026-08-27T16:38:35Z | 2026-09-14T11:41:59Z | 18 days |
| objectstack-ai#6367 | 137 | 2026-09-04T07:56:39Z | 2026-09-10T17:55:24Z | 6 days |
| objectstack-ai#7623 | 63 | 2026-09-15T09:00:39Z | 2026-09-15T09:00:39Z | single
page, identical |

On objectstack-ai#6023 the wait exemption was being judged on an August comment. The
gather now takes the
window through `seatPostRowsFor`. It buys nothing new — this population
is a **subset** of
H44's seat leg, the memo is shared, and asking here only makes the
gather loop the first
asker; the page-less fetch it replaces was itself wasted, because H44's
leg overwrote that
cache entry a pass below. `latestSeatMarker`'s contract is unchanged,
and a null page is left
out of `seatMarkers` exactly as the throw was.

**⚠️ H38's live verdicts do not move, and the reason is pinned rather
than left to be
discovered:** `seatPostLastEventMs` maxes the comment stamps against the
post's own
`updated_at`, and GitHub bumps that field whenever a comment is added,
so the post's own
field already dominates. The page choice decides `T_seat` exactly where
`updated_at` cannot
— a comment EDITED after the post's last bump, which is the one 「seat
writing home」 signal
that lives only in the rows. Two self-test cases pin both halves of
that.

## Reverse verification

**Ablation, per card, from the committed state** (mutation proven on
disk by blob hash before
each reading; restored with `git checkout HEAD -- <path>`, restore
proven by an empty
`git diff HEAD` and a blob hash equal to HEAD's):

- **Card 1** — revert the predicate leg, keep its cases ⇒ **3 cases red
by name**:
`H44 gate: ⭐ a VACANT triage post IS fetched — the held leg alone made
this row dead letter on it`,
`H44 gate: ⭐ …and a 🟢 Routine triage post too, which seatIsHeld excludes
BY NAME`,
`H65 scope: ⭐ H44's seat leg now REACHES the Routine triage post,
through this very predicate`.
- **Card 2** — revert the capture to the page-less read, keep its cases
⇒ **1 case red by name**:
`seat window: …and taken through the memo by more than one row, which is
what makes it shared`
(got 2, want 3). ⚠️ Stated precisely: that source-text pin is the ONLY
case that can observe
this, and the file says why in `seatWindowAudit`'s own header — the
window lives inside
`sweepInto`, which takes no injectable transport, so the property a spy
would prove has to be
proved on the text. The other 16 cases of this card pin the CONTRACT
(which marker and which
`T_seat` each window yields) at function level and stay green under the
ablation by
construction. That is the designed instrument, not a gap discovered
late.

**`seatWindowAudit`, before → after:** `pagelessPaths` 1 → 1 ·
`commentPaths` 4 → 4 ·
`purchaseSites` 1 → 1 · `readers` **2 → 3**. Only the reader count
moves, and its pin is
rewritten in place with the superseded statement quoted above it.

**Live patrol, `origin/main` `de9c319a` vs this head, both exit 0**,
same corpus both runs
(408 open pm-/p0-labeled, 507 unscoped, 13 open PRs, 683 recently-merged
PRs):

| family | origin/main | this head |
|---|---|---|
| H32 | 0 | 0 |
| H38 | 1 | 1 |
| H44 | 29 | **30** |
| H56 | 6 | 6 |
| H64 | 10 | 10 |
| H65 | 1 | 1 |

Total half-states 476 → 477. **The one row that moved is `H44 objectstack-ai#6015`** —
the vacant triage
post — naming comment `5626818520` as the oldest of 10 offending
comments on it, which is
the offline prediction reproduced exactly. No row was lost. Coverage
clause: the seat leg
reads `4 of 4` on `origin/main` and `5 of 5` here; H44's corpus 1488
comments across 398
threads → 1506 across 399, the 18 added being exactly the triage post's
newest page; H32's
`3 of 3` and H65's `1 of 1` are unchanged.

⚠️ A third commit was needed for that coverage clause: the live run
printed
`5 of 5 HELD seat post(s)` while one of the five was the vacant triage
post. Both readings
are legal English, so only a live run could catch it. The clause now
names both legs; the
pin that asserted the HELD wording is rewritten in place, not deleted.

**Self-test:** 4235 on the old base → 4245 after card 1 → 4261 after
card 2 → 4269 after
taking `main` in (PR objectstack-ai#18326's 8 cases) → **4270** after the
coverage-clause pin. Three pins
were rewritten in place rather than deleted, each with its superseded
statement quoted above
it: the H65 scope case that asserted H44's gate reads `false` on a
Routine triage post, the
audit's reader count, and the H44 coverage clause's HELD wording.

## Gates

All 41 commands derived by `node scripts/pm/dispatch-gates.mjs
--commands scripts/pm/check-half-states.mjs`
were run at this head and reconciled with `--ran`; the battery `pnpm
check:pm-dispatch-gates`
is among them. ESLint on the file is clean, and the file carries no
control bytes.

## Acceptance notes

Out of scope, noted and not filed: the seat-post window is now correct
for every reader, but
the property is only observable from source text, because the window
lives inside `sweepInto`
which takes no injectable transport — a spy-able seam would let a case
assert the gather's
window directly instead of counting call sites. Successor: whoever next
edits `sweepInto`'s
transport; nobody is queued on it today, and inventing the seam under
these two cards would
have widened the surface well past them.

Non-governed (`scripts/pm/**`) — left DRAFT for the owning seat to land
in-seat after review.
No reviewers requested, no auto-merge armed, no ccr route touched.
`skip-changeset`: nothing
published moves — `scripts/pm/**` is not in any package's `files[]`.

Fixes objectstack-ai#18324
Fixes objectstack-ai#18325

---
_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/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant