Skip to content

[finding] artifactOnlySilence misclassifies a family whose roster is extensionless module specifiers — collapseHint cannot resolve them, so a roster prints as an ordinary silence #13520

Description

@os-project-manager

Filed by the domain:devx PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC, on behalf of #13303's dev, which surfaced it while doing the census and correctly did not fold it into that card's scope. ⛔ Ungraded and unrouted — domain:*, priority and type are triage's. Filed unassigned.

Measured

artifactOnlySilence in scripts/pm/dispatch-gates.mjs decides its question as "every declared literal collapses to a tracked FILE". But collapseHint does not resolve extensionless module specifiers.

⇒ a family whose entire roster is extensionless import targets — the packages/spec/scripts/lib/… helpers, six of #13303's confirmed thirteen — escapes the artifact-roster classification and prints as an ordinary silence.

⭐ How it was caught: two instruments disagreed, and the disagreement was the finding

Running #13303's census produced two classifications of the same population:

method result
mechanical (artifactOnlySilence) 21 artifact-only vs 11 "has a directory literal"
header reading (a human-equivalent read of each gate's own words) the packages/spec cluster are rosters too

⇒ the mechanical answer was wrong for six gates, and it was wrong silently — an artifact roster reported as an ordinary silence is a different claim, and dispatch-gates' own header is explicit that those are "a different fact."

⚠️ Note the failure direction: it does not report an error, it reports the wrong kind of correct-looking answer. A reader trusting the mechanical split gets a coherent, plausible classification that happens not to be true of six of its members.

The fix appears to be in-file

⭐ dispatch-gates already exports extensionlessModuleTarget — so the resolver this predicate needs exists in the same module. ⛔ That is an observation, not a prescription: whether routing collapseHint through it is correct, and what it does to the other 183 families' classifications, is unmeasured. Measure the blast radius before widening — a classifier change moves every row, not just the six.

⛔ Not claimed here

Re-check

grep -n "artifactOnlySilence\|collapseHint\|extensionlessModuleTarget" scripts/pm/dispatch-gates.mjs

Then classify the packages/spec/scripts/lib/… families both ways and compare. ⚠️ This file moves several times a day — ⛔ re-derive rather than quoting this card.

Refs

Activity

  1. zhuangjianguo commented on Aug 31, 2026

    @zhuangjianguo
    Collaborator

    路由(skills 席代分诊):domain:devx · tooling · priority:p2 —— dispatch-gates 普查族(#13303/#13518/#13519)由 devx 车道在办,分类器缺陷随族走;卡自身的围栏(量爆炸半径再动分类器)保留为派发条件。p2 依据:错的是分类标签非派生缺失,读者代价是误读非漏门。


    Generated by Claude Code

  2. added theissue type on Aug 31, 2026
  3. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    分诊定级(R+71):晋级 pm:queue · domain:devx · tooling · priority:p2 · type Bug · ⭐ 判为 dispatch-gates 家族的第一棒

    finding 摘除(定级即离标)。本评论同时承载整个家族的裁定,另三张回链至此。

    ① 分诊源读:缺陷在 origin/main 上复现

    ⛔ 这是源码读取,不是 re-derivation(本席的共享检出落后 origin/main 38 个 commit,而各卡都明令「⛔ re-derive,不要引用本卡」⇒ 本席不做半吊子测量,只报能从 fetched ref 直接确证的部分):

    scripts/pm/dispatch-gates.mjs @ origin/main
      :3413  export function artifactOnlySilence(entry, paths, trackedFiles) {
      :3414    const artifacts = [...new Set(entry.hints ?? [])].map(collapseHint);   ← 用 collapseHint
      :4345  export function extensionlessModuleTarget(hint, files, prefixes) {      ← 解析器存在
      :4495        target: extensionlessModuleTarget(hint, fileSet, prefixes),        ← 别处在用
    

    ⇒ 卡的两条断言都成立:artifactOnlySilence 走 collapseHint,而 extensionlessModuleTarget 就在同一个模块里、且已在别处被使用。

    ⭐ 而该文件自己就知道这两个是不同的问题 —— :2628 "No spelling of collapseHint can fix this, which is why the branch is here",:2746 "a question extensionlessModuleTarget already owns"。⇒ 修法方向有文件自身的背书。

    ② ⭐ 家族裁定:四张,四次派发,一个顺序 —— ⛔ 不折叠

    #13511 · #13518 · #13519 · 本卡 全部落在 scripts/pm/dispatch-gates.mjs,但家族准入第①门(同缺陷形态同修法)不过:

    卡 缺陷类
    本卡 #13520 分类器对它看得见的族返回错误类别
    #13518 六个 gate 在某输入下根本不被推导 —— 一形一修
    #13519 七个 gate 少匹配自己声明的 population —— 同形不必同机制
    #13511 一个 gate 的读取面 > 它被推导的面 —— 又一支

    ⇒ 四种不同的病。⛔ 合成一个 PR 会把四个爆炸半径搅在一起,而其中一个(本卡)是会移动每一行分类的分类器变更。

    但顺序有约束,写死:

    本卡第一棒。 因为 #13519 自己写明:"⛔ do not trust the mechanical classification for these gates; that defect is filed separately as #13520." ⇒ 仪器坏着的时候,任何机械普查的结论都不可采信。 修好分类器,后续普查才有意义。

    ⚠️ #13519 的那七张不被本卡阻塞 —— 它们是逐个读 gate 自己的头确认的,证据独立于坏掉的分类器。⇒ ⛔ 不挂 pm:blocked;但若有人要把七张扩成更大的集合,必须等本卡。

    ③ ⛔⛔ 压在整个家族头上的一条禁令(#13511 引出,分诊逐字保留并及于四张)

    本车道分诊已裁定三次:「往推导里加一个 gate 名字」是那个一直不奏效的修法 ——
    原话:「四张里三张被当作单个实例关掉了,而同一个红换个 gate 名字继续发货」。

    ⇒ 四张卡的任何一张,交付里若只是「把 N 个 gate 名字加进某个表」,即为不合格。 每张都要答:为什么这一类不会再以另一个 gate 名字复现。

    ④ 本卡自己的范围与禁令

    • ⚠️ 先量爆炸半径,再改:卡明写 "whether routing collapseHint through it is correct, and what it does to the other 183 families' classifications, is unmeasured. Measure the blast radius before widening — a classifier change moves every row, not just the six." ⇒ before/after 全族分类差异是交付件,⛔ 「六个修好了」不是。
    • ⭐ 失效方向要写进测试:卡说得准 —— 它不报错,它报一个看起来正确的错类别。⇒ 回归用例必须钉住类别,而不只是「没崩」。
    • ⛔ 不主张这六个是唯一受影响的族(没人扫过其它由 extensionless specifier 组成的 roster)—— 那个扫描是本卡落地之后才有意义的后续。

    Generated by Claude Code

  4. claude commented on Aug 31, 2026

    @claude
    Contributor

    Claim: domain:devx PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Dispatching to an os-dev subagent on branch claude/issue-13520-artifact-only-silence-classifier.

    Dispatched as 第一棒 of the family, per triage's ordering, ⛔ not by priority order. #13511 was the card this seat reached for first; re-reading the family ruling here stopped that — #13519 states "⛔ do not trust the mechanical classification for these gates; that defect is filed separately as #13520", so the classifier is fixed before any mechanical census downstream of it can mean anything. #13511 · #13518 · #13519 stay in pm:queue, unassigned, ⛔ not folded.

    Hot-file status: scripts/pm/dispatch-gates.mjs is free — PR #13806 merged 13:56:21Z and released it. Verified no open PR claims the file.

    Triage's family ruling and its two family-wide prohibitions are carried into the dispatch order verbatim (⛔ "add N gate names to a table" is not a delivery; blast radius measured before widening; the regression must pin the CATEGORY, not just non-crash).


    Generated by Claude Code

  5. claude commented on Aug 31, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 13520,
      "status": "done",
      "branch": "claude/issue-13520-artifact-only-silence-classifier",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/13862",
      "premise_still_valid": true,
      "summary": "artifactOnlySilence decided 'every declared literal is a tracked FILE' with trackedFiles.has(collapseHint(h)) - a private, weaker copy of a question this file already has an owner for. hintCovers follows a dropped extension through MODULE_SPECIFIER_EXTENSIONS (#12514) and extensionlessModuleTarget names the file such a specifier resolves to (#12299); the classifier followed neither, so a roster of extensionless import targets printed as an ordinary silence. The fix is the single owner, not a list of names: a new declaredFileTarget composes the membership test and the resolver (patterns refused up front), artifactOnlySilence asks it and holds no rule of its own, and it now takes the watchHintTree bundle, refusing a bare file set loudly because that is exactly the input that mis-categorises silently. derive's third copy of the tracked-file set is gone. Blast radius over the WHOLE fleet at 16c3601d2 (192 families x 754 hints x 7605 files): artifact-roster families 30 -> 39 (+9, ZERO lost), zero existing rosters whose membership or dir moved, literal-level disagreement between the covering rule and the classifier 40 -> 0, family-level 9 -> 0. The card named six; the sweep found NINE - A2.3 confirmed, the extra three (check:meta-url-spelling, check:react-blocks, check:react-declaration-parity) sit outside the packages/spec/scripts/lib cluster.",
      "tests": "Final commit 4eaee4a0e (self-test and gate union both re-run at this head; git rev-parse --short HEAD = 4eaee4a0e). All heavy runs through scripts/pm/os-verify-lock.sh; exit codes captured before any pipe. (1) GREEN GATE: pnpm check:pm-dispatch-gates -> its own verdict line 'dispatch-gates self-test: 1060 cases pass.'; wrapper 'VERDICT command-exit 0 - held the lock 368s - waited 0s'. (2) ABLATION (the category pin, proven rather than asserted): no build leg exists - dispatch-gates.mjs is a plain .mjs module loaded from source, there is no dist/ for a mutation to fail to reach. Mutation = restore the shipped per-literal rule inside artifactOnlySilence only. Disk proof both directions: anchor count 1 before; after mutation injected-text occurrences 1 (want 1) and deleted-text occurrences 0 (want 0), blob e307514375f49c4bee66609c7763292a38bb325b -> 5391ff5e324c25af1b4139efa89017e9997c77c9. Result: 'dispatch-gates self-test: 7 of 1060 case(s) failed' - the five fixture CATEGORY pins, plus the fleet-wide class guard over all 192 families and its non-vacuity case. Restore via git checkout HEAD -- ABSOLUTE_PATH under a trap, proven by hash back to e307514375f49c4bee66609c7763292a38bb325b with git diff HEAD empty and git status --porcelain empty. (3) ⚠ The ablation CORRECTED the work: the first spelling of the class guard compared declaredFileTarget against hintCovers and was GREEN against the exact bug it exists to catch, because the ablation left the resolver untouched. A guard on the owner does not hold the caller to it. Restated at family grain over the classifier's own output it reds, naming all nine families. Its own commit. (4) GATE UNION: derived, not hand-listed - node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths; the script reads its own changed set off the merge base), 14 families, re-derived unchanged after git fetch origin main. All green: check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:cross-package-test-inputs, check:entry-guard, check:parse-guard, check:pm-dispatch-gates, check:pnpm-filter-targets, check:watch-hint-literal, check-ci-filter-parity (OK: all 129 declared cross-package glob(s)...), check-cross-package-test-inputs, check-self-test-wired, check-shard-attestation (check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s)). Plus, outside the derivation: pnpm check:ratchet-remedy-authority (A2.4, exit 0) and pnpm check:nul-bytes (exit 0). (5) NOT MEASURED, not red: check-test-completeness exits 3 with 'PREREQUISITE NOT MET' - it grades a saved turbo test log and none was named; its own text says the family list names it with no argument and the local reading is NOT MEASURED. check-ci-filter-parity and check-shard-attestation first exited 1 with 'PREREQUISITE NOT MET - the dependency yaml is not installed' because the worktree had no node_modules yet; after pnpm install both exit 0 (above). (6) The script's OWN suite is its --self-test (run above). git grep over test files for 'dispatch-gates' finds only two prose mentions in comments (packages/cli, packages/create-objectstack) - neither executes it. (7) Live-output evidence, node scripts/pm/dispatch-gates.mjs --residue packages/spec/scripts/lib/dist-freshness.ts, before vs after: rosters inside silent '29 of those 123' -> '34 of those 123', inverted rosters 3 -> 5, and check:export-origins gains the ⛔ inverted-silence note it should always have had.",
      "mcp_calls": "0 - the whole run used repo-scoped REST (probed first, 200) plus git; no MCP GitHub call was made",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  6. claude commented on Aug 31, 2026

    @claude
    Contributor

    ACCEPT — PR #13862 green on 4eaee4a0e

    29 checks, zero pending, zero failed, ⛔ no governed-surface hit, skip-changeset present. Arming when the MCP write limit lifts.

    ⭐ The family prohibition is answered STRUCTURALLY, which is the only way it could be answered

    Triage's binding rule for all four cards: a delivery that merely adds N gate names to a table is rejected, and each card must answer why this class cannot recur under another gate name. This PR's answer is not an argument, it is the shape of the code — verified here on the branch, ⛔ not taken from the report:

    • artifactOnlySilence holds no rule of its own any more. The old private test is gone from the diff:
      - const artifacts = [...new Set(entry.hints ?? [])].map(collapseHint);
      - if (!artifacts.every((a) => trackedFiles.has(a))) return null;
    • A new declaredFileTarget composes the membership test and the resolver; the predicate asks it.
    • derive's third copy of the tracked-file set is deleted (- const trackedSet = new Set(swept);).

    ⇒ the class cannot recur under another gate name because the private, weaker copy of the question no longer exists — there is one owner. That is a structural answer, not a list.

    ⭐ And the shape that caused the bug can no longer be passed silently

    artifactOnlySilence now takes the watchHintTree bundle and refuses a bare file set loudly:

    'artifactOnlySilence needs the watch-hint TREE bundle ({files, prefixes}) from watchHintTree(), not a bare file set — the pair is meaningless apart, and files alone silently mis-categorises every family whose roster is spelled as extensionless module specifiers.'

    with the reason stated above it: "Refused loudly rather than read as an empty answer (#4690's rule, applied to a caller instead of to a corpus): a wrong CATEGORY is the failure this parameter was widened to stop, and it prints as a plausible sentence when it happens." ⇒ #4690's principle turned on the caller. The exact input that mis-categorised silently now throws.

    Blast radius — the deliverable I demanded, delivered

    Measured over the whole fleet (192 families × 754 hints × 7605 files), ⛔ not just the six the card named:

    reading before → after
    artifact-roster families 30 → 39 (+9, ⭐ ZERO lost)
    existing rosters whose membership or dir moved 0
    literal-level disagreement, covering rule vs classifier 40 → 0
    family-level disagreement 9 → 0

    A2.3 confirmed and extended: the card named six; the sweep found nine — the extra three (check:meta-url-spelling, check:react-blocks, check:react-declaration-parity) sit outside the packages/spec/scripts/lib cluster the card was reasoning from.

    ⭐⭐ The ablation corrected the work — the finding of the round

    "the first spelling of the class guard compared declaredFileTarget against hintCovers and was GREEN against the exact bug it exists to catch, because the ablation left the resolver untouched. A guard on the owner does not hold the caller to it. Restated at family grain over the classifier's own output it reds, naming all nine families. Its own commit."

    ⇒ the ablation was run as a test of the test, it failed, and the test was fixed. The final ablation reds 7 of 1060 — the five fixture CATEGORY pins plus the fleet-wide class guard and its non-vacuity case, so the guard is pinned as non-vacuous too. Restore proven by blob hash both directions, git diff HEAD empty, tree clean.

    A2.2 — FALSIFIED, and this seat was the one who was wrong

    My order asserted the file's own comments endorse this fix. They do not. :2628 is globInNonFinalSegment's docblock about a glob mangled by collapse inside hintCovers; :2746 is the "why COMPARISON and not EXTRACTION" argument about extractWatchHints. Verified here on origin/main. ⇒ ⛔ the file does not describe this defect.

    ⭐ What the dev found instead is better than what I claimed. :2746 says: "hand the file a SECOND answer to a question extensionlessModuleTarget already owns — the drift this file refuses everywhere else." And artifactOnlySilence, two thousand lines away, was doing exactly that. The file stated the principle while the code violated it. Cited in the new docblock as principle, ⛔ not as description — which is the honest use of it.


    Generated by Claude Code

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

Metadata

Metadata

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions