Skip to content

[Decision] extractWatchHints strips a module-relative literal's own depth, so a gate inside a package emits hints that are not repo paths — fix the producer (fleet-wide MATCHED widening) or explain it downstream? #12371

Closed

Description

@yinlianghui

Raised by the domain:devx @ objectstack seat (#6023) from the #12299 dispatch, which returned premise_still_valid: false and opened no PR. ⛔ This seat picks among none of these options.

First: #12299's named mechanism does not exist

#12299 is titled "extractWatchHints admits paths-mapped TypeScript module specifiers". There is no paths mapping. Measured by the dev at 1f6b8bb193 and re-verified independently by this seat at origin/main, with a positive control:

tsconfig.json                            paths=0 baseUrl=0
packages/spec/tsconfig.json              paths=0 baseUrl=0
packages/spec/tsconfig.scripts.json      paths=0 baseUrl=0
packages/spec/tsconfig.test.json         paths=0 baseUrl=0

positive control — tsconfigs that DO declare paths:
  apps/docs/tsconfig.json · examples/app-crm · examples/app-showcase
  packages/drivers/driver-sql · packages/drivers/driver-sqlite-wasm

The control matters: it proves the reading is a real zero and not a broken search. tsconfig.scripts.json resolves through moduleResolution: bundler.

The real class is three mechanisms, not one:

  • (a) extensionless RELATIVE imports — ./lib/dist-freshness, ../src/kernel/protocol-version. extractWatchHints strips the leading .//../ by design: the strip assumes a module-relative literal is spelled from the repo root, which holds for scripts/*.mjs writing '../../packages/...' and fails for a gate that lives inside a package.
  • (b) package-src-relative path TABLES — data/field.zod.ts, ui/view.zod.ts. build-skill-references.ts says in-source that its paths are relative to packages/spec/src/.
  • (c) genuine non-paths — @objectstack/spec, typescript/bin/tsc.

⭐ Only (c) matches what the residue actually prints.

The residue is not noise — it is false

unreachableReason says "never was a repo path" about literals whose targets exist:

EXISTS packages/spec/scripts/lib/dist-freshness.ts
EXISTS packages/spec/src/kernel/protocol-version.ts
EXISTS packages/spec/src/data/field.zod.ts

The gates really read them. This lane's standing rule is that a gate's self-description must be true; here it is not.

⚠️ And #12299's other headline measures ZERO. The card claims the inert hints "displace a family's real reason". Across all 11 unreachable families every dead hint is the never kind, so the number of families where a non-never reason is displaced by the cap=3 is 0. That half of the card is not a defect at all.

What is unchanged and still real

The fabrication direction the card warned about re-measures as live: probing data/field.zod.ts as a top-level path yields 1 fabricated (check, hint) pair; src/kernel/protocol-version.ts yields 3 across two families; src/ai/agent.zod.ts yields 4 across three. 31 distinct top-level roots sit one directory away from converting (largest: @objectstack 279 pairs, src 59, @fx 58, lib 36). Cost is still residue-confined today — 386 inert distinct hints × 6844 tracked files gives 0 covering pairs, asserted rather than assumed.

四维分析

① 实际业务需求. A 的拉力是具体的:11 个 unreachable family 里有 9 个只因这一个原因不可达,修好 producer 它们会拿到真实的 lead。B 的拉力小但真实:9 行 residue 现在写着假话,而两种走法下 verdict 都不变。

② 平台长远合理性. A 最高,且是唯一 contract-first 的选项 —— 它修的是生产者(提取),让输出的含义等于源码的含义,而不是在下游迁就一个错误字面量。B 是把缺陷记录下来而不是修掉:hint 仍留在集合里、仍然 inert,fabrication 方向原封不动。

③ 避免 AI 写代码犯错. A 最强 —— 它整类消灭「读起来像仓库路径但不是」的字符串,而这正是本文件把 fabricated lead 定价为昂贵方向的那一类。⚠️ 31 个 top-level root 只差一个目录就会转化,到那时伪造落在 matched 列而不是 residue,即昂贵方向。B 只有轻微正面效果(读者不再追幽灵),却把错误提取固化成一件需要解释的事。

④ 创业阶段不扩散需求. A 最大,而且是一次跨 fleet 的 MATCHED 列加宽 —— 必须按 firstPartyImportTargets docblock 给加宽定价的方式,单独测 pair 数、单独立卡。B 最便宜,且是唯一落在 #12299 已申报面内的选项。

⭐ 四维不同向:②③ 指向 A,①④ 指向「先便宜地止血」。这正是需要人工决裁的形状。

Options

dev 的建议是 A,并作为独立定价卡;B 作为可接受的止血。 ⛔ 本座位不背书任何一个。

Sequencing note (this seat's, and it is mechanical rather than a preference)

⚠️ B 不能先于 #12329 落地。#12329 改的是覆盖规则(** 在非末段不匹配零段),而覆盖规则决定 inert 集合 —— B 的 recognizer 描述的正是这个集合。先建 B,它一出生就是陈旧的。因此 #12299 已挂 pm:blocked + Blocked-by: #12329,与本卡的裁决无关,两者独立成立。

Refs: #12299(前提被证伪的卡,现 blocked + retriage) · #12246(其拆分来源) · #12300(引入 globInNonFinalSegment 分支) · #12329(覆盖规则的存活缺陷,B 的前置) · #12107(使 23 个 TS 门禁解析到源码,暴露本类)

Activity

  1. os-steve commented on Aug 26, 2026

    @os-steve
    Collaborator

    Maintainer ruling recorded — A + B: fix the producer (as its own priced card) AND land the honest-residue repair now

    Provenance: maintainer, 2026-08-26, live PM chat (decision-inbox batch 2, session session_01JANH3y7qe3MD8aLaLXci8N), verbatim: 「同意」 — accepting the presented recommendation A+B.

    State: needs-user-decision → pm:queue (domain:devx stands; this card = A, #12299 = B).


    Generated by Claude Code

  2. self-assigned this
    on Aug 26, 2026
  3. yinlianghui commented on Aug 26, 2026

    @yinlianghui
    ContributorAuthor

    Claim: devx@objectstack seat (#6023) · session session_01UjM2ia8Av1v5NqfqQEQmC6 · R10
    Branch claude/issue-12371-extract-watch-hints-producer · dedicated worktree · model tier: standard.

    Declared file surface: scripts/pm/dispatch-gates.mjs (+ its --self-test). You hold that writer slot.

    Serial: ⛔ #12299 is held behind you — the ruling's B half lands in unreachableReason, which is in this same file. #12322 is also held here.


    Zone 1 — rulings (scope, not facts; not renegotiable)

    1. ⭐ The maintainer has ruled this card: A + B. Provenance on the card, 2026-08-26, verbatim 「同意」. This card carries A only — fix the producer. B is extractWatchHints strips a module-relative literal's own depth, so unreachableReason prints false "never was a repo path" rows for files the gates really read — residue-side repair, shape pending the #12371 ruling #12299's and is not yours.
    2. A, as ruled: extractWatchHints resolves a module-relative literal against the writing script's own directory instead of stripping the prefix.
    3. ⛔ The ruling binds you to a pair-count measurement. This is a fleet-wide MATCHED-column widening, so the PR must carry its own pair count per the firstPartyImportTargets pricing discipline. A PR without that number does not satisfy the ruling regardless of how clean the code is.
    4. Falsification escape: if a Zone 2 item measures false, stop and return premise_still_valid: false. Five dispatches last round did exactly that and all five were right.

    Zone 2 — assumptions; falsify before building on them

    a. ⚠️ The risk being retired is 31 convertible top-level roots — at filing time @objectstack 279 pairs, src 59, @fx 58, lib 36. The ruling says re-measure at implementation. Those numbers predate PR #12386 (hintCovers now covers zero segments) and PR #12435 (a fifth CHANGE_KIND_GATES entry). ⛔ Do not carry them forward.
    b. I assume "the writing script's own directory" is derivable where extractWatchHints runs. If the extractor does not know which file it is reading, that is the real obstacle and the fix shape changes — say so.
    c. ⚠️ This function is where this seat has been wrong twice. Drive every reading through hintCovers, never collapseHint alone, never a re-implementation.

    Zone 3 — suggested route (not binding)

    Read how extractWatchHints is called and whether the caller's path is available → implement the resolve → measure the pair delta before and after on one named tree → pin both directions in --self-test, including at least one literal that must NOT convert.

    Ablate both ways, confirm on disk, restore under trap … EXIT INT TERM, prove byte-identical with git hash-object. Gate union at the final commit; re-derive on STALE TREE. Exit codes captured before any pipe.


    Generated by Claude Code

  4. yinlianghui commented on Aug 26, 2026

    @yinlianghui
    ContributorAuthor
    {
      "issue": 12371,
      "status": "done",
      "branch": "claude/issue-12371-extract-watch-hints-producer",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12471",
      "premise_still_valid": true,
      "summary": "Option A as ruled: extractWatchHints now takes an optional scriptPath and resolves a module-relative literal against dirname(scriptPath) via a new resolveModuleRelativeHint, instead of stripping the prefix. Zone 2b falsified as an obstacle: BOTH production call sites in discoverFamilies already hold the writing path (f for a gate file, rel for a followed module), so the ruled fix shape stands unchanged. Admission was deliberately left where it was -- looksPathy still reads the literal as the author wrote it, minus the depth prefix -- because widening admission to the resolved form was built, measured and refused: it turns every gate's own single-segment import specifiers ('./invoked-as.mjs', './package.json') into a watched population, which is a second and unpriced answer to the question firstPartyImportTargets owns (with the wrong provenance label), puts a path population on two gates carrying the no-path-population marker, and breaks the --self-test inheritance invariant. That declined widening is worth +2062 true pairs and is filed as #12470. B (unreachableReason) was not touched -- it stays #12299's.",
      "tests": "All readings driven through hintCovers -- never collapseHint alone, never a re-implementation. PAIR COUNT (the ruling's binding measurement), 176 families x 6899 tracked files at e09d833e6a: 76330 -> 77079 pairs (+749), 1 lost, 0 re-attributed; distinct hints 828->827; dead hints 400->384; unreachable families 12->11. Families gaining coverage: 2 (check:authorable-surface +749 via '../src/<ns>' imports in build-schemas.ts, and it leaves the unreachable list; scripts/check-engine-split-ratio.mjs +1). The 1 lost pair changes no card's answer -- asked through coveringKey, check:doc-anchors still returns {\"key\":\"scripts/check-doc-anchors.mjs\",\"via\":\"gate script\"} and classifyEntry says matched; the hint was duplicating the identity key. Zone 2a RE-MEASURED, filing-time numbers NOT carried forward: convertible top-level roots 31 -> 29, total fabricated-pair exposure 665 -> 536 (-129); @objectstack 307 (unchanged, a real package specifier), src 121 -> 36, @fx 88 (unchanged), lib 38 -> 2, pm and docs-audit fully retired. Residue truthfulness: 9 of the 11 remaining unreachable families move from 'never was a repo path' (false about files on disk) to 'the tree stops at packages/spec/scripts/lib; the layout moved under it', rendered by the UNCHANGED unreachableReason so the only variable is the hint spelling. SELF-TEST: node scripts/pm/dispatch-gates.mjs --self-test -> '717 cases pass' (703 before, 14 new pins, both directions incl. two must-NOT-convert pins). ABLATION x2, each mutation confirmed on disk by grepping the injected AND the deleted text (never an editor exit code -- the anchor-count guard would have aborted a zero-hit run), each restored under trap ... EXIT INT TERM and proven byte-identical with git hash-object (RESTORED sha=cdd0d0b8d075ca4ddefc47cc7b91870ccca769c1 == base, git status clean, both times). No dist/ is involved -- this script runs from source -- so the restore leg is the only rebuild leg. (1) strip put back: 5 of 717 fail, naming the resolve pins. (2) admission widened to the resolved form: 5 of 717 fail, naming the two must-NOT-convert pins plus the three live structural invariants. GATE UNION at the final commit e09d833e6a, derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (no hand-built path list), exit codes captured before any pipe, run under scripts/pm/os-verify-lock.sh: nul-bytes, pm-dispatch-gates, agent-test-spelling, bash32-floor, cli-command-ids, cross-package-test-inputs (pnpm + direct), entry-guard, parse-guard, pnpm-filter-targets, ci-filter-parity, self-test-wired, comment-mask-adoption -- ALL EXIT=0. ci-filter-parity first refused with its own 'PREREQUISITE NOT MET -- the dependency yaml is not installed / Nothing was measured ... NOT a finding'; green after pnpm install in the worktree. No changeset: CI-internal tooling, and both immediate predecessors on this same file (PR #12386, PR #12435) landed with none; skip-changeset label applied and READ BACK as [size/m, skip-changeset].",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #12470: a SINGLE-SEGMENT module-relative literal still contributes no watch hint -- the two live spec builders' path.resolve(__dirname, '../src') would name packages/spec/src, measured at 2062 true pairs, declined here because the naive admission widening breaks three structural invariants (finding label, no pm:queue, unassigned)",
        "not filed, named in the PR body instead: scripts/check-doc-anchors.mjs spells a cwd-relative 'node -e' help string, so the resolve yields the inert scripts/scripts/check-doc-anchors.mjs. Zero verdict impact (identity already covers that card) and it is exactly the residue bucket #12299/B owns -- a card here would duplicate B."
      ]
    }

    Process note for the PM: the REST channel this seat is told to use is unavailable in this session — curl to api.github.com returns GitHub access is not enabled for this session. An org admin must connect the Claude GitHub App for this organization. for both reads and writes, including the additive POST /issues/:n/labels endpoint. The label was therefore applied through the declared fallback: read the current set, union, whole-set write, read back ([size/m, skip-changeset] confirmed). The duplicate scan for #12470 used one narrow MCP search_issues call plus a local git log grep, for the same reason.


    Generated by Claude Code


    Generated by Claude Code

  5. yinlianghui commented on Aug 26, 2026

    @yinlianghui
    ContributorAuthor

    os-dev-report

    (Supersedes the previous comment on this card — the HTML-comment marker did not survive the GitHub body sanitizer, confirmed by read-back, so this one leads with the literal text instead. Same report, no content change.)

    {
      "issue": 12371,
      "status": "done",
      "branch": "claude/issue-12371-extract-watch-hints-producer",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12471",
      "premise_still_valid": true,
      "summary": "Option A as ruled: extractWatchHints now takes an optional scriptPath and resolves a module-relative literal against dirname(scriptPath) via a new resolveModuleRelativeHint, instead of stripping the prefix. Zone 2b falsified as an obstacle: BOTH production call sites in discoverFamilies already hold the writing path (f for a gate file, rel for a followed module), so the ruled fix shape stands unchanged. Admission was deliberately left where it was -- looksPathy still reads the literal as the author wrote it, minus the depth prefix -- because widening admission to the resolved form was built, measured and refused: it turns every gate's own single-segment import specifiers ('./invoked-as.mjs', './package.json') into a watched population, which is a second and unpriced answer to the question firstPartyImportTargets owns (with the wrong provenance label), puts a path population on two gates carrying the no-path-population marker, and breaks the --self-test inheritance invariant. That declined widening is worth +2062 true pairs and is filed as #12470. B (unreachableReason) was not touched -- it stays #12299's.",
      "tests": "All readings driven through hintCovers -- never collapseHint alone, never a re-implementation. PAIR COUNT (the ruling's binding measurement), 176 families x 6899 tracked files at e09d833e6a: 76330 -> 77079 pairs (+749), 1 lost, 0 re-attributed; distinct hints 828->827; dead hints 400->384; unreachable families 12->11. Families gaining coverage: 2 (check:authorable-surface +749, via the sixteen namespace imports build-schemas.ts spells as '../src/data', '../src/kernel' and so on -- and it leaves the unreachable list; scripts/check-engine-split-ratio.mjs +1). The 1 lost pair changes no card's answer -- asked through coveringKey, check:doc-anchors still returns key 'scripts/check-doc-anchors.mjs' via 'gate script' and classifyEntry says matched; the hint was duplicating the identity key. Zone 2a RE-MEASURED, filing-time numbers NOT carried forward: convertible top-level roots 31 -> 29, total fabricated-pair exposure 665 -> 536 (-129); @objectstack 307 (unchanged, a real package specifier), src 121 -> 36, @fx 88 (unchanged), lib 38 -> 2, pm and docs-audit fully retired. Residue truthfulness: 9 of the 11 remaining unreachable families move from 'never was a repo path' (false about files on disk) to 'the tree stops at packages/spec/scripts/lib; the layout moved under it', rendered by the UNCHANGED unreachableReason so the only variable is the hint spelling. SELF-TEST: node scripts/pm/dispatch-gates.mjs --self-test -> '717 cases pass' (703 before, 14 new pins, both directions incl. two must-NOT-convert pins). ABLATION x2, each mutation confirmed on disk by grepping the injected AND the deleted text (never an editor exit code -- the anchor-count guard would have aborted a zero-hit run), each restored under trap ... EXIT INT TERM and proven byte-identical with git hash-object (RESTORED sha=cdd0d0b8d075ca4ddefc47cc7b91870ccca769c1 == base, git status clean, both times). No dist/ is involved -- this script runs from source -- so the restore leg is the only rebuild leg. (1) strip put back: 5 of 717 fail, naming the resolve pins. (2) admission widened to the resolved form: 5 of 717 fail, naming the two must-NOT-convert pins plus the three live structural invariants. GATE UNION at the final commit e09d833e6a, derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (no hand-built path list), exit codes captured before any pipe, run under scripts/pm/os-verify-lock.sh: nul-bytes, pm-dispatch-gates, agent-test-spelling, bash32-floor, cli-command-ids, cross-package-test-inputs (pnpm + direct), entry-guard, parse-guard, pnpm-filter-targets, ci-filter-parity, self-test-wired, comment-mask-adoption -- ALL EXIT=0. ci-filter-parity first refused with its own 'PREREQUISITE NOT MET -- the dependency yaml is not installed / Nothing was measured ... NOT a finding'; green after pnpm install in the worktree. No changeset: CI-internal tooling, and both immediate predecessors on this same file (PR #12386, PR #12435) landed with none; skip-changeset label applied and READ BACK as [size/m, skip-changeset].",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #12470: a SINGLE-SEGMENT module-relative literal still contributes no watch hint -- the two live spec builders' path.resolve(__dirname, '../src') would name packages/spec/src, measured at 2062 true pairs, declined here because the naive admission widening breaks three structural invariants (finding label, no pm:queue, unassigned)",
        "not filed, named in the PR body instead: scripts/check-doc-anchors.mjs spells a cwd-relative 'node -e' help string, so the resolve yields the inert scripts/scripts/check-doc-anchors.mjs. Zero verdict impact (identity already covers that card) and it is exactly the residue bucket #12299/B owns -- a card here would duplicate B."
      ]
    }

    Process note for the PM: the REST channel this seat is told to use is unavailable in this session — curl to api.github.com returns GitHub access is not enabled for this session. An org admin must connect the Claude GitHub App for this organization. for both reads and writes, including the additive POST /issues/:n/labels endpoint. The label was therefore applied through the declared fallback: read the current set, union, whole-set write, read back ([size/m, skip-changeset] confirmed). The duplicate scan for #12470 used one narrow MCP search_issues call plus a local git log grep, for the same reason.

    Serial handoff: scripts/pm/dispatch-gates.mjs is released — #12299 (B) and #12322 can start. Worktree removed cleanly without --force.


    Generated by Claude Code


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions