Skip to content

[finding] check-widening-tells 的 T1 把函数参数标注 ctx: z.RefinementCtx, 读成「Zod object 上的新键」—— 每个新增对象级 refinement check 的 PR 都白得一条假 T1 与一个 C5/exit 4,而最省事的补法恰是错的那个 #18721

Closed

Description

@os-bill

⏱️ 本卡所有读数取自同一动作:2026-09-17T16:51Z。逐条本席第一手实测,真阳性对照与假阳性并列。

一句话

scripts/pm/check-widening-tells.mjs 的 T1 匹配器把函数参数的类型标注读成了「Zod object schema 上的新键」。本仓库每一个导出的对象级 refinement check 都带同一个 ctx: z.RefinementCtx, 参数 ⇒ 任何新增一个这种 check 的 PR 都会白得一条 T1,并因此在 check-clause2-carriers --pair 上白得一个 C5 / exit 4。

实测:假阳性与它的真阳性对照并排

⛔ 假阳性(本轮 PR #18720 的真实 diff)
   ⏱️ 本块全部读数取于 2026-09-17T16:51Z
   git diff 72dd95fa5a..<head> -- packages/spec/src/ui/dashboard.zod.ts > d.patch
   node scripts/pm/check-widening-tells.mjs --declaration no --diff d.patch
   → EXIT=4
     ✗ T1 packages/spec/src/ui/dashboard.zod.ts:628
          — a new key on a Zod object schema — the accept set gains a spelling an author may now write

   而 :624-632 逐行打印出来是:
     export function checkDashboardWidgetMetricMeasureArity(
       widget: { id?: unknown; type?: unknown; values?: unknown },
       ctx: z.RefinementCtx,          ← :628,**第二个参数**
     ): void {
   ⇒ 现场**没有任何对象字面量**。

⭐ 真阳性对照(同一把 matcher、同一个文件路径,合成一条真的新键)
   +  brandNewAuthorableKey: z.string().optional(),
   → EXIT=4
     ✗ T1 packages/spec/src/ui/dashboard.zod.ts:701 — a new key on a Zod object schema
   ⇒ 匹配器对**真**新键是响的,所以上面那条是**假阳性**,⛔ 不是「匹配器不工作」。

⚠️ 本席第一次的亮控读 0 —— 那是**本席的对照建错了**(合成路径 `x.zod.ts` 不在已声明面上,匹配器压根不判它),
   ⛔ 不是关于匹配器的读数。换成仓内真实路径后它就响了。

形状是通用的,⛔ 不是这一个 PR 的偶然

ctx: z.RefinementCtx, 是本仓库对象级 refinement check 的固定签名。⇒ 每一个新增这类 check 的 PR 都会:

  1. 得一条假 T1;
  2. 于是 --pair 走 C5、exit 4;
  3. 而 C5 的处方是「要么改声明为 yes,要么修匹配器」—— 在这条假阳性上,改声明是错的补法,可它恰恰是最省事的那条。

⇒ 代价不是噪音,是把席位往错误的补法上推。

⭐ 脚本自己就规定了本卡该存在

C5 行的失败文本逐字:「if the tell is FALSE, repair it here in the matcher (scripts/pm/check-widening-tells.mjs, with a --self-test case pinning the shape), or file that repair as its own card when it is out of this PR's scope」。

本轮 PR #18720 的文件面是 packages/spec/**,scripts/pm/** 在它之外 ⇒ 走「另立卡」那一支。

建议的修法(⛔ 非裁定)

  • A:T1 在判定前先排除函数参数列表上下文(在 export function …( 与其闭合 ) 之间的 名: 类型, 行),并加一条 --self-test 用例把 ctx: z.RefinementCtx, 这个形状钉死。
  • B:更窄 —— 只把 z.RefinementCtx 这个类型名列入例外。⚠️ 更省事但更脆:换一个参数类型名就漏。
  • C:A + 把真阳性对照也钉进 --self-test,免得修假阳性时把真阳性一起修没了。⭐ 本席认为这条最值,因为上面那次「亮控建错」说明这两个方向很容易只测一边。

查重(MCP search_issues 含 closed)

⭐ #18560 / PR #18700 是近邻,⛔ 不是孪生 —— 本席逐个读过两者的态:都已 closed,题目是「check-widening-tells is BLIND to this repo's own declaring helpers」与「teach check-widening-tells the declaring helpers」。⇒ 那是假阴性方向(该响而没响),本卡是假阳性方向(不该响而响),同一把 matcher 的两个相反方向。

⚠️ 它们大概率想要同一个复核者、同一块 --self-test;⛔ 但本席不代并卡,合不合由维护者定。

出处

domain:spec seat 2(座位贴 #18549)复核 PR #18720 / 卡 #17779 时,dev 在 out_of_scope_findings 里交出来的;本席自己重跑了假阳性、并补了它缺的真阳性对照(以及记录了本席第一次把对照建错的那一次)。


Generated by Claude Code

Activity

  1. os-justin commented on Sep 17, 2026

    @os-justin
    Collaborator

    Claim: PM loop round 1
    Session: session_01Gqi43smmqjJ5sUrhfoPeKu
    Branch: claude/issue-18721-refinement-ctx-parameter-false-t1
    Worktree: objectstack-issue-18721
    Domain: domain:skills
    Seat: domain:skills#1
    File surface: scripts/pm/check-widening-tells.mjs only (T1 fires on + ctx: z.RefinementCtx, — the second parameter of an exported object-level refinement, this repo's prescribed signature — as 「a new key on a Zod object schema」, so every PR that adds such a check earns a false T1 and a C5 / exit 4 on --pair, and the cheapest repair (re-declare yes) is the wrong one; the seat REPRODUCED it on main 94b3f37be at 2026-09-17T21:24Z with the card's own probe (git diff 72dd95fa5a..09e16a5745 -- packages/spec/src/ui/dashboard.zod.ts, 202 lines, --declaration no): exit 4, T1 at dashboard.zod.ts:628, the line being + ctx: z.RefinementCtx, — although #17618's parameter decline already exists (PARAMETER_LIST_HEAD :2004, 「Does this side-line sit inside a function's PARAMETER LIST? — over its own hunk」 :2008, the header :283–:300 naming PR #17616's identical line as the measured case and the pin :4837 「#17618's parameter decline is untouched by the wider vocabulary」 on a three-line synthetic hunk); deliverable = the seat's reading of WHY the existing decline misses THIS diff (the hunk shape — the header export function …( and the parameter sit in one 202-line hunk of a real PR, not the three-line synthetic the pin uses; find the exact branch that fails), the decline repaired at that branch (the card's route C: A + the true-positive control pinned beside it), ⛔ not route B (a type-name exception), ⛔ not a re-declare; pins = the card's exact probe as a fixture (the real hunk shape, T1 silent at :628) AND the card's true-positive control (+ brandNewAuthorableKey: z.string().optional(), on the same file, T1 FIRES with its line) in one battery, plus the existing :4837 pin untouched; the card's near-neighbour #18560 / PR #18700 (the false-NEGATIVE direction, closed) is read, ⛔ not reopened; an ablation from the committed fix that reds exactly the new probe pin and nothing pre-existing); non-governed scripts/pm/** ⇒ draft, skip-changeset, the seat lands it by CCR after the in-seat contract-tier review
    Container & model: M(现有参数排除为何漏 + 修那一支 + 真阳性对照同钉), mode:subagent, model: claude-opus-5 — node scripts/pm/dispatch-gates.mjs --tier --repo objectstack-ai/objectstack scripts/pm/check-widening-tells.mjs at 2026-09-17T19:21Z on the fast-forwarded tree (f1c9bb305): 「Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s)」 — unchanged surface; default tier taken; the seat's contract-tier review at delivery
    Clause-②: no
    Thread-read: the card body (filed by the domain:spec seat 2 with its labels; no grading comment on the thread at 2026-09-17T21:24Z)
    Serial constraints cleared: PR #18750 (#18702, the file-local resolver + the placeholderFree( row) on this file LANDED e2050cefc at 2026-09-17T19:36:09Z; 30 open PRs' file lists read at 2026-09-17T20:54Z — none touches scripts/pm/check-widening-tells.mjs (the one PR opened since, #18795, is this seat's own on skills/**); os-verify-lock.sh --status at 2026-09-17T21:24Z: lock is free; shared checkout at origin/main 94b3f37be. Hot-file queue behind this card: none. Slot: the one freed by #18527's delivery (PR #18795, four-piece pending CI); #18683 and #18664 in flight; PR #18784 (#18661) in the merge queue; PR #18786 (#18693) LANDED 94b3f37be at 2026-09-17T21:22:43Z. Total order at 2026-09-17T21:24Z: p2 — #18536 (SKILL.md serial) · #18699 (behind PR #18725) · #18743 (SKILL.md serial) · #18744 (HELD: its measurement burns the shared GraphQL pool) · #18755 (SKILL.md serial) · #18764 (behind #18683); p3 by age — #18622 · #18662 (behind PR #18784) · #18665 (SKILL.md serial) · #18686 (behind #18744) · #18705 (MANDATORY, published skill) · this card.
    Ruling-ref: the card body


    Generated by Claude Code

  2. os-justin commented on Sep 17, 2026

    @os-justin
    Collaborator

    os-dev-report

    {
      "issue": 18721,
      "status": "done",
      "branch": "claude/issue-18721-refinement-ctx-parameter-false-t1",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/18804",
      "head_sha": "7dd97e2bdff0d3991a58cd69bb38d70782564b16",
      "files": [
        "scripts/pm/check-widening-tells.mjs"
      ],
      "session": "session_01Gqi43smmqjJ5sUrhfoPeKu",
      "premise_still_valid": true,
      "summary": "The card's premise reproduces on the CURRENT tip (origin/main 7572329069, not only the card's 94b3f37be): the rebuilt probe exits 4 with T1 at dashboard.zod.ts:628 on `+  ctx: z.RefinementCtx,`, and the true-positive control on the same real file fires at :701 on the same run - a false positive, not a dead instrument. WHY, measured: `enclosingDelimiter` abandoned its walk (answering `null`, which every caller reads as 'keep the tell firing') at the FIRST closer arriving with an empty stack, and a real hunk's first line is CONTEXT - here `  });`, the tail of the previous declaration - so the reading was over 108 lines before the hunk showed the `export function ...(` head. Repaired at that branch (route A / C, NOT route B's type-name exception): an underflow drops the closer and the walk continues, because the openers a hunk shows are strictly inside the ones it did not, so the shown stack is a suffix of the real one; an empty shown stack still answers `null`, so the reading stays positive-evidence-only. Pinned in one new 14-case battery in both directions, including the removed-side half where the repair closes a false NEGATIVE too. SCHEMA_PROPERTY_FORMS untouched at 10 rows; #18560 / PR #18700 and #18702 / PR #18750 rows, fixtures and batteries untouched. Assignee was empty at pickup and was left empty, as the os-dev definition prescribes.",
      "measurements": {
        "before_false_positive": "git diff 72dd95fa5a..09e16a5745 -- packages/spec/src/ui/dashboard.zod.ts (202 lines, ONE hunk) | node scripts/pm/check-widening-tells.mjs --declaration no --diff d.patch -> exit 4, 'T1 packages/spec/src/ui/dashboard.zod.ts:628 - a new key on a Zod object schema', patch line 116 = '+  ctx: z.RefinementCtx,'. Taken on origin/main 7572329069.",
        "before_true_positive_control": "'+  brandNewAuthorableKey: z.string().optional(),' inserted into DashboardWidgetSchema in the worktree as a REAL git diff on the REAL path, then `git checkout HEAD -- packages/spec/src/ui/dashboard.zod.ts` -> exit 4, 'T1 packages/spec/src/ui/dashboard.zod.ts:701'. The matcher works; the :628 row is false. (The card's own first control was a mis-build on a synthetic path; this one is on the probe's path.)",
        "why_with_proving_line": "Instrumented over the real patch: newFile[0] kind=context text='  });' is the hunk's FIRST line and is THE PROVING LINE - its two closers underflow a stack that has seen no opener, so enclosingDelimiter returned null at the branch `if (stack.length === 0) return null;` (:1985 on main). inParameterList -> false, enclosingDelimiter -> null, while the SAME walk restarted at the function head (newFile index 108) answers {opener:'(',head:'export function checkDashboardWidgetMetricMeasureArity'}. Not the 202-line hunk length, not the object-literal type on the first parameter (its braces close on their own line), not the distance to the head - one branch.",
        "repair": "scripts/pm/check-widening-tells.mjs enclosingDelimiter(): `if (stack.length === 0) return null;` -> `if (stack.length === 0) continue;` plus its reasoning comment; docblock rewritten; new header section 'The eleventh accidental variable #18721 removed - a hunk's LEADING CONTEXT'; the #17618 header paragraph gains the one-sentence correction that its decline did not reach a real diff until this round.",
        "pins": "New battery \"#18721 - a hunk's LEADING CONTEXT is not a reason to abandon the parameter reading\", 14 cases, registered in SELF_TEST_BATTERIES at 14. Both directions in one battery: PR #18720's own hunk reduced only as far as the failing branch requires (leading context + function head + object-literal-typed first parameter + the ctx line) silent at :628 with a CLEAN verdict; the true-positive control on the same file FIRING at :701; a new key behind the same underflowing context still firing; a real key after the parameter list closes still firing with the shape member as the reported row; the branch itself (opener after underflow / underflow with no opener / no leak past the close / no hunk crossing); and the OLD side, where the repair removes a phantom #16943 budget so a genuine key FIRES. The existing #18560-battery pin (#17618's parameter decline untouched by the wider vocabulary) and #17618's own underflow pin are byte-unchanged and green.",
        "self_test": "node scripts/pm/check-widening-tells.mjs --self-test -> exit 0 :: 473 cases pass (459 before, +14). No pre-existing case changed its verdict.",
        "after_readings": "probe -> exit 0 (clean, no widening tell); true-positive control -> exit 4 at :701; node scripts/pm/check-clause2-carriers.mjs --pair 18804 -> exit 0 (the C5 the card describes is gone).",
        "ablation": "From the COMMITTED fix (HEAD 7dd97e2bdf), under `trap restore EXIT INT TERM` with absolute paths: mutation `continue` -> `return null` applied by a node literal replace that refuses any anchor count other than 1; on-disk proof = anchor counts removed-text 1->0, injected-text 0->1, blob 025f8e51ec -> 9459620c63. Readings under the ablation: --self-test exit 1 with 6 of 473 cases failed - ALL SIX in the new #18721 battery, ZERO pre-existing; probe exit 4 (T1 back at :628); control exit 4. Restore verified by hash (9459620c63 -> 025f8e51ec == HEAD blob) and by `git diff HEAD` = 0 lines. No build/dist exists for a scripts/pm/*.mjs file, so ablation-dist-preflight is not applicable and the hash+anchor-count proof is the on-disk evidence. An earlier attempt was a NO-OP (perl with a double-escaped pattern: anchor counts 1->1, blob unchanged); the script's own guard refused it and exited non-zero rather than reporting a reading - recorded rather than silently re-run.",
        "gates": "Derived from the worktree with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no hand-fed paths; 1 path vs merge base 757232906). 29 families, ALL exit 0. Reconciled with --ran: 29 derived, 29 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero - every row carries its exit code). node scripts/check-ci-filter-parity.mjs :: 0 | check-closing-keyword-parity.mjs :: 0 | check-closing-keyword-parity.mjs --self-test :: 0 | check-comment-mask-corpus.mjs :: 0 | check-declaration-mirrors.mjs :: 0 | check-declaration-mirrors.mjs --self-test :: 0 | check-scripts-symbol-anchors.mjs :: 0 | check-scripts-symbol-anchors.mjs --self-test :: 0 | check-self-test-wired.mjs :: 0 | check-self-test-wired.mjs --self-test :: 0 | check-self-test-workflow-commands.mjs :: 0 | check-self-test-workflow-commands.mjs --self-test :: 0 | check-whole-set-label-write.mjs :: 0 | check-whole-set-label-write.mjs --self-test :: 0 | scripts/pm/bare-root-worklist.mjs --self-test :: 0 | pnpm check:agent-test-spelling :: 0 | check:bash32-floor :: 0 | check:cli-command-ids :: 0 | check:cross-package-test-inputs :: 0 | check:driver-memory-census :: 0 | check:entry-guard :: 0 | check:nul-bytes :: 0 | check:parse-guard :: 0 | check:pm-dispatch-gates :: 0 (DETACHED, 742.1s, 1809 cases) | check:pm-widening-tells :: 0 | check:pnpm-filter-targets :: 0 | check:ratchet-remedy-authority :: 0 | check:refd-timer-probe :: 0 | check:watch-hint-literal :: 0. PLUS, outside the derived set: pnpm check:pm-clause2-carriers :: 0 (838 cases, the gate that consumes this file's verdict) and repo-wide `pnpm lint` (eslint . --no-inline-config) :: 0, run on the final commit 7dd97e2bdf - a full run, not a narrowing. ONE red in the first sweep, fixed: check-scripts-symbol-anchors.mjs exit 1 - the new header cited a path:LINE form and a line number is not an anchor form; rewritten to the symbol anchor packages/spec/src/ui/dashboard.zod.ts#checkDashboardWidgetMetricMeasureArity, green on re-run. NOT MEASURED, by the deriver's own account: 2 families taking a value from the workflow, 1 path-scheduled CI job (Test Core shards), 11 wide-population families, 53 artifact-roster families and 14 changeset-pending families are each outside the 29 and are CI's.",
        "pr_state": "Draft, base main, `Fixes #18721` as body line 1, `Clause-②: no` in the body AT CREATION, label `skip-changeset` (additive POST, read back: labels = [skip-changeset], no drift). Body read back in full after the write: byte-identical to what was sent except the trailing newline GitHub strips; exactly ONE footer, session-URL form; no angle-bracket fragment survives anywhere in it. `needs:contract-review` is NOT on the PR - neither added nor removed by this run."
      },
      "tests": "node scripts/pm/check-widening-tells.mjs --self-test -> exit 0 :: 473 cases pass (459 -> 473). Probe after the repair -> exit 0 clean; true-positive control -> exit 4 at dashboard.zod.ts:701; check-clause2-carriers --pair 18804 -> exit 0. 29 derived gate families all exit 0, reconciled with --ran (0 UNRUN, 0 NOT-MEASURED, derived); plus check:pm-clause2-carriers :: 0 and repo-wide pnpm lint :: 0 at 7dd97e2bdf. Ablation from the committed fix: mutation proved on disk by blob hash (025f8e51ec -> 9459620c63) and anchor counts (1->0 / 0->1), --self-test exit 1 with exactly 6 of 473 failing and all six in the new battery, restore verified by hash back to 025f8e51ec and `git diff HEAD` = 0 lines, under a trap.",
      "mcp_calls": "0 - no MCP GitHub tool was called, read or write.",
      "api_writes": "3 REST proxy writes: POST /repos/objectstack-ai/objectstack/pulls (draft PR 18804); POST /repos/objectstack-ai/objectstack/issues/18804/labels (skip-changeset, additive, Content-Type: application/json); POST /repos/objectstack-ai/objectstack/issues/18721/comments (this report). Reads: GET the card, its comments, the PR and its labels. Plus git pushes on the feature branch only (one force-with-lease on my own unshared branch to squash a wip commit; nobody else has pushed it and the PR carries no reviewer).",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: the #17618 battery's pin \"a closer with an empty stack is UNDERFLOW - the hunk began inside something it never showed\" still asserts `=== null` and is still green and still literally true, but its sentence now describes the ANSWER rather than a walk that stops. Left byte-unchanged on purpose. Successor: the next repair round on scripts/pm/check-widening-tells.mjs (this file has taken #18560, #18640, #18702 and now #18721), i.e. the domain:skills seat.",
        "noted, not filed: PARAMETER_LIST_HEAD reads the callable head only from text on the SAME line as the `(`, so a declaration split across lines (`export function` on one line, `  checkThing(` on the next) still leaves the tell firing. Pre-existing, deliberate per that constant's own docblock ('a prefix this list does not know leaves the tell where it was' - the loud direction), and unchanged by this round. Successor: the same next repair round.",
        "PROCESS NOTE for the seat: the PR body carries no `## Acceptance notes` section - the two rows above were written after the body was posted, and the os-dev definition forbids me PATCHing a PR body after creation. If the seat wants them on the PR, they belong in a review comment, verbatim from this report."
      ]
    }

    Generated by Claude Code

  3. os-justin commented on Sep 17, 2026

    @os-justin
    Collaborator

    ACCEPT — in-seat contract-tier review of PR #18804 (head 7dd97e2bd, report 5721757240) by the skills seat, 2026-09-17T22:13Z. Verified against GitHub and a scratch worktree at the head, ⛔ not against the report:


    Generated by Claude Code

  4. os-justin commented on Sep 17, 2026

    @os-justin
    Collaborator

    LANDED — PR #18804 (#18721, check-widening-tells' parameter decline survives a hunk's leading context) merged through the queue at 2026-09-17T22:39:56Z (merged_at), squash a7bafc29a (single parent e7eb4e918, an ancestor of origin/main; enclosingDelimiter's underflow branch reads continue on main — an underflowing closer is dropped and the walk goes on, an empty shown stack still answers null; the #18721 battery of 14 registered, its --self-test at this head answered 473 cases); armed by this seat through the CCR route after the in-seat ACCEPT 5721940740 — ready 2026-09-17T22:13:59Z, added_to_merge_queue 2026-09-17T22:14:01Z. The card auto-closed completed at 2026-09-17T22:39:57Z on the PR's Fixes; pm:dispatched is stripped in the same act. Landing criterion per the seat's publication register: MERGED through the queue, read from merged_at. The seat's own three probe readings stand: the card's probe exit 4 at :628 on main before, exit 0 at the head after, and the true-positive control at :701 firing at the head — so every PR adding an object-level refinement with ctx: z.RefinementCtx stops earning a false T1 and a C5 on --pair. Serial behind it on this file: none queued.


    Generated by Claude Code

  5. added a commit that references this issue on Sep 28, 2026
    a7bafc2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions