Skip to content

feat(pm): mechanise the prior-ruling search for decision cards - #17515

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-17009-prior-ruling-search
Sep 13, 2026
Merged

os-zhuang merged 4 commits into
mainfrom
claude/issue-17009-prior-ruling-search

Conversation

@os-litant

@os-litant os-litant commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

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.mjs chain 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:

  • at filing — the card's 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 under single … No layer has a NULL arm」);
  • at presentation — the presenting seat re-ran the card's own re-check commands 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-check commands 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.

node scripts/pm/check-prior-rulings.mjs --card 16934
node scripts/pm/check-prior-rulings.mjs --card 16934 --terms single,posture,tenant
node scripts/pm/check-prior-rulings.mjs --terms single,posture,tenant     # no card, no token
node scripts/pm/check-prior-rulings.mjs --card 16934 --json --top 20
node scripts/pm/check-prior-rulings.mjs --self-test

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/main with 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:

corpus scanned named?
docs/adr/** decision units 463 (399 headings + 64 bullets) yes — ranked, top-N
AGENTS.md 940 non-blank lines yes — top 5
packages/spec/src/** docblocks 11,380 count only + 3 pointers

Exit codes, deliberately identical to check-half-states.mjs:

  • 0 — the read completed, 0 candidates or 400 alike (report-only).
  • 1 — --self-test failed.
  • 3 — PREREQUISITE NOT MET. Prints Prior rulings read: unresolved, never none. none is a finding; unresolved is the absence of one, and a seat pasting none because 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):

四棱后一行: backtick — Prior rulings read: TERMS arrow N hits; ADR-xxxx Dn pipe none — backtick;未搜成整行写 unresolved。

Three spelling decisions, all mine to pin, all declared:

  1. N counts ADR decision CANDIDATES, not raw grep lines. The line records a prior-ruling reading, and "2,214 matching lines" is not one.
  2. unresolved is 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.
  3. The named set is accepted decisions only. 〈升级与决策〉③ (⛔ 不推翻既有维护者裁决) binds on accepted rulings. Non-accepted hits are still printed in the report body, flagged, so a proposed near-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 through package.json and 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:

check-prior-rulings: corpus read at origin/main (c7af6bd6)
  card: objectstack-ai/objectstack#16934 — [Decision] Under `single` posture, which tenant wall …
  terms (42): single tenantid data posture tenant wall driver-sql posture-independent … [title]
              adr-0105 tenancy postures adr-0021 boot layer inert getreadscope … [governing]
  governing-text: present (inline form)

  docs/adr: 270 hit of 463 searched
  AGENTS.md: 124 hit of 940 searched
  packages/spec/src: 3893 hit of 11381 searched (not named — see below)

  ADR decision candidates: 270; distinct-term tiers {"1":143,"2":69,"3":30,"4":13,"5":9,"6":4,"7":1,"8":1}
      ADR-0131 D8 — One predicate, computed once
        docs/adr/0131-…-no-null-organization-id.md:483   terms: single,posture,tenant,engine,driver,adr-0105,tenancy,layer
        status: Accepted (2026-09-04) — accepted by the merge that landed it on `main` …
      ADR-0131 D3 — A row exists only when an organization authored it …
      ADR-0076 D10 — "protocol" is a contract, not an implementation package …
      ADR-0105 D1 — Tenancy becomes a three-posture spectrum …
      ADR-0120 D5 — Authoring gates

    Prior rulings read: single,tenantid,data,posture,tenant,wall,driver-sql,posture-independent,auto-scope,answers,admin,engine (+30 more) → 270 hits; ADR-0131 D8, ADR-0131 D3, ADR-0076 D10, ADR-0105 D1, ADR-0120 D5

--card 17427 (a card that DOES carry a governing-text line) and --card 17321 (whose own governing text cites ADR-0087 §D2/§D3):

#17427  22 terms · docs/adr 124/463 · AGENTS.md 76/940
        Prior rulings read: environmenttype,post,cloud,environments,refuse,values,wire,honour,seven,leave,silent,default (+10 more) → 124 hits; ADR-0006 D2, ADR-0119 D3, ADR-0128 D3

#17321  62 terms · docs/adr 264/463 · AGENTS.md 251/940
        Prior rulings read: filter,losslessly,mappable,subset,record-form,real,conversion,stored,rows,loading,re-saving,refused (+50 more) → 264 hits; ADR-0087 D3, ADR-0131 D13, ADR-0087 D7

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/src rather than name it

Measured first: the whole 20 MB corpus reads in ~600 ms through one git cat-file --batch per corpus, so runtime never needed bounding. What needed bounding was OUTPUT. On #16934's terms, docs/adr returns 270 nameable decisions and AGENTS.md 124 lines, but the spec docblocks return 3,893 of 11,380.

  • 实际业务需求 — the consumer is a seat about to present five cards. 3,893 named docblocks is not a reading anyone acts on; it is a wall that makes the 270 real candidates harder to see. The count, by contrast, is acted on: it tells the seat whether the term set is narrow enough to trust.
  • 项目长远合理性 — the distinction is structural, not a size threshold: an ADR decision has a D plus 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.
  • 防 AI 犯错 — the count is never suppressed. "Searched but too broad to name" and "not searched" must not render alike, so the corpus prints its scanned and hit totals in every mode, and three top pointers so the claim is checkable.
  • 创业阶段不扩散 — the alternative (a docblock-to-symbol attributor) is a parser this repo has no other caller for. Declined.

(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 open needs-user-decision cards, 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 text section).

  • 实际业务需求 — a derivation resting on that line produces an EMPTY term set on 85% of the real population and prints 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.
  • 防 AI 犯错 — an absent carrier is ANNOUNCED (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.
  • 项目长远合理性 — as Governing text: adoption rises the reader gets strictly better without a code change, because the carrier is additive rather than required.
  • 创业阶段不扩散 — ⛔ no new required field on cards, and ⛔ no gate demanding the carrier. That would be a protocol expansion nobody asked for; this PR only reads what is there.

(c) Word-boundary matching instead of the charter's substring git grep -iE

(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 opens Accepted (2026-07-27) — … and says withdrawn of a withdrawn proposal further along. Both were being dropped from the pasted reading.

  • 防 AI 犯错 — hiding a standing ruling is the ONE failure this reader may not have, and the lenient-looking rule was producing it. Every measured shape states its verdict first, so position is the honest reader. unknown is kept as its own loud bucket and ⛔ never folded into proposed, and the raw status text prints beside every candidate so the classification is checkable without opening the ADR.
  • Both live shapes are now self-test cases.

(e) PM_SWEEP_REPO, not a --repo flag — a declared deviation from the suggested route

The dispatch suggested --repo owner/name. Declined, for the reason check-widening-tells.mjs gives ("⛔ there is no --repo flag, 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 from check-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. --rev and --top were 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.mjs and that this PR must keep it working. Measured on origin/main, with a positive control (needs-user-decision hits 5 scripts, so the search works):

git grep -n "os-decision-facets" -- .
  .claude/skills/pm-dispatch/references/decision-analysis.md:39      ← the declaration, and nothing else

The three four-facet mentions in check-half-states.mjs are prose inside other rows' remedy sentences, not a check. So the marker os-decision-facets is 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 fetch bypasses the proxy in the seat's container. GITHUB_TOKEN and GH_TOKEN both hold the literal proxy-injected; curl gets 200 and an unrouted fetch gets 401. The reader re-execs with --use-env-proxy before 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 --ran with exit codes recorded reconciles clean:

Run reconciliation — 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN.
  EXIT CODES — all 69 accounted famil(ies) carry one, so the NOT-MEASURED count above is DERIVED from them.
✓ dispatch-gates --ran: 69 derived famil(ies) accounted for — 69 run, 0 NOT-MEASURED
  (a DERIVED zero — all 69 recorded an exit code and none of them is 3).

Every one of the 69 exited 0. Named verdict lines for the families this diff actually moves:

node scripts/pm/check-prior-rulings.mjs --self-test                EXIT=0
  · live-tree existence pin: pinned
  ✓ check-prior-rulings self-test: 99 cases pass (…)

pnpm check:pm-skill-ratchet                                        EXIT=0
  ✓ check-skill-line-ratchet: …/decision-analysis.md is 50 lines (ceiling 50; headroom 0).
  ✓ check-skill-line-ratchet: …/decision-analysis.md: widest table row is 0 bytes (pin 0; headroom 0).

pnpm check:pm-skill-id-lint                                        EXIT=0
pnpm check:self-test-wired                                         EXIT=0
pnpm check:self-test-workflow-commands                             EXIT=0
pnpm check:nul-bytes                                               EXIT=0
pnpm check:pm-dispatch-gates                                       EXIT=0
pnpm check:workflow-step-name-quoting                              EXIT=0

Six families first answered PREREQUISITE NOT MET (exit 3) on a fresh worktree with no dist/ — 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 build was 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/main moved 10 commits during this run, so origin/main was 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:

npx eslint --no-inline-config . --format json     EXIT=0
  6575 files linted (count read from --format json), 0 files with findings

Path face:

node scripts/pm/check-governed-merges.mjs --test THE-4-FILES     EXIT=3
  governed-surface predicate: 1 of 4 path(s) hit the register (5 surfaces, repo-agnostic).
    ⛔  GOVERNED — a human merge is the review record for this PR (#9495 regime).
        .claude/** ×1 — .claude/skills/pm-dispatch/references/decision-analysis.md

Expected GOVERNED. This PR stays DRAFT; the maintainer merges. ⛔ No seat flips it ready, enqueues it, or arms auto-merge.

Changeset: none, and skip-changeset is applied. Nothing published moves — the diff is scripts/pm/, .claude/, .github/workflows/ and one root package.json script entry, none of which appears in any package's files[].

Acceptance notes

  • noted, not filed: os-decision-facets is 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 (answer and answers are 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.json change 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

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>
@os-litant os-litant added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 10, 2026 — with Claude
@github-actions github-actions Bot added ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation labels Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

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 PATCH /pulls is measured to append a second, bare attribution footer while keeping the session-URL one, and the body above read back byte-identical with exactly one footer. Comments are the well-behaved surface for an increment.

origin/main was merged in (clean) and the derivation re-run on the merged head produced the identical 69 families — none added, none gone. All 69 were then re-run on that head:

Run reconciliation — 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN.
  EXIT CODES — all 69 accounted famil(ies) carry one, so the NOT-MEASURED count above is DERIVED from them.
✓ dispatch-gates --ran: 69 derived famil(ies) accounted for — 69 run, 0 NOT-MEASURED
  (a DERIVED zero — all 69 recorded an exit code and none of them is 3).

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:

node scripts/pm/check-clause2-carriers.mjs --pair 17515          EXIT=0
  ✓ check-clause2-carriers: PR #17515 / card #17009 — the clause-② declaration is readable in the
    fixed spelling and both carriers agree, and its diff carries no widening tell.
    ⚠️ A tell is not a proof and its absence is not one either.

Control-character sweep over the diff's four files, beyond what the gate does: grep -naP over the C0 range plus DEL — no match.

skip-changeset applied through the additive endpoint (POST .../issues/17515/labels) and compare-read back: stored labels are size/xl, skip-changeset. size/xl was set by another actor and is left untouched.

One standing caveat, stated rather than chased

origin/main moved 10 commits during this run, was merged, and has moved 3 more since — the last of them touching scripts/pm/check-half-states.mjs, which this PR imports from (DEFAULT_SWEEP_REPO, resolveSweepRepo, proxyRearmPlan, PROXY_FLAG, PROXY_REARM_GUARD). All five exports were re-checked against the current tip and all five still exist, so the cross-import holds. Chasing a moving main to a standstill is not convergence; CI on this PR's head is the reading that counts, and this is a governed PR a human merges.

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>

Copy link
Copy Markdown
Collaborator Author

维护者速读

改了什么 —— 新脚本 scripts/pm/check-prior-rulings.mjs(1268 行,自测 99 例):给一个卡号,从卡的标题和 Governing text: 取词,在全部 ADR 决定项、AGENTS.md 与 spec 注释里搜已被接受的裁决,按相关度排出,并打印一行 Prior rulings read: … 供座位贴进决策卡;package.json 一条 check:pm-prior-rulings + lint.yml 一步只跑自测(⛔ 不把活读升级成门禁)。协议文档 references/decision-analysis.md 在 50/50 上限内新增「四棱后一行」规则(113 B),付账方式:把旧规则「四棱行同受六项写法约束」的约束半句并进「正文六项」那条(现读「四轴从业务立场论证(含四棱行)」,117 B),旧行成为真重复后删除。

为什么改 —— 同类错误四天两次:一张决策卡带着「推翻 ADR-0131 已裁事项」的建议摆到您面前,因为填卡与呈报都漏了那条 ADR。#17009 D1 把「先搜既有裁决」从文字规则变成机器动作;用出事那张卡(#16934)实测,ADR-0131 D8 在 270 个候选里排第一。

风险与代价(含回滚) —— 只读、只报告,不写卡、不打标签、不判断;CI 只跑它的自测。回滚 = 删脚本、删 package.json / lint.yml 两处接线、把文档那两行还原。第一版补丁直接删旧行未先并入,复核退回后改正,规则没有丢家。CI 在 head f4dbf03e 上全绿(35 过 / 10 跳)。

席位意见 —— 通过。本席在契约复审档复核两轮:自测 99 例本席在 head 上跑过;活钻取 --card 16934 本席跑过,排第一;棘轮 50/50、frame-sync、id-lint、--pair 均 0;文档改动逐行读过。受管面(.claude/**):本席不翻 ready、不入队、不批准。D2(半态巡查加四棱块存在行)另行派发,不在本 PR。

你要做的 —— 一个动作:人工合并本 draft PR。

skills 席,session session_01YKEjmbYNvYWJvWGSWx26zK,2026-09-10T19:47Z。


Generated by Claude Code

This was referenced Sep 12, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review September 13, 2026 08:05
@os-zhuang
os-zhuang enabled auto-merge September 13, 2026 08:05
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit ea1da64 Sep 13, 2026
51 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-17009-prior-ruling-search branch September 13, 2026 08:35
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
… own subject or the zero is void (objectstack-ai#17837)

The zero-hit rule bound the control to HIT, not to be SCOPED to the
claim: a control can prove the instrument works while pointed at a
different corpus, path shape, module/package boundary or quoting, and
the zero then comes back as a reassuring negative that survives review
(three misses in one hotcrm shift, two more measured by the triage
seat). The rule now says the control shares the claim's own subject and
the target's failure mode (a phrase that wraps across lines needs a
control that wraps the same way), or the zero is VOID rather than
negative. Landed in all three carriers of the discipline in one PR, per
SKILL.md 「一条规则在本文与核心条款一处改动,另一处同 PR 同改」: `pm-dispatch/SKILL.md`
:161-:162, `references/core-rules.md` :45, and the dev-side reading in
`.claude/agents/os-dev.md` :54. Every file stays at its ratchet ceiling;
the payment is density inside the same section, itemised below. Not a
new gate.

Part of objectstack-ai#17569

## Acceptance notes

**Premises (re-taken on `origin/main` `0cd841a16` at 2026-09-12T10:03Z,
after `git fetch origin main`):**

1. SKILL.md :161 read 「- 零命中必须用确定存在的邻近词反查,否则零命中不成立。」 (78 bytes); file
812 lines, CEILINGS row 812, `MAX_LINE_BYTES = 120` — holds. The ratchet
script lives at `scripts/pm/check-skill-line-ratchet.mjs` (the dispatch
word spelled it without the `pm/` segment; same script).
2. core-rules.md :45 read 「- 零命中必须用确定存在的邻近词反查才成立;仓不可达时 ⛔ 不得当成查过且干净。」
(110 bytes); 151 lines at ceiling 151 — holds; it IS the rule's
core-rules mirror and is edited here.
3. os-dev.md :54 read 「 - 空结果要同会话一个已知必中的控制词答了命中才算读数。」 (83 bytes); 403
lines at ceiling 403 — holds.
4. SKILL.md :175 and core-rules.md :51 are the dedupe-control siblings —
left byte-identical (the new clause is general; no cross-reference
needed).
5. Open-PR scan re-taken at 2026-09-12T10:07Z over all 24 open PRs' file
lists (REST `pulls/{n}/files`): zero hits on the three files. Control,
same corpus and same regex family: 5 of those PRs touch `.claude/` paths
(objectstack-ai#17828 and objectstack-ai#17803 on `platform-readings.md`, objectstack-ai#17823 `rest-channel.md`,
objectstack-ai#17809 a hook, objectstack-ai#17515 `decision-analysis.md`) — the zero is scoped.
`origin/main` moved 3 commits (to `51b024a16`) during the run; none
touched the three files.
6. Sibling clause from PR objectstack-ai#17566 (spec-property-retirement SKILL.md :59)
read: 「零编写实例普查要并跑一个同族已知存活的键作对照,两读数都报;同得零即没测出。」 — the new wording keeps
that vocabulary (a control shares the claim's family/subject; a control
that measures nothing voids the reading) rather than introducing a
second idiom.
7. The card quotes a dispatch-template sentence 「pair every zero with a
control word that must hit」 as a second carrier inside SKILL.md. On
`origin/main` no such sentence exists (grep for `must hit`, `pair every
zero`, `control word` over `.claude/**` and `scripts/pm/**` returns
zero; control: `零命中` hits :161, :542, :724) — the template left in the
rules-only rewrite. No second carrier to edit.

**Edited lines and byte counts (each ≤ 120):**

| file | line | bytes | text |
|:--|:--|--:|:--|
| SKILL.md | :161 | 113 | 「- 零命中须用确定存在的邻近词反查;控制词须与主张同主体,否则该零作废,不是阴性。」 |
| SKILL.md | :162 (new) | 114 | 「- 同主体 =
同语料、同路径形、同包界、同引法、同失效形态:跨行短语配跨行控制词。」 |
| SKILL.md | :165 (merged) | 105 | 「- 时间戳形如 `YYYY-MM-DDThh:mmZ`,树读数另带
ref 或 tip;无时间戳的读数按未取处理。」 |
| core-rules.md | :44 | 112 | 「- 核验 main 用 fetch 后的 `origin/main`,⛔
不用共享检出树;仓不可达 ⛔ 不当查过且干净。」 |
| core-rules.md | :45 | 113 | 「-
零命中须用必中词反查,且与主张同语料/路径形/包界/引法/失效形态,否则该零作废。」 |
| os-dev.md | :54 | 113 | 「   - 空结果要同会话已知必中、与主张同主体同失效形态的控制词答了命中才算读数。」 |

**How density was paid (all inside the 平台读数纪律 section of each file):**

- SKILL.md 812/812: the clause takes two lines (:161-:162, +1); the two
timestamp lines (:164 「时间戳形如 …,树读数另带 ref 或 tip。」 and :165
「无时间戳的读数是格式错误不是现值,读者按未取处理。」) merged into one (−1). Dropped words:
「是格式错误不是现值,读者」 — the operative half 「按未取处理」 survives, matching
core-rules' own wording of that rule.
- core-rules.md 151/151: no two neighbouring lines in the section merge
under 120 bytes, so the clause is one line (:45) with the five sames
inline; the 仓不可达 half of the old :45 moved onto the `origin/main` line
(:44). Dropped words: 「确定存在的邻近」 → 「必中」, 「时」, 「不得当成」 → 「不当」, 「的工作树」 →
「树」, 「先」. The wrapped-phrase example is carried by SKILL.md only (terser
register).
- os-dev.md 403/403: one line, byte-for-byte replacement of :54 (83 →
113 bytes); the 范围 list is otherwise untouched.

**Frame block pin:** `sed -n '734,755p'
.claude/skills/pm-dispatch/SKILL.md | md5sum` =
`3327d02c56f8a0eca88569dad2270f32` before and after (net line count
above it is 0, so the block did not move).

**Gates (derived with `node scripts/pm/dispatch-gates.mjs --commands` in
the worktree, 18 commands; reconciliation via `--ran`: 「18 derived, 18
run, 0 NOT-MEASURED, 0 UNRUN」):** all 18 exit 0 at head `9e15e315f`.
`pnpm check:pm-skill-ratchet` verdict lines: 「SKILL.md is 812 lines
(ceiling 812; headroom 0)」 · 「core-rules.md is 151 lines (ceiling 151;
headroom 0)」 · 「os-dev.md is 403 lines (ceiling 403; headroom 0)」. `pnpm
check:skill-frame-sync`: 「the one declared copy of the decision frame is
internally coherent … 4 axes」. `pnpm check:nul-bytes`: 「OK (scanned 8465
text file(s) …; no raw ASCII control bytes)」.
`check:doc-formula-expressions` first answered exit 3 PREREQUISITE NOT
MET (`@objectstack/formula` and `@objectstack/lint` unbuilt) — not a
measurement; built both under the verify lock (VERDICT command-exit 0,
169 s) and re-ran: exit 0.

**Changeset:** `.claude/**` publishes nothing from any released package
(`node scripts/check-changeset-fixed.mjs` exit 0; no `files[]` of any
package covers `.claude/`) — `skip-changeset` applied by additive POST
and read back.

**Deferred rider (⛔ not in this PR):** the card's two
`references/platform-readings.md` rows (`git grep -- 'a/**/*.ext'` skips
files directly under `a/`; `grep -c $'\x00'` is not a NUL probe) wait on
that file's serial behind PR objectstack-ai#17828 and PR objectstack-ai#17803. objectstack-ai#17569 stays open for
the rider PR; hence `Part of`, not a closing keyword.

**Governed surface:** `.claude/**` — this PR stays a draft at the human
terminal; nothing here flips it ready or arms auto-merge.

## 维护者速读(草稿)

**改了什么:** 「零命中必须配控制词反查」这条规则,在三处载体(pm-dispatch SKILL.md、核心条款
core-rules.md、os-dev 开发 agent 定义)各加一句:控制词必须与主张同主体 ——
同语料、同路径形、同包界、同引法、同失效形态 —— 否则这个零作废,不算阴性读数。三个文件行数不变,各自仍顶着 ratchet 上限。

**为什么改:**
一个班次里三次「控制词命中了、零仍是假的」:控制词证明工具能用,却没证明工具对准了主张。旧文只要求控制词「命中」,不要求它「与主张同一件事」,于是错误读数反而带着安心感回来、过了复核。

**风险与代价(含回滚):** 纯文本规则,不加门禁、不加脚本;付费方式是同节内合并两行时间戳规则(SKILL.md)与把「仓不可达」半句挪到
origin/main 那一行(core-rules)。回滚 = revert 这一个 commit,无生成物、无依赖。

**席位意见:** (留空,席位定稿成评论)

**你要做的:** 读三处新句是否表达了你要的判据;确认后人工合并(governed surface)。rider
半张(platform-readings 两行)另开 PR,本 PR 不关卡。

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

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…t block, and `finding` outliving its grade (objectstack-ai#18033)

Fixes objectstack-ai#17009
Fixes objectstack-ai#16904

Both cards' D2 halves. objectstack-ai#17009's D1 landed as `ea1da645` (PR objectstack-ai#17515) and
objectstack-ai#16904's as `ae1b2588` (PR objectstack-ai#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 objectstack-ai#16904's D1 report left unfired. No `--write`
path anywhere, no label written, no gate added.

## H62 (objectstack-ai#17009) — a decision card whose face carries no four-facet block

An open `needs-user-decision` card whose **body** carries no
`os-decision-facets` marker, 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 is `check-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 (objectstack-ai#16904) — a graded card still wearing `finding`

An open card carrying `finding` beside a grading state or any
`priority:*`. The rule is **reused by import, never copied** — but the
arrow is forced, not chosen: 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 and every self-test. So the screen
moved **down** into the module that owns the vocabulary, and
`sweep-stale-finding.mjs` re-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-finding` in `package.json` and a self-test
step in `lint.yml`. `check-self-test-wired` population **208 → 209**,
green in both directions — the tool shipped a 91-case self-test that
nothing in CI ran.

## Acceptance greps, both directions

| reading | before | after |
|:--|--:|--:|
| `H62 (objectstack-ai#17009)` / `H63 (objectstack-ai#16904)` in `check-half-states.mjs` | 0 / 0 | 3
/ 3 |
| lit control `Decision-box dependency flags (instruction ④)` | 4 | 4 |
| `os-decision-facets` reader in `check-half-states.mjs` | 0 | 3 |
| `check:pm-stale-finding` in `package.json` | 0 | 1 |
| `check:pm-stale-finding` in `lint.yml` | 0 | 1 |
| `check-half-states --self-test` cases | 3806 | 3915 |
| `sweep-stale-finding --self-test` cases | 91 | 91 |
| `check-self-test-wired` scripts CI runs | 208 | 209 |
| `check:declared-population-live` declaring families with an
unreachable population | 1 | 0 |

**Untouched, proved by md5 of the whole exported function body** — H4
`1d13e919…`, H13 `fa79039c…`, H14 `1e88cafd…`, H22 `a45048e0…`, H29
`aaa6ea92…`: identical before and after. The instruction-④ section,
`.claude/**` and the write-path half of `finding` stripping 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_limit` core 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 `absent`. ⚠️ 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.

## Gates

Derived on the merged head `1d52dd97` with `dispatch-gates --commands
--repo objectstack-ai/objectstack` (no paths): **74 families, 74 run,
every one exit 0**. Reconciled with `--ran` carrying 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 after `pnpm build` under 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 own `NODE_OPTIONS` imposed (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 lint` run repo-wide rather than narrowed: `eslint
--no-inline-config . --format json` exit 0, **6723 files linted, 0 with
findings** — no narrowing claimed. Control-character sweep over every
touched file: no match.

⚠️ `origin/main` moved 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

1. **One file beyond the claim's surface.**
`scripts/pm/sweep-stale-finding.mjs` is 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.
2. **No header-roster entry.** The header roster stops at H28; every row
since carries its own banner block above its predicate instead. These
two follow that convention — adding two entries to a list 33 consecutive
rows are absent from would make the header lie about its own coverage.
3. **Two more commits, both derivation bookkeeping the wiring forced,
neither weakening a self-test.**
- Wiring the sweep into `package.json` makes it a declaring family for
`dispatch-gates`, and `check:declared-population-live` reds on the
module-level fixture's repo slug. Fixed by the gate's own remedy: the
`no-path-population` marker (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-test` then red on one case of 1682 (CI run
34759389219, `Lint & Repo Gates` step 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 its `ROOT_WALK_RESIDUE_LEDGER` row 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 the objectstack-ai#15753 one the row was
written about.
4. **`origin/main` merged** (`1d52dd97`) because the derivation warned
the tree was stale on a file it derives from — and it was load-bearing:
the merged `check-clause2-carriers.mjs` adds `check:pm-clause2-carriers`
to this card's family list (73 → 74).

`skip-changeset`: `scripts/pm/**`, a root `package.json` script and a
workflow publish nothing from any released package.

Authored in Claude Code session `session_01DAcomhvR9kKizeYgg89Vo8`.


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

---------

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 dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation needs-user-decision size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants