Skip to content

ci: publish the platform checklist status pages to the repository wiki on a schedule - #19162

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-19157-checklist-status-workflow
Sep 20, 2026
Merged

os-zhuang merged 4 commits into
mainfrom
claude/issue-19157-checklist-status-workflow

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #19157 — the workflow half (scope item 7), and the last half of that card.

Clause-②: yes (widening)

⚠️ Merge order. The ledger half is a separate draft on claude/issue-19157-checklist-planned-status, and it is what adds pnpm gen:checklist-status. Merging this file first leaves a scheduled job that fails until that PR lands — loudly, which is the right direction, but avoidable. Please land the ledger PR first.

⛔ This PR is human-merged, and not because anything here is governed: .github/workflows/** is not on the governed register. The seat token that opened it lacks the workflow scope, so it cannot land this file itself. That is why the workflow is alone in its own pull request instead of riding the ledger change.

What this does

A weekly schedule: plus workflow_dispatch runs pnpm gen:checklist-status, publishes its page set to the repository wiki, and mirrors the report into the job summary. The page set is the maintainer's 「一个可以人工阅读确认的入口」 for 「平台真的功能清单,以及实现状态」:

  • one index page Platform-Checklist — a row per area, which is a row per definition item: area, active count, planned count, link;
  • one Checklist-AREA page per area listing every item as id · title · priority · status · personas, with the planned items in their own section first.

Sixteen pages on the tree as it stands (index + 15 areas). The count is a property of the ledger and is asserted nowhere: the generator writes what it reads.

Why the wiki, and not a page in this tree

Three shapes were ruled out on the card before this one, and each rejection is load-bearing in the file's header:

  • a STATUS.md committed here — a third artifact to keep fresh, whose stale copy reads exactly as authoritative as a current one;
  • a check:checklist-status gate pairing that page against the generator — explicitly ruled out: staleness here is tolerated, not gated, the same standing decision that keeps check:platform-checklist out of per-PR CI;
  • a regen-on-edit rule on whoever touches an area JSON — a rule nobody can enforce is a rule that silently stops being followed.

What is left is a schedule. The wiki is one stable URL, outside branch protection and the merge queue, so publishing there costs no review and blocks no pull request. This job writes to the wiki only — never a commit to this repository, never an issue, never a label.

Two refusals rather than quiet passes

  • Fewer than two pages produced fails the run. An empty ledger and a generator that stopped reading it render the same zero, and the second must never be published as the platform's capability list.
  • An unclonable wiki fails naming the remedy, instead of skipping the publish. An uninitialised wiki cannot be cloned, and a job that quietly skipped would report success forever while nothing was ever published — absence must be loud.

A third guard sits last: a generate step that recorded no exit code at all fails the run, because nothing downstream could tell that apart from exit_code=0.

Page ownership is the generator's naming rule and nothing else — the publish step deletes Platform-Checklist.md and Checklist-*.md before copying, so an area dropped from the ledger does not leave its page standing, and a hand-written wiki page is never collateral.

Measured, and one thing deliberately NOT

The generator, its page set and the counts were measured in the sibling PR; what belongs to this file is its own shape:

  • the YAML parses, and every run: block passes bash -n;
  • triggers are exactly schedule (weekly, 37 3 * * 1, minute offset off the hour and off the sibling patrols), workflow_dispatch, and a pull_request paths-filtered to this one file — so it blocks no unrelated pull request, the posture every patrol in this repo keeps. ⛔ No merge_group:, ⛔ no pull_request_target:;
  • permissions: contents: write and nothing else; concurrency keeps one publisher at a time.

⊘ NOT MEASURED: a green workflow_dispatch run. A workflow can only be dispatched once it exists on the default branch, so this acceptance item cannot be taken before this PR merges — by construction, not by omission. The pull_request trigger above exists partly to close that gap early: a run on this PR exercises checkout, Node, pnpm, the generator and the summary rendering on a real runner, and publishes nothing. After merge, one manual workflow_dispatch takes the remaining reading: 16 pages on the wiki, index counts equal to the command output.

⚠️ One environment prerequisite the first run will surface: the repository wiki must already exist. If it has never been initialised, the publish step fails and names the remedy (open the Wiki tab, create any first page once); every run after that overwrites the generated pages.

skip-changeset: a workflow file publishes nothing from any released package.


Generated by Claude Code

…i on a schedule

A weekly `schedule:` plus `workflow_dispatch` runs `pnpm gen:checklist-status`
and publishes its page set to the repository wiki — one index
(`Platform-Checklist`, a row per area with its active and planned counts) and
one `Checklist-<area>` page per area, planned items in their own section first
— mirroring the report into the job summary.

The wiki rather than the tree, by ruling: a generated page committed here is a
third artifact to keep fresh whose stale copy reads exactly as authoritative as
a current one, and the paired `check:` that would have kept it honest was
ruled out too — staleness is tolerated, not gated. The wiki is one stable URL
outside branch protection and the merge queue, so publishing costs no review
and blocks no pull request.

Two refusals rather than quiet passes: fewer than two pages produced fails the
run (an empty ledger and a generator that stopped reading it render the same
zero), and an unclonable wiki fails naming the remedy instead of skipping the
publish. A `pull_request` run — paths-filtered to this file alone — proves the
generator on a real runner and writes nothing.

This file is alone in its pull request and lands by a human merge: the seat
token that opened it lacks the `workflow` scope.

Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
Co-authored-by: Claude <noreply@anthropic.com>
os-steve and others added 3 commits September 19, 2026 13:49
…mily

`scripts/pm/dispatch-gates.mjs` requires every PATHS-FILTERED workflow to
either discover a `check:` family or declare why it has none, and this one is
paths-filtered by its `pull_request:` trigger (filtered to itself, per the
maintainer's amendment). It ran neither: its only working step invokes
`pnpm gen:checklist-status`, a generator, so `extractCheckInvocations` found
nothing and `declaredNoCheckFamiliesReason` read null. One case of 1866 in
`dispatch-gates --self-test`, and it took the required `Lint & Repo Gates`
context with it.

⛔ The fix is the declaration the tool itself prescribes — the marker the
workflow CARRIES, read fresh on every run — and ⛔ not an exemption list in the
script, which is the second copy of a fact that this whole family exists to
retire. Three workflows in the tree already carry it in exactly this form.

The reason is not an apology: the paired `check:checklist-status` was
explicitly ruled out, staleness of this reading entry is tolerated rather than
gated, and publishing on a schedule is what that ruling left. ⛔ Nothing about
the schedule, the wiki publish or the job-summary mirror is touched.

⚠️ The declaration owns ONE line: a comment directly beneath it in the same
form is read as a CUT reason and refused by name, so the paragraph above it
says not to wrap it.

Measured on this head through the tool's own exported predicates: gaps for
this file [] (was ["checklist-status.yml"]), whole-tree gaps [] over 38
workflows of which 16 are paths-filtered, and the reason reads WHOLE.

Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
Co-authored-by: Claude <noreply@anthropic.com>
…laration beside it

The declaration added to `checklist-status.yml` is the SEVENTH in the tree, and
the live-marker census pins the roster by NAME with an exact match — so adding
one reds it. That is the pin working, and its own comment says why it is a
roster and not a count: "a bare count reddens for a seventh declaration without
saying which six were already read". The maintenance it forces is this commit.

⛔ The two halves are atomically coupled and cannot be split across PRs: the
declaration without this row reds the census, and this row without the
declaration reds it too (the census reads the live files). They land together
or `dispatch-gates --self-test` is red on `main` either way.

Also bumps the companion `liveMarkerCensus.length === 6` guard, which is the
count half of the same reading, and the prose that states it.

Measured on this head: `node scripts/pm/dispatch-gates.mjs --self-test`
EXIT=0, 1866 cases pass (it was EXIT=1, 2 of 1866, with the declaration alone).
The three rows this touches all read green: the paths-filtered guard at
"gaps: none", the census at seven rows, and "every live reason ENDS on its own
marker line (cut: none)".

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

Copy link
Copy Markdown
Collaborator Author

Seat disposition — the shape stays ONE PR, and this PR now awaits a HUMAN merge

Seat domain:spec#4. Head e56de0963252c1e77a7ae9089610c18bf093175e — read back at GET /commits/… → 200, 4 commits, append-only (26cfdf2f88… is an ancestor), mergeable_state: clean.

The question the dev handed up, answered

The # dispatch-gates: no-check-families -- REASON declaration this PR adds is the seventh in the tree, and the live-marker census pins that roster by name with an exact match — so adding one reds it, and the row must be added with it. ⇒ this PR is no longer .github/workflows/** alone; it also touches scripts/pm/dispatch-gates.mjs.

⭐ Ruling: it stays ONE PR. The two halves are atomically coupled — the declaration without the row reds the census, and the row without the declaration reds it too (the census reads live files) — so splitting them would leave --self-test RED on main in between. ⛔ A landing sequence that reds the default branch between two of its own steps is not a shape this seat will order.

⭐ And the census is behaving exactly as designed. Its own comment says the roster is named rather than counted precisely so that 「a bare count reddens for a seventh declaration without saying which six were already read」. ⇒ the maintenance it forced here is the pin working, ⛔ not friction to route around.

⛔ The path surface changed, so the tier was RE-DERIVED rather than carried over

node scripts/pm/check-governed-merges.mjs --pr 19162 → exit 0: 「0 of 2 path(s) hit the register (6 surfaces, repo-agnostic) · ✅ NOT governed」. ⇒ the tier did not move. ⛔ This is the register's own verdict on the FINAL file list, ⛔ not a reading carried forward from when this PR was one file — the register has grown several times in two days and a stale tier reading is recall, not measurement.

The fix, and why it is a fix rather than a silencing

The mechanism is the one the tool itself prescribes, in the exact one-line form three workflows in the tree already use. Measured through the tool's own exported predicates before the edit — trigger paths 1, extractCheckInvocations [], declaredNoCheckFamiliesReason null ⇒ gap — and after: gaps [] for the file, and [] whole-tree across 38 workflows / 16 paths-filtered.

⛔ Nothing suppressed, ⛔ no ignore list, ⛔ the guard not weakened. The workflow still parses to the same 3 triggers, the same cron, the same self-only paths filter and the same 7 steps — the schedule, the wiki publish and the job-summary mirror are untouched.

⭐ Three rows carry it, and the third is the one that matters: 「every live reason … ENDS on its own marker line (cut: none)」 — a cut refusal, so the one-line spelling is confirmed, ⛔ not assumed, with its lit control green beside it.

Readings, each with its reach

reading value
dispatch-gates --self-test on this head EXIT=0, 1866 pass, 0 failures
intermediate — the declaration alone EXIT=1, 2 of 1866 (the census plus its companion)
intermediate — the head before this work EXIT=1, 1 of 1866 (gaps: checklist-status.yml)
PR check runs, read by the seat at 2026-09-19T08:27:31Z 32 runs · 24 success · 8 skipped · 0 not-green; required Lint & Repo Gates success, and step 32 PM dispatch-gates self-test reads success
check-expected-skips --pr 19162 exit 0 — all 8 skips in the roster, each with a named reason

⚠️ Build Core, Temporal Conformance, Dogfood and the rest are skipped by the paths filter on this diff, ⛔ not failures — which is exactly why entry eligibility is 「every check green or an expected skip」 and ⛔ not the required subset.

⛔ This PR is HUMAN-MERGED, and that does not move

The maintainer's Amendment 2 says so. ⭐ It rests on the seat token's missing workflow scope, ⛔ not on the governed register — which has just re-confirmed this PR is not governed. ⚠️ And the seat measured that the token can perform a server-side update-branch on a branch carrying .github/workflows/** (HTTP 202 earlier on this PR). ⇒ ⛔ what the token can do is not a licence; the maintainer's word stands regardless.

⇒ the PR is flipped ready so a person can act on it, and ⛔ it is not enqueued and ⛔ not auto-merged by this seat.

Ordering — ⭐ discharged, and the note in the header is now moot

PR #19161 merged at 2026-09-19T05:12:32Z; this branch carries it (base updated to c229223e93, and scripts/gen-checklist-status.mjs plus the gen:checklist-status script are on main — measured, ⛔ not assumed). ⇒ the workflow header's ordering warning is moot but left untouched: the narrative is the seat's, and the dispatch's scope was the one gap.

Noted, ⛔ not filed

The dev reports that pgrep -f with a name pattern matched its own wrapper shells and another session's wait loop carrying the same pattern, so three waits read 「STILL RUNNING」 for a self-test that had already written its verdict. AGENTS.md §8 already prescribes recording the PID and ⛔ never matching a name. ⇒ ⛔ no new finding — the existing rule biting, and a name-matched kill would have hit another agent.


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 19, 2026 08:28
Merged via the queue into main with commit e6a03e6 Sep 20, 2026
34 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-19157-checklist-status-workflow branch September 20, 2026 09:08
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…d of sweeping them (objectstack-ai#19220)

Fixes objectstack-ai#17797

The deliverable is a **classifier**, not a sweep. Three kinds of
sentence live under one
phrase and take different repairs, and only one of them is a defect.

## ⭐ The card's own headline number had decayed — which is the card's
best self-evidence

The card states that `scripts/pm/dispatch-gates.mjs` "alone carries
**40** sentences
containing `measured on this tree`". Re-measured at `0046a41b4`,
2026-09-20T00:30Z, with
the card's own case-sensitive probe: **14**, of which **3** carry a
revision by the card's
own rev probe. A card about bare magnitudes decaying in silence had its
own bare magnitude
decay by 65% before anyone read it. ⛔ The `40` is not carried anywhere
in this PR.

## 1. The declared population — which files, which phrase forms

All readings below are mine, taken at `0046a41b4` (this branch's merge
base),
2026-09-20T00:30Z. ⚠️ Every one of them decays; the tool added here
re-derives them.

| probe | occurrences | files |
|---|---:|---:|
| the card's probe — `measured on this tree`, case-sensitive, over
`scripts/` | 46 | 25 |
| all three spellings that actually occur, case-insensitive — `measured
on this tree`, `re-measured on this tree`, `measured against this tree`
| 121 | 47 |
| the same, excluding the two files scoped out below | 74 | 45 |

⚠️ **That is an upper bound on a population, ⛔ not a count of defects.**
The card says so
in its own words and this PR does not contradict it.

Three findings about the population itself, each of which says a phrase
probe is not a
population:

- **The card's probe is case-folded shut.** Lower-case-only reaches 46
of the 121 sentences
  that carry the phrase in some spelling. The gap is not rounding.
- **A line-based probe cannot see a sentence that wraps.** The phrase
itself straddles a
  line break in five files, so no `grep` of it lists them at all:
`scripts/check-examples-live-imports.mjs`,
`scripts/check-parse-guard.mjs`,
  `scripts/check-self-test-wired.mjs`, `scripts/eslint-fatal-guard.mjs`,
`scripts/i18n-bundle-surface.mjs`. The tool reads sentences, so it holds
them.
- **⭐ And the card's own named instance carries no phrase at all.** It
is a bare corpus
size in the present tense. A population defined by the phrase would have
reported the
tree clean at the one site everybody already agreed was broken — so the
population is
declared in two forms, FORM A (the phrase) and FORM B (declared sites
the phrase cannot
reach, currently the hand-written-corpus size in
`scripts/docs-audit/README.md`).

## 2. The classifier

`scripts/pm/measurement-claim-triage.mjs`:

```
node scripts/pm/measurement-claim-triage.mjs              the population + every verdict
node scripts/pm/measurement-claim-triage.mjs --review     the kind-3 residue alone
node scripts/pm/measurement-claim-triage.mjs --self-test  the controls, both directions
```

It records **no** number about its own population — the scope line it
prints is the number,
re-derived on the tree it runs against. That is the durable half of the
fix: the reason the
card's `40` went stale is that it was written down instead of
re-derivable.

Four tests, applied in that order. Each can only move a hit **out** of
the residue, and each
prints the cue that moved it, so every verdict is auditable against the
sentence.

- **T1 FRAME** — is the magnitude scoped to a moment that has passed, or
to a counterfactual
condition? (`before this gate was written`, `at the commit that took
it`, `an earlier
revision`, `with the reservation removed`, `was auditing`) ⇒ **KIND 1**.
- **T2 ANCHOR** — does the sentence carry a revision or a date? ⇒ **KIND
1**.
⛔ A bare card number is deliberately **not** an anchor: `#NNNN` occurs
in every kind of
sentence here, and admitting it would clear the residue by matching
everything.
- **T3 LOUDNESS** — would the drift announce itself? Three ways: a named
failure direction
(a zero whose falsifier is spelled out is the commonest), a pointer at
the instrument that
reprints the value, or a live assertion that reds on drift. ⇒ **KIND
2**.
- **T0 MAGNITUDE** — last, and weakest: nothing decays if nothing is
counted. A ZERO counts,
  because kind 2 is defined on one.
- Everything else is **REVIEW**, a *candidate* bucket. ⛔ Reading its
size as a finding count
  is the error the card exists to prevent.

**T4 — level or relation — is a judgement and no regex decides it.**
Does a reader act on
the magnitude's **level**, or on a relation (a ratio, an ordering, an
existence claim, a set
equality) that the level's drift survives? A level is kind 3; a relation
is an
ILLUSTRATION, and rewriting it buys nothing. T4 is recorded per site in
the tool's `TRIAGE`
table with its reason, and a REVIEW hit with no row prints UNTRIAGED and
reds the self-test.

⛔ **Every `TRIAGE` row is keyed by an excerpt, never by a line number**
— the card cited its
own known instance at `:493` and it was at `:555` by the time anyone
read it.

### The window, and a bug worth recording

The first build read a fixed ±6-line block and **cleared** the one site
everybody had agreed
was broken: six lines above it an unrelated sentence cites a revision,
and a block window
handed that anchor to a claim that has none. T2 and T3 now read the
**sentence**; T1 also
sees the sentence before (a narrative frame leads), T3 also the sentence
after (a named
failure direction trails — the card's own kind-2 exemplar spells the
zero in one sentence
and names its falsifier in the next); ⛔ T2 sees neither. That control is
pinned in the
self-test.

## 3. ⭐ Controls, both directions

`node scripts/pm/measurement-claim-triage.mjs --self-test` → **green**,
six controls:

| control | site | expects |
|---|---|---|
| kind 1 — a citation carrying its revision, **left alone** |
`scripts/check-tier-file-adoption.mjs` ("Measured on this tree at
`d03c3c96d6` …") | `KIND-1` |
| kind 2 — a zero whose failure direction is named, **left alone** |
`scripts/docs-audit/affected-docs.mjs` ("zero commands declare `static
topic` …" + "The failure direction if that ever changes …") | `KIND-2` |
| kind 2 — a refusal a live assertion holds, **left alone** |
`scripts/check-skill-compatibility-version.mjs` ("the refusal is pinned
in the self-test …") | `KIND-2` |
| **kind 3 — the card's named instance, CAUGHT** |
`scripts/docs-audit/README.md` (pre-repair text) | `REVIEW` |
| kind 3 repaired — the same site now points at the gate |
`scripts/docs-audit/README.md` (post-repair text) | `KIND-2` |
| ⛔ the one-sentence anchor rule — a neighbour's revision does **not**
clear a bare magnitude | synthetic | `REVIEW` |

Each control carries a liveness key that must still be present in the
file it was quoted
from (and the pre-repair one carries an `absent` key instead — if that
sentence comes back,
so has the defect). ⛔ A control quoting a sentence the tree no longer
holds passes in
silence, which is the failure this whole PR is about. The self-test also
reds on a **dead
cue**: every cue must still match something in the population or the
controls — nine that
matched nothing were deleted rather than left as decoration.

## 4. The kind-3 hits and their repairs

Five, judged out of a pre-repair review residue of 27. After the repairs
the tool reports
`87 claim(s) over 48 file(s) — KIND-1 35 · KIND-2 11 · NO-FIGURE 19 ·
REVIEW 22`, and all 22
survivors are recorded ILLUSTRATION. ⛔ No fresh bare number is written
anywhere in this diff.

**Repaired by pointing at the live instrument** (objectstack-ai#16200's preferred
shape):

- `scripts/docs-audit/README.md` — "the anchor derivation reads the same
**178**-page corpus
the old one did". Present tense, and **wrong today**: `pnpm
check:docs-audit-scope` printed
`195 hand-written doc(s)` at 2026-09-20T00:11:30Z, my own reading.
Repaired to name the
corpus without sizing it and to send the reader to that gate, and it
says out loud that
  the figure is **deliberately absent**.

**Repaired by pinning the reading to the commit that recorded it** — the
historical figure
does work a live reading cannot (it justifies a decision taken at that
moment) and no gate
reprints it. Each carries a deliberately-absent note, so the next author
does not helpfully
restore a present-tense figure:

- `scripts/check-pnpm-filter-targets.mjs` — the `scripts/**`
declaration-honesty ratio, now
`Measured at 52a41b7 (2026-08-23)`. The counts move with every file
added under
  `scripts/`; the SHARE is what the paragraph argues.
- `scripts/check-published-files.mjs` — the same shape over the whole
publishable workspace,
  now `Measured at 52a41b7 (2026-08-23)`.
- `scripts/check-skill-compatibility-version.mjs` — the four precision
ratios deciding which
roots are declared, now `measured at f29e897 (2026-08-22)`, with a
pointer to
`scripts/pm/bare-root-worklist.mjs`, which carries this gate's
package-root rows with
  their own dates.
- `scripts/check-slot-lookup-ratchet.mjs` — `46s`, the worst-ageing kind
of figure: a
wall-clock reading is a reading of one **box** as much as of one tree,
so it decays without
the tree moving at all. Now `Measured at 46s when this was recorded
(99ca662,
2026-08-19)`, with the order (seconds against a CI round) left as the
load-bearing claim.

Each repair's excerpt is recorded `REPAIRED` in `TRIAGE`, and the
self-test reds if it
returns to the population.

### ⛔ What the classifier deliberately did NOT touch

The 22 remaining REVIEW hits are recorded `ILLUSTRATION` with a reason
each. Two worth naming,
because they are the shape a sweep would have destroyed:

- `scripts/docs-audit/README.md` lines carrying `178` in the historical
narrative — the card
predicted these were kind 1 and judging each one agrees. Two more, at
the `--all` backstop
and the cost note, already point at `check-audit-scope.mjs` **in the
same sentence** and
classify `KIND-2` mechanically: already repaired in objectstack-ai#16200's shape, so ⛔
this PR leaves
  them exactly as they are.
- `scripts/check-dispatcher-error-vocabulary.mjs`'s per-glob member
counts: kind 2, not
kind 3 — the docblock names what would falsify them and
`PUBLISHED_SOURCE_FACE_FLOOR`
  plus the per-glob presence pin red in that direction.

## ⛔ Two files are outside the file surface, for two independent reasons

**`scripts/pm/dispatch-gates.mjs` and
`scripts/pm/check-widening-tells.mjs` are not edited
here**, and both reasons are carried as data in the tool's `EXCLUDED`
table so every run
prints them rather than leaving them in prose:

1. **Live conflict.** `dispatch-gates.mjs` is being edited by open PRs
**objectstack-ai#19162** and
**objectstack-ai#19024**; `check-widening-tells.mjs` by **objectstack-ai#19153** and **objectstack-ai#19024**.
Editing underneath
   them is a conflict, not duplicated work.
2. **The card's own verification constraint**:
`scripts/pm/dispatch-gates.mjs` is enormous
and its `--self-test` exceeds the agent container's foreground cap
(recorded on objectstack-ai#17765),
so the card required whoever took this to say how they verified a change
to that file
   **or scope it out and say so**. This is the saying-so.

⇒ The residue those two hold is **un-swept**, and the tool's scope line
says so on every run
instead of reading as complete.

Also untouched, and reported rather than edited: `.claude/**`,
`skills/**`, `docs/adr/**`,
`AGENTS.md`, `CLAUDE.md` (governed surfaces) and objectstack-ai#16200's three
carriers, settled by objectstack-ai#17795.

## Verification

Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`
at `60f9b648a` — 6 paths, 722 changed lines. **35 derived families, 35
run, all exit 0**;
reconciled with `--ran`: `35 derived famil(ies) accounted for — 35 run,
0 UNRUN`.

- `node scripts/pm/measurement-claim-triage.mjs --self-test` → `✓
controls hold in BOTH
  directions (6 controls, 27 recorded judgements)`.
- `pnpm check:pm-dispatch-gates` → `✓ dispatch-gates self-test: 1866
cases pass`,
  **593.2s on this box**, `VERDICT command-exit 0`, run detached under
`scripts/pm/os-verify-lock.sh` (held 594s, waited 0s),
2026-09-20T00:23:16Z–00:33:10Z.
⚠️ The reference line at `content/docs/qa/platform-readings.md:432`
records 430–450s; this
container reads well above that band, and this run is one more point in
it.
- ESLint, narrowed and the narrowing measured: `npx eslint
--no-inline-config --format json`
over the five changed `.mjs` files at `60f9b648a` → **5 files linted, 0
errors, 0
warnings** (counts read from the JSON reporter). The narrowing excludes
nothing:
`eslint.config.mjs` enables type-aware linting for **no** configuration
in this repo (no
`parserOptions.project`, no typed rules), so this diff cannot move the
verdict on a file
  it does not touch. The repo-wide `eslint .` run is CI's.
- ⊘ **NOT MEASURED** — `pnpm check:published-readme-exports` and `pnpm
check:dts-closure`
both exited **3, PREREQUISITE NOT MET** (46 packages' `dist/` not
built). Neither is in
the derived family; both are artifact-roster families whose roster sits
under `scripts/`,
run here only because silence there is not evidence. This diff changes
no package source,
so neither could be moved by it. ⛔ Recorded as not measured, not as a
pass.
- No changeset: measured, not assumed. The root manifest is `private:
true` and no package's
`files[]` names the repo-root `scripts/` tree, so nothing published
moves. Labelled
  `skip-changeset`.

## Acceptance notes (out of scope, noted and not filed)

- `scripts/pm/dispatch-gates.mjs` holds 42 of the 121 phrase occurrences
(all spellings) and
is un-swept here for the two reasons above. **Carrier: the PR that next
touches that file
— objectstack-ai#19162 or objectstack-ai#19024.** Worth a card of its own once they land; the tool
classifies it the
  moment its `EXCLUDED` row is removed.
- `scripts/pm/check-widening-tells.mjs` holds 5, same disposition.
**Carrier: objectstack-ai#19153 /
  objectstack-ai#19024.**
- The tool is **not CI-wired**: a `package.json` script and a workflow
step both sit outside
this card's file surface, so `--self-test` is run by hand and its
controls watch nothing on
their own. The docblock says so out loud. ⛔ Do not read a green run here
as CI coverage.
**Carrier: none today** — it needs a seat that owns the root manifest
and the lint
workflow. Raised as an open question in the report rather than filed,
because wiring it is
  a decision about CI cost, not a defect.

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

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…gineCanRollBack, not the engine-only probe (objectstack-ai#18997) (objectstack-ai#19286)

Fixes objectstack-ai#18997

Clause-②: no

`getDiscovery()` derived `capabilities.transactionalBatch` from the
ENGINE alone — `typeof this.engine?.transaction === 'function'` — while
`runAtomicBatch` refuses `batchData({ atomic: true })` with `501
NOT_IMPLEMENTED` on `engineCanRollBack(this.engine)`, which asks the
DEFAULT DRIVER as well. `engine.transaction` is a function on every real
engine, so `/discovery` answered `true` for exactly the compositions
that then 501 — and the 501's own remedy text sends the caller to that
bit ("probe `capabilities.transactionalBatch` on /discovery first"). The
prescribed remedy routed the caller to a signal that was wrong in the
only case the remedy exists for.

One line at the read site: the advertisement now asks the predicate the
refusal already trusts.

## What a consumer that sees `true` today will see after this lands

`/discovery` is a published surface, so this is stated rather than
assumed.

Two compositions stop advertising `true` and begin advertising `false`.
Both already answered `501 NOT_IMPLEMENTED` to an atomic batch before
this change, so **nothing that was accepted becomes refused** — only a
lying value becomes honest:

- **(a) a default driver with no `beginTransaction` at all** —
pre-existing on `main`, not introduced by objectstack-ai#18063;
- **(b) a default driver that inherits `beginTransaction` and declares
`supports.transactionsUnsupported`** — the population objectstack-ai#18063 added. The
shipped example is `TursoDriver` on its remote transport
(`packages/drivers/driver-turso/src/turso-driver.ts`,
`transactionsUnsupported: this.transportMode === 'remote'`).

A client that read `true` and proceeded was taking the 501; it now reads
`false` and takes its non-atomic fallback ahead of the failure, which is
what probing the capability was for. A client that hard-asserts
`transactionalBatch === true` at startup against such a composition now
fails at startup instead of at its first atomic batch. The REST
producer's own AND with `api.enableBatch`
(`packages/rest/src/rest-server.ts`) is unchanged and inherits the
corrected input.

Everything else is byte-identical: the key, its type, its `description`
and its place in the closed capability vocabulary do not move.

## Clause-② — measured, not argued

The claim comment grades this `no` on the ground that it narrows. The
falsifier was to be measured, not assumed: if any composition advertised
`true` where `origin/main` advertises `false`, that would be a widening.

Enumerated over all 16 compositions of the four inputs the two
predicates can read (engine has `transaction`; driver registry
inspectable; driver has `beginTransaction`; driver declares
`transactionsUnsupported`), comparing `main`'s derivation with this one:

```
compositions: 16 | widened (false -> true): 0 | narrowed (true -> false): 3
VERDICT: NO WIDENING — accept set untouched, advertisement narrows only
```

The 3 narrowed rows are population (a) x2 (driver has no
`beginTransaction`, with and without the declaration) and population (b)
x1. `Clause-②: no` holds.

## Both directions, or the card is not delivered

"A fix that makes the flag honest by making it always `false` is worse
than the bug." So the TRUE arm is pinned beside the FALSE arm, and each
composition is driven through `batchData({ atomic: true })` in the same
test — the advertisement and the refusal have to agree composition by
composition.


`packages/metadata-protocol/src/protocol.discovery-transactional-batch-honesty.test.ts`
(6 cases):

- a default driver that CAN roll back: still `true`, and the atomic
batch commits;
- a host whose driver registry is not inspectable (test doubles,
metadata-only hosts): still `true` — the engine-level probe is all there
is, which is what `engineCanRollBack` documents;
- population (a): `false`, and the 501 arrives with the remedy sentence
this card is about, having attempted no write;
- population (b): `false`, same refusal;
- no `transaction()` at all: `false` (unchanged);
- and the rule itself — the advertised bit equals `engineCanRollBack()`
for every composition, so a future composition that satisfies one end
and not the other reds here even if nobody adds a case for it.

**Against the shipped driver**, measured on the built `dist/` after the
fix, not inferred:

```
real MemoryDriver: typeof beginTransaction = function | supports.transactionsUnsupported = undefined | driverSupportsTransactions = true
/discovery capabilities.transactionalBatch = {"enabled":true}
```

The default dev composition keeps its `true`.

## Evidence

- **Pre-fix reproduction.** The pin file was written first and run
unmodified against unmodified `protocol.ts`: `Tests 3 failed | 3 passed
(6)` — the two population cases and the predicate-agreement case red,
the TRUE-arm controls already green. Post-fix: `Test Files 1 passed (1)
· Tests 6 passed (6)`.
- **Ablation** through `scripts/ablation-replace.mjs`, from the
committed state, with on-disk proof: anchor `1 -> 0`, blob `0b04d1ff5d3a
-> 7c63e7631f05`, then `3 failed | 3 passed`; restore verified `blob ==
HEAD (0b04d1f)` and `git diff HEAD` empty. No `dist` preflight is
owed: the pin imports `./protocol.js`, so the mutation is on the
resolution path directly.
- **Package suite** `pnpm --filter @objectstack/metadata-protocol test`:
`Test Files 181 passed | 3 skipped (184) · Tests 2591 passed | 19
skipped (2610)`. `typecheck` exit 0, and `tsc --noEmit --listFiles`
names the new pin file among the 750 files in the program, so the type
check really covers it.
- **Cross-package consumer**
`packages/objectql/src/protocol-discovery.test.ts` against the rebuilt
`metadata-protocol` dist: `32 passed (32)` — including the
real-`ObjectQL`-engine pin that must keep advertising `true`.
- **Gates**, derived from the actual diff with `node
scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` and reconciled with `--ran`: 59 derived, **56 run, all exit
0**, 3 NOT MEASURED. The 3 exited `3` (PREREQUISITE NOT MET, nothing
measured) because they read a whole-workspace `dist/` this checkout does
not carry — `check:dual-build-cjs-loads`, `check:lean-entry-closure`,
`check:type-check-debt`; CI's `Build Core` supplies the prerequisite
they refuse without. `check:plugin-teardown-shape --self-test` also
exited 3 at first (its fixture commit is unreachable in a shallow clone)
and exits 0 after fetching that commit.
- **`pnpm lint`** (repo-wide `eslint . --no-inline-config`): exit 0.

All readings are from `objectstack-ai/objectstack` at this branch's head
commit.

## Acceptance notes

- `publishPackageDrafts` in the same file keeps an engine-only probe
(`typeof this.engine?.transaction === 'function'`) for its own `inTxn`
helper. Noted, not filed, and deliberately not changed here: it
advertises nothing to a caller, and both of its branches deliver the
same guarantee today — `engine.transaction()` takes its declared
non-transactional path (ADR-0119 D1) for exactly the drivers the probe
would have excluded, so swapping the predicate there is a no-op in
behaviour. It would become a real question only for whoever makes
draft-publish atomicity a promise rather than a best effort.
- The runtime dispatcher's hardcoded `transactionalBatch: { enabled:
false }` (`packages/runtime/src/http-dispatcher.ts`) is correct and
untouched: that face mounts no `/batch` route.
- Scope held to `packages/metadata-protocol/src/` plus the changeset.
`packages/spec` is not touched — the spec half of objectstack-ai#18063 is landed and
correct.
- `origin/main` moved two commits while this was in flight (`objectstack-ai#19162`
checklist-status workflow, `objectstack-ai#19242` hono-server). Neither touches this
package; the `dispatch-gates.mjs` change in that range is to its own
self-test roster and derives the same 59 families for this diff. Not
merged down; the merge queue rebuilds on current `main`.

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

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… so a deferred table stops deriving as a scan surface (objectstack-ai#19308)

Fixes objectstack-ai#19260

Clause-②: no

`EXCLUSION_DECL_NAME` in `scripts/pm/dispatch-gates.mjs` listed no
`DEFERRED`, while
`scripts/check-issue-citations.mjs` declares its exclusion table under
exactly that word —
`DEFERRED_SURFACES` / `DEFERRED_GLOBS` — and applies it as an exclusion:
`surfaceFor` opens by
returning `null` for every deferred glob. So the derivation read an
**exclusion** table as a
**scan** surface, and a `.changeset/**` path was told it triggers a gate
that looks at nothing
there. That derivation is the one PR objectstack-ai#19259 (card objectstack-ai#18224) trips on, and
the assertion it reds —
「a changeset path alone reaches NO value-bearing family any more」 — is
correct and is untouched
here.

One `DEFERRED` branch in the predicate; the measured census in the
docblock above it re-measured
and rewritten in the same stroke; one self-test case beside the existing
named-spelling cases.

⛔ Nothing is renamed in `check-issue-citations.mjs` —
`DEFERRED_SURFACES` and `DEFERRED_GLOBS`
are exported and pinned by that gate's own self-test, and this docblock
says the fix belongs on
the derivation side: 「an author's next spelling should be met by this
predicate rather than by a
rediscovery of this card」.

## Acceptance notes

### Firing pair, both directions, on PR objectstack-ai#19259's head

The script derives from the tree it LIVES in — `ROOT` comes from
`import.meta.url`, and
`trackedFiles()` reads that root — so a `cwd` does **not** redirect it
at another tree. The fixed
file was therefore copied into a detached worktree of PR objectstack-ai#19259's head
(`6d1272b3850e5c0e756745dc6486d20aa330653d`, blob `5d78406d6` before,
`8216826a5` while mutated)
and run from there; the probe was restored with `git checkout HEAD --
scripts/pm/dispatch-gates.mjs`
back to blob `5d78406d6`, `git diff HEAD` empty.
`.changeset/test-abc.md` did not need to exist.

| probe tree `6d1272b385` | `--commands` lines | `pnpm
check:issue-citations` rows |
|---|---|---|
| subject `.changeset/test-abc.md`, before | 19 | **1** |
| subject `.changeset/test-abc.md`, after | 18 | **0** |
| control `packages/types/src/index.ts`, before | 44 | **1** |
| control `packages/types/src/index.ts`, after | 44 | **1** |

The subject's two command lists differ on exactly one line — `diff`
reports hunk `13d12`, deleting
the `pnpm check:issue-citations` line and nothing else —
the control's two lists are byte-identical. Command: `node
scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack PATH`, exit 0 on all four runs.

### The census: the method it never stated, and what it now reads

The block named its method only in prose and named no command, so the
method is now spelled out in
the block itself: enumerate `topLevelDecls` over every tracked JS/TS
file under `scripts/`, keep the
non-callable declarations, test each name against the predicate, and
price the arm by diffing
`extractWatchHints` against a build of this module whose predicate
matches nothing. Attribution is
the regex engine's own — leftmost position first, then alternation order
(the old text said
"first-match attribution" without saying which of the two, and they
disagree on a multi-word name
such as `EXCLUDED_SKIP`).

Measured over the objectstack-ai/objectstack tree at `e6a03e6491`, with
the export-visibility of
six internal bindings as the only difference from the shipped file (a
throwaway copy, never
committed):

| reading | docblock before | re-measured, no `DEFERRED` | re-measured,
with `DEFERRED` |
|---|---|---|---|
| tracked JS/TS files under `scripts/` | 230 | 287 | 287 |
| top-level VALUE declarations | 3,513 | 4,697 | 4,697 |
| identifiers matching the predicate | 64 | 73 | **75** |
| of those, carrying a string literal | 58 | 65 | **66** |
| hints the set moves | 14 | 16 | **24** |
| files it moves them on | 6 | 7 | **8** |
| change no derivation / really leave one | 8 / 6 | — | **13 / 11** |

Per-word tally, same ordering as the block: `SKIP 54, EXCLUDED 10,
EXCLUSION 4, NOISE 2, SKIPPED 2,
DEFERRED 2, EXCLUSIONS 1, and EXCLUDE / EXCLUDES / IGNORE / IGNORED 0` —
four zero-scoring arms now,
not three, because `EXCLUDES` went 1 → 0 on its own. ⚠️ **Every one of
those numbers was already
stale before this card**: the middle column is the same tree with the
OLD predicate, so the drift
from `230 / 3,513 / 64 / 58 / 14` is the tree moving, not the arm.
Nothing reds when it does, which
is why the block now carries the tree ref it was read on.

`DEFERRED` matches exactly two declarations on this tree, both in
`check-issue-citations.mjs`, and
both are that gate's own exclusion table. It moves 8 hints off that
gate, 5 of which cost nothing —
the test globs sit under the gate's own `packages/**` inclusion, so
`hintCovers` still reaches a
test path — and **3 of which were fabricated leads**: `.changeset/**`
(the one the card measured),
plus `scripts/**` and `docs/adr/**`, which nobody had named. The gate
stays reachable through
`packages/**`, `packages/**/src/**/*.ts` and its siblings, which is what
keeps the control lit.

Re-verified after the merge of `origin/main` `e6a03e6491`: every figure
above reproduces, and the
edit adds no top-level VALUE declaration to this file (112 before, 112
after), so the 4,697 holds
for the delivered tree.

### Gates — every exit code captured before any pipe

| command | exit | verdict line |
|---|---|---|
| `node scripts/pm/dispatch-gates.mjs --self-test` (this branch) | 0 |
`✓ dispatch-gates self-test: 1867 cases pass.` |
| `node scripts/pm/dispatch-gates.mjs --self-test` (pristine
`e6a03e6491`) | 0 | `✓ dispatch-gates self-test: 1866 cases pass.` |
| `pnpm check:pm-dispatch-gates` (detached, under the shared lock) | 0 |
`check:pm-dispatch-gates: the battery took 1012.6s on this box.` |
| `pnpm lint` (repo-wide, `eslint . --no-inline-config`) | 0 | no output
|
| the other 27 derived families | 0 each | listed below |

Case count 1866 → 1867: the one new case is the `DEFERRED` entry in the
named-spelling loop beside
the existing exclusion-vocabulary cases, ⛔ not at the tail of
`selfTest()` — the tail regions that
PR objectstack-ai#19162 (`:22173`–`:22203`, now landed as `e6a03e6`) and PR objectstack-ai#19024
(`:23763`) touch are untouched
here, and the merge of `origin/main` carrying objectstack-ai#19162 was clean. Inside
the battery the case
「⭐ a changeset path alone reaches NO value-bearing family any more」 —
the one red on PR objectstack-ai#19259's
head — now reads green.

Families derived with `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack`
(28, recomputed post-merge and identical to the pre-merge list) and
reconciled with `--ran`:
`28 derived famil(ies) accounted for — 28 run, 0 NOT-MEASURED (a DERIVED
zero — all 28 recorded an
exit code and none of them is 3)`. The 27 besides the battery:
`check-ci-filter-parity`, `check-closing-keyword-parity` (+ self-test),
`check-comment-mask-corpus`,
`check-declaration-mirrors` (+ self-test),
`check-scripts-symbol-anchors` (+ self-test),
`check-self-test-wired` (+ self-test),
`check-self-test-workflow-commands` (+ self-test),
`check-whole-set-label-write` (+ self-test),
`check:agent-test-spelling`, `check:bash32-floor`,
`check:cli-command-ids`, `check:cross-package-test-inputs`,
`check:declared-population-live`,
`check:driver-memory-census`, `check:entry-guard`, `check:nul-bytes`,
`check:parse-guard`,
`check:pnpm-filter-targets`, `check:ratchet-remedy-authority`,
`check:refd-timer-probe`,
`check:watch-hint-literal` — all exit 0.

Control-byte self-scan beyond the gate, exit captured before any pipe:
`grep -naP` over the changed file for the C0 range plus DEL exits 1 —
none present.

### Line budget and shape

`scripts/pm/dispatch-gates.mjs` 28,345 → 28,371 lines against
`origin/main` `e6a03e6491`:
**net +26** (+64 / −38), inside the +40 the dispatch set. Measured
against the 28,337 the dispatch
quoted at `7d0f911`/`7ec8534`, the file reads +34, of which +8 are PR
objectstack-ai#19162's own. `git diff --stat`
shows one file.

No changeset: `scripts/` ships in no published package's `files[]`
(checked across every non-private
manifest in the tree), and `scripts/pm/**` is on the non-publishing fast
track. ⚠️ So `Check Changeset`
needs the `skip-changeset` label, and this dispatch forbids label writes
to the dev — **that one write
is the seat's**, not left undone by accident.

### Noted, not filed

**The census in this docblock was already stale before this card, and
nothing reds when it goes
stale.** The middle column of the census table above is the same tree
read with the OLD predicate:
`230 → 287` files, `3,513 → 4,697` declarations, `64 → 73` matches, `14
→ 16` moved hints, and the
tally's `EXCLUDES 1 → 0`. The sibling gate keeps its own census as code
with a `measuredOn` ref
(`CENSUS_17512` in `check-issue-citations.mjs`, marked 「⛔ Readings, not
a budget」); this one is
prose in a comment, so it rots silently. Not filed — it is an
observation about a missing guard,
not a reproducible defect, a broken declared contract or an authoring
trap. 承接者: the standing
queue on this same file (objectstack-ai#19070 → objectstack-ai#19104 → objectstack-ai#19105 → objectstack-ai#19106 → objectstack-ai#19172),
any of which reads this block.
The mitigation this PR does ship is the tree ref and the spelled-out
method, so the next reader can
tell a stale number from a current one.

## 维护者速读(草稿)

**改了什么** — 派单工具 `dispatch-gates` 里那张「哪些常量名代表『这个门禁不看这里』」的词表,补上了
`DEFERRED` 这个拼写。顺带把该处注释里那份实测普查重新测了一遍并改写,因为它早已过期。

**为什么改** — `check-issue-citations` 这个门禁用 `DEFERRED_SURFACES`
声明它**故意不看**的路径。
词表不认这个词,于是工具把「不看的清单」读成了「要看的清单」,反过来告诉开发者:改一个 changeset
文件会触发这个门禁 —— 而该门禁对 changeset 路径其实什么都不做。后果不止是一条假线索:它还让 PR objectstack-ai#19259
在 `lint.yml` 第 32 步整条中止,objectstack-ai#18224 自己新加的两步从未执行,卡住了一张 p2。实测这条修法还顺手消掉
另外两条没人发现的假线索(`scripts/**` 与 `docs/adr/**`)。

**风险与代价(含回滚)** — 风险很低:改动是一个正则分支加注释,只影响派单提示里「你该跑哪些门禁」这份
清单,不影响任何门禁自身的判定,也不改任何对外发布的包。方向上只会**少给**一条线索、不会多给,而这一侧
的失效代价是「一张卡多跑一轮 CI」,比反方向「每张卡都被塞一条假线索」便宜得多 —— 这个不对称是该文件自己
写下的判据。回滚 = revert 这一个提交,单文件、无生成物、无迁移。

**席位意见**

**你要做的** — ① 这是 draft PR,按席位流程补 `## Contract review` 记录后再走 ready +
auto-merge;
② `skip-changeset` 标签需要席位来打(本单禁止开发侧写标签),否则 `Check Changeset` 会红;
③ 落地后 PR objectstack-ai#19259 / 卡 objectstack-ai#18224 即可重跑,它那条断言本身是对的、本 PR 未动。

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

---------

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

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

Projects

None yet

3 participants