Skip to content

fix(pm): check-widening-tells T2 reads the construct that encloses a member — an import list is never a closed set - #21679

Closed
objectstack-fleet[bot] wants to merge 7 commits into
mainfrom
claude/issue-21465-widening-tells-t2-enclosing-construct
Closed

objectstack-fleet[bot] wants to merge 7 commits into
mainfrom
claude/issue-21465-widening-tells-t2-enclosing-construct

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21465
Clause-②: no

What

scripts/pm/check-widening-tells.mjs T2 read any bare string or bare …Schema, line on the contract source surface as "a new member of a closed set", whatever enclosed it. It now reads the construct that ENCLOSES the member, from the hunk, as the triage ruling (5961317165) directs. One file changes; no other file, no changeset (scripts/pm/** publishes nothing).

  • enclosingConstruct(side, index) names the innermost opener the hunk shows (through the existing enclosingDelimiters, by the text left of it on its own line), and, only when the hunk shows no opener at all, the first closer the hunk shows ahead of the line (closerAhead, the walk's forward mirror, with the same triggers).
  • One verdict table, T2_CONSTRUCT_VERDICTS, pinned one fixture per construct by the exported T2_CONSTRUCT_FIXTURES (the ruling's enumeration pin):
construct named by T2
z.enum z.enum([, or z.literal( fires
z.union z.union([, z.discriminatedUnion(…, [, .or( fires
as const array const X = [ … ] as const fires, or narrowing (below)
import list import { silent
export list export { silent
function argument list any other callee's ( silent
object literal any other { silent

Size — why each block is needed

+802 / −22, one file. By block:

lines block why it cannot go
103 header section #21465 this file records every T2/T1 precision change with its measured corpus and its quiet direction; the census, the price and the residual live here and nowhere else
19 the T2 line in the header's tell list, and stale sentences in the #19099 section and the enclosingDelimiters docblock they said "no reader suppresses on this flag", which this PR makes false
391 the reader (212 code, 169 docblock, 10 blank): closerAhead, enclosingCloser, T2_CONSTRUCT_VERDICTS, the head regexes, enclosingConstruct, codeOfLine, refusalOnlyReaders, t2MemberVerdict the classifier the ruling asks for (lines 1, 2, 4), the forward walk the live hunk needs (its opener is above the hunk), and the narrowing reading (line 3) with its polarity; the docblock names every walk trigger, which #19099 requires of the first reader that declines on the frames
60 wiring in tellsInFile / wideningTells / wideningRefusal / verdictLines, and narrowingLines both sides read before the budget; the narrowing row is printed beside the rows and moves no exit code
114 fixtures: T2_CONSTRUCT_FIXTURES (7, the enumeration pin), DENY_SET_SOURCE, PR_21463_HUNK the ruling's one-fixture-per-construct pin, the narrowing fixture's head blob, and the live bytes
115 54 self-test cases, the battery pin, the success line each construct, each trigger in each direction, the budget both ways, the price, and the narrowing controls; every block of the reader is driven red by an ablation below

Nothing here is a new tell, a new gate, a new flag or a new exit code.

Reading 1 — the repro, before and after

PR #21463's diff at head 52c4c42d72 (git diff 086ad0aa68...52c4c42d72; the merge base is the main side of that branch's last merge), --declaration no --diff:

tree exit rows
before, fea67065a3 4 T2 ui/component.zod.ts:34, :35, :36 (RowHeightSchema, / RowColorConfigSchema, / ListViewSchema,); T1 :3805, :3833, :3834, :3835, :3836
after, 40c0fec9b1 4 T1 :3805, :3833, :3834, :3835, :3836 — the three T2 rows are gone and the five true T1 rows still fire

The narrowing instance from PR #21425's ACCEPT (5955887463), on its squash commit (git diff 32d5769080^ 32d5769080):

tree exit output
before 4 T2 ui/dashboard.zod.ts:785 … :789, the five SINGLE_SERIES_CHART_TYPES members
after 0 narrowing: 5 member line(s) join a module-private as const set whose every reader REFUSES its members …, naming the five lines and the binding

Reading 2 — self-test counts and the floor

  • node scripts/pm/check-widening-tells.mjs --self-test: 553 → 607 cases pass (fea67065a3 → 40c0fec9b1).
  • All 54 new cases sit in the existing battery T2 — a new member of a closed set. Its pin rises 13 → 67, its measured count, so none of them can stop running silently.
  • SELF_TEST_BATTERY_FLOOR stays 16. AGENTS.md's floor rule pins battery NAMES with minimum counts and fails when the roster falls below its pinned battery count. No battery was added or removed (29 before, 29 after), so the rule asks nothing of that constant here. The 16-against-29 gap predates this PR (Acceptance notes).

Corpus census — what moves, row for row

The clone is shallow with one boundary (ce13bb8dc7), excluded. 2,054 non-merge commits touching packages/spec/src are reachable from fea67065a3; 2,007 have non-test .ts diffs, giving 6,111 file diffs. Each was judged through wideningTells with headBlobSource live (blob resolution asserted through both modules before counting), by the version at fea67065a3 (a compare worktree) and by this branch.

Ablations

All from the committed final head 40c0fec9b1 (file blob c8af0779bede), through scripts/ablation-replace.mjs in WRAP mode, under the shared verify lock. Every leg printed ok mutation landed and ok restored: blob == HEAD (c8af0779bede) and git diff HEAD is empty (10 of 10), and git status --porcelain was empty afterwards.

leg mutation --self-test
A1 delete the added-side silent decline 11 of 607 fail: the 4 silent enumeration fixtures, the live instance, the closer-only brace, the JSDoc reset, the lone-slash closer, the no-spend pin, and the two #16943 description: pins (one side now pays, the other does not)
A2 delete the removed-side decline 3 fail: a removed specifier buys no unit; the .describe( coin gain; the price
A3 the narrowing verdict returned as fires 5 fail: the as const enumeration fixture, the head-blob closer, the narrowing exit code, the two read-count pins
A4 guard pattern's ! made optional (polarity ignored) 1 fails: POLARITY
A5 if (unreadable) → if (unreadable && false) 2 fail: the UP regex trigger; the price
A6 delete the DOWN regex trigger 2 fail: the DOWN regex trigger; the unlexable members-only region
A7 disable the closer-only } reading 4 fail: the live instance (2), the closer-only brace, the lone-slash closer
A8 read the refusal in raw text, strings included 1 fails: a string saying "throw"
A9 drop the no-value-return condition 1 fails: the members-only region that returns a value
A10 read the head blob on every call instead of once per file 2 fail: the two read-count pins

An eleventh leg, run at 121a3f3e0d, dropped a per-binding memo of refusalOnlyReaders and stayed GREEN: the memo changed no verdict and no read count. It was therefore deleted (40c0fec9b1) rather than kept unpinned.

⚠️ One earlier A5 run (at b5cf545ba7) was a no-op and is not counted: its replacement was a substring of its anchor, so the replacement count did not rise, and ablation-replace refused and restored without running the self-test. Every leg above was re-run at the final head with a countable replacement.

Gates

Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 30 commands, identical to the dispatch's list, re-derived unchanged at the final head. All 30 ran at 40c0fec9b1 in one run under scripts/pm/os-verify-lock.sh, each exit code captured before any pipe. The lock held 27m17s on a shared box; check:pm-dispatch-gates alone took 1,239s (✓ dispatch-gates self-test: 1976 cases pass), and check:pm-widening-tells printed 607 cases pass.

command exit wall
node scripts/check-ci-filter-parity.mjs 0 0s
node scripts/check-closing-keyword-parity.mjs 0 9s
node scripts/check-closing-keyword-parity.mjs --self-test 0 4s
node scripts/check-comment-mask-corpus.mjs 0 109s
node scripts/check-declaration-mirrors.mjs 0 0s
node scripts/check-declaration-mirrors.mjs --self-test 0 0s
node scripts/check-scripts-symbol-anchors.mjs 0 5s
node scripts/check-scripts-symbol-anchors.mjs --self-test 0 4s
node scripts/check-self-test-wired.mjs 0 3s
node scripts/check-self-test-wired.mjs --self-test 0 6s
node scripts/check-self-test-workflow-commands.mjs 0 74s
node scripts/check-self-test-workflow-commands.mjs --self-test 0 4s
node scripts/check-whole-set-label-write.mjs 0 2s
node scripts/check-whole-set-label-write.mjs --self-test 0 0s
node scripts/pm/bare-root-worklist.mjs --self-test 0 34s
pnpm check:agent-test-spelling 0 4s
pnpm check:bash32-floor 0 2s
pnpm check:cli-command-ids 0 13s
pnpm check:cross-package-test-inputs 0 29s
pnpm check:driver-memory-census 0 4s
pnpm check:entry-guard 0 55s
pnpm check:gitlink-declared 0 2s
pnpm check:nul-bytes 0 3s
pnpm check:parse-guard 0 2s
pnpm check:pm-dispatch-gates 0 1239s
pnpm check:pm-widening-tells 0 1s
pnpm check:pnpm-filter-targets 0 3s
pnpm check:ratchet-remedy-authority 0 5s
pnpm check:refd-timer-probe 0 15s
pnpm check:watch-hint-literal 0 6s

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.list (each line COMMAND :: exit N, written from the run's own record): Run reconciliation — 30 derived, 30 run, 0 NOT-MEASURED, 0 UNRUN. / ✓ dispatch-gates --ran: 30 derived famil(ies) accounted for — 30 run, 0 NOT-MEASURED, derived at 40c0fec9b1, exit 0. Repo-wide lint (pnpm lint) is CI's run and was not run locally: NOT MEASURED here, reason: CI-owned.

Self-referential check: node scripts/pm/check-widening-tells.mjs --declaration no --diff on this branch's own diff (git diff fea67065a3...HEAD) exits 0 — 1 changed file(s) — 0 judged against a declared surface (no widening tell), 1 NOT MEASURED. scripts/pm/ is on no tell surface, so the instrument says nothing about this PR; Clause-②: no rests on the diff publishing nothing.

No other test names this script (git grep -l check-widening-tells over *.test.*: 0 hits; control scripts/pm/ over the same glob: 5 files), so its suite is its own --self-test.

Acceptance notes

Found on the way, not fixed here:

  • The battery-roster floor is lax. SELF_TEST_BATTERY_FLOOR = 16 against 29 declared batteries, so up to 13 batteries could be deleted with no red. The reference instrument (scripts/check-agent-model-declared.mjs) pins its floor equal to its roster (18 and 18). Not touched: this PR adds no battery. Carrier: none.
  • T2's opener reading fires on prose. CLOSED_SET_OPENER matches z.enum( / z.union( inside string literals, so a prose fragment naming a constructor reads as a new closed set (seen in the census at shared/retired-key.ts:329, migrations/registry.ts:5670 and migrations/entries/semantic/18.analytics-time-dimension-date-range-vocabulary-closed.ts:37). Opener lines are not member lines, so this PR's reading does not reach them; it is the next member of the T2 false-positive family, for the triage ruling's fixture table rather than a new card. Carrier: none.

维护者速读(草稿)

改了什么:PM 工具 check-widening-tells 的 T2 读数(「闭集新增成员」)以前只看一行长得像不像成员,现在先看这一行被什么结构包着。import / export 列表、函数实参、对象字面量里的元素一律不算闭集成员;z.enum、z.union、as const 数组照旧报。另外,一个只被「拒收谓词」读的私有 as const 集合加成员,现在单独报为「收窄」,不再算放宽。

为什么改:PR #21463 只是多 import 了三个 schema,工具就报了三条「闭集新增成员」。按规则,一个只收窄、顺带加 import 的 PR,会被这个假读数逼着把 Clause-② 写成 yes,往治理记录里写一个并不存在的放宽。在 2,054 个提交上实测:285 条 T2 假读数消失,T1 / T4 一条不动。

风险与代价(含回滚):新增 1 条假报(一处 JSDoc 里斜杠让工具读不懂,宁可多报),新增 1 条真报(以前被删掉的说明文字「抵扣」掉的真实新枚举值)。理论上漏报的形状(把实参变成闭集的 helper)在实测中为 0,已写进文件头。只改一个 PM 脚本,不发布任何包;回滚即 revert 本 PR。

席位意见:

你要做的:无需动作;席位达档复核通过后经合并队列落地。


Generated by Claude Code

claude added 7 commits October 4, 2026 02:14
…member

An import/export specifier list, a call's argument list and an object
literal never hold a closed-set member; an `as const` set read only by a
refusal predicate is reported as narrowing beside the rows.

Claude-Session: https://claude.ai/code/session_01CB6W87z22K2yjUCDyVrJRk
Co-authored-by: Claude <noreply@anthropic.com>
…d the narrowing polarity

One fixture per construct the triage ruling names, each with its T2
verdict; PR 21463's hunk read off its closer, with the cut-hunk control;
both walks' triggers; the budget on both sides; and the narrowing
reading bracketed by the controls that keep firing. The header records
the corpus census and the price.

Claude-Session: https://claude.ai/code/session_01CB6W87z22K2yjUCDyVrJRk
Co-authored-by: Claude <noreply@anthropic.com>
A string saying "throw" refuses nothing, and a members-only region that
returns a value is an allow-list by another spelling; both now keep T2
firing, each pinned by a control.

Claude-Session: https://claude.ai/code/session_01CB6W87z22K2yjUCDyVrJRk
Co-authored-by: Claude <noreply@anthropic.com>
…ady bounds the reads

The memo changed no verdict and no read count (its ablation stayed
green), so it goes; the two read-count cases keep pinning the per-file
read.

Claude-Session: https://claude.ai/code/session_01CB6W87z22K2yjUCDyVrJRk
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/l label Oct 4, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 4, 2026
os-project-manager pushed a commit that referenced this pull request Oct 4, 2026
…not_planned at first grade, citing ruling 208

instrument-discipline.md says a report-only instrument gets no card, no
dev and no PR, and that its only in-flight work is deletion (ruling 208).
Nothing on the triage path read that line, so the grade never asked it.
One sentence in the grading block of references/triage-duties.md now
does: a card whose fix lands in a report-only instrument (the widening
tell, the half-state patrols) closes not_planned at first touch, citing
ruling 208, and the instrument's only in-flight work is deletion. No new
gate, label or script.

Enumeration pin (the two instances where the line was not read; a third
is a red reading of this sentence, not a new card):
  - #20969 / PR #21016 (landed)
  - #21465 / PR #21679 (held)

Paid in place, net 0 lines (120 -> 120, ceiling unchanged, no re-wrap):
the deleted line is the triage-side restatement of the filing door's four
classes; its owning copy stays in references/filing-gate.md (the class
list and the refusal of everything else, one line, with the four class
heads on the lines below it), reached from the pointer line kept right
under the deleted one.

Claude-Session: https://claude.ai/code/session_01CB6W87z22K2yjUCDyVrJRk
Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Oct 4, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Closed, unmerged, on the maintainer's word: 「21679 同意关闭」

Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-05T07:06Z. ⛔ Not a merge, ⛔ not a claim.

@objectstack-fleet objectstack-fleet Bot closed this Oct 5, 2026
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…t_planned at first grade, citing ruling 208 (objectstack-ai#21691)

Fixes objectstack-ai#21681

Clause-②: no

Tier S (`.claude/**` only). Draft; this run does not flip it ready,
queue it or arm auto-merge — the owning seat lands it after its at-tier
contract review.

## What changed

One protocol sentence in the grading block of
`.claude/skills/pm-dispatch/references/triage-duties.md`, as the triage
ruling on the card (comment 5976725561) specifies: a card whose fix
lands in a report-only instrument (the widening tell, the half-state
patrols — the judge-intent class `instrument-discipline.md` names) is
closed `not_planned` at first touch, citing ruling 208, and the
instrument's only in-flight work is deletion. No new gate, label or
script.

It is paid in place: one restated line of the same file is deleted, so
the file stays at 120 lines under its 120 ceiling. No other line is
touched (no re-wrap), and `SKILL.md`, `instrument-discipline.md` and
`scripts/pm/check-skill-line-ratchet.mjs` are untouched.

The enumeration pin — the two instances where the rule was not read — is
named in the commit message: objectstack-ai#20969 / PR objectstack-ai#21016 (landed) and objectstack-ai#21465 / PR
objectstack-ai#21679 (held). A third such card is a red reading of this sentence, not
a new card.

## Reading 1 (读数一): placement and payment

Tree: `objectstack-ai/objectstack` at `bc85776a87` (branch head), base
`251a7dd4b4`.

- **New sentence, line 64** (120 bytes; the line cap is 120), directly
under the two existing close-at-grade lines (the 「无则关」 line and the
open-P0/P1 `tooling` line), so the grading block now carries all three
close-at-grade cases together:

```text
- 只报告仪器(放宽 tell、半状态巡查)的修复卡首触即关 not_planned,引裁决 208;在途只有删除。
```

- **Deleted line, was line 40** — the triage-side restatement of the
filing door's four classes:

```text
- 立卡门四类:① 缺陷 / ② 维护者决定 / ③ 直派任务 / ④ 协调节点,⛔ 其余一律不立卡。
```

- **Its owning copy, kept on the reading path:**
`references/filing-gate.md` line 11, the same rule (four classes only,
everything else refused), with the four class heads on lines 12 / 16 /
17 / 18:

```text
- 立卡只为四类,正文首行写立卡门类别 ①–④;⛔ 其余一律不立卡,不论它叫什么。
- ① 有具名落点或复现的产品缺陷,即 `pm:queue` 的定义;其内的 `finding` 限三类且带 `reach:`。
- ② 只有维护者能做的决定,落卡即带「维护者速读」与四棱块。
- ③ 维护者直派的任务,正文引其原话。
- ④ 协调节点:跨仓/跨层父单与它的逐层子单。
```

The triage reader reaches it through the pointer line kept right under
the deleted one (now line 40): `首行写立卡门类别 ①–④;四类定义、⛔ 清单、三答与配额见
references/filing-gate.md`. The deleted line entered the protocol as the
summary of that file, in the same commit that created `filing-gate.md`
(`37ed9ae04b`), and moved here verbatim with the principle-only cut; it
is a restatement rather than a ruled clause: the ruled clause, with its
provenance quotation, lives in `filing-gate.md` and is unchanged.

- **Line count:** 120 → 120 (ceiling 120, headroom 0, unchanged). Diff:
1 file, +1 / −1.

## Reading 2 (读数二): gate output

Run at `bc85776a87`, after the final commit, exit codes captured before
any pipe:

```text
pnpm check:pm-skill-ratchet   exit 0
✓ check-skill-line-ratchet self-test: 157 cases pass.
✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/references/triage-duties.md: widest table row is 0 bytes (pin 0; headroom 0).
✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/references/triage-duties.md is 120 lines (ceiling 120; headroom 0).
✓ check-skill-line-ratchet: declared cross-file moves: 2, total ceilings down 477 lines.

pnpm check:pm-skill-id-lint   exit 0
✓ check-skill-id-lint self-test: 14 cases pass.
✓ check-skill-id-lint: 34 file(s) clean (pattern /#[0-9]{3,}/g).
```

`ruling 208` is spelled without a hash prefix, the way
`instrument-discipline.md` records it; the issue and PR numbers of the
pin are in the commit message only.

The full derived set (`node scripts/pm/dispatch-gates.mjs --commands`,
no paths, change set read from the merge base) is the same 18 commands
the dispatch listed, all exit 0. `--ran` reconciliation: `18 derived, 18
run, 0 NOT-MEASURED, 0 UNRUN` (a derived zero — every line carried its
exit code). One first-run `exit 3` is not a measurement:
`check:doc-formula-expressions` refused with PREREQUISITE NOT MET
(unbuilt `@objectstack/formula` / `@objectstack/lint`); after the build
it names (run under the shared verify lock, `VERDICT command-exit 0`) it
exited 0. `check:nul-bytes` exit 0, plus a control-byte self-scan of the
edited file: zero hits.

No changeset: `.claude/**` publishes nothing.

## Acceptance notes

- **Wording differs from the seat's suggested line, meaning unchanged.**
The suggestion measured 152 bytes against the 120-byte line cap. The
landed line drops the explicit `instrument-discipline.md` pointer; both
of its key terms still lead there (「只报告的仪器」 is that file's rule line,
and 「裁决 208」 its ruling record), and `reading-discipline.md` keeps its
pointer to it. 「在途只有删除」 echoes that file's own 「它的在途工作只有删除」.
- **The class is the instrument's role, not the path.** The sentence
covers a fix to a report-only, judge-intent instrument. A fix to a
hard-gate face that happens to share a file with a patrol is outside it,
and the sentence does not widen the class beyond the two instruments the
ruling names. Observation only, nothing filed. carrier: the triage seat,
applying the sentence at grade time.
- **Not decided here, as the ruling lists it:** whether the code PR
objectstack-ai#21016 landed is deleted under the report-only rule is the maintainer's
question; this PR does not touch `scripts/pm/check-widening-tells.mjs`.

## 维护者速读(草稿)

**改了什么:** 分诊职责文件 `triage-duties.md` 的定级段加了一句:修复落在「只报告仪器」(放宽
tell、半状态巡查)上的卡,首次定级就以 not_planned 关闭,并引裁决
208;这类仪器的在途工作只有删除。同时删掉同一文件里一行重复的「立卡门四类」摘要(完整原文仍在 `filing-gate.md`),文件仍是
120 行,行数上限不动。

**为什么改:** 裁决 208 早就规定只报告的仪器不立卡、不派开发、不开
PR,但分诊定级时并不读这条,于是两张这类卡被定级入队、派了开发,一张已经合并,一张被扣住,白花两轮开发。把规则写进分诊每次都读的定级段,首次定级就能拦住。

**风险与代价(含回滚):**
只改内部协议的一行文字,不新增门禁、标签或脚本。风险是分诊把与巡查同文件的硬门禁修复也误关,句子按「只报告」身份判断而不按文件路径,可以避免。已经合并的那份代码要不要删,不在本
PR 范围,留给您另行决定。回滚就是撤销这一个 commit。

**席位意见:**

**你要做的:** 无需操作:本 PR 是 Tier S(只改 `.claude/**`),席位复核通过后经合并队列落地;不同意请回一句。

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

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

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

Projects

None yet

2 participants