feat(pm): mechanise the prior-ruling search for decision cards - #17515
Conversation
Mechanises the governing-text search the director charter already prescribes (`references/lanes/director.md:45`): given a decision card, derive terms from its title and its `Governing text:` carrier, search the ADR decision corpus, `AGENTS.md` and the `packages/spec/src` docblocks on `origin/main`, and print the candidate rulings plus the exact line a seat pastes into the four-facet block. Report-only, decides nothing, writes nothing. Exit codes follow the `check-half-states.mjs` convention, with 3 (PREREQUISITE NOT MET) printing `unresolved` rather than the `none` that would record a search that never ran. The board resolver and the proxy re-exec plan are imported from `check-half-states.mjs` rather than copied. Claude-Session: https://claude.ai/code/session_01YKEjmbYNvYWJvWGSWx26zK Co-authored-by: Claude <noreply@anthropic.com>
Adds the fixed `Prior rulings read:` line to the four-facet block and wires `check:pm-prior-rulings` into package.json and the lint workflow's self-test step, beside the sibling report-only gates. The file is at its 50-line ceiling, so the new line is paid for by deleting a restatement rather than by raising the ceiling: the removed bullet re-stated the four-axis writing rule that the 正文六项 item 「四轴从业务立场论证」 already carries verbatim, and 「每卡 = 六项写法 + 四棱块」 already binds the six items to every card. Claude-Session: https://claude.ai/code/session_01YKEjmbYNvYWJvWGSWx26zK Co-authored-by: Claude <noreply@anthropic.com>
…ior-ruling-search
Verification — the merged-head re-run landed (the body's pending line, resolved)Posted as a comment rather than edited into the body on purpose: a bare REST
Every one of the 69 exited 0 on the merged head. No family exited 1, 2 or 3. Clause-② declaration checked against the real diff rather than trusted: Control-character sweep over the diff's four files, beyond what the gate does:
One standing caveat, stated rather than chased
This PR stays DRAFT. ⛔ No seat flips it ready, enqueues it, or arms auto-merge. Generated by Claude Code |
Review reading on the density payment: the deleted bullet carried one half the six items did not — that the four-facet BLOCK is itself bound by the writing rules, where the 正文六项 item binds the body's four-axis prose and 「每卡 = 六项写法 + 四棱块」 only says a card has both parts. Deleting it lost that binding. So the payment becomes a FOLD rather than a deletion: the six-item rule now reads 「四轴从业务立场论证(含四棱行)」 and carries the binding, which makes the removed bullet a true duplicate. The maintainer's suggested parenthetical measured 129 bytes against the ratchet's 120-byte per-line budget; this spelling is 117. File stays at 50 lines, ceiling 50 — nothing raised. Claude-Session: https://claude.ai/code/session_01YKEjmbYNvYWJvWGSWx26zK Co-authored-by: Claude <noreply@anthropic.com>
维护者速读改了什么 —— 新脚本 为什么改 —— 同类错误四天两次:一张决策卡带着「推翻 ADR-0131 已裁事项」的建议摆到您面前,因为填卡与呈报都漏了那条 ADR。#17009 D1 把「先搜既有裁决」从文字规则变成机器动作;用出事那张卡(#16934)实测,ADR-0131 D8 在 270 个候选里排第一。 风险与代价(含回滚) —— 只读、只报告,不写卡、不打标签、不判断;CI 只跑它的自测。回滚 = 删脚本、删 席位意见 —— 通过。本席在契约复审档复核两轮:自测 99 例本席在 head 上跑过;活钻取 你要做的 —— 一个动作:人工合并本 draft PR。 skills 席,session Generated by Claude Code |
Part of #17009 — deliverable D1 only (the reader plus the one fixed line). D2, the half-state row beside the facet block, is serial on the
check-half-states.mjschain and is not in this PR, so the card stays open. #17009 is not closed here.The class, and the two failures this mechanises
A decision card reached batch presentation carrying a recommendation to re-rule something an accepted ADR already decides. The governing-text step failed at both points where it applies:
Governing text:line named ADR-0105, ADR-0021 D-C, two code symbols and a maintainer ruling, and did not name ADR-0131, whose D8 answers the question verbatim (「nothing undersingle… No layer has a NULL arm」);re-checkcommands and one targeted grep, not the charter's step,references/lanes/director.md:45: 「呈报前逐卡逐仓重跑git grep -n -iE 'TERMS' origin/main -- AGENTS.md docs/adr packages/spec/src」.The same class had already been answered once with TEXT (the
Governing text:requirement and that grep line). This PR is the mechanical half: the charter's grep, run by a script instead of by a human who is about to present five cards.A card's own
re-checkcommands cannot catch this class. They are authored by the filer to re-verify the filer's PREMISES, so they confirm the code still looks as described and say nothing about whether the QUESTION is still open. A premise pass is not a prior-ruling pass.The reader's contract
scripts/pm/check-prior-rulings.mjs— report-only. It has no write path to GitHub in any mode, and that is asserted structurally in its own self-test rather than promised in prose.Inputs. A card number, read once over REST from
PM_SWEEP_REPO; or--terms, which replaces the derivation entirely (a seat passing terms has read the card and is narrowing on purpose). Terms come from the card TITLE plus its governing-text carrier when one exists, and every term is printed with its provenance.Corpus, read at
origin/mainwith the tip printed, because a seat's checkout can be behind and a stale answer is wrong in exactly the direction that matters — it cannot see the ruling that just landed:docs/adr/**decision unitsAGENTS.mdpackages/spec/src/**docblocksExit codes, deliberately identical to
check-half-states.mjs:0— the read completed, 0 candidates or 400 alike (report-only).1—--self-testfailed.3— PREREQUISITE NOT MET. PrintsPrior rulings read: unresolved, nevernone.noneis a finding;unresolvedis the absence of one, and a seat pastingnonebecause a token was missing would record a search that never ran.2— could not complete for a reason the file cannot classify.⛔ No retry loop on
403/429, on any path: the fleet runs on one shared identity, so a retry spends somebody else's quota to learn the same thing twice. That is asserted structurally in the self-test too.Output ends with the exact line a seat pastes into the four-facet block.
The one fixed line
references/decision-analysis.md「## 四棱卡面块固定形状」 gains one bullet (the literal is in the file; angle-bracket placeholders are spelled out here so the body survives storage):Three spelling decisions, all mine to pin, all declared:
Ncounts ADR decision CANDIDATES, not raw grep lines. The line records a prior-ruling reading, and "2,214 matching lines" is not one.unresolvedis stated in the protocol text, not only in the tool. It is the whole anti-false-green property; a rule that lives only in a script is a rule the seat pasting by hand never sees.proposednear-miss is visible rather than silently dropped.The line is paid for by a FOLD, not by a ceiling raise and not by a re-wrap. The file was at 50/50. The removed bullet — 「四棱行同受六项写法约束:论据从业务立场写,机制名词只作括号补充。」 — was two halves. Its CONTENT half duplicated the 正文六项 item 「四轴从业务立场论证:论据是客户、产品或事故后果,机制名词只作括号补充」 almost verbatim. Its BINDING half did not: it said the four-facet BLOCK is itself bound by those writing rules, where the six-item entry binds the body's four-axis prose and 「每卡 = 六项写法 + 四棱块」 only says a card carries both parts. A first attempt deleted the bullet outright and was sent back at review — 「被砍内容若在别处无家,砍掉就是丢规则」. So the binding was folded into the six-item entry, which now reads 「四轴从业务立场论证(含四棱行)」, and only then was the bullet removed as a true duplicate. The maintainer's suggested parenthetical 「(四棱行同受此约束)」 measured 129 B against the ratchet's 120-byte per-line budget, so the landed spelling is 「(含四棱行)」 at 117 B. Net: 50 lines, ceiling 50; ⛔ no ceiling moved, and no rule lost a home.
The ratchet also enforces a 120-byte per-line budget, which the first two spellings failed (145 B, then 122 B). The landed line is 113 B, and the byte budget is what pushed the generator's name (
check:pm-prior-rulings, 24 B with its backticks) out of the line. It stays discoverable throughpackage.jsonand the CI step.Live drill (read-only, 3 REST requests, one per card)
--card 16934, the case the card was filed on. ADR-0131 D8 — the ruling the card omitted — comes back at rank 1 of 270:--card 17427(a card that DOES carry a governing-text line) and--card 17321(whose own governing text cites ADR-0087 §D2/§D3):On #17321 the reader independently surfaces ADR-0087 D3, which is the ruling that card's own author had to find by hand. That is the intended behaviour demonstrated on a card nobody tuned it for.
Judgement calls, on the four axes
(a) Bounding the corpus — count
packages/spec/srcrather than name itMeasured first: the whole 20 MB corpus reads in ~600 ms through one
git cat-file --batchper corpus, so runtime never needed bounding. What needed bounding was OUTPUT. On #16934's terms,docs/adrreturns 270 nameable decisions andAGENTS.md124 lines, but the spec docblocks return 3,893 of 11,380.Dplus number identity and a docblock has none once you leave its file. Naming is offered exactly where identity exists, so the rule does not rot as the corpus grows.(b) Term derivation — the title is primary, because the ideal population does not exist
The obvious design derives terms from the
Governing text:line. Measured on the live board: of 27 openneeds-user-decisioncards, 4 carry a governing-text carrier and 23 carry none — and the 4 use two different shapes (one inline**Governing text:**, three as a## Governing textsection).0 hits; none. That is the exact false green this card was filed against, rebuilt one layer down. The title is the only always-present source, so it is primary and the carrier is additive in both measured shapes.governing-text: absent, with the 23-of-27 measurement inline) rather than passed over silently. Loud beats lenient: the seat learns the terms came from the title alone and can narrow with--terms.Governing text:adoption rises the reader gets strictly better without a code change, because the carrier is additive rather than required.(c) Word-boundary matching instead of the charter's substring
git grep -iEsingleposture, which tenant wall is the platform's? driver-sql's posture-independenttenantIdauto-scope answers a platform admin 0/0/0/0 on/datawhile the engine path, analytics and the memory driver answer 12/30/40/14 #16934's terms: substring returns 215 candidates, word-boundary 166, and the termdataalone drops from 110 units to 38 because it stops matching insidemetadata/database. The card's own omitted ruling moves UP, rank 3 → rank 2. Less noise and a better answer are not a trade-off, so there is nothing to trade.\bis wrong here (it treats-and.as boundaries, sodriver-sqlwould match insidedriver-sqlite); the character class is spelled out as "identifier character" and both directions are pinned in the self-test.--termsreproduces any hand grep exactly.(d) Status classification reads the FIRST status word, not a precedence scan
The drill caught this, and it is the defect worth reading closely. Precedence-scanning ("superseded beats accepted, anywhere in the line") misclassified two live accepted ADRs: ADR-0076's status opens
Accepted — …and mentions a supersession of a sub-part later; ADR-0105's opensAccepted (2026-07-27) — …and sayswithdrawnof a withdrawn proposal further along. Both were being dropped from the pasted reading.unknownis kept as its own loud bucket and ⛔ never folded intoproposed, and the raw status text prints beside every candidate so the classification is checkable without opening the ADR.(e)
PM_SWEEP_REPO, not a--repoflag — a declared deviation from the suggested routeThe dispatch suggested
--repo owner/name. Declined, for the reasoncheck-widening-tells.mjsgives ("⛔ there is no--repoflag, deliberately") and one of this file's own: the CORPUS is always this checkout, so a flag naming another board would read a card from one repo and answer it out of another repo's rulings with nothing in the output saying so. The board resolver and the proxy re-exec plan are imported fromcheck-half-states.mjs, not copied — one source each, so this reader and the patrol cannot disagree about which board is read or how it is reached.--revand--topwere added.A falsified premise, and a transport reading
The facet-block presence check does not exist. The dispatch and the card both state that an H row on facet-block presence lives in
check-half-states.mjsand that this PR must keep it working. Measured onorigin/main, with a positive control (needs-user-decisionhits 5 scripts, so the search works):The three
four-facetmentions incheck-half-states.mjsare prose inside other rows' remedy sentences, not a check. So the markeros-decision-facetsis declared 「首行机器可寻标记」 and read by nothing — which also means only 5 of the 27 open decision cards carry it. Nothing in this PR depends on that premise (the new line is extractable by the same literal grep either way), but D2 was graded as "an H row BESIDE the existing facet-block presence check", and there is no existing one for it to sit beside. That is the seat's to re-read when D2 is dispatched.Node's
fetchbypasses the proxy in the seat's container.GITHUB_TOKENandGH_TOKENboth hold the literalproxy-injected;curlgets 200 and an unroutedfetchgets 401. The reader re-execs with--use-env-proxybefore its one request, using the imported plan. Without it the tool would report a credential problem and send a seat hunting for a secret that does not exist.Verification
Gate families derived from the FINAL diff,
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(71 families), reconciled with--ran. Exit codes captured before any pipe. Verdict lines quoted from each gate's own output.All 69 families ran;
dispatch-gates --ranwith exit codes recorded reconciles clean:Every one of the 69 exited 0. Named verdict lines for the families this diff actually moves:
Six families first answered
PREREQUISITE NOT MET(exit 3) on a fresh worktree with nodist/—check:dts-closure,check:dual-build-cjs-loads,check:lean-entry-closure,check:sourcemap-no-sources-content,check:type-check-debt,check:doc-formula-expressions. Those are NOT MEASURED, not findings.pnpm buildwas run under the shared verify lock (VERDICT command-exit 0 · held the lock 420s, 73/73 tasks) and all six were re-run to exit 0.Control-character sweep beyond the gate, over the diff's four files:
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'— no match.origin/mainmoved 10 commits during this run, soorigin/mainwas merged in (clean) and the derivation re-run on the merged head: identical 69 families, none added, none gone, and the STALE-TREE warning cleared. The full 69 are being re-run on the merged head; that result is appended here when it lands.Full-repo lint, run outright rather than narrowed:
Path face:
Expected GOVERNED. This PR stays DRAFT; the maintainer merges. ⛔ No seat flips it ready, enqueues it, or arms auto-merge.
Changeset: none, and
skip-changesetis applied. Nothing published moves — the diff isscripts/pm/,.claude/,.github/workflows/and one rootpackage.jsonscript entry, none of which appears in any package'sfiles[].Acceptance notes
noted, not filed:os-decision-facetsis a machine-findable marker with no machine reading it anywhere in the tree, and only 5 of 27 open decision cards carry it. 承接者: the skills seat, when it re-grades D2 on this card — D2's stated shape ("an H row beside the existing facet-block presence check") assumes a check that does not exist. Not filed separately because it lands inside an already-queued deliverable on this same card.noted, not filed:references/decision-analysis.md〈适用面〉's 「只适用新记录,存量 ⛔ 不回改」 duplicates SKILL.md 〈全体座位的不变量〉's 「四维中文只管新记录,存量英文块 ⛔ 不迁移」. A future density payment on this file could take it; it was not taken here because it is outside the section this change touches. 承接者: the next PR that needs a line in this file.noted, not filed:the reader's term derivation does not stem plurals (answerandanswersare separate terms). Measured cost on the drill: a handful of duplicate terms, no missed candidate. A stemmer is a dependency this repo has no other caller for. 承接者: 无.Clause-②: no — internal PM tooling and protocol text; no accept set, no published surface, and no spec schema moves. The one
package.jsonchange is a script alias, not an export.维护者速读(草稿)
改了什么 —— 决策卡呈报前那一步「有没有人已经裁过这件事」,从人工搜索变成一条命令。给一个卡号,它自动从卡的标题和
Governing text:里取词,在全部 ADR 决定、AGENTS.md 和 spec 注释里搜,把已被接受的裁决按相关度排出来,并打印一行给座位贴进决策卡。为什么改 —— 同一类错误四天内发生两次:一张决策卡带着「推翻 ADR-0131 已裁事项」的建议摆到了您面前,因为填卡的人和呈报的人都漏了那条 ADR。上一次的补救是加文字规则,这次是把规则变成机器动作。用出事那张卡实测,漏掉的裁决现在排第一。
风险与代价(含回滚) —— 只读、只报告,不改任何卡、不打标签、不下判断,哪条裁决管用仍由座位读、由您裁。回滚就是删掉这个脚本和文档里那一行,没有别的东西依赖它。协议文档卡在 50 行上限,新增这一行不抬上限:先把旧规则里「四棱行也受同一写法约束」这半句并进「正文六项」那条(现读作「四轴从业务立场论证(含四棱行)」),旧规则才成为真重复被删掉。第一版直接删、未先并入,已在复核时被退回并改正 —— 规则没有丢家。
席位意见 —— (留空,席位定稿)
你要做的 —— 这是受管面(
.claude/**),PR 保持草稿,由您合并。不需要您做别的决定。Authored by Claude Code, session
https://claude.ai/code/session_01YKEjmbYNvYWJvWGSWx26zK. (Attribution is stated here as prose: a REST body edit appends its own footer, so sending one too stores two.)Generated by Claude Code