Repository navigation
feat(pm): two report-only patrol rows — the decision card's four-facet block, and finding outliving its grade - #18033
Conversation
…acet block The `os-decision-facets` marker is declared machine-findable by references/decision-analysis.md 〈四棱卡面块固定形状〉 and, measured on origin/main, is read by nothing: one hit in the whole tree, its own declaration, against a positive control (needs-user-decision reaches seven files under scripts/). This adds the first reader. H62 is report-only: an OPEN card carrying the decision label whose BODY carries no marker, in either spelling. Presence is a literal containment test over the raw body, which is the extraction rule the shape names for itself (「提取按字面 grep」); code-stripping and a positional test are both refused by name because each produces 「注释形读不到永不读作无四棱块」, the one direction of error the shape forbids. The prior-ruling line the shape puts after the four facets is read as a SECOND reading on the same row — present / unresolved / absent — and never as a second row. Its writer is check-prior-rulings.mjs, coupled here by source text rather than by import: both import directions were measured and neither runs (static is a TDZ cycle, dynamic deadlocks against this module's own top-level await). Population is the unscoped open listing, for H31's reason; the row costs no request and writes nothing. H4 / H13 / H14 / H22 / H29 are byte-unchanged. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
…ire the sweep's self-test in CI The state model makes `finding` transient — 「观察类记录,恒 = 待首次定级;定级即 离标」 — and the population has been re-measured and hand-cleared three times (385 of 395, then 185 of 226, then 38 by hand, then the one-time sweep to 0 on both boards). It refills because the strip has to be passed by the caller on every grading write. The write-path half is its own card; this is the read half: H63 reports an open card carrying the marker beside a grading state or any `priority:*`, so the refill is visible on the patrol instead of being re-measured by whoever next looks. The rule is REUSED, not copied — and the import can only point one way, which was measured rather than assumed: the sweep already imports `PM_STATE_LABELS` from the patrol and derives module-level constants from it, so the patrol importing the sweep throws `ReferenceError: Cannot access 'PM_STATE_LABELS' before initialization` the moment the patrol is the entry point, which it is on every run. So the screen moves down into the module that owns the vocabulary, and the sweep re-exports it under the names its callers and its 91-case self-test already use — unchanged, and pinned by source text from the patrol's own self-test so a copy cannot re-grow. The row does not contradict H13 (its set carries the marker, which is why H13 is silent on a graded carrier), H22 (open-only) or H29 (a card KIND, not a position); the docblock names each. CI wiring the D1 report left unfired: `check:pm-stale-finding` in package.json and a self-test step in lint.yml. `check-self-test-wired` population 208 → 209, green both directions — the tool shipped a self-test nothing ran. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
Wiring `check:pm-stale-finding` into package.json makes this tool a DECLARING family for `dispatch-gates`, and `check:declared-population-live` reds on it: the module-level run fixture's repo slug is the only path-shaped literal in the file, so the derivation read it as a declared population of one path that names no tracked file — the same output a gate with no literals produces. Both halves of the gate's own remedy: the `no-path-population` marker, which is true of this tool (its whole input is the GitHub API), and the fixture moved into the self-test body, where the module-body mask reaches it. Top-level VALUE declarations are deliberately never masked — being unreferenced at module scope is what a declared population IS — so the comment on the moved fixture says ⛔ do not hoist it. `check:declared-population-live`: 250 declaring families with 1 unreachable → 249, all reaching. The sweep's 91-case self-test is unchanged. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
|
CI red, being worked — skills seat (session Generated by Claude Code |
CI red at `Lint & Repo Gates` step 32 (run 34759389219): `dispatch-gates self-test: 1 of 1682 case(s) failed` — 「every gate CI runs whose own source sweeps the repo root is DECLARED whole-tree, declared path-less, derivable by path, or a justified row in ROOT_WALK_RESIDUE_LEDGER — listed but no longer a member: check:pm-half-states」. Root cause, measured: this PR's self-test reads two sibling sources as program text — `sweep-stale-finding.mjs`, to pin that it still ALIASES the one stale-`finding` screen rather than re-growing a copy, and `check-prior-rulings. mjs`, to pin that the prior-ruling line's writer still prints the key the patrol greps. The derivation reads those literals and now places the family BY PATH (`dispatch-gates scripts/pm/sweep-stale-finding.mjs` ⇢ 「program text read by scripts/pm/check-half-states.mjs」), so it left the residue population — and the ledger reds on a listed family that is no longer a member, by its own contract: a stale exclusion is an exclusion nobody measures. The repair is the ledger's, ⛔ not the self-test's: the row goes, with a note saying why the family left and why this placement is the OPPOSITE of the #15753 one the row was written about — that one came from a noise-floor constant and said the opposite of what the constant declares, while these two literals are exactly what the self-test reads. A card touching either sibling now really does owe this gate. `dispatch-gates --self-test`: 1682 of 1682 pass, exit 0. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
…trol-rows-facets-and-stale-finding
Contract reviewHead: ① derived judgments — two report-only patrol rows and the CI wiring the D1 left unfired:
Seat measurements on the head tree ( ② semver: nothing published; ③ boundary flags: Implemented-by: Verdict: PASS — both D2 halves as claimed (5653347020, 5653347348): two report-only rows, one rule for the stale- Generated by Claude Code |
… the live board is re-read every run (objectstack-ai#18054) Fixes objectstack-ai#18045 Maintainer ruling, verbatim and untranslated (skills seat chat, 2026-09-13T14:58Z): 「现在就派发处理 备份缺口」 ## Premises, all four checked against `origin/main` c185d08 before any edit - **P1 holds.** `selectWalkPlan` reaches `incremental` only once `history` completes, so while the backfill runs no walk ever carries a recent `since`. Confirmed on `board-archive` tip `b7c5f578`: `walk_phase: history`, `next_since: null`, `resume.stopped_by: budget`, history cursor `2026-08-03T11:51:21Z`, `board.read_at: null`, `count_check: pending`. - **P2 holds.** 5232 records under `board/issues/`, highest number **17460**; 18010, 18020 and 18025 are absent. Read with `git ls-tree` over a shallow fetch of the branch into a private ref — the shared checkout was never switched. - **P3 holds.** 16 open pull requests, none touching `scripts/pm/board-snapshot.mjs`. (objectstack-ai#18033 touches `scripts/pm/check-half-states.mjs`, which this file imports from; no overlap in this diff.) - **P4 holds.** `.github/workflows/board-snapshot.yml` is unchanged — same cron, same 800-request default, one `GITHUB_TOKEN`, no retry loop. ## The fix, in `scripts/pm/board-snapshot.mjs` alone A bounded **delta** walk now runs first on every run once the open set is complete: `state=all&sort=updated&direction=asc&since=...`, archiving every row it sees — open or closed, issue or pull request. The history backfill then continues from its own cursor with the remaining budget, its resume semantics untouched. A spent slice is **not** a run stop; only the run budget and a rate-limit refusal stop a run, exactly as before. **The budget split: 300 of 800, and 500 to the backfill.** A quiet six-hour window moves a few dozen numbers, so a steady-state delta costs one listing page plus a comment read per changed number and the backfill keeps very nearly the whole budget. A catch-up delta buys around 250 numbers a run, closing a multi-day gap in a day of scheduled runs rather than in the weeks the backfill needs to reach the same rows. In the worst case the backfill still gets 500 — five eighths of its former pace, slowed but never starved. An unbounded delta could starve it for good, which is why `DELTA_REQUEST_SLICE` exists at all. **New manifest keys.** `walk.delta = { complete, cursor, since, slice }` sits beside `walk.history` and is never merged into it. `next_since` becomes the delta's high-water mark instead of the backfill's February cursor. `resume.phase` now names the WALK that stopped (`open` / `delta` / `history` / `incremental`) rather than the phase. `run` gains `delta_ran`, `delta_completed_here` and `delta_requests` — in `run`, which `materialManifest` strips, so a per-run counter cannot commit a manifest-only diff every run. The board's own open count is bought right after the delta and before the backfill, so `count_check` stops being permanently `pending` in the history phase. ## One declared deviation from the card's fix shape The card spells the delta anchor as "the previous manifest's `generated_at`, minus a small skew". Implemented instead as: the delta's own stored cursor; else `walk.open_set.completed_at` minus the skew; else `next_since`; else `generated_at` minus the skew. On the measured manifest `generated_at` is 2026-09-13T14:22Z while the board was last enumerated in full at 2026-09-10T15:39Z, so anchoring on the stamp would have declared a window this tool never walked and skipped the exact three-day gap the card was filed about. The skew is 30 minutes, two runs' worth of the workflow's own 15-minute job timeout. ## Verification - `node scripts/pm/board-snapshot.mjs --self-test`: **89 cases across 8 batteries before, 125 across 10 after**, exit 0 both. Two new batteries; the count-check battery's floor moves 15 to 17 and the roster floor 8 to 10. - The self-test gains an **offline harness** that drives `snapshot()` end to end against an injected `fetch` — no network, no token, restored in a `finally`. The card assumed one existed; it did not, so the walk order and the budget split had no instrument at all. - **Ablation.** Deleting the 25-line delta step turns **10 of 125** cases red, THE REGRESSION among them (blob `f3723530` to `e0cdbd7b`, restored to `f3723530`, `git diff HEAD` empty). A first attempt stayed **green**: over a four-row board the backfill reaches "today" inside one page, so the row landed on disk whether or not a delta ran. The harness now carries a 900-row closed backlog, so the backfill provably cannot reach that day inside 800 requests — that fix is its own commit. - `origin/main` (226970b) is merged in; `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives the same **33** commands on the merged head as before it. Every one runs in the foreground with `$?` captured before any pipe, and `--ran` reconciles the list; the verdicts are in the `os-dev-report` comment on the card. - `skip-changeset`: the root package is private and no published package's `files[]` carries `scripts/`, so this diff publishes nothing. --- _Generated by [Claude Code](https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #17009
Fixes #16904
Both cards' D2 halves. #17009's D1 landed as
ea1da645(PR #17515) and #16904's asae1b2588(PR #17994, plus the seat's one-time sweep, readings 5652947318); these two rows are the last deliverable on each card, so merging this closes both.Two report-only rows in
scripts/pm/check-half-states.mjs, one per card, plus the CI wiring #16904's D1 report left unfired. No--writepath anywhere, no label written, no gate added.H62 (#17009) — a decision card whose face carries no four-facet block
An open
needs-user-decisioncard whose body carries noos-decision-facetsmarker, in either spelling (plain-text line or the HTML-comment form). Presence is a literal containment test over the raw body, which is the extraction rule the shape names for itself (「提取按字面 grep」); code-stripping and a positional read are refused by name in the docblock because each produces 「注释形读不到永不读作无四棱块」, the one direction of error the shape forbids. Body only: 「落卡即带」 makes the block part of the card face, so a marker in a comment is not the shape, and the row buys no thread.The
Prior rulings read:line is a second reading on the same row —present/unresolved/absent, in the row text, never a second row. Its writer ischeck-prior-rulings.mjs, coupled here by source text: both import directions were measured and neither runs (static is a TDZ cycle; dynamic deadlocks against this module's own top-level await). The docblock records both measurements so nobody retries them.H63 (#16904) — a graded card still wearing
findingAn open card carrying
findingbeside a grading state or anypriority:*. The rule is reused by import, never copied — but the arrow is forced, not chosen: the sweep already importsPM_STATE_LABELSfrom the patrol and derives module-level constants from it, so the patrol importing the sweep throwsReferenceError: Cannot access 'PM_STATE_LABELS' before initializationthe moment the patrol is the entry point, which it is on every run and every self-test. So the screen moved down into the module that owns the vocabulary, andsweep-stale-finding.mjsre-exports it under the names its callers and its 91-case self-test already use. The patrol's own self-test pins by source text that the sweep still aliases that definition and has not re-grown a copy.The row does not contradict the pins it sits beside, and the docblock names each: H13's set carries
finding(which is exactly why H13 is silent on a graded carrier — the card is visible, to the wrong reader); H22 drops it and this row is open-only; H29 drops it as a card KIND rather than a position, and this row explicitly does not claim otherwise.Wiring:
check:pm-stale-findinginpackage.jsonand a self-test step inlint.yml.check-self-test-wiredpopulation 208 → 209, green in both directions — the tool shipped a 91-case self-test that nothing in CI ran.Acceptance greps, both directions
H62 (#17009)/H63 (#16904)incheck-half-states.mjsDecision-box dependency flags (instruction ④)os-decision-facetsreader incheck-half-states.mjscheck:pm-stale-findinginpackage.jsoncheck:pm-stale-findinginlint.ymlcheck-half-states --self-testcasessweep-stale-finding --self-testcasescheck-self-test-wiredscripts CI runscheck:declared-population-livedeclaring families with an unreachable populationUntouched, proved by md5 of the whole exported function body — H4
1d13e919…, H13fa79039c…, H141e88cafd…, H22a45048e0…, H29aaa6ea92…: identical before and after. The instruction-④ section,.claude/**and the write-path half offindingstripping are untouched.Live readings, read-only, taken from this tree
H63: the landed sweep's own dry run against this board — 33 open cards carry
finding, 0 stale, 33 genuinely ungraded, 0 unjudged; 1 listing, 0 writes,rate_limitcore 14747 → 14746. Zero is the expected reading right after the seat's one-time sweep (19 stripped here, 36 on objectui), and it is what the row exists to watch refill.H62: the decision inbox is 1 open card today, not the 27 the D1 measured — it fires on that one, second reading⚠️ No live specimen carries the marker, so the clean direction has no lit control on today's board; its controls are the self-test's fixtures, in both spellings.
absent.Gates
Derived on the merged head
1d52dd97withdispatch-gates --commands --repo objectstack-ai/objectstack(no paths): 74 families, 74 run, every one exit 0. Reconciled with--rancarrying a recorded exit code per family: 「74 derived famil(ies) accounted for — 74 run, 0 NOT-MEASURED (a DERIVED zero — all 74 recorded an exit code and none of them is 3)」.Five build-reading families first answered
PREREQUISITE NOT MET(exit 3) in a fresh worktree — recorded as NOT MEASURED, never as findings — and all five exit 0 afterpnpm buildunder the shared verify lock (VERDICT command-exit 0 · held 391s · waited 0s, 73/73 tasks). One of the five,check:type-check-debt, refused a second time on a heap ceiling my ownNODE_OPTIONSimposed (its own line says so: 「tsc runs under --max-old-space-size=4096 MB — the caller's NODE_OPTIONS, which is tighter」) and exits 0 at the CI-shaped 6144.pnpm lintrun repo-wide rather than narrowed:eslint --no-inline-config . --format jsonexit 0, 6723 files linted, 0 with findings — no narrowing claimed. Control-character sweep over every touched file: no match.origin/mainmoved again after this merge; the derivation says the tree is 1 commit behind on 3 files it derives from. The merge queue rebuilds and re-runs on that generation, which is where that half is measured.Deviations, declared
scripts/pm/sweep-stale-finding.mjsis edited to hand the screen down, because ruling ② mandates reuse by import and the only import direction that runs points the other way (measured; the crash is reproduced in the docblock). Its public API, its behaviour and its 91-case self-test are unchanged.package.jsonmakes it a declaring family fordispatch-gates, andcheck:declared-population-livereds on the module-level fixture's repo slug. Fixed by the gate's own remedy: theno-path-populationmarker (true — the tool's whole input is the GitHub API) plus moving the fixture into the self-test body, where the module-body mask reaches it.dispatch-gates --self-testthen red on one case of 1682 (CI run 34759389219,Lint & Repo Gatesstep 32): 「listed but no longer a member: check:pm-half-states」. The patrol's two sibling source-text pins make the family placeable by path (dispatch-gates scripts/pm/sweep-stale-finding.mjs⇢ 「program text read by scripts/pm/check-half-states.mjs」), so it left the root-walk residue population and itsROOT_WALK_RESIDUE_LEDGERrow went stale — which that ledger reds on by its own contract. The row is removed with a note; the placement is true and is the opposite of theextractWatchHintsreads a noise-floor EXCLUSION constant as a watch hint:.changesetderivescheck:pm-half-statesonto every changeset-adding card #15753 one the row was written about.origin/mainmerged (1d52dd97) because the derivation warned the tree was stale on a file it derives from — and it was load-bearing: the mergedcheck-clause2-carriers.mjsaddscheck:pm-clause2-carriersto this card's family list (73 → 74).skip-changeset:scripts/pm/**, a rootpackage.jsonscript and a workflow publish nothing from any released package.Authored in Claude Code session
session_01DAcomhvR9kKizeYgg89Vo8.Generated by Claude Code