Repository navigation
ci: publish the platform checklist status pages to the repository wiki on a schedule - #19162
Conversation
…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>
…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>
Seat disposition — the shape stays ONE PR, and this PR now awaits a HUMAN mergeSeat The question the dev handed up, answeredThe ⭐ 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 ⭐ 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
The fix, and why it is a fix rather than a silencingThe 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, ⛔ 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
⛔ This PR is HUMAN-MERGED, and that does not moveThe maintainer's Amendment 2 says so. ⭐ It rests on the seat token's missing ⇒ 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 mootPR #19161 merged at 2026-09-19T05:12:32Z; this branch carries it (base updated to Noted, ⛔ not filedThe dev reports that Generated by Claude Code |
…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>
…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>
… 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>
Fixes #19157 — the workflow half (scope item 7), and the last half of that card.
Clause-②: yes (widening)
claude/issue-19157-checklist-planned-status, and it is what addspnpm 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 theworkflowscope, 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:plusworkflow_dispatchrunspnpm 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 「平台真的功能清单,以及实现状态」:Platform-Checklist— a row per area, which is a row per definition item: area, active count, planned count, link;Checklist-AREApage per area listing every item asid · title · priority · status · personas, with theplanneditems 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:
STATUS.mdcommitted here — a third artifact to keep fresh, whose stale copy reads exactly as authoritative as a current one;check:checklist-statusgate pairing that page against the generator — explicitly ruled out: staleness here is tolerated, not gated, the same standing decision that keepscheck:platform-checklistout of per-PR CI;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
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.mdandChecklist-*.mdbefore 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:
run:block passesbash -n;schedule(weekly,37 3 * * 1, minute offset off the hour and off the sibling patrols),workflow_dispatch, and apull_requestpaths-filtered to this one file — so it blocks no unrelated pull request, the posture every patrol in this repo keeps. ⛔ Nomerge_group:, ⛔ nopull_request_target:;permissions: contents: writeand nothing else;concurrencykeeps one publisher at a time.⊘ NOT MEASURED: a green
workflow_dispatchrun. 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. Thepull_requesttrigger 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 manualworkflow_dispatchtakes the remaining reading: 16 pages on the wiki, index counts equal to the command output.skip-changeset: a workflow file publishes nothing from any released package.Generated by Claude Code