Skip to content

check-spec-symbol-derivation's barrel skip misses a bare export { X }, so a re-export without a module specifier is recorded at the barrel instead of the declaration #7275

Closed

Description

@yinlianghui

Filed by the dev seat implementing #6291, out of scope for that card's fence (scripts/check-spec-symbol-derivation.mjs was in scope, but this hole predates the card and fixing it would move findings the card's measurement is anchored on). Unassigned, for triage.

The hole

scanFile deliberately skips a barrel re-export, with the reasoning written at the site:

A barrel re-exporting a module this scan already covers — a relative path or an @object-ui/* sibling — is not a second finding. Whatever it points at gets judged at its own declaration site; reporting the barrel too would just make one fork look like four, and would make the fix land in the barrel rather than at the declaration.

The guard implementing it reads the re-export's module specifier:

if (fromModule && (fromModule.startsWith(".") || fromModule.startsWith("@object-ui/"))) continue;

A bare export { X } — the form written over a local import in the same file — has no module specifier at all. fromModule is null, the skip does not fire, and the name is recorded as a re-export finding located at the barrel.

The live instance

packages/plugin-list/src/index.tsx:15:

export { ListView, ListViewBlock, ViewSwitcher, ObjectGallery };

@object-ui/plugin-list:ListView is in the ALLOW map, and after #6291 that entry is kept alive by this barrel line, not by the component at packages/plugin-list/src/ListView.tsx:747 — the component is skipped structurally by the new rendersJsx narrowing. Its two siblings in the same ALLOW map, @object-ui/auth:AuthProvider and @object-ui/plugin-list:UserFilters, had their entries deleted in #6291 because they had no such bare re-export and so stopped matching anything. Three entries written on one judgement, and only one survives — on an accident of barrel spelling.

Why it matters beyond the cosmetic asymmetry

The skip's own stated purpose is that a fix should land at the declaration. A finding attributed to the barrel points a reader at the wrong file, and it makes one fork look like two. It also means the ALLOW / DEBT ratchets can be satisfied — or falsified — by editing a barrel rather than the shape, which is precisely the failure mode the skip exists to prevent.

Suggested shape (not prescriptive)

For a bare export { X }, resolve X against the file's own imports: if it is bound by an import from a relative path or an @object-ui/* sibling, apply the same skip. The information is already collected one screen up, where specBindings is built from the file's import declarations — the same loop can record non-spec local bindings and their source module.

Do not simply skip every bare export { X }: import { X } from spec; export { X } is genuine derivation and is already handled by the specBindings.has(localName) arm, while const X = …; export { X } in the same file is a real local declaration that must stay visible.

Not fixed under #6291

That card's population figures (rule 1 +25 before narrowings, +19 after) are measured against the current behaviour, and changing what the barrel arm reports would move them. It is also outside the split ruled on that card, which is about the export modifier filter, not the re-export arm.

Refs: #6291 (the card that surfaced it) · PR #6284 (the census) · #7265 (the DEBT anchor)

Activity

  1. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    分诊 · domain:devx / priority:p3 / pm:queue

    Anchor read, not guessed. scripts/check-spec-symbol-derivation.mjs ⇒ domain:devx。

    ⭐ 复核 objectui origin/main a472b07(2026-09-05T01:09:11Z),机制与活实例都精确成立:

    check-spec-symbol-derivation.mjs:1636   const fromModule = stmt.moduleSpecifier && ts.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : null;
    check-spec-symbol-derivation.mjs:1646   if (fromModule && (fromModule.startsWith(".") || fromModule.startsWith("@object-ui/"))) continue;
    
    packages/plugin-list/src/index.tsx:15   export { ListView, ListViewBlock, ViewSwitcher, ObjectGallery };
    

    ⇒ ⭐ :1636 明写了 : null,:1646 的守卫以 fromModule && 开头 —— 所以一个裸 export { X }(无 module specifier)⇒ fromModule 为 null ⇒ skip 不触发 ⇒ 名字被记为发生在 barrel 上的 re-export finding。机制是读出来的,不是推的。

    定级 p3

    关卡今天不误判任何实质问题:被记录的名字确实是一个 re-export,只是位置记错了。⇒ 没有假绿、没有假红。

    ⛔ 但不是噪音,因为它违反了那条 skip 自己写下的目的:

    一个 barrel 重新导出本扫描已覆盖的模块 …… 不是第二个发现。它指向的东西会在自己的声明点被判断;把 barrel 也报出来只会让一个分叉看起来像四个,并且会让修复落在 barrel 而不是声明处。

    ⇒ ⭐ 一个归属到 barrel 的发现,把读者指向错误的文件 —— 而更要紧的是卡面的下一句:它意味着 ALLOW / DEBT 棘轮可以通过编辑一个 barrel 来被满足或被证伪,而不是通过改形状,而那正是这条 skip 存在要防的失败模式。

    ⭐ 那个活实例本身就是最好的论据

    @object-ui/plugin-list:ListView 在 ALLOW 里,而 #6291 之后那条条目是靠这一行 barrel 活着的,⛔ 不是靠 packages/plugin-list/src/ListView.tsx:747 的那个组件 —— 组件已被新的 rendersJsx 收窄结构性跳过。

    ⚠️ 而它的两个同判据兄弟(@object-ui/auth:AuthProvider、@object-ui/plugin-list:UserFilters)在 #6291 里被删掉了,因为它们没有这样一行裸 re-export,于是不再匹配任何东西。

    ⇒ ⭐ 三条基于同一次判断写下的条目,只有一条活下来 —— 靠的是 barrel 拼写上的一个偶然。 这句话比任何论证都更能说明为什么它值一张卡。

    边界测试 —— Bug/整理,无人工地板

    改的是一个 gate 的归属逻辑。⚠️ 但它会移动发现的位置,因此会影响 ALLOW / DEBT 两个棘轮的匹配 —— ⇒ 修复 PR 必须同时处理那三条条目的去留(ListView 会改为在声明点被判断或被结构性跳过,而它的两个兄弟可能需要以正确的理由重新写回)。⛔ 这是本卡的真实规模,不是一行 diff。

    ⭐ 修法与两条禁止 —— 卡面写得很完整,原样保留

    建议形状:对裸 export { X },把 X 对着本文件自己的 import 解析:若它由一条来自相对路径或 @object-ui/* 兄弟的 import 绑定,就套用同样的 skip。⭐ 信息已经在一屏之上收集好了 —— specBindings 就是从本文件的 import 声明构建的,同一个循环可以顺手记下非 spec 的本地绑定及其来源模块。

    ⛔ 不要简单地跳过每一个裸 export { X }:

    • import { X } from spec; export { X } 是真正的派生,已由 specBindings.has(localName) 那一臂处理;
    • const X = …; export { X } 在同一文件里是真实的本地声明,必须保持可见。

    ⛔ 为什么 #6291 没做 —— 采纳

    那张卡的总体数字(规则 1 收窄前 +25、收窄后 +19)是按当前行为测的,改变 barrel 那一臂报什么会移动它们;而且它在那张卡所裁的拆分之外 —— 那次裁的是导出修饰符过滤器,不是 re-export 臂。⇒ 当时不做是对的。

    ⛔ 本席不认领、不派发、不代裁。


    Generated by Claude Code

  2. claude commented on Sep 6, 2026

    @claude
    Contributor

    Claim: PM loop round R44
    Session: session_013uAaxiwgYDybsTNV9xwa1M
    Branch: claude/issue-7275-bare-reexport-barrel-skip
    Worktree: objectui-issue-7275
    Domain: domain:devx
    File surface: scripts/check-spec-symbol-derivation.mjs (the re-export arm at :1614–:1646 and the ALLOW/DEBT maps around :492/:588/:605), scripts/__tests__/check-spec-symbol-derivation.test.ts; ⛔ no packages/** source, ⛔ no barrel rewritten to make a ledger entry match (stop on breach; explain in the report)
    Container & model: S–M, not mechanical (the barrel arm must resolve a bare re-export against the file's own imports without skipping genuine derivation or local declarations, and the three ALLOW/DEBT entries written on one judgement must be re-decided on the corrected attribution), mode:subagent, model: opus — objectstack's node scripts/pm/dispatch-gates.mjs --tier REFUSES --repo objectstack-ai/objectui (measured this round: gate families are a property of the tree it runs in), so the tier is the PM's per-card judgment under the script's declared policy floor sonnet · default opus · ceiling fable; no path-derived mandate applies to this surface.
    Clause-②: no
    Serial constraints cleared: none — no open PR at 2026-09-06T04:30Z touches scripts/check-spec-symbol-derivation.mjs or its test (last landed #7617 on 2026-09-04); in-flight #7270 is .github/workflows/** only


    Generated by Claude Code

  3. claude commented on Sep 6, 2026

    @claude
    Contributor

    Correction to the claim's serial reading (5556901907) — one open PR DOES touch this file

    domain:devx @ objectui execution seat, PM session session_013uAaxiwgYDybsTNV9xwa1M, R44, 2026-09-06T04:37Z.

    The claim said no open PR touches scripts/check-spec-symbol-derivation.mjs. That reading came from the open-PR title list, not from each PR's file surface, and it is wrong: PR #7685 (objectui#7122, domain:spec seat, draft, head 7be4b2456, held on the 17.3.0 pin decision) carries one hunk on this file — ALLOW map @@ -670,46 +670,18 @@, retiring the two objectui#7580 BreakpointName / BreakpointColumnMap entries (+12/−40).

    Disposition, not a hold: #7685 is a draft on a maintainer decision with no landing date, so this card is not serialised behind it. The two edit sets are non-adjacent (#7275: the re-export arm around :1614–:1646, the ListView ALLOW entry at :588 and the tombstones at :492 / :605; #7685: :670–:716), and the merge queue arbitrates whichever lands second. Owed by this card's PR: (1) the dev reads #7685's hunk before editing and keeps its own hunks out of :670–:716; (2) the PR body names the overlap in one sentence so the second lander knows to merge main first. The dev has been told the same in-session.


    Generated by Claude Code

  4. claude commented on Sep 6, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 7275,
      "status": "done",
      "branch": "claude/issue-7275-bare-reexport-barrel-skip",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7961",
      "premise_still_valid": true,
      "summary": "All three of the card's premises re-verified on my base d39ede24e (origin/main had moved from the PM's f10e49f36): the guard at the re-export arm still keyed on the module specifier, a bare `export { X }` still yielded fromModule === null, and packages/plugin-list/src/index.tsx:15 was the tree's ONLY live instance. The import loop that builds specBindings now also records every binding's source module, and a bare re-export resolves its LOCAL name (propertyName ?? name, so the alias form is judged on the local) against that map before the existing barrel skip is applied to it; forms carrying a specifier are untouched, the specBindings derivation arm is unchanged, and an import from a third-party module is still reported for the same reason `export { X } from 'some-lib'` is. A3 was MEASURED, not assumed: with the scanner fixed and the entry still in place the gate failed with ratchet 3 naming @object-ui/plugin-list:ListView, so the entry is deleted with a tombstone in the shape of the two beside it; AuthProvider and UserFilters are NOT re-added because the corrected scan does not report them either. A4 measured over the full corpus: the only movement in the whole tree is the removal of `re-export ListView` at that barrel.",
      "tests": "Corpus identical across every measurement (1348 files, 4959 spec export names; the probe aborts if either count drifts from what the gate prints). BEFORE/AFTER full finding dumps via the gate's own exported scanFile: 36 findings -> 35, `diff` output is exactly one removed row, `re-export ListView packages/plugin-list/src/index.tsx:15`; every other row byte-identical. Gate verdict lines: base `OK spec symbol derivation: 1348 files scanned against 4959 spec export names; 20 declared dialects, 14 untriaged collisions in 7 packages` (exit 0) -> scanner-fixed-but-entry-present `a spec-named symbol is hand-written, not derived: @object-ui/plugin-list:ListView is in ALLOW but no longer collides with a spec export name` (exit 1) -> final `19 declared dialects, 14 untriaged collisions in 7 packages` (exit 0). Rule 2 / rule 4 verdict lines unmoved (2 declared deliberate copies, 19 unbacked claims in 5 packages; no member citation). Suite: `pnpm exec vitest run --maxWorkers=2 scripts/__tests__/check-spec-symbol-derivation.test.ts` from the repo root, `Test Files 1 passed (1) / Tests 60 passed (60)` (55 before, 5 new); that file is the only importer of the edited module in the repo, every other reference is prose. Heavy runs went through the shared lock with OS_VERIFY_LOCK_SLOT=issue-7275, `VERDICT command-exit 0` on each (queued 94s behind objectui-issue-7693's build on the first).",
      "gates": "All exit 0 on the final tree, exit code captured by redirect-then-capture and the gate's own verdict line quoted: check:spec-symbols (`OK ... 19 declared dialects ...`), type-check:scripts (tsc, no output), lint:root (`32 problems (0 errors, 32 warnings)`, exit 0 — root-scoped eslint, covers both changed files), check:control-bytes (`OK (scanned 6412 tracked text file(s); skipped 85 binary)`), check-changeset-presence (`No source or published contract of a released package changed in this range, so no changeset is owed.` — objectui has no skip-changeset mechanism, so nothing was labelled), check-governed-queue-guard --test on both paths (`NOT GOVERNED - 2 path(s) checked against 5 governed surface(s); none matched`). Additionally derived by hand (dispatch-gates.mjs refuses --repo objectstack-ai/objectui, so the family list came from objectui's own package.json) and run green: check:entry-guard, check:unreferenced-sources, check:vi-mock-specifiers, check:vi-mock-inherit, check:shell-escape-residue, lint:coverage, type-check:coverage. Repo-wide `pnpm lint` / `pnpm test` are CI's run, not owed here. Self-scan for control bytes on both changed files: grep -naP over the control-byte class, no hits.",
      "probe": "PLANTED PROBE (predicted, then observed), on the live barrel itself so both directions are measured in one file: appended `const NavigationConfig = { mode: 'page' }; export { NavigationConfig };` to packages/plugin-list/src/index.tsx. Plant proven on disk before reading any result (marker count 0 -> 1, blob 0480461a -> e436c5e2). Gate exit 1, reporting BOTH halves: `const NavigationConfig ... :16` and `re-export NavigationConfig ... :17` — while the sibling re-exports one line up stayed silent. Restored: blob back to the HEAD blob 0480461a, `git diff HEAD` empty, marker count back to 0. ABLATION on the committed tree (trap EXIT INT TERM with absolute paths; restore is `git checkout HEAD -- path`, never the bare form): removed only the two new lines, mutation proven on disk (anchor count 1 -> 0, blob ba20382b -> f01c6018). Fixtures: `2 failed | 58 passed` — exactly cases (a) sibling-import and (e) alias, the two that depend on the branch; (b) spec-import, (c) local declaration and (d) third-party import stayed green, which is what says the ablation removed the resolution rather than the whole arm. DIRECTION REPORTED AS OBSERVED, not as the template predicts: the gate reds in the REVERSE direction from the pre-ledger-edit run — with the ALLOW entry now deleted the reappearing barrel finding is not re-excused but FRESH (`@object-ui/plugin-list declares 1 spec-named symbol the spec already owns: re-export 'ListView' packages/plugin-list/src/index.tsx:15`). Restore proven by blob equality to HEAD and an empty `git diff HEAD`, not by an exit code.",
      "final_sha": "ab22b86d693977826d1576f79ad01bed01861fed (short ab22b86d6); base d39ede24e",
      "files_changed": [
        "scripts/check-spec-symbol-derivation.mjs",
        "scripts/__tests__/check-spec-symbol-derivation.test.ts"
      ],
      "mcp_calls": "0 - every GitHub read and write in this run went through repo-scoped REST (curl): the PR #7685 file patch, the PR creation, this comment, and the read-backs. No MCP GitHub tool was invoked, and no dedup search was needed because nothing was filed.",
      "deviations": [
        "Channel: all GitHub traffic via REST curl rather than MCP (a repo-scoped REST read answered 200 on first probe, so the cheaper channel was used throughout). Declared per the channel rule.",
        "Gate-family derivation: scripts/pm/dispatch-gates.mjs lives in objectstack and refuses --repo objectstack-ai/objectui, so the family list was derived by hand from objectui's own package.json scripts; the derived set is enumerated in `gates`.",
        "One extra hunk beyond the named surface, in the same file: the rule-2 header clause that reads 'the ALLOW entries for AuthProvider / ListView / UserFilters already make' named three entries of which my diff deletes the last surviving one. Left as-is it would be a statement my own diff falsifies, so it now names the judgement and points at the tombstones. Comment only, no behaviour.",
        "Commit trailer is `Co-authored-by: Claude <noreply@anthropic.com>` with no model identifier, per the dispatch's prohibition, in place of the harness-supplied attribution string that names one. Matches the trailer shape on recent main commits.",
        "PR #7685 overlap handled per the PM correction: its hunk (ALLOW map, lines 670-716) was read from the REST patch field before editing, my hunks are non-adjacent by construction, and the PR body carries the one-sentence overlap statement."
      ],
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  5. claude commented on Sep 6, 2026

    @claude
    Contributor

    ACCEPT — PR #7961 (head ab22b86d6): a bare export { X } is now judged by where X was imported from; the one live instance moves off the barrel, and the ledger entry it kept alive is deleted on the same judgement as its two siblings.

    domain:devx @ objectui execution seat, PM session session_013uAaxiwgYDybsTNV9xwa1M, R44, 2026-09-06T04:52Z. Report 5557004448 reviewed against GitHub and the tree, ⛔ not against its narrative.

    item reading (mine)
    PR shape draft ✅ · base main ✅ · first line Fixes #7275 ✅ · no other closing keyword adjacent to a card number ✅ · one platform footer ✅ · the #7685 overlap sentence is in the body ✅
    Path surface (get_files) exactly 2: scripts/check-spec-symbol-derivation.mjs (+80/−45), scripts/__tests__/check-spec-symbol-derivation.test.ts (+139/−9). git diff --name-only d39ede24e..ab22b86d6 -- packages/ = 0 — no barrel rewritten. Governed guard NOT GOVERNED. No changeset owed (gate verdict)
    Hunks vs PR #7685's (:670–:716) the gate file's hunks start at old lines 93 / 582 / 878 / 1610 / 1643 — none inside #7685's region; a three-way merge stays clean whichever lands second
    The rule, read on the branch :1665 const localName = (el.propertyName ?? el.name).text; — alias form resolves by the LOCAL name; the import loop now records each binding's source; a specifier-carrying export is untouched; the specBindings derivation arm is unchanged; a third-party import stays reported (fixture (d))
    A3 measured, not assumed scanner fixed + entry still present ⇒ ratchet 3 reds naming @object-ui/plugin-list:ListView ("in ALLOW but no longer collides") ⇒ entry deleted with a tombstone beside AuthProvider / UserFilters ("@object-ui/plugin-list:ListView": { 0 on the branch; tombstone at :587); siblings NOT re-added because the corrected scan reports neither. Rule-2 header clause that named the three entries is reworded to point at the tombstones — the one hunk beyond the named lines, comment only, and it is the sentence the diff itself falsified
    A4 measured over the whole corpus 1348 files / 4959 spec names identical across every run; full finding dumps 36 → 35, diff = exactly one removed row re-export ListView packages/plugin-list/src/index.tsx:15; verdict lines 20 declared dialects → 19, 14 untriaged collisions in 7 packages unmoved; rules 2 / 4 unmoved
    Probe / ablation planted const NavigationConfig = …; export { NavigationConfig }; on the live barrel (blob 0480461a → e436c5e2, proven on disk) ⇒ gate reports BOTH the const and the re-export while the sibling re-exports stay silent; restored to the HEAD blob. Ablation of only the resolution branch on the committed tree ⇒ exactly fixtures (a) sibling-import and (e) alias red, (b)/(c)/(d) green — the ablation removed the resolution, not the arm; the reappearing barrel finding is FRESH, not re-excused (reported as observed, direction opposite to the template) — restore proven by blob equality and empty git diff HEAD
    Gates check:spec-symbols OK (19 dialects) · suite 60/60 (55 before, 5 new) — the only importer of the module · type-check:scripts 0 · lint:root 0 errors · check:control-bytes OK (6412) · check-changeset-presence none owed · governed guard NOT GOVERNED · plus the hand-derived family (check:entry-guard, check:unreferenced-sources, check:vi-mock-specifiers, check:vi-mock-inherit, check:shell-escape-residue, lint:coverage, type-check:coverage) green — quoted at ab22b86d6
    CI on head at 2026-09-06T04:51Z 17 success · 9 in progress · 3 skipped · 0 failing

    No findings filed; none owed. Triage's core sentence is now false in the good direction: none of the three entries written on one judgement survives on an accident of barrel spelling.

    Landing plan

    Path surface clean ⇒ flipped ready at 2026-09-06T04:52Z; auto-merge armed only when every check is green; enqueue confirmed by added_to_merge_queue. On MERGED the card closes via Fixes; strip pm:dispatched and clear the assignee in the same stroke. If #7685 lands first, the queue rebuild re-runs the gate on the merged tree — the two hunks are disjoint, nothing more is owed.


    Generated by Claude Code

  6. claude commented on Sep 6, 2026

    @claude
    Contributor

    LANDED — PR #7961 merged 2026-09-06T05:19:21Z (queued 05:04:00Z). Verified on origin/main b31370093 by CONTENT, ⛔ not by the merge event.

    domain:devx @ objectui execution seat, PM session session_013uAaxiwgYDybsTNV9xwa1M, R44, 2026-09-06T05:21Z.

    reading expected got
    propertyName ?? el.name (the local-name resolution) in scripts/check-spec-symbol-derivation.mjs ≥ 1 1
    "@object-ui/plugin-list:ListView": { ALLOW entry 0 0
    plugin-list:ListView tombstone mention ≥ 1 1
    control — auth:AuthProvider tombstone still present ≥ 1 2 ✅ probe fires
    packages/plugin-list/src/index.tsx barrel line untouched 1 1

    Card closed by Fixes. Stripping pm:dispatched and clearing the assignee in this stroke. Draft PR #7685's hunk on the same file (:670–:716) is disjoint — nothing owed there beyond its own merge of main.


    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:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repofindingpriority:p3tooling

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions