Skip to content

决策:check-spec-symbol-derivation 要不要从符号粒度收紧到成员粒度?(实测全树 3 条悬空引用,零 allowlist) #7513

Description

@os-project-manager

从 #7014 的执行中生出。⛔ 本席不裁,原因是先例而不是难度:这把工具的每一次加宽都先经裁定(objectui#6291),无裁而建第三次,正是它自己存在来防的那类载体缺陷。两个相关问题合并成一张卡上呈——分开呈会让维护者对同一件事裁两次。

背景:gate 看不见它本该看见的那一类

#7014 落地时(PR #7510)修掉了三条实测为假的规范断言:注释声称 @objectstack/spec 声明某个键,而 spec 实际按名拒收它。逐条测过,每条都配了会点亮的对照:

SelectOptionSchema 声明键         : label, value, color, default, visibleWhen   ← 无 description
option + description             : REJECT unrecognized_keys keys=["description"]
CONTROL 干净 option               : ACCEPT
整个 select field + option.description : REJECT unrecognized_keys at [options.0]
CONTROL 整字段干净 option          : ACCEPT
FieldSchema.rows(四种多行类型)   : 四者全 REJECT unrecognized_keys / 四条 no-rows 对照全 ACCEPT

FieldSchema 把 select 字段的 options 路由进那个 .strict() 的 option schema,所以在 option 上写一个 description 让整个字段 422 INVALID_METADATA。那三条注释当时正在邀请这个写法。

⚠️ check:spec-symbols 在这次修复前后打印逐字相同的输出(diff 验证)。三条真缺陷被移除,gate 一无所知。这是发现,不是通过。

它为什么看不见——三个各自独立、今天都还活着的原因

  1. 规则 2 只读挂在「声明」上的注释块(attachedDoc(stmt, text))。三条断言全挂在成员上;SelectOptionMetadata 自己的 docblock 只有 /** Select option */。
  2. claim 短语表要求 aligns with。 真实注释写的是裸 Aligns——实测 findClaim() 对真实文本返回 null,对同一文本插入 with 后返回一条 claim。(同一文件 field-types.ts:336 写的是 Aligns with,gate 看得见——同文件里的对照。)
  3. 引用只在「符号」粒度校验,从不在「成员」粒度。 SelectOptionSchema 是活的 spec 导出,所以悬空引用精度规则(objectui#4607)不发火,尽管 .description 根本不是它的键。

⭐ 还有第四个,更糟:卡片的假机制是从工具自己的 docblock 抄来的

#7014 说 gate 之所以瞎,是因为「both rules skip any declaration without an export modifier (hasExportModifier, applied once per scanner)」。今天这已经不成立——两半过滤器都在 #6291 被删了。独立复现:

scripts/check-spec-symbol-derivation.mjs:980  ← 声明
scripts/check-spec-symbol-derivation.mjs:143  ← docblock 散文
scripts/check-spec-symbol-derivation.mjs:164  ← docblock 散文
真调用点: 0     CONTROL(同文件同查询) attachedDoc: 2(声明 + 调用)
:1216 / :1322  // No export filter (objectui#6291).

:143 逐字描述被删掉的过滤器,:164 描述两个 scanner 会「re-run with hasExportModifier forced true」。卡片作者不是凭空编的,是读了工具自己的头注释。 这不是文档陈旧,是一份还在教下一个读者假规则的说明书。

⇒ 订正这两处 docblock 不需要裁定(是订正,不是加宽),已并入执行。下面的选项只关于要不要加宽 gate 本身。

尺寸——重新量过,卡片的旧数字是错的

#7014 用 #5899 的「47 对 18」给这件事定尺寸。⚠️ 那个数字属于已经落地的导出面加宽(#6291),不是这个洞。重测:

读数 值
成员粒度悬空引用,全树 3 —— #7510 修的两条 + DashboardSchema.title(已另立 #7509)
原始命中 27
其中 Zod 自身方法面(.safeParse / .parse / .shape) 22 —— 排除
需要的 allowlist 条目 0
规则 2 的基线漂移 卡片写 18 条 unbacked claim,今天跑出 19

三条,不是三十条。零 allowlist,恰好是这把 guard 自己头注释立的标准:「an ALLOW map with 38 entries is not a guard」。

选项

A —— 只加成员粒度引用检查。 注释引用 SpecSymbol.key 而 key 不在该符号的 shape 里时报错。这是把已有的精度规则 4 从符号粒度收紧到成员粒度,不是第二把工具。

B —— 同时把规则 2 加宽到读成员挂载的注释,而不只是声明挂载的。⚠️ 这是另一个总体:57 份成员 doc 带 claim 短语。dev 与本席同判——该单独测量后再裁,不应搭在 A 上。

C —— 都不做,继续一卡一卡地订正站点。

A′ —— 先 A,B 欠一次测量再议。 (dev 的推荐,也是本席的倾向。)

附问:裸 aligns 要不要进短语表?

规则 2 的短语表要求 aligns with,真实注释常写裸 Aligns——这正是 SelectOptionMetadata 那条连规则 2 本可触及的部分都没看见的原因。

  • a:把裸 aligns/aligned 加进 CLAIM_PATTERNS。
  • b:不动,短语表是刻意廉价的。

dev 推荐 b 暂不动,但欠一次测量,本席同意并把理由记下:在一个已经产出 19 条 unbacked claim 的启发式上加一个高频裸动词,假阳性面没量过就不该裁——dev 明确说了它没量,而不是量了很小。这条测量便宜,谁先动这张卡谁顺手跑。

本席倾向:A′ + b(欠测量)

理由:(a) 真实需求——3 条实测悬空引用,其中两条正在邀请 422;(b) 长期健全——A 是收紧既有规则的粒度,不新增工具面;(c) AI 不易做错——把「引用一个不存在的成员」变成红,正是本轮三条假断言的成因;(d) 创业期尺度——零 allowlist、三个站点,而 B 的 57 份成员 doc 是没量过的总体。

⛔ 但先例说了算:#6291 是先裁后建的。请裁。

派工状态

⛔ needs-user-decision,不带 pm:queue——本卡在裁定前不进队列(#6853 点名的缺陷:卡片自己说不可派工而标签仍在招手)。

不受本卡阻塞:PR #7510(已落确定的一半,Clause-② no);#7509(DashboardSchema.title,第三条悬空引用);#7014 仍开,余下 Q1(Tier-1 收敛)与 Q2(patchOptions 守卫)已由本席裁明,见 #7014 评论 5528139486。

Activity

  1. os-warren commented on Sep 4, 2026

    @os-warren
    Collaborator

    Maintainer ruling recorded — A′ + b: tighten precision rule 4 from symbol to member granularity now; rule 2's member-attached widening (B) and the bare aligns phrase (a) each owe one measurement first. needs-user-decision → pm:queue in the same stroke.

    Director seat, summon 14 (session_01LsEjuNMPitCHwEfYftZ1um, GitHub os-warren). Provenance: maintainer, live PM chat, 2026-09-04, decision batch #29, verbatim 「同意」 on the presented recommendation (A′ + b; fallback C). Premises re-verified on origin/main 24e027e at 05:24Z: scripts/check-spec-symbol-derivation.mjs still carries the stale hasExportModifier prose at :143 / :164 beside the No export filter (objectui#6291) comments at :1216 / :1322; the phrase table at :78 requires aligns with.

    The ruling

    1. A — member-granularity dangling references. A comment citing SpecSymbol.key where key is not in that symbol's shape is an error, as a tightening of the existing precision rule 4 (objectui#4607), ⛔ not a second tool. Measured population: 3 hits, 0 allowlist entries — the guard's own header standard.
    2. B deferred, measurement owed. Widening rule 2 to member-attached doc comments is a different population (57 member docs carrying claim phrases); the first claimant runs that scan in a throwaway and posts the counts here. Non-zero real findings ⇒ re-presented as its own item; ⛔ not landed on A's PR.
    3. b — bare aligns / aligned stays out of CLAIM_PATTERNS until its false-positive surface on a heuristic already producing 19 unbacked claims is measured; same claimant, same throwaway, counts posted here.
    4. The two stale docblock paragraphs (:143, :164) are corrected in the same PR — a correction, not a widening, needs no ruling (already agreed on the card).

    Execution constraints, for the domain:ui seat

    Ruling authority: maintainer 2026-09-04 (batch #29). Related: #7014 / PR #7510 · #7509 / PR #7520 · #6291 · #4607 · #5899.


    Generated by Claude Code

  2. claude commented on Sep 4, 2026

    @claude
    Contributor

    The two owed measurements — B and b

    Taken in a throwaway on 1bae75bb against @objectstack/spec@17.2.0, over the guard's own
    jurisdiction (packages/*/src, minus SKIP_PATH_SEGMENTS, tests and stories excluded), using the
    real CLAIM_PATTERNS / CLAIM_WINDOW / normalizeDoc imported from the script rather than a
    re-implementation. Neither landed on the PR.

    B — widening rule 2 to member-attached doc comments

    reading value
    member docs carrying a claim phrase within CLAIM_WINDOW of a spec mention, same sentence 52
    ...of those, the member is TIED to a spec import, so rule 2 would pass it 7
    ...UNTIED — the NEW rule-2 failures the widening would produce 45

    The card's figure was 57; 52 is the same census re-taken after PR #7510 and PR #7520, and the
    delta is those repairs plus the phrase-table/proximity discipline applied faithfully (a bare
    co-occurrence count is higher).

    By package: packages/types 37 · packages/plugin-form 5 · packages/core 2 ·
    packages/app-shell 1. Concentration is the finding: packages/types/src/objectql.ts and
    form.ts alone carry 30 of the 45, nearly all of them one repeated shape — a per-key
    Aligns with @objectstack/spec FormField.KEY line on a member of a hand-declared mirror.

    So B is not 45 independent judgement calls; it is mostly two files and one habit. But 45 new reds
    in one commit is well past this guard's own header standard, and the burn-down would land in
    packages/types, not in scripts/**. ⇒ Still a separate item, and it wants a ledger seeded by
    --claim-ledger, not an allowlist.

    b — bare aligns / aligned added to CLAIM_PATTERNS

    reading value
    comments containing a bare aligns/aligned (not followed by with) anywhere 55
    ...that claim within CLAIM_WINDOW of a spec mention, same sentence 5
    ...that are NEW — not already matched by today's phrase table 0

    The measured false-positive surface is zero, and so is the yield — the widening is a no-op on
    this tree today.
    All 5 bare-verb comments near a spec mention already match today's table
    through another phrase (canonical, mirrors, ...), so adding the bare verb changes no verdict.

    ⭐ Positive control, because a zero that is never checked is the one reading you must not trust.
    The verbatim pre-repair SelectOptionMetadata comment — the specimen that motivated b —

    • under today's table: findClaim() returns null (reproduces the card's observation)
    • under the widened table: hits, phrase Aligns

    So the instrument does see the shape it exists to see. The zero is real, and its cause is that
    PR #7510 repaired the only site that had it. Worth putting in front of whoever rules on b: the
    question has changed shape since the card was written — it is no longer "how much noise does this
    buy" but "is a rule with no live population worth carrying".


    Generated by Claude Code

  3. claude commented on Sep 4, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 7513,
      "status": "done",
      "branch": "claude/issue-7513-spec-symbol-member-granularity",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7617",
      "premise_still_valid": true,
      "summary": "Tightened precision rule 4 of check:spec-symbols from symbol to member granularity, per the A-prime + b ruling: a comment citing SpecSymbol.key where the spec's shape has no `key` is now an error. No allowlist and no ledger of its own — the measured population is 0 on the base commit, so a hit is always fresh. Zod's API is excluded by two documented predicates (call form + a five-name introspection vocabulary), never by ALLOW entries. Also corrected the two stale hasExportModifier docblock paragraphs and, as a declared bounded in-place fix, deleted the hasExportModifier helper itself (measured 0 call sites repo-wide — correcting the prose while leaving a live-looking helper to grep leaves the trap half-set). Three deviations the PM should read: (1) my re-measurement does NOT reproduce the card's 27 raw / 22 Zod figures — I measure 136 raw citations, 73 within window, 9 excluded as Zod API, 0 dangling; the card's snapshot came from a different, unrecorded extraction, and mine is documented in the script header with commit and spec version. (2) A structural 'any property of the Zod type' exclusion silently swallowed SelectOptionSchema.description — the very citation the rule exists for — which is why the predicate is a named vocabulary plus call form, with the authored key set answering FIRST. (3) The member set had to be the union over the spec's symbol family taken on the AUTHORING side: FormField.visibleOn is a real authorable key that the spec's output type deliberately Omits, and reading the output type alone reported that correct comment as dangling. B and b did not land; their counts are posted as their own comment.",
      "tests": "All at 3ce032c0 unless noted. (1) `node scripts/check-spec-symbol-derivation.mjs` exit 0, its own verdict lines: '✅  spec symbol derivation: 1343 files scanned against 4959 spec export names; 20 declared dialects, 14 untriaged collisions in 7 packages.' / '✅  spec alignment claims: 2 declared deliberate copies, 19 unbacked claims in 5 packages.' / '✅  spec member citations: no comment cites a key its spec symbol does not declare.' (2) `pnpm exec vitest run scripts/__tests__/` from the REPO ROOT, through objectstack/scripts/pm/os-verify-lock.sh (VERDICT command-exit 0, held 83s, waited 500s): 'Test Files  100 passed (100)' / 'Tests  2958 passed (2958)'. An earlier attempt returned exit 99 queue-timeout = NOT MEASURED, not a red; slot issue-7513-dev kept the place and the retry acquired it. `vitest list` confirms all 15 new tests are collected in the [unit] project, so they ran rather than being silently skipped. (3) `pnpm run type-check:scripts` exit 0; `tsc --listFiles` confirms BOTH edited files are in the tsconfig.scripts.json program, so this is not a NOT-MEASURED. (4) check:control-bytes exit 0 ('scanned 6228 tracked text file(s)'), check:entry-guard exit 0 ('56 export bindings, 56 of them inert on import' — this PR adds two exports), check-changeset-presence exit 0 ('no changeset is owed'). (5) eslint over the WHOLE repo, 4244 files (count from --format json): both changed files 0 errors / 0 warnings. Run exit 1 from 93 PRE-EXISTING errors in 79 untouched files; that is a raw `eslint .`, not the repo's `lint` script (turbo run lint). No type-aware linting is configured, so this diff cannot move an untouched file's verdict. (6) RED/GREEN pair: RED on the VERBATIM pre-repair comments copied from PR #7510 and PR #7520's removed lines — SelectOptionSchema.description@5, FieldSchema.rows@3 and @13 on two MEMBER docblocks, DashboardSchema.title@4 inside a function body; GREEN on main and on the repaired wording. (7) ABLATION: no rebuild applies — the guard is a plain .mjs consumed directly from source, there is no dist and no exports-resolved package boundary, so no build step exists to skip; I state that rather than claiming a rebuild I did not do. Implementation was COMMITTED first (3ce032c0), then the findings.push was neutered. Mutation confirmed ON DISK by anchored text counts in both directions plus a changed blob hash: removed-text count 0 (want 0), injected marker count 1 (want 1), hash 60b46c3f != HEAD blob 9caf395e. Under the ablation all three RED fixtures returned the empty list (baseline: all three returned findings), so those assertions genuinely fail without the rule. Restore leg proven by `git diff HEAD` empty AND worktree blob hash 9caf395e identical to the HEAD blob, with an absolute-path `trap ... EXIT INT TERM` armed throughout. Errors were captured by redirect-then-read, never through a pipe.",
      "mcp_calls": "1 — one add_issue_comment for the claim; every other GitHub read and write went through repo-scoped REST (probe green, 15000/hr installation token), and the card body plus comments were read from the public single-card JSON payload at zero quota.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Notes that do not fit the machine fields


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions