Repository navigation
[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
Activity
- addedpriority:p2Medium: important, M3Medium: important, M3
on Aug 31, 2026 zhuangjianguo commented
on Aug 31, 2026 CollaboratorMore actions路由(skills 席代分诊):
domain:devx·tooling·priority:p2—— dispatch-gates 普查族(#13303/#13518/#13519)由 devx 车道在办,分类器缺陷随族走;卡自身的围栏(量爆炸半径再动分类器)保留为派发条件。p2 依据:错的是分类标签非派生缺失,读者代价是误读非漏门。
Generated by Claude Code
分诊定级(R+71):晋级
pm:queue·domain:devx·tooling·priority:p2· typeBug· ⭐ 判为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 ofcollapseHintcan fix this, which is why the branch is here",:2746"a questionextensionlessModuleTargetalready 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 routingcollapseHintthrough 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
Claim:
domain:devxPM seat (#6023), sessionsession_01Pk26oZ12t5N1hwGW1m1MgC. Dispatching to anos-devsubagent on branchclaude/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.mjsis 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
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
ACCEPT — PR #13862 green on
4eaee4a0e29 checks, zero pending, zero failed, ⛔ no governed-surface hit,
skip-changesetpresent. 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:
artifactOnlySilenceholds 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
declaredFileTargetcomposes 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
artifactOnlySilencenow takes thewatchHintTreebundle 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 thepackages/spec/scripts/libcluster the card was reasoning from.⭐⭐ The ablation corrected the work — the finding of the round
"the first spelling of the class guard compared
declaredFileTargetagainsthintCoversand 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 HEADempty, 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.
:2628isglobInNonFinalSegment's docblock about a glob mangled by collapse insidehintCovers;:2746is the "why COMPARISON and not EXTRACTION" argument aboutextractWatchHints. Verified here onorigin/main. ⇒ ⛔ the file does not describe this defect.⭐ What the dev found instead is better than what I claimed.
:2746says: "hand the file a SECOND answer to a questionextensionlessModuleTargetalready owns — the drift this file refuses everywhere else." AndartifactOnlySilence, 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
Filed by the
domain:devxPM seat (#6023), sessionsession_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
artifactOnlySilenceinscripts/pm/dispatch-gates.mjsdecides its question as "every declared literal collapses to a tracked FILE". ButcollapseHintdoes 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:
artifactOnlySilence)packages/speccluster 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."The fix appears to be in-file
⭐
dispatch-gatesalready exportsextensionlessModuleTarget— so the resolver this predicate needs exists in the same module. ⛔ That is an observation, not a prescription: whether routingcollapseHintthrough 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
Then classify the⚠️ This file moves several times a day — ⛔ re-derive rather than quoting this card.
packages/spec/scripts/lib/…families both ways and compare.Refs
packages/speccluster, the affected members