tooling(pm): take node:path as a namespace in dispatch-gates so no fixture can shadow it - #14988

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-14688-selftest-path-shadow
Sep 3, 2026
Merged

tooling(pm): take node:path as a namespace in dispatch-gates so no fixture can shadow it#14988
os-steve merged 1 commit into
mainfrom
claude/issue-14688-selftest-path-shadow

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#14688

selfTest() binds fixture values to two names this module imports from node:path, so a later case reaching for either gets the fixture instead of the function and dies at run time. Parse-time checks cannot see it; the run-time message names a real binding, so it reads like a broken import rather than a shadow.

What changed

The module took five bare names from node:path. It now takes one namespace binding, and the 149 genuine call sites are spelled through it. Nothing else moved — this is a binding-name change, as the triage scope ruling requires.

The namespace is spelled nodePath, not path — measured, not stylistic

The obvious spelling is refuted by this file. Measured on the merged tree with a scope-resolving AST pass over every identifier reference (not a grep — the file is full of resolve(/join( inside fixture strings and prose):

namespace candidateconflicts
path1
nodePath0

This file already binds path locally 16 times — 3 block consts and 13 parameters. One of them is load-bearing:

functionreadTrackedSource(path){returnreadFileSync(join(ROOT,path),'utf8');}

Under a path namespace that line becomes path.join(ROOT, path), calling .join on a string parameter — re-creating the exact defect this PR closes, in the same file, on the same day. nodePath occurs nowhere in scripts/.

Why the namespace rather than renaming the two fixtures

Renaming the fixtures fixes today's two instances. The namespace removes the possibility: after this change zero module-scope node:path bindings remain, so there is nothing left for a fixture to shadow. The AST pass confirms the end state — the only surviving bare references to the five names are the 3 deliberate fixture uses, each binding to its own local fixture.

There is a second, structural gain. The two shadowed names were dangerous precisely because no existing case inside selfTest() called them, so the shadow armed a trap for a future case instead of breaking a present one. nodePath is called ~100 times inside selfTest() itself, so any future binding of that name fails immediately, at the moment it is written, rather than lying in wait.

The diff is mechanically reviewable

Every changed line is a prefix insertion. Stripping the inserted prefix from the result reproduces the previous file byte for byte except the import line:

$ sed 's/nodePath\.//g' scripts/pm/dispatch-gates.mjs ...stripped
$ diff previous-file stripped ...one hunk, at line 363:
previous: import { basename, join, dirname, relative, resolve } from 'node:path';
now: import * as nodePath from 'node:path';

That single hunk is the whole semantic content of a 140-line diff. No assertion, fixture value, string literal or comment moved; the file is the same 17,974 lines.

Verification

The battery is the verdict, and it is identical.pnpm check:pm-dispatch-gates before and after, both under scripts/pm/os-verify-lock.sh:

before (base 5bc2f27): ✓ dispatch-gates self-test: 1288 cases pass.
after (head 28943b59): ✓ dispatch-gates self-test: 1288 cases pass.

Stronger than the count: all 1,289 streamed case lines diff byte-identical between the two runs, 0 failures on each side.

Reverse verification, both legs injected at the same program point (immediately after the fixture binding), each mutation proven on disk by occurrence count and object hash, each restored to the HEAD blob with git diff HEAD empty:

legtreeprobereading
Aunfixed baserelative(ROOT, ROOT)TypeError: relative is not a function
Bthis branchnodePath.relative(ROOT, ROOT)returned "", and typeof relative is still string

Leg B's second reading is the point worth stating plainly: the fixture is untouched and the bare name is still a string. The defect is closed by removing the module binding that could be shadowed, not by making the bare spelling work.

node --check passes on both trees and is not offered as evidence — by construction it cannot see this defect class.

Gates. Re-derived after the final commit from the actual change set (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, no paths): 20 runnable families. 19 ran green; check-test-completeness.mjs exits 3, which is its own NOT MEASURED code — it grades a saved turbo run test log and states "⛔ It is not a red, and there is nothing here to fix."

ESLint narrowed to the changed file, with the invariance evidence: eslint's own --print-config reports 2 active rules for this file, neither type-aware; --format json reports 1 file linted, 0 errors, 0 warnings. No project/projectService is configured anywhere in eslint.config.mjs, so this diff cannot move the verdict of any file it does not touch.

Scope

Binding names only, one file. Out of scope and untouched, as the dispatch fences them: the --tier --residue refusal table (#14753) and the derivation coverage work (#14880) both remain open and queued behind this. #14672 stays exactly as merged — its ROOT-derived workaround is left in place and is still correct; only the comment above it now describes a hazard that no longer exists, which is a follow-up rather than a rider here.

skip-changeset: scripts/pm/** publishes nothing from any package.


🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…xture can shadow it
`selfTest()` binds fixture values to two names the module imports from
`node:path` — `const relative = [...].join('\n')` (function scope, so it
shadows for the whole battery) and `const resolve = () => 'pnpm check:x'`
(one nested block). A later case reaching for either gets the fixture, not
the function, and dies at run time with `TypeError: relative is not a
function`. The message names a real binding, so it reads like a broken
import rather than a shadow; parse-time checks cannot see it at all.
The module now takes one namespace binding instead of five bare ones, so
there is no module-scope `node:path` name left for a fixture to shadow —
the class closes rather than today's two instances. The namespace is spelled
`nodePath`, NOT `path`: this file already binds `path` locally 16 times (3
block consts, 13 parameters), and one of them, `readTrackedSource(path)`,
calls `join(ROOT, path)` — under a `path` namespace that line would become
`path.join(ROOT, path)` on a string parameter, re-creating the very defect
being closed. `nodePath` is bound nowhere in the tree.
Binding names only. Stripping the inserted `nodePath.` prefix from the
result reproduces the previous file byte for byte except the import line, so
no assertion, fixture value, string or comment moved; the battery's verdict
is identical before and after.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@os-steve
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

tooling(pm): take node:path as a namespace in dispatch-gates so no fixture can shadow it - #14988

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-14688-selftest-path-shadow
Sep 3, 2026
Merged

tooling(pm): take node:path as a namespace in dispatch-gates so no fixture can shadow it#14988
os-steve merged 1 commit into
mainfrom
claude/issue-14688-selftest-path-shadow

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#14688

selfTest() binds fixture values to two names this module imports from node:path, so a later case reaching for either gets the fixture instead of the function and dies at run time. Parse-time checks cannot see it; the run-time message names a real binding, so it reads like a broken import rather than a shadow.

What changed

The module took five bare names from node:path. It now takes one namespace binding, and the 149 genuine call sites are spelled through it. Nothing else moved — this is a binding-name change, as the triage scope ruling requires.

The namespace is spelled nodePath, not path — measured, not stylistic

The obvious spelling is refuted by this file. Measured on the merged tree with a scope-resolving AST pass over every identifier reference (not a grep — the file is full of resolve(/join( inside fixture strings and prose):

namespace candidateconflicts
path1
nodePath0

This file already binds path locally 16 times — 3 block consts and 13 parameters. One of them is load-bearing:

functionreadTrackedSource(path){returnreadFileSync(join(ROOT,path),'utf8');}

Under a path namespace that line becomes path.join(ROOT, path), calling .join on a string parameter — re-creating the exact defect this PR closes, in the same file, on the same day. nodePath occurs nowhere in scripts/.

Why the namespace rather than renaming the two fixtures

Renaming the fixtures fixes today's two instances. The namespace removes the possibility: after this change zero module-scope node:path bindings remain, so there is nothing left for a fixture to shadow. The AST pass confirms the end state — the only surviving bare references to the five names are the 3 deliberate fixture uses, each binding to its own local fixture.

There is a second, structural gain. The two shadowed names were dangerous precisely because no existing case inside selfTest() called them, so the shadow armed a trap for a future case instead of breaking a present one. nodePath is called ~100 times inside selfTest() itself, so any future binding of that name fails immediately, at the moment it is written, rather than lying in wait.

The diff is mechanically reviewable

Every changed line is a prefix insertion. Stripping the inserted prefix from the result reproduces the previous file byte for byte except the import line:

$ sed 's/nodePath\.//g' scripts/pm/dispatch-gates.mjs ...stripped
$ diff previous-file stripped ...one hunk, at line 363:
previous: import { basename, join, dirname, relative, resolve } from 'node:path';
now: import * as nodePath from 'node:path';

That single hunk is the whole semantic content of a 140-line diff. No assertion, fixture value, string literal or comment moved; the file is the same 17,974 lines.

Verification

The battery is the verdict, and it is identical.pnpm check:pm-dispatch-gates before and after, both under scripts/pm/os-verify-lock.sh:

before (base 5bc2f27): ✓ dispatch-gates self-test: 1288 cases pass.
after (head 28943b59): ✓ dispatch-gates self-test: 1288 cases pass.

Stronger than the count: all 1,289 streamed case lines diff byte-identical between the two runs, 0 failures on each side.

Reverse verification, both legs injected at the same program point (immediately after the fixture binding), each mutation proven on disk by occurrence count and object hash, each restored to the HEAD blob with git diff HEAD empty:

legtreeprobereading
Aunfixed baserelative(ROOT, ROOT)TypeError: relative is not a function
Bthis branchnodePath.relative(ROOT, ROOT)returned "", and typeof relative is still string

Leg B's second reading is the point worth stating plainly: the fixture is untouched and the bare name is still a string. The defect is closed by removing the module binding that could be shadowed, not by making the bare spelling work.

node --check passes on both trees and is not offered as evidence — by construction it cannot see this defect class.

Gates. Re-derived after the final commit from the actual change set (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, no paths): 20 runnable families. 19 ran green; check-test-completeness.mjs exits 3, which is its own NOT MEASURED code — it grades a saved turbo run test log and states "⛔ It is not a red, and there is nothing here to fix."

ESLint narrowed to the changed file, with the invariance evidence: eslint's own --print-config reports 2 active rules for this file, neither type-aware; --format json reports 1 file linted, 0 errors, 0 warnings. No project/projectService is configured anywhere in eslint.config.mjs, so this diff cannot move the verdict of any file it does not touch.

Scope

Binding names only, one file. Out of scope and untouched, as the dispatch fences them: the --tier --residue refusal table (#14753) and the derivation coverage work (#14880) both remain open and queued behind this. #14672 stays exactly as merged — its ROOT-derived workaround is left in place and is still correct; only the comment above it now describes a hazard that no longer exists, which is a follow-up rather than a rider here.

skip-changeset: scripts/pm/** publishes nothing from any package.


🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…xture can shadow it
`selfTest()` binds fixture values to two names the module imports from
`node:path` — `const relative = [...].join('\n')` (function scope, so it
shadows for the whole battery) and `const resolve = () => 'pnpm check:x'`
(one nested block). A later case reaching for either gets the fixture, not
the function, and dies at run time with `TypeError: relative is not a
function`. The message names a real binding, so it reads like a broken
import rather than a shadow; parse-time checks cannot see it at all.
The module now takes one namespace binding instead of five bare ones, so
there is no module-scope `node:path` name left for a fixture to shadow —
the class closes rather than today's two instances. The namespace is spelled
`nodePath`, NOT `path`: this file already binds `path` locally 16 times (3
block consts, 13 parameters), and one of them, `readTrackedSource(path)`,
calls `join(ROOT, path)` — under a `path` namespace that line would become
`path.join(ROOT, path)` on a string parameter, re-creating the very defect
being closed. `nodePath` is bound nowhere in the tree.
Binding names only. Stripping the inserted `nodePath.` prefix from the
result reproduces the previous file byte for byte except the import line, so
no assertion, fixture value, string or comment moved; the battery's verdict
is identical before and after.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@os-steve
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

tooling(pm): take node:path as a namespace in dispatch-gates so no fixture can shadow it - #14988

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-14688-selftest-path-shadow
Sep 3, 2026
Merged

tooling(pm): take node:path as a namespace in dispatch-gates so no fixture can shadow it#14988
os-steve merged 1 commit into
mainfrom
claude/issue-14688-selftest-path-shadow

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#14688

selfTest() binds fixture values to two names this module imports from node:path, so a later case reaching for either gets the fixture instead of the function and dies at run time. Parse-time checks cannot see it; the run-time message names a real binding, so it reads like a broken import rather than a shadow.

What changed

The module took five bare names from node:path. It now takes one namespace binding, and the 149 genuine call sites are spelled through it. Nothing else moved — this is a binding-name change, as the triage scope ruling requires.

The namespace is spelled nodePath, not path — measured, not stylistic

The obvious spelling is refuted by this file. Measured on the merged tree with a scope-resolving AST pass over every identifier reference (not a grep — the file is full of resolve(/join( inside fixture strings and prose):

namespace candidateconflicts
path1
nodePath0

This file already binds path locally 16 times — 3 block consts and 13 parameters. One of them is load-bearing:

functionreadTrackedSource(path){returnreadFileSync(join(ROOT,path),'utf8');}

Under a path namespace that line becomes path.join(ROOT, path), calling .join on a string parameter — re-creating the exact defect this PR closes, in the same file, on the same day. nodePath occurs nowhere in scripts/.

Why the namespace rather than renaming the two fixtures

Renaming the fixtures fixes today's two instances. The namespace removes the possibility: after this change zero module-scope node:path bindings remain, so there is nothing left for a fixture to shadow. The AST pass confirms the end state — the only surviving bare references to the five names are the 3 deliberate fixture uses, each binding to its own local fixture.

There is a second, structural gain. The two shadowed names were dangerous precisely because no existing case inside selfTest() called them, so the shadow armed a trap for a future case instead of breaking a present one. nodePath is called ~100 times inside selfTest() itself, so any future binding of that name fails immediately, at the moment it is written, rather than lying in wait.

The diff is mechanically reviewable

Every changed line is a prefix insertion. Stripping the inserted prefix from the result reproduces the previous file byte for byte except the import line:

$ sed 's/nodePath\.//g' scripts/pm/dispatch-gates.mjs ...stripped
$ diff previous-file stripped ...one hunk, at line 363:
previous: import { basename, join, dirname, relative, resolve } from 'node:path';
now: import * as nodePath from 'node:path';

That single hunk is the whole semantic content of a 140-line diff. No assertion, fixture value, string literal or comment moved; the file is the same 17,974 lines.

Verification

The battery is the verdict, and it is identical.pnpm check:pm-dispatch-gates before and after, both under scripts/pm/os-verify-lock.sh:

before (base 5bc2f27): ✓ dispatch-gates self-test: 1288 cases pass.
after (head 28943b59): ✓ dispatch-gates self-test: 1288 cases pass.

Stronger than the count: all 1,289 streamed case lines diff byte-identical between the two runs, 0 failures on each side.

Reverse verification, both legs injected at the same program point (immediately after the fixture binding), each mutation proven on disk by occurrence count and object hash, each restored to the HEAD blob with git diff HEAD empty:

legtreeprobereading
Aunfixed baserelative(ROOT, ROOT)TypeError: relative is not a function
Bthis branchnodePath.relative(ROOT, ROOT)returned "", and typeof relative is still string

Leg B's second reading is the point worth stating plainly: the fixture is untouched and the bare name is still a string. The defect is closed by removing the module binding that could be shadowed, not by making the bare spelling work.

node --check passes on both trees and is not offered as evidence — by construction it cannot see this defect class.

Gates. Re-derived after the final commit from the actual change set (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, no paths): 20 runnable families. 19 ran green; check-test-completeness.mjs exits 3, which is its own NOT MEASURED code — it grades a saved turbo run test log and states "⛔ It is not a red, and there is nothing here to fix."

ESLint narrowed to the changed file, with the invariance evidence: eslint's own --print-config reports 2 active rules for this file, neither type-aware; --format json reports 1 file linted, 0 errors, 0 warnings. No project/projectService is configured anywhere in eslint.config.mjs, so this diff cannot move the verdict of any file it does not touch.

Scope

Binding names only, one file. Out of scope and untouched, as the dispatch fences them: the --tier --residue refusal table (#14753) and the derivation coverage work (#14880) both remain open and queued behind this. #14672 stays exactly as merged — its ROOT-derived workaround is left in place and is still correct; only the comment above it now describes a hazard that no longer exists, which is a follow-up rather than a rider here.

skip-changeset: scripts/pm/** publishes nothing from any package.


🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…xture can shadow it
`selfTest()` binds fixture values to two names the module imports from
`node:path` — `const relative = [...].join('\n')` (function scope, so it
shadows for the whole battery) and `const resolve = () => 'pnpm check:x'`
(one nested block). A later case reaching for either gets the fixture, not
the function, and dies at run time with `TypeError: relative is not a
function`. The message names a real binding, so it reads like a broken
import rather than a shadow; parse-time checks cannot see it at all.
The module now takes one namespace binding instead of five bare ones, so
there is no module-scope `node:path` name left for a fixture to shadow —
the class closes rather than today's two instances. The namespace is spelled
`nodePath`, NOT `path`: this file already binds `path` locally 16 times (3
block consts, 13 parameters), and one of them, `readTrackedSource(path)`,
calls `join(ROOT, path)` — under a `path` namespace that line would become
`path.join(ROOT, path)` on a string parameter, re-creating the very defect
being closed. `nodePath` is bound nowhere in the tree.
Binding names only. Stripping the inserted `nodePath.` prefix from the
result reproduces the previous file byte for byte except the import line, so
no assertion, fixture value, string or comment moved; the battery's verdict
is identical before and after.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@os-steve
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

tooling(pm): take node:path as a namespace in dispatch-gates so no fixture can shadow it - #14988

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-14688-selftest-path-shadow
Sep 3, 2026
Merged

tooling(pm): take node:path as a namespace in dispatch-gates so no fixture can shadow it#14988
os-steve merged 1 commit into
mainfrom
claude/issue-14688-selftest-path-shadow

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#14688

selfTest() binds fixture values to two names this module imports from node:path, so a later case reaching for either gets the fixture instead of the function and dies at run time. Parse-time checks cannot see it; the run-time message names a real binding, so it reads like a broken import rather than a shadow.

What changed

The module took five bare names from node:path. It now takes one namespace binding, and the 149 genuine call sites are spelled through it. Nothing else moved — this is a binding-name change, as the triage scope ruling requires.

The namespace is spelled nodePath, not path — measured, not stylistic

The obvious spelling is refuted by this file. Measured on the merged tree with a scope-resolving AST pass over every identifier reference (not a grep — the file is full of resolve(/join( inside fixture strings and prose):

namespace candidateconflicts
path1
nodePath0

This file already binds path locally 16 times — 3 block consts and 13 parameters. One of them is load-bearing:

functionreadTrackedSource(path){returnreadFileSync(join(ROOT,path),'utf8');}

Under a path namespace that line becomes path.join(ROOT, path), calling .join on a string parameter — re-creating the exact defect this PR closes, in the same file, on the same day. nodePath occurs nowhere in scripts/.

Why the namespace rather than renaming the two fixtures

Renaming the fixtures fixes today's two instances. The namespace removes the possibility: after this change zero module-scope node:path bindings remain, so there is nothing left for a fixture to shadow. The AST pass confirms the end state — the only surviving bare references to the five names are the 3 deliberate fixture uses, each binding to its own local fixture.

There is a second, structural gain. The two shadowed names were dangerous precisely because no existing case inside selfTest() called them, so the shadow armed a trap for a future case instead of breaking a present one. nodePath is called ~100 times inside selfTest() itself, so any future binding of that name fails immediately, at the moment it is written, rather than lying in wait.

The diff is mechanically reviewable

Every changed line is a prefix insertion. Stripping the inserted prefix from the result reproduces the previous file byte for byte except the import line:

$ sed 's/nodePath\.//g' scripts/pm/dispatch-gates.mjs ...stripped
$ diff previous-file stripped ...one hunk, at line 363:
previous: import { basename, join, dirname, relative, resolve } from 'node:path';
now: import * as nodePath from 'node:path';

That single hunk is the whole semantic content of a 140-line diff. No assertion, fixture value, string literal or comment moved; the file is the same 17,974 lines.

Verification

The battery is the verdict, and it is identical.pnpm check:pm-dispatch-gates before and after, both under scripts/pm/os-verify-lock.sh:

before (base 5bc2f27): ✓ dispatch-gates self-test: 1288 cases pass.
after (head 28943b59): ✓ dispatch-gates self-test: 1288 cases pass.

Stronger than the count: all 1,289 streamed case lines diff byte-identical between the two runs, 0 failures on each side.

Reverse verification, both legs injected at the same program point (immediately after the fixture binding), each mutation proven on disk by occurrence count and object hash, each restored to the HEAD blob with git diff HEAD empty:

legtreeprobereading
Aunfixed baserelative(ROOT, ROOT)TypeError: relative is not a function
Bthis branchnodePath.relative(ROOT, ROOT)returned "", and typeof relative is still string

Leg B's second reading is the point worth stating plainly: the fixture is untouched and the bare name is still a string. The defect is closed by removing the module binding that could be shadowed, not by making the bare spelling work.

node --check passes on both trees and is not offered as evidence — by construction it cannot see this defect class.

Gates. Re-derived after the final commit from the actual change set (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, no paths): 20 runnable families. 19 ran green; check-test-completeness.mjs exits 3, which is its own NOT MEASURED code — it grades a saved turbo run test log and states "⛔ It is not a red, and there is nothing here to fix."

ESLint narrowed to the changed file, with the invariance evidence: eslint's own --print-config reports 2 active rules for this file, neither type-aware; --format json reports 1 file linted, 0 errors, 0 warnings. No project/projectService is configured anywhere in eslint.config.mjs, so this diff cannot move the verdict of any file it does not touch.

Scope

Binding names only, one file. Out of scope and untouched, as the dispatch fences them: the --tier --residue refusal table (#14753) and the derivation coverage work (#14880) both remain open and queued behind this. #14672 stays exactly as merged — its ROOT-derived workaround is left in place and is still correct; only the comment above it now describes a hazard that no longer exists, which is a follow-up rather than a rider here.

skip-changeset: scripts/pm/** publishes nothing from any package.


🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…xture can shadow it
`selfTest()` binds fixture values to two names the module imports from
`node:path` — `const relative = [...].join('\n')` (function scope, so it
shadows for the whole battery) and `const resolve = () => 'pnpm check:x'`
(one nested block). A later case reaching for either gets the fixture, not
the function, and dies at run time with `TypeError: relative is not a
function`. The message names a real binding, so it reads like a broken
import rather than a shadow; parse-time checks cannot see it at all.
The module now takes one namespace binding instead of five bare ones, so
there is no module-scope `node:path` name left for a fixture to shadow —
the class closes rather than today's two instances. The namespace is spelled
`nodePath`, NOT `path`: this file already binds `path` locally 16 times (3
block consts, 13 parameters), and one of them, `readTrackedSource(path)`,
calls `join(ROOT, path)` — under a `path` namespace that line would become
`path.join(ROOT, path)` on a string parameter, re-creating the very defect
being closed. `nodePath` is bound nowhere in the tree.
Binding names only. Stripping the inserted `nodePath.` prefix from the
result reproduces the previous file byte for byte except the import line, so
no assertion, fixture value, string or comment moved; the battery's verdict
is identical before and after.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@os-steve
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

tooling(pm): take node:path as a namespace in dispatch-gates so no fixture can shadow it - #14988

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-14688-selftest-path-shadow
Sep 3, 2026
Merged

tooling(pm): take node:path as a namespace in dispatch-gates so no fixture can shadow it#14988
os-steve merged 1 commit into
mainfrom
claude/issue-14688-selftest-path-shadow

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#14688

selfTest() binds fixture values to two names this module imports from node:path, so a later case reaching for either gets the fixture instead of the function and dies at run time. Parse-time checks cannot see it; the run-time message names a real binding, so it reads like a broken import rather than a shadow.

What changed

The module took five bare names from node:path. It now takes one namespace binding, and the 149 genuine call sites are spelled through it. Nothing else moved — this is a binding-name change, as the triage scope ruling requires.

The namespace is spelled nodePath, not path — measured, not stylistic

The obvious spelling is refuted by this file. Measured on the merged tree with a scope-resolving AST pass over every identifier reference (not a grep — the file is full of resolve(/join( inside fixture strings and prose):

namespace candidateconflicts
path1
nodePath0

This file already binds path locally 16 times — 3 block consts and 13 parameters. One of them is load-bearing:

functionreadTrackedSource(path){returnreadFileSync(join(ROOT,path),'utf8');}

Under a path namespace that line becomes path.join(ROOT, path), calling .join on a string parameter — re-creating the exact defect this PR closes, in the same file, on the same day. nodePath occurs nowhere in scripts/.

Why the namespace rather than renaming the two fixtures

Renaming the fixtures fixes today's two instances. The namespace removes the possibility: after this change zero module-scope node:path bindings remain, so there is nothing left for a fixture to shadow. The AST pass confirms the end state — the only surviving bare references to the five names are the 3 deliberate fixture uses, each binding to its own local fixture.

There is a second, structural gain. The two shadowed names were dangerous precisely because no existing case inside selfTest() called them, so the shadow armed a trap for a future case instead of breaking a present one. nodePath is called ~100 times inside selfTest() itself, so any future binding of that name fails immediately, at the moment it is written, rather than lying in wait.

The diff is mechanically reviewable

Every changed line is a prefix insertion. Stripping the inserted prefix from the result reproduces the previous file byte for byte except the import line:

$ sed 's/nodePath\.//g' scripts/pm/dispatch-gates.mjs ...stripped
$ diff previous-file stripped ...one hunk, at line 363:
previous: import { basename, join, dirname, relative, resolve } from 'node:path';
now: import * as nodePath from 'node:path';

That single hunk is the whole semantic content of a 140-line diff. No assertion, fixture value, string literal or comment moved; the file is the same 17,974 lines.

Verification

The battery is the verdict, and it is identical.pnpm check:pm-dispatch-gates before and after, both under scripts/pm/os-verify-lock.sh:

before (base 5bc2f27): ✓ dispatch-gates self-test: 1288 cases pass.
after (head 28943b59): ✓ dispatch-gates self-test: 1288 cases pass.

Stronger than the count: all 1,289 streamed case lines diff byte-identical between the two runs, 0 failures on each side.

Reverse verification, both legs injected at the same program point (immediately after the fixture binding), each mutation proven on disk by occurrence count and object hash, each restored to the HEAD blob with git diff HEAD empty:

legtreeprobereading
Aunfixed baserelative(ROOT, ROOT)TypeError: relative is not a function
Bthis branchnodePath.relative(ROOT, ROOT)returned "", and typeof relative is still string

Leg B's second reading is the point worth stating plainly: the fixture is untouched and the bare name is still a string. The defect is closed by removing the module binding that could be shadowed, not by making the bare spelling work.

node --check passes on both trees and is not offered as evidence — by construction it cannot see this defect class.

Gates. Re-derived after the final commit from the actual change set (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, no paths): 20 runnable families. 19 ran green; check-test-completeness.mjs exits 3, which is its own NOT MEASURED code — it grades a saved turbo run test log and states "⛔ It is not a red, and there is nothing here to fix."

ESLint narrowed to the changed file, with the invariance evidence: eslint's own --print-config reports 2 active rules for this file, neither type-aware; --format json reports 1 file linted, 0 errors, 0 warnings. No project/projectService is configured anywhere in eslint.config.mjs, so this diff cannot move the verdict of any file it does not touch.

Scope

Binding names only, one file. Out of scope and untouched, as the dispatch fences them: the --tier --residue refusal table (#14753) and the derivation coverage work (#14880) both remain open and queued behind this. #14672 stays exactly as merged — its ROOT-derived workaround is left in place and is still correct; only the comment above it now describes a hazard that no longer exists, which is a follow-up rather than a rider here.

skip-changeset: scripts/pm/** publishes nothing from any package.


🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…xture can shadow it
`selfTest()` binds fixture values to two names the module imports from
`node:path` — `const relative = [...].join('\n')` (function scope, so it
shadows for the whole battery) and `const resolve = () => 'pnpm check:x'`
(one nested block). A later case reaching for either gets the fixture, not
the function, and dies at run time with `TypeError: relative is not a
function`. The message names a real binding, so it reads like a broken
import rather than a shadow; parse-time checks cannot see it at all.
The module now takes one namespace binding instead of five bare ones, so
there is no module-scope `node:path` name left for a fixture to shadow —
the class closes rather than today's two instances. The namespace is spelled
`nodePath`, NOT `path`: this file already binds `path` locally 16 times (3
block consts, 13 parameters), and one of them, `readTrackedSource(path)`,
calls `join(ROOT, path)` — under a `path` namespace that line would become
`path.join(ROOT, path)` on a string parameter, re-creating the very defect
being closed. `nodePath` is bound nowhere in the tree.
Binding names only. Stripping the inserted `nodePath.` prefix from the
result reproduces the previous file byte for byte except the import line, so
no assertion, fixture value, string or comment moved; the battery's verdict
is identical before and after.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@os-steve
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

tooling(pm): take node:path as a namespace in dispatch-gates so no fixture can shadow it - #14988

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-14688-selftest-path-shadow
Sep 3, 2026
Merged

tooling(pm): take node:path as a namespace in dispatch-gates so no fixture can shadow it#14988
os-steve merged 1 commit into
mainfrom
claude/issue-14688-selftest-path-shadow

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#14688

selfTest() binds fixture values to two names this module imports from node:path, so a later case reaching for either gets the fixture instead of the function and dies at run time. Parse-time checks cannot see it; the run-time message names a real binding, so it reads like a broken import rather than a shadow.

What changed

The module took five bare names from node:path. It now takes one namespace binding, and the 149 genuine call sites are spelled through it. Nothing else moved — this is a binding-name change, as the triage scope ruling requires.

The namespace is spelled nodePath, not path — measured, not stylistic

The obvious spelling is refuted by this file. Measured on the merged tree with a scope-resolving AST pass over every identifier reference (not a grep — the file is full of resolve(/join( inside fixture strings and prose):

namespace candidateconflicts
path1
nodePath0

This file already binds path locally 16 times — 3 block consts and 13 parameters. One of them is load-bearing:

functionreadTrackedSource(path){returnreadFileSync(join(ROOT,path),'utf8');}

Under a path namespace that line becomes path.join(ROOT, path), calling .join on a string parameter — re-creating the exact defect this PR closes, in the same file, on the same day. nodePath occurs nowhere in scripts/.

Why the namespace rather than renaming the two fixtures

Renaming the fixtures fixes today's two instances. The namespace removes the possibility: after this change zero module-scope node:path bindings remain, so there is nothing left for a fixture to shadow. The AST pass confirms the end state — the only surviving bare references to the five names are the 3 deliberate fixture uses, each binding to its own local fixture.

There is a second, structural gain. The two shadowed names were dangerous precisely because no existing case inside selfTest() called them, so the shadow armed a trap for a future case instead of breaking a present one. nodePath is called ~100 times inside selfTest() itself, so any future binding of that name fails immediately, at the moment it is written, rather than lying in wait.

The diff is mechanically reviewable

Every changed line is a prefix insertion. Stripping the inserted prefix from the result reproduces the previous file byte for byte except the import line:

$ sed 's/nodePath\.//g' scripts/pm/dispatch-gates.mjs ...stripped
$ diff previous-file stripped ...one hunk, at line 363:
previous: import { basename, join, dirname, relative, resolve } from 'node:path';
now: import * as nodePath from 'node:path';

That single hunk is the whole semantic content of a 140-line diff. No assertion, fixture value, string literal or comment moved; the file is the same 17,974 lines.

Verification

The battery is the verdict, and it is identical.pnpm check:pm-dispatch-gates before and after, both under scripts/pm/os-verify-lock.sh:

before (base 5bc2f27): ✓ dispatch-gates self-test: 1288 cases pass.
after (head 28943b59): ✓ dispatch-gates self-test: 1288 cases pass.

Stronger than the count: all 1,289 streamed case lines diff byte-identical between the two runs, 0 failures on each side.

Reverse verification, both legs injected at the same program point (immediately after the fixture binding), each mutation proven on disk by occurrence count and object hash, each restored to the HEAD blob with git diff HEAD empty:

legtreeprobereading
Aunfixed baserelative(ROOT, ROOT)TypeError: relative is not a function
Bthis branchnodePath.relative(ROOT, ROOT)returned "", and typeof relative is still string

Leg B's second reading is the point worth stating plainly: the fixture is untouched and the bare name is still a string. The defect is closed by removing the module binding that could be shadowed, not by making the bare spelling work.

node --check passes on both trees and is not offered as evidence — by construction it cannot see this defect class.

Gates. Re-derived after the final commit from the actual change set (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, no paths): 20 runnable families. 19 ran green; check-test-completeness.mjs exits 3, which is its own NOT MEASURED code — it grades a saved turbo run test log and states "⛔ It is not a red, and there is nothing here to fix."

ESLint narrowed to the changed file, with the invariance evidence: eslint's own --print-config reports 2 active rules for this file, neither type-aware; --format json reports 1 file linted, 0 errors, 0 warnings. No project/projectService is configured anywhere in eslint.config.mjs, so this diff cannot move the verdict of any file it does not touch.

Scope

Binding names only, one file. Out of scope and untouched, as the dispatch fences them: the --tier --residue refusal table (#14753) and the derivation coverage work (#14880) both remain open and queued behind this. #14672 stays exactly as merged — its ROOT-derived workaround is left in place and is still correct; only the comment above it now describes a hazard that no longer exists, which is a follow-up rather than a rider here.

skip-changeset: scripts/pm/** publishes nothing from any package.


🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…xture can shadow it
`selfTest()` binds fixture values to two names the module imports from
`node:path` — `const relative = [...].join('\n')` (function scope, so it
shadows for the whole battery) and `const resolve = () => 'pnpm check:x'`
(one nested block). A later case reaching for either gets the fixture, not
the function, and dies at run time with `TypeError: relative is not a
function`. The message names a real binding, so it reads like a broken
import rather than a shadow; parse-time checks cannot see it at all.
The module now takes one namespace binding instead of five bare ones, so
there is no module-scope `node:path` name left for a fixture to shadow —
the class closes rather than today's two instances. The namespace is spelled
`nodePath`, NOT `path`: this file already binds `path` locally 16 times (3
block consts, 13 parameters), and one of them, `readTrackedSource(path)`,
calls `join(ROOT, path)` — under a `path` namespace that line would become
`path.join(ROOT, path)` on a string parameter, re-creating the very defect
being closed. `nodePath` is bound nowhere in the tree.
Binding names only. Stripping the inserted `nodePath.` prefix from the
result reproduces the previous file byte for byte except the import line, so
no assertion, fixture value, string or comment moved; the battery's verdict
is identical before and after.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@os-steve
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

tooling(pm): take node:path as a namespace in dispatch-gates so no fixture can shadow it - #14988

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-14688-selftest-path-shadow
Sep 3, 2026
Merged

tooling(pm): take node:path as a namespace in dispatch-gates so no fixture can shadow it#14988
os-steve merged 1 commit into
mainfrom
claude/issue-14688-selftest-path-shadow

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#14688

selfTest() binds fixture values to two names this module imports from node:path, so a later case reaching for either gets the fixture instead of the function and dies at run time. Parse-time checks cannot see it; the run-time message names a real binding, so it reads like a broken import rather than a shadow.

What changed

The module took five bare names from node:path. It now takes one namespace binding, and the 149 genuine call sites are spelled through it. Nothing else moved — this is a binding-name change, as the triage scope ruling requires.

The namespace is spelled nodePath, not path — measured, not stylistic

The obvious spelling is refuted by this file. Measured on the merged tree with a scope-resolving AST pass over every identifier reference (not a grep — the file is full of resolve(/join( inside fixture strings and prose):

namespace candidateconflicts
path1
nodePath0

This file already binds path locally 16 times — 3 block consts and 13 parameters. One of them is load-bearing:

functionreadTrackedSource(path){returnreadFileSync(join(ROOT,path),'utf8');}

Under a path namespace that line becomes path.join(ROOT, path), calling .join on a string parameter — re-creating the exact defect this PR closes, in the same file, on the same day. nodePath occurs nowhere in scripts/.

Why the namespace rather than renaming the two fixtures

Renaming the fixtures fixes today's two instances. The namespace removes the possibility: after this change zero module-scope node:path bindings remain, so there is nothing left for a fixture to shadow. The AST pass confirms the end state — the only surviving bare references to the five names are the 3 deliberate fixture uses, each binding to its own local fixture.

There is a second, structural gain. The two shadowed names were dangerous precisely because no existing case inside selfTest() called them, so the shadow armed a trap for a future case instead of breaking a present one. nodePath is called ~100 times inside selfTest() itself, so any future binding of that name fails immediately, at the moment it is written, rather than lying in wait.

The diff is mechanically reviewable

Every changed line is a prefix insertion. Stripping the inserted prefix from the result reproduces the previous file byte for byte except the import line:

$ sed 's/nodePath\.//g' scripts/pm/dispatch-gates.mjs ...stripped
$ diff previous-file stripped ...one hunk, at line 363:
previous: import { basename, join, dirname, relative, resolve } from 'node:path';
now: import * as nodePath from 'node:path';

That single hunk is the whole semantic content of a 140-line diff. No assertion, fixture value, string literal or comment moved; the file is the same 17,974 lines.

Verification

The battery is the verdict, and it is identical.pnpm check:pm-dispatch-gates before and after, both under scripts/pm/os-verify-lock.sh:

before (base 5bc2f27): ✓ dispatch-gates self-test: 1288 cases pass.
after (head 28943b59): ✓ dispatch-gates self-test: 1288 cases pass.

Stronger than the count: all 1,289 streamed case lines diff byte-identical between the two runs, 0 failures on each side.

Reverse verification, both legs injected at the same program point (immediately after the fixture binding), each mutation proven on disk by occurrence count and object hash, each restored to the HEAD blob with git diff HEAD empty:

legtreeprobereading
Aunfixed baserelative(ROOT, ROOT)TypeError: relative is not a function
Bthis branchnodePath.relative(ROOT, ROOT)returned "", and typeof relative is still string

Leg B's second reading is the point worth stating plainly: the fixture is untouched and the bare name is still a string. The defect is closed by removing the module binding that could be shadowed, not by making the bare spelling work.

node --check passes on both trees and is not offered as evidence — by construction it cannot see this defect class.

Gates. Re-derived after the final commit from the actual change set (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, no paths): 20 runnable families. 19 ran green; check-test-completeness.mjs exits 3, which is its own NOT MEASURED code — it grades a saved turbo run test log and states "⛔ It is not a red, and there is nothing here to fix."

ESLint narrowed to the changed file, with the invariance evidence: eslint's own --print-config reports 2 active rules for this file, neither type-aware; --format json reports 1 file linted, 0 errors, 0 warnings. No project/projectService is configured anywhere in eslint.config.mjs, so this diff cannot move the verdict of any file it does not touch.

Scope

Binding names only, one file. Out of scope and untouched, as the dispatch fences them: the --tier --residue refusal table (#14753) and the derivation coverage work (#14880) both remain open and queued behind this. #14672 stays exactly as merged — its ROOT-derived workaround is left in place and is still correct; only the comment above it now describes a hazard that no longer exists, which is a follow-up rather than a rider here.

skip-changeset: scripts/pm/** publishes nothing from any package.


🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…xture can shadow it
`selfTest()` binds fixture values to two names the module imports from
`node:path` — `const relative = [...].join('\n')` (function scope, so it
shadows for the whole battery) and `const resolve = () => 'pnpm check:x'`
(one nested block). A later case reaching for either gets the fixture, not
the function, and dies at run time with `TypeError: relative is not a
function`. The message names a real binding, so it reads like a broken
import rather than a shadow; parse-time checks cannot see it at all.
The module now takes one namespace binding instead of five bare ones, so
there is no module-scope `node:path` name left for a fixture to shadow —
the class closes rather than today's two instances. The namespace is spelled
`nodePath`, NOT `path`: this file already binds `path` locally 16 times (3
block consts, 13 parameters), and one of them, `readTrackedSource(path)`,
calls `join(ROOT, path)` — under a `path` namespace that line would become
`path.join(ROOT, path)` on a string parameter, re-creating the very defect
being closed. `nodePath` is bound nowhere in the tree.
Binding names only. Stripping the inserted `nodePath.` prefix from the
result reproduces the previous file byte for byte except the import line, so
no assertion, fixture value, string or comment moved; the battery's verdict
is identical before and after.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@os-steve
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

tooling(pm): take node:path as a namespace in dispatch-gates so no fixture can shadow it - #14988

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-14688-selftest-path-shadow
Sep 3, 2026
Merged

tooling(pm): take node:path as a namespace in dispatch-gates so no fixture can shadow it#14988
os-steve merged 1 commit into
mainfrom
claude/issue-14688-selftest-path-shadow

Conversation

@claude

@claudeclaudeBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#14688

selfTest() binds fixture values to two names this module imports from node:path, so a later case reaching for either gets the fixture instead of the function and dies at run time. Parse-time checks cannot see it; the run-time message names a real binding, so it reads like a broken import rather than a shadow.

What changed

The module took five bare names from node:path. It now takes one namespace binding, and the 149 genuine call sites are spelled through it. Nothing else moved — this is a binding-name change, as the triage scope ruling requires.

The namespace is spelled nodePath, not path — measured, not stylistic

The obvious spelling is refuted by this file. Measured on the merged tree with a scope-resolving AST pass over every identifier reference (not a grep — the file is full of resolve(/join( inside fixture strings and prose):

namespace candidateconflicts
path1
nodePath0

This file already binds path locally 16 times — 3 block consts and 13 parameters. One of them is load-bearing:

functionreadTrackedSource(path){returnreadFileSync(join(ROOT,path),'utf8');}

Under a path namespace that line becomes path.join(ROOT, path), calling .join on a string parameter — re-creating the exact defect this PR closes, in the same file, on the same day. nodePath occurs nowhere in scripts/.

Why the namespace rather than renaming the two fixtures

Renaming the fixtures fixes today's two instances. The namespace removes the possibility: after this change zero module-scope node:path bindings remain, so there is nothing left for a fixture to shadow. The AST pass confirms the end state — the only surviving bare references to the five names are the 3 deliberate fixture uses, each binding to its own local fixture.

There is a second, structural gain. The two shadowed names were dangerous precisely because no existing case inside selfTest() called them, so the shadow armed a trap for a future case instead of breaking a present one. nodePath is called ~100 times inside selfTest() itself, so any future binding of that name fails immediately, at the moment it is written, rather than lying in wait.

The diff is mechanically reviewable

Every changed line is a prefix insertion. Stripping the inserted prefix from the result reproduces the previous file byte for byte except the import line:

$ sed 's/nodePath\.//g' scripts/pm/dispatch-gates.mjs ...stripped
$ diff previous-file stripped ...one hunk, at line 363:
previous: import { basename, join, dirname, relative, resolve } from 'node:path';
now: import * as nodePath from 'node:path';

That single hunk is the whole semantic content of a 140-line diff. No assertion, fixture value, string literal or comment moved; the file is the same 17,974 lines.

Verification

The battery is the verdict, and it is identical.pnpm check:pm-dispatch-gates before and after, both under scripts/pm/os-verify-lock.sh:

before (base 5bc2f27): ✓ dispatch-gates self-test: 1288 cases pass.
after (head 28943b59): ✓ dispatch-gates self-test: 1288 cases pass.

Stronger than the count: all 1,289 streamed case lines diff byte-identical between the two runs, 0 failures on each side.

Reverse verification, both legs injected at the same program point (immediately after the fixture binding), each mutation proven on disk by occurrence count and object hash, each restored to the HEAD blob with git diff HEAD empty:

legtreeprobereading
Aunfixed baserelative(ROOT, ROOT)TypeError: relative is not a function
Bthis branchnodePath.relative(ROOT, ROOT)returned "", and typeof relative is still string

Leg B's second reading is the point worth stating plainly: the fixture is untouched and the bare name is still a string. The defect is closed by removing the module binding that could be shadowed, not by making the bare spelling work.

node --check passes on both trees and is not offered as evidence — by construction it cannot see this defect class.

Gates. Re-derived after the final commit from the actual change set (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, no paths): 20 runnable families. 19 ran green; check-test-completeness.mjs exits 3, which is its own NOT MEASURED code — it grades a saved turbo run test log and states "⛔ It is not a red, and there is nothing here to fix."

ESLint narrowed to the changed file, with the invariance evidence: eslint's own --print-config reports 2 active rules for this file, neither type-aware; --format json reports 1 file linted, 0 errors, 0 warnings. No project/projectService is configured anywhere in eslint.config.mjs, so this diff cannot move the verdict of any file it does not touch.

Scope

Binding names only, one file. Out of scope and untouched, as the dispatch fences them: the --tier --residue refusal table (#14753) and the derivation coverage work (#14880) both remain open and queued behind this. #14672 stays exactly as merged — its ROOT-derived workaround is left in place and is still correct; only the comment above it now describes a hazard that no longer exists, which is a follow-up rather than a rider here.

skip-changeset: scripts/pm/** publishes nothing from any package.


🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…xture can shadow it
`selfTest()` binds fixture values to two names the module imports from
`node:path` — `const relative = [...].join('\n')` (function scope, so it
shadows for the whole battery) and `const resolve = () => 'pnpm check:x'`
(one nested block). A later case reaching for either gets the fixture, not
the function, and dies at run time with `TypeError: relative is not a
function`. The message names a real binding, so it reads like a broken
import rather than a shadow; parse-time checks cannot see it at all.
The module now takes one namespace binding instead of five bare ones, so
there is no module-scope `node:path` name left for a fixture to shadow —
the class closes rather than today's two instances. The namespace is spelled
`nodePath`, NOT `path`: this file already binds `path` locally 16 times (3
block consts, 13 parameters), and one of them, `readTrackedSource(path)`,
calls `join(ROOT, path)` — under a `path` namespace that line would become
`path.join(ROOT, path)` on a string parameter, re-creating the very defect
being closed. `nodePath` is bound nowhere in the tree.
Binding names only. Stripping the inserted `nodePath.` prefix from the
result reproduces the previous file byte for byte except the import line, so
no assertion, fixture value, string or comment moved; the battery's verdict
is identical before and after.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@os-steve