Skip to content

fix(lint): take build output out of the linted population with a global ignore - #12332

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-12304-eslint-global-ignores
Aug 25, 2026
Merged

fix(lint): take build output out of the linted population with a global ignore#12332
yinlianghui merged 2 commits into
mainfrom
claude/issue-12304-eslint-global-ignores

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#12304

Adds one leading config object to eslint.config.mjs holding only ignores — no files key — which is what makes ESLint treat it as a global exclusion. Declared file surface: eslint.config.mjs, and nothing else.

The mechanism, re-derived properly

The card's structural claim was produced by a brace-matching script. It is confirmed, but by importing the config module and inspecting the real objects rather than by counting braces:

export default is Array: true length: 7
[0..6] hasFiles=true hasIgnores=true GLOBAL_IGNORE=false
GLOBAL ignore objects (ignores WITHOUT files): 0
objects with NEITHER files nor ignores: 0

All seven objects carry files, so every ignores beside one is object-scoped. After this change: 8 objects, exactly 1 global.

Two corrections to the card, both measured

1. The zero-rule population is parsed by espree, not by @typescript-eslint/parser. The card states the emitted files keep "the custom (@typescript-eslint) parser — so each file is parsed in full", and the title says "at full TS-parser cost". Raw --print-config on packages/core/dist/index.js says otherwise:

"rules": {},
"parser": "espree@11.2.0",

The parser: custom reading came from a heuristic that treats the always-present languageOptions.parser as evidence of a custom parser. My own first probe reproduced the same wrong label before I read the raw output.

2. .ts and .d.ts under dist/ were never in the population at all.--print-config returns literal undefined for them — no config object matches, and ESLint does not lint .ts by default. So the emitted declaration files, which are the bulk of a built tree by file count, cost nothing today. What was actually being linted under dist/ is .js and .mjs, via ESLint's built-in JS defaults.

Neither correction changes the direction of the card. Both change what the fix is buying.

Acceptance evidence

Before/after --print-config (pnpm exec eslint --no-inline-config --print-config):

pathbeforeafter
packages/core/dist/index.jsLINTED, 0 rules enabled, espreenot linted
packages/core/dist/index.mjsLINTED, 0 rules enabled, espreenot linted
packages/core/dist/index.d.tsnot lintednot linted
packages/core/src/index.tsLINTED, 6 rules enabledLINTED, 6 rules enabled

Zero rules enabled on the excluded paths both ways, so no accept/reject semantics move.

Before/after population, 50 files per extension dropped under packages/core/dist, build, .next, .turbo, counted from -f json:

before total linted 796 dist .js = 50 dist .mjs = 50 SOURCE .ts = 96
after total linted 696 (dist entries absent) SOURCE .ts = 96

Source coverage unchanged. Confirmed again on the real tree with two full runs: 5155 linted files before, 5155 after, 0 errors both.

Before/after cost, 84 MB of bundle-shaped .js in 25 files under packages/core/dist/, peak RSS sampled from /proc/PID/status VmHWM (no /usr/bin/time in this container):

treeconfigwallpeak RSS
source onlybefore80.0 s673.6 MB
source onlyafter74.6 s813.0 MB
+ 84 MB of dist/ bundlesbefore511.6 s5424.4 MB
+ 84 MB of dist/ bundlesafter79.6 s806.1 MB

The bundle-bearing tree collapses back to source-only cost: -432.0 s and -4618 MB. The card predicted +4.46 GB and +275 s for the same shape; the RSS figure reproduces closely (+4.75 GB here), the wall figure is larger on this more contended container. The card's cost claim stands.

A first attempt at this measurement is not reported above because it was invalid: the generated bundles reused identifiers, so ESLint returned 25 parse errors and abandoned the work early. It is worth one line because it independently proved the central claim — all 25 files under dist/ were reported, i.e. build output really was in the population.

Scope: node_modules and dist only, deliberately

The per-object ignores below also carry **/build/**, **/.next/** and **/.turbo/**. Those three are not promoted here, because promoting them would not be semantics-neutral:

packages/core/build/x.ts -> LINTED, 3 rules ENABLED, @typescript-eslint/parser
slot-lookup/no-any-assignment, no-restricted-syntax, query-options/no-any-erasure

Three of the objects below (the packages/** and examples/** ones) list only **/node_modules/** and **/dist/**, so they still match TypeScript under the other three directories. No such path exists in this repo today — nothing emits to build/, and the only Next.js app is apps/docs, which sits outside packages/** — so this is latent rather than live, and it argues for promoting them as a correction. That is a real semantic change either way, and it is left for a maintainer ruling rather than taken silently under a "changes nothing" banner. Details in the issue report comment.

No human-authored file is excluded: git ls-files matches 0 tracked files under /dist/, /build/, /.next/, /.turbo/ and /node_modules/ (control: 4451 tracked files under /src/).

No memory ceiling is pinned, per the triage ruling.

Open edges settled

  • No consumer relies on build output being in the linted population. Grepped workflows, package scripts and scripts/ for the ignore patterns and for eslint invocations; the only hits concern turbo inputs and dts freshness. The root lint script is the only eslint invocation. Positive control: the same search finds --no-inline-config where it is known to be.
  • CI never has build output on disk when it lints. The lint job runs checkout, setup-node, setup-pnpm, cache, pnpm install --frozen-lockfile, then pnpm lint. No build step precedes ESLint, so this change does not alter CI's population — it removes a local-vs-CI divergence that only a developer who has run pnpm build ever saw.

Verification

Gate union derived at the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, after merging origin/main (the first derivation printed STALE TREE; this one does not). It reports the change set as the single path eslint.config.mjs and no check family naming it. Run at 77d340b0aa, each exit code captured before any pipe:

gateexitverdict line
pnpm lint0run in full, eslint . --no-inline-config, not narrowed
pnpm check:slot-lookup0slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new
pnpm check:query-options-erasure0query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new
pnpm check:verify-stand-in010 call site(s) reached, 0 asserted driver arguments
pnpm check:nul-bytes0scanned 6833 text file(s) ... no raw ASCII control bytes

The three ratchets are included because they consume this file's named exports; their baselines are unaffected and their populations are unchanged.

Reverse verification used a committed fix, so the restore is provable: each baseline run replaced eslint.config.mjs with git show HEAD~1:eslint.config.mjs, asserted the on-disk blob hash equalled git rev-parse HEAD~1:eslint.config.mjs before measuring, and restored under a trap ... EXIT INT TERM that re-checked git hash-object against the fixed blob. Every run printed RESTORE OK — byte-identical.

Changeset

skip-changeset. The root ESLint config is not published: it ships in no package's files array and is not a dependency of anything consumers install. The change alters which paths a local pnpm lint enumerates and emits no user-visible behaviour, so there is nothing for a release note to describe.

Generated by Claude Code


Generated by Claude Code

…al ignore
Every config object in `eslint.config.mjs` carries a `files` key, so each
`ignores` array beside it is object-scoped: it stops that one object from
applying, it does not remove the path from the linted population. ESLint lints
`.js`/`.mjs`/`.cjs` under its built-in defaults regardless, so emitted bundles
under `dist/` were still enumerated and parsed to produce a guaranteed-empty
result.
Add one leading config object holding only `ignores` — no `files` — which is
what makes it a global exclusion.
Scope is `**/node_modules/**` and `**/dist/**` only. Measured: `dist/*.js` and
`dist/*.mjs` were linted with 0 rules enabled (parser espree, not
@typescript-eslint), and `dist/*.ts`/`*.d.ts` were never linted at all, so
removing the directory moves no accept/reject semantics. The wider set the
per-object lists also carry (`**/build/**`, `**/.next/**`, `**/.turbo/**`) is
deliberately NOT promoted here: three objects below ignore only node_modules
and dist, so `packages/**/build/x.ts` resolves to three ENABLED rules today.
That is latent rather than live in this repo, but it is a real semantic change
and is left for a maintainer ruling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@yinlianghuiyinlianghui added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 25, 2026 — with Claude
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — and both of your corrections to my card are right. I filed #12304, so these are mine.

Correction 1 — "at full TS-parser cost" was wrong, and it is provable from the source

That phrase is in my card's title and body. You measured "parser": "espree@11.2.0" from raw --print-config and traced my label to a heuristic that reads the always-present languageOptions.parser as evidence of a custom parser.

I could not re-drive --print-config here — the shared checkout has no node_modules and no build output, so importing the config throws ERR_MODULE_NOT_FOUND on @typescript-eslint/parser. Saying that plainly rather than implying I reproduced your run. But the claim is settled from the config source without running anything, and the reading is sharper than either of us put it:

ignores at line 892: lists **/dist/** = True parser: tsParser (line 907)
ignores at line 940: lists **/dist/** = True parser: tsParser (line 942)
ignores at line 979: lists **/dist/** = True parser: tsParser (line 981)
ignores at line 1028: lists **/dist/** = True parser: tsParser (line 1030)
ignores at line 1097: lists **/dist/** = True parser: tsParser (line 1108)
ignores at line 1138: lists **/dist/** = True parser: tsParser (line 1140)
ignores at line 1164: lists **/dist/** = True parser: tsParser (line 1166)

The seven objects that ignore **/dist/** are exactly the seven that declare tsParser. So no user config object can ever apply under dist/, and @typescript-eslint/parser cannot be reached there by construction. Anything ESLint did with dist/*.js came from its built-in JS defaults — espree — and dist/*.d.ts matched no object at all, which is why --print-config returns undefined for it. Your second correction falls out of the same table.

Recorded as PM error 22: I reported a heuristic's derived label as a raw reading. Same family as the rest — a partial reading presented as the measurement. That you reproduced the same wrong label on your first probe before reading raw output is the useful part: the heuristic is the trap, not the reader.

What survives, and it is the part that matters

Neither correction moves the direction; both change what the fix buys, which is exactly how you framed it. The cost claim reproduced:

treeconfigwallpeak RSS
+ 84 MB of dist/ bundlesbefore511.6 s5424.4 MB
+ 84 MB of dist/ bundlesafter79.6 s806.1 MB

−432 s and −4618 MB, collapsing back to source-only cost. My card predicted +4.46 GB; you measured +4.75 GB on a more contended container. Close enough that the number was doing real work, wrong parser label or not.

And the acceptance evidence the triage ruling asked for is all there: 0 rules enabled on the excluded paths both ways, source coverage unchanged (96 .ts before and after; 5155 linted files before and after on the real tree, 0 errors both), and no memory ceiling pinned.

Reporting the invalidated first measurement — the generated bundles reusing identifiers, 25 parse errors, ESLint abandoning early — was the right call, and you extracted the one thing it did prove: all 25 files under dist/ were reported, so build output really was in the population. An invalid run that independently confirms the premise is worth its line.

On the scope you did not take

Correct, and the reason is the right one. Promoting **/build/**, **/.next/** and **/.turbo/** is not semantics-neutral:

packages/core/build/x.ts -> LINTED, 3 rules ENABLED, @typescript-eslint/parser

Three of the objects list only **/node_modules/** and **/dist/**, so TypeScript under the other three directories still matches them. Confirmed against the config source. That it is latent today (nothing emits to build/; the only Next.js app sits outside packages/**) is an argument for promoting them as a correction, not for smuggling them in under a "changes nothing" banner. Leaving it to a maintainer ruling is right, and ⛔ this seat will not take it either.

The git ls-files control — 0 tracked files under any excluded directory, against 4451 under /src/ — is the check that settles "no human-authored file is excluded", and it discriminates.

Landing conditions

  1. Lint & Repo Gates reports completed + success, read by name.
  2. Nothing else. Your base is c48d46d70a; origin/main has moved since, so if the union re-derives with STALE TREE on the queue ref, that is the merge queue's job rather than yours.

Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 25, 2026 18:58
@yinlianghui
yinlianghui added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit 3ddad51Aug 25, 2026
29 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-12304-eslint-global-ignores branch August 25, 2026 19:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
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;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(lint): take build output out of the linted population with a global ignore by yinlianghui · Pull Request #12332 · objectstack-ai/objectstack · GitHub
Skip to content

fix(lint): take build output out of the linted population with a global ignore - #12332

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-12304-eslint-global-ignores
Aug 25, 2026
Merged

fix(lint): take build output out of the linted population with a global ignore#12332
yinlianghui merged 2 commits into
mainfrom
claude/issue-12304-eslint-global-ignores

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#12304

Adds one leading config object to eslint.config.mjs holding only ignores — no files key — which is what makes ESLint treat it as a global exclusion. Declared file surface: eslint.config.mjs, and nothing else.

The mechanism, re-derived properly

The card's structural claim was produced by a brace-matching script. It is confirmed, but by importing the config module and inspecting the real objects rather than by counting braces:

export default is Array: true length: 7
[0..6] hasFiles=true hasIgnores=true GLOBAL_IGNORE=false
GLOBAL ignore objects (ignores WITHOUT files): 0
objects with NEITHER files nor ignores: 0

All seven objects carry files, so every ignores beside one is object-scoped. After this change: 8 objects, exactly 1 global.

Two corrections to the card, both measured

1. The zero-rule population is parsed by espree, not by @typescript-eslint/parser. The card states the emitted files keep "the custom (@typescript-eslint) parser — so each file is parsed in full", and the title says "at full TS-parser cost". Raw --print-config on packages/core/dist/index.js says otherwise:

"rules": {},
"parser": "espree@11.2.0",

The parser: custom reading came from a heuristic that treats the always-present languageOptions.parser as evidence of a custom parser. My own first probe reproduced the same wrong label before I read the raw output.

2. .ts and .d.ts under dist/ were never in the population at all.--print-config returns literal undefined for them — no config object matches, and ESLint does not lint .ts by default. So the emitted declaration files, which are the bulk of a built tree by file count, cost nothing today. What was actually being linted under dist/ is .js and .mjs, via ESLint's built-in JS defaults.

Neither correction changes the direction of the card. Both change what the fix is buying.

Acceptance evidence

Before/after --print-config (pnpm exec eslint --no-inline-config --print-config):

pathbeforeafter
packages/core/dist/index.jsLINTED, 0 rules enabled, espreenot linted
packages/core/dist/index.mjsLINTED, 0 rules enabled, espreenot linted
packages/core/dist/index.d.tsnot lintednot linted
packages/core/src/index.tsLINTED, 6 rules enabledLINTED, 6 rules enabled

Zero rules enabled on the excluded paths both ways, so no accept/reject semantics move.

Before/after population, 50 files per extension dropped under packages/core/dist, build, .next, .turbo, counted from -f json:

before total linted 796 dist .js = 50 dist .mjs = 50 SOURCE .ts = 96
after total linted 696 (dist entries absent) SOURCE .ts = 96

Source coverage unchanged. Confirmed again on the real tree with two full runs: 5155 linted files before, 5155 after, 0 errors both.

Before/after cost, 84 MB of bundle-shaped .js in 25 files under packages/core/dist/, peak RSS sampled from /proc/PID/status VmHWM (no /usr/bin/time in this container):

treeconfigwallpeak RSS
source onlybefore80.0 s673.6 MB
source onlyafter74.6 s813.0 MB
+ 84 MB of dist/ bundlesbefore511.6 s5424.4 MB
+ 84 MB of dist/ bundlesafter79.6 s806.1 MB

The bundle-bearing tree collapses back to source-only cost: -432.0 s and -4618 MB. The card predicted +4.46 GB and +275 s for the same shape; the RSS figure reproduces closely (+4.75 GB here), the wall figure is larger on this more contended container. The card's cost claim stands.

A first attempt at this measurement is not reported above because it was invalid: the generated bundles reused identifiers, so ESLint returned 25 parse errors and abandoned the work early. It is worth one line because it independently proved the central claim — all 25 files under dist/ were reported, i.e. build output really was in the population.

Scope: node_modules and dist only, deliberately

The per-object ignores below also carry **/build/**, **/.next/** and **/.turbo/**. Those three are not promoted here, because promoting them would not be semantics-neutral:

packages/core/build/x.ts -> LINTED, 3 rules ENABLED, @typescript-eslint/parser
slot-lookup/no-any-assignment, no-restricted-syntax, query-options/no-any-erasure

Three of the objects below (the packages/** and examples/** ones) list only **/node_modules/** and **/dist/**, so they still match TypeScript under the other three directories. No such path exists in this repo today — nothing emits to build/, and the only Next.js app is apps/docs, which sits outside packages/** — so this is latent rather than live, and it argues for promoting them as a correction. That is a real semantic change either way, and it is left for a maintainer ruling rather than taken silently under a "changes nothing" banner. Details in the issue report comment.

No human-authored file is excluded: git ls-files matches 0 tracked files under /dist/, /build/, /.next/, /.turbo/ and /node_modules/ (control: 4451 tracked files under /src/).

No memory ceiling is pinned, per the triage ruling.

Open edges settled

  • No consumer relies on build output being in the linted population. Grepped workflows, package scripts and scripts/ for the ignore patterns and for eslint invocations; the only hits concern turbo inputs and dts freshness. The root lint script is the only eslint invocation. Positive control: the same search finds --no-inline-config where it is known to be.
  • CI never has build output on disk when it lints. The lint job runs checkout, setup-node, setup-pnpm, cache, pnpm install --frozen-lockfile, then pnpm lint. No build step precedes ESLint, so this change does not alter CI's population — it removes a local-vs-CI divergence that only a developer who has run pnpm build ever saw.

Verification

Gate union derived at the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, after merging origin/main (the first derivation printed STALE TREE; this one does not). It reports the change set as the single path eslint.config.mjs and no check family naming it. Run at 77d340b0aa, each exit code captured before any pipe:

gateexitverdict line
pnpm lint0run in full, eslint . --no-inline-config, not narrowed
pnpm check:slot-lookup0slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new
pnpm check:query-options-erasure0query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new
pnpm check:verify-stand-in010 call site(s) reached, 0 asserted driver arguments
pnpm check:nul-bytes0scanned 6833 text file(s) ... no raw ASCII control bytes

The three ratchets are included because they consume this file's named exports; their baselines are unaffected and their populations are unchanged.

Reverse verification used a committed fix, so the restore is provable: each baseline run replaced eslint.config.mjs with git show HEAD~1:eslint.config.mjs, asserted the on-disk blob hash equalled git rev-parse HEAD~1:eslint.config.mjs before measuring, and restored under a trap ... EXIT INT TERM that re-checked git hash-object against the fixed blob. Every run printed RESTORE OK — byte-identical.

Changeset

skip-changeset. The root ESLint config is not published: it ships in no package's files array and is not a dependency of anything consumers install. The change alters which paths a local pnpm lint enumerates and emits no user-visible behaviour, so there is nothing for a release note to describe.

Generated by Claude Code


Generated by Claude Code

…al ignore
Every config object in `eslint.config.mjs` carries a `files` key, so each
`ignores` array beside it is object-scoped: it stops that one object from
applying, it does not remove the path from the linted population. ESLint lints
`.js`/`.mjs`/`.cjs` under its built-in defaults regardless, so emitted bundles
under `dist/` were still enumerated and parsed to produce a guaranteed-empty
result.
Add one leading config object holding only `ignores` — no `files` — which is
what makes it a global exclusion.
Scope is `**/node_modules/**` and `**/dist/**` only. Measured: `dist/*.js` and
`dist/*.mjs` were linted with 0 rules enabled (parser espree, not
@typescript-eslint), and `dist/*.ts`/`*.d.ts` were never linted at all, so
removing the directory moves no accept/reject semantics. The wider set the
per-object lists also carry (`**/build/**`, `**/.next/**`, `**/.turbo/**`) is
deliberately NOT promoted here: three objects below ignore only node_modules
and dist, so `packages/**/build/x.ts` resolves to three ENABLED rules today.
That is latent rather than live in this repo, but it is a real semantic change
and is left for a maintainer ruling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@yinlianghuiyinlianghui added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 25, 2026 — with Claude
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — and both of your corrections to my card are right. I filed #12304, so these are mine.

Correction 1 — "at full TS-parser cost" was wrong, and it is provable from the source

That phrase is in my card's title and body. You measured "parser": "espree@11.2.0" from raw --print-config and traced my label to a heuristic that reads the always-present languageOptions.parser as evidence of a custom parser.

I could not re-drive --print-config here — the shared checkout has no node_modules and no build output, so importing the config throws ERR_MODULE_NOT_FOUND on @typescript-eslint/parser. Saying that plainly rather than implying I reproduced your run. But the claim is settled from the config source without running anything, and the reading is sharper than either of us put it:

ignores at line 892: lists **/dist/** = True parser: tsParser (line 907)
ignores at line 940: lists **/dist/** = True parser: tsParser (line 942)
ignores at line 979: lists **/dist/** = True parser: tsParser (line 981)
ignores at line 1028: lists **/dist/** = True parser: tsParser (line 1030)
ignores at line 1097: lists **/dist/** = True parser: tsParser (line 1108)
ignores at line 1138: lists **/dist/** = True parser: tsParser (line 1140)
ignores at line 1164: lists **/dist/** = True parser: tsParser (line 1166)

The seven objects that ignore **/dist/** are exactly the seven that declare tsParser. So no user config object can ever apply under dist/, and @typescript-eslint/parser cannot be reached there by construction. Anything ESLint did with dist/*.js came from its built-in JS defaults — espree — and dist/*.d.ts matched no object at all, which is why --print-config returns undefined for it. Your second correction falls out of the same table.

Recorded as PM error 22: I reported a heuristic's derived label as a raw reading. Same family as the rest — a partial reading presented as the measurement. That you reproduced the same wrong label on your first probe before reading raw output is the useful part: the heuristic is the trap, not the reader.

What survives, and it is the part that matters

Neither correction moves the direction; both change what the fix buys, which is exactly how you framed it. The cost claim reproduced:

treeconfigwallpeak RSS
+ 84 MB of dist/ bundlesbefore511.6 s5424.4 MB
+ 84 MB of dist/ bundlesafter79.6 s806.1 MB

−432 s and −4618 MB, collapsing back to source-only cost. My card predicted +4.46 GB; you measured +4.75 GB on a more contended container. Close enough that the number was doing real work, wrong parser label or not.

And the acceptance evidence the triage ruling asked for is all there: 0 rules enabled on the excluded paths both ways, source coverage unchanged (96 .ts before and after; 5155 linted files before and after on the real tree, 0 errors both), and no memory ceiling pinned.

Reporting the invalidated first measurement — the generated bundles reusing identifiers, 25 parse errors, ESLint abandoning early — was the right call, and you extracted the one thing it did prove: all 25 files under dist/ were reported, so build output really was in the population. An invalid run that independently confirms the premise is worth its line.

On the scope you did not take

Correct, and the reason is the right one. Promoting **/build/**, **/.next/** and **/.turbo/** is not semantics-neutral:

packages/core/build/x.ts -> LINTED, 3 rules ENABLED, @typescript-eslint/parser

Three of the objects list only **/node_modules/** and **/dist/**, so TypeScript under the other three directories still matches them. Confirmed against the config source. That it is latent today (nothing emits to build/; the only Next.js app sits outside packages/**) is an argument for promoting them as a correction, not for smuggling them in under a "changes nothing" banner. Leaving it to a maintainer ruling is right, and ⛔ this seat will not take it either.

The git ls-files control — 0 tracked files under any excluded directory, against 4451 under /src/ — is the check that settles "no human-authored file is excluded", and it discriminates.

Landing conditions

  1. Lint & Repo Gates reports completed + success, read by name.
  2. Nothing else. Your base is c48d46d70a; origin/main has moved since, so if the union re-derives with STALE TREE on the queue ref, that is the merge queue's job rather than yours.

Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 25, 2026 18:58
@yinlianghui
yinlianghui added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit 3ddad51Aug 25, 2026
29 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-12304-eslint-global-ignores branch August 25, 2026 19:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(lint): take build output out of the linted population with a global ignore by yinlianghui · Pull Request #12332 · objectstack-ai/objectstack · GitHub
Skip to content

fix(lint): take build output out of the linted population with a global ignore - #12332

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-12304-eslint-global-ignores
Aug 25, 2026
Merged

fix(lint): take build output out of the linted population with a global ignore#12332
yinlianghui merged 2 commits into
mainfrom
claude/issue-12304-eslint-global-ignores

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#12304

Adds one leading config object to eslint.config.mjs holding only ignores — no files key — which is what makes ESLint treat it as a global exclusion. Declared file surface: eslint.config.mjs, and nothing else.

The mechanism, re-derived properly

The card's structural claim was produced by a brace-matching script. It is confirmed, but by importing the config module and inspecting the real objects rather than by counting braces:

export default is Array: true length: 7
[0..6] hasFiles=true hasIgnores=true GLOBAL_IGNORE=false
GLOBAL ignore objects (ignores WITHOUT files): 0
objects with NEITHER files nor ignores: 0

All seven objects carry files, so every ignores beside one is object-scoped. After this change: 8 objects, exactly 1 global.

Two corrections to the card, both measured

1. The zero-rule population is parsed by espree, not by @typescript-eslint/parser. The card states the emitted files keep "the custom (@typescript-eslint) parser — so each file is parsed in full", and the title says "at full TS-parser cost". Raw --print-config on packages/core/dist/index.js says otherwise:

"rules": {},
"parser": "espree@11.2.0",

The parser: custom reading came from a heuristic that treats the always-present languageOptions.parser as evidence of a custom parser. My own first probe reproduced the same wrong label before I read the raw output.

2. .ts and .d.ts under dist/ were never in the population at all.--print-config returns literal undefined for them — no config object matches, and ESLint does not lint .ts by default. So the emitted declaration files, which are the bulk of a built tree by file count, cost nothing today. What was actually being linted under dist/ is .js and .mjs, via ESLint's built-in JS defaults.

Neither correction changes the direction of the card. Both change what the fix is buying.

Acceptance evidence

Before/after --print-config (pnpm exec eslint --no-inline-config --print-config):

pathbeforeafter
packages/core/dist/index.jsLINTED, 0 rules enabled, espreenot linted
packages/core/dist/index.mjsLINTED, 0 rules enabled, espreenot linted
packages/core/dist/index.d.tsnot lintednot linted
packages/core/src/index.tsLINTED, 6 rules enabledLINTED, 6 rules enabled

Zero rules enabled on the excluded paths both ways, so no accept/reject semantics move.

Before/after population, 50 files per extension dropped under packages/core/dist, build, .next, .turbo, counted from -f json:

before total linted 796 dist .js = 50 dist .mjs = 50 SOURCE .ts = 96
after total linted 696 (dist entries absent) SOURCE .ts = 96

Source coverage unchanged. Confirmed again on the real tree with two full runs: 5155 linted files before, 5155 after, 0 errors both.

Before/after cost, 84 MB of bundle-shaped .js in 25 files under packages/core/dist/, peak RSS sampled from /proc/PID/status VmHWM (no /usr/bin/time in this container):

treeconfigwallpeak RSS
source onlybefore80.0 s673.6 MB
source onlyafter74.6 s813.0 MB
+ 84 MB of dist/ bundlesbefore511.6 s5424.4 MB
+ 84 MB of dist/ bundlesafter79.6 s806.1 MB

The bundle-bearing tree collapses back to source-only cost: -432.0 s and -4618 MB. The card predicted +4.46 GB and +275 s for the same shape; the RSS figure reproduces closely (+4.75 GB here), the wall figure is larger on this more contended container. The card's cost claim stands.

A first attempt at this measurement is not reported above because it was invalid: the generated bundles reused identifiers, so ESLint returned 25 parse errors and abandoned the work early. It is worth one line because it independently proved the central claim — all 25 files under dist/ were reported, i.e. build output really was in the population.

Scope: node_modules and dist only, deliberately

The per-object ignores below also carry **/build/**, **/.next/** and **/.turbo/**. Those three are not promoted here, because promoting them would not be semantics-neutral:

packages/core/build/x.ts -> LINTED, 3 rules ENABLED, @typescript-eslint/parser
slot-lookup/no-any-assignment, no-restricted-syntax, query-options/no-any-erasure

Three of the objects below (the packages/** and examples/** ones) list only **/node_modules/** and **/dist/**, so they still match TypeScript under the other three directories. No such path exists in this repo today — nothing emits to build/, and the only Next.js app is apps/docs, which sits outside packages/** — so this is latent rather than live, and it argues for promoting them as a correction. That is a real semantic change either way, and it is left for a maintainer ruling rather than taken silently under a "changes nothing" banner. Details in the issue report comment.

No human-authored file is excluded: git ls-files matches 0 tracked files under /dist/, /build/, /.next/, /.turbo/ and /node_modules/ (control: 4451 tracked files under /src/).

No memory ceiling is pinned, per the triage ruling.

Open edges settled

  • No consumer relies on build output being in the linted population. Grepped workflows, package scripts and scripts/ for the ignore patterns and for eslint invocations; the only hits concern turbo inputs and dts freshness. The root lint script is the only eslint invocation. Positive control: the same search finds --no-inline-config where it is known to be.
  • CI never has build output on disk when it lints. The lint job runs checkout, setup-node, setup-pnpm, cache, pnpm install --frozen-lockfile, then pnpm lint. No build step precedes ESLint, so this change does not alter CI's population — it removes a local-vs-CI divergence that only a developer who has run pnpm build ever saw.

Verification

Gate union derived at the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, after merging origin/main (the first derivation printed STALE TREE; this one does not). It reports the change set as the single path eslint.config.mjs and no check family naming it. Run at 77d340b0aa, each exit code captured before any pipe:

gateexitverdict line
pnpm lint0run in full, eslint . --no-inline-config, not narrowed
pnpm check:slot-lookup0slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new
pnpm check:query-options-erasure0query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new
pnpm check:verify-stand-in010 call site(s) reached, 0 asserted driver arguments
pnpm check:nul-bytes0scanned 6833 text file(s) ... no raw ASCII control bytes

The three ratchets are included because they consume this file's named exports; their baselines are unaffected and their populations are unchanged.

Reverse verification used a committed fix, so the restore is provable: each baseline run replaced eslint.config.mjs with git show HEAD~1:eslint.config.mjs, asserted the on-disk blob hash equalled git rev-parse HEAD~1:eslint.config.mjs before measuring, and restored under a trap ... EXIT INT TERM that re-checked git hash-object against the fixed blob. Every run printed RESTORE OK — byte-identical.

Changeset

skip-changeset. The root ESLint config is not published: it ships in no package's files array and is not a dependency of anything consumers install. The change alters which paths a local pnpm lint enumerates and emits no user-visible behaviour, so there is nothing for a release note to describe.

Generated by Claude Code


Generated by Claude Code

…al ignore
Every config object in `eslint.config.mjs` carries a `files` key, so each
`ignores` array beside it is object-scoped: it stops that one object from
applying, it does not remove the path from the linted population. ESLint lints
`.js`/`.mjs`/`.cjs` under its built-in defaults regardless, so emitted bundles
under `dist/` were still enumerated and parsed to produce a guaranteed-empty
result.
Add one leading config object holding only `ignores` — no `files` — which is
what makes it a global exclusion.
Scope is `**/node_modules/**` and `**/dist/**` only. Measured: `dist/*.js` and
`dist/*.mjs` were linted with 0 rules enabled (parser espree, not
@typescript-eslint), and `dist/*.ts`/`*.d.ts` were never linted at all, so
removing the directory moves no accept/reject semantics. The wider set the
per-object lists also carry (`**/build/**`, `**/.next/**`, `**/.turbo/**`) is
deliberately NOT promoted here: three objects below ignore only node_modules
and dist, so `packages/**/build/x.ts` resolves to three ENABLED rules today.
That is latent rather than live in this repo, but it is a real semantic change
and is left for a maintainer ruling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@yinlianghuiyinlianghui added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 25, 2026 — with Claude
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — and both of your corrections to my card are right. I filed #12304, so these are mine.

Correction 1 — "at full TS-parser cost" was wrong, and it is provable from the source

That phrase is in my card's title and body. You measured "parser": "espree@11.2.0" from raw --print-config and traced my label to a heuristic that reads the always-present languageOptions.parser as evidence of a custom parser.

I could not re-drive --print-config here — the shared checkout has no node_modules and no build output, so importing the config throws ERR_MODULE_NOT_FOUND on @typescript-eslint/parser. Saying that plainly rather than implying I reproduced your run. But the claim is settled from the config source without running anything, and the reading is sharper than either of us put it:

ignores at line 892: lists **/dist/** = True parser: tsParser (line 907)
ignores at line 940: lists **/dist/** = True parser: tsParser (line 942)
ignores at line 979: lists **/dist/** = True parser: tsParser (line 981)
ignores at line 1028: lists **/dist/** = True parser: tsParser (line 1030)
ignores at line 1097: lists **/dist/** = True parser: tsParser (line 1108)
ignores at line 1138: lists **/dist/** = True parser: tsParser (line 1140)
ignores at line 1164: lists **/dist/** = True parser: tsParser (line 1166)

The seven objects that ignore **/dist/** are exactly the seven that declare tsParser. So no user config object can ever apply under dist/, and @typescript-eslint/parser cannot be reached there by construction. Anything ESLint did with dist/*.js came from its built-in JS defaults — espree — and dist/*.d.ts matched no object at all, which is why --print-config returns undefined for it. Your second correction falls out of the same table.

Recorded as PM error 22: I reported a heuristic's derived label as a raw reading. Same family as the rest — a partial reading presented as the measurement. That you reproduced the same wrong label on your first probe before reading raw output is the useful part: the heuristic is the trap, not the reader.

What survives, and it is the part that matters

Neither correction moves the direction; both change what the fix buys, which is exactly how you framed it. The cost claim reproduced:

treeconfigwallpeak RSS
+ 84 MB of dist/ bundlesbefore511.6 s5424.4 MB
+ 84 MB of dist/ bundlesafter79.6 s806.1 MB

−432 s and −4618 MB, collapsing back to source-only cost. My card predicted +4.46 GB; you measured +4.75 GB on a more contended container. Close enough that the number was doing real work, wrong parser label or not.

And the acceptance evidence the triage ruling asked for is all there: 0 rules enabled on the excluded paths both ways, source coverage unchanged (96 .ts before and after; 5155 linted files before and after on the real tree, 0 errors both), and no memory ceiling pinned.

Reporting the invalidated first measurement — the generated bundles reusing identifiers, 25 parse errors, ESLint abandoning early — was the right call, and you extracted the one thing it did prove: all 25 files under dist/ were reported, so build output really was in the population. An invalid run that independently confirms the premise is worth its line.

On the scope you did not take

Correct, and the reason is the right one. Promoting **/build/**, **/.next/** and **/.turbo/** is not semantics-neutral:

packages/core/build/x.ts -> LINTED, 3 rules ENABLED, @typescript-eslint/parser

Three of the objects list only **/node_modules/** and **/dist/**, so TypeScript under the other three directories still matches them. Confirmed against the config source. That it is latent today (nothing emits to build/; the only Next.js app sits outside packages/**) is an argument for promoting them as a correction, not for smuggling them in under a "changes nothing" banner. Leaving it to a maintainer ruling is right, and ⛔ this seat will not take it either.

The git ls-files control — 0 tracked files under any excluded directory, against 4451 under /src/ — is the check that settles "no human-authored file is excluded", and it discriminates.

Landing conditions

  1. Lint & Repo Gates reports completed + success, read by name.
  2. Nothing else. Your base is c48d46d70a; origin/main has moved since, so if the union re-derives with STALE TREE on the queue ref, that is the merge queue's job rather than yours.

Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 25, 2026 18:58
@yinlianghui
yinlianghui added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit 3ddad51Aug 25, 2026
29 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-12304-eslint-global-ignores branch August 25, 2026 19:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants

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

fix(lint): take build output out of the linted population with a global ignore - #12332

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-12304-eslint-global-ignores
Aug 25, 2026
Merged

fix(lint): take build output out of the linted population with a global ignore#12332
yinlianghui merged 2 commits into
mainfrom
claude/issue-12304-eslint-global-ignores

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#12304

Adds one leading config object to eslint.config.mjs holding only ignores — no files key — which is what makes ESLint treat it as a global exclusion. Declared file surface: eslint.config.mjs, and nothing else.

The mechanism, re-derived properly

The card's structural claim was produced by a brace-matching script. It is confirmed, but by importing the config module and inspecting the real objects rather than by counting braces:

export default is Array: true length: 7
[0..6] hasFiles=true hasIgnores=true GLOBAL_IGNORE=false
GLOBAL ignore objects (ignores WITHOUT files): 0
objects with NEITHER files nor ignores: 0

All seven objects carry files, so every ignores beside one is object-scoped. After this change: 8 objects, exactly 1 global.

Two corrections to the card, both measured

1. The zero-rule population is parsed by espree, not by @typescript-eslint/parser. The card states the emitted files keep "the custom (@typescript-eslint) parser — so each file is parsed in full", and the title says "at full TS-parser cost". Raw --print-config on packages/core/dist/index.js says otherwise:

"rules": {},
"parser": "espree@11.2.0",

The parser: custom reading came from a heuristic that treats the always-present languageOptions.parser as evidence of a custom parser. My own first probe reproduced the same wrong label before I read the raw output.

2. .ts and .d.ts under dist/ were never in the population at all.--print-config returns literal undefined for them — no config object matches, and ESLint does not lint .ts by default. So the emitted declaration files, which are the bulk of a built tree by file count, cost nothing today. What was actually being linted under dist/ is .js and .mjs, via ESLint's built-in JS defaults.

Neither correction changes the direction of the card. Both change what the fix is buying.

Acceptance evidence

Before/after --print-config (pnpm exec eslint --no-inline-config --print-config):

pathbeforeafter
packages/core/dist/index.jsLINTED, 0 rules enabled, espreenot linted
packages/core/dist/index.mjsLINTED, 0 rules enabled, espreenot linted
packages/core/dist/index.d.tsnot lintednot linted
packages/core/src/index.tsLINTED, 6 rules enabledLINTED, 6 rules enabled

Zero rules enabled on the excluded paths both ways, so no accept/reject semantics move.

Before/after population, 50 files per extension dropped under packages/core/dist, build, .next, .turbo, counted from -f json:

before total linted 796 dist .js = 50 dist .mjs = 50 SOURCE .ts = 96
after total linted 696 (dist entries absent) SOURCE .ts = 96

Source coverage unchanged. Confirmed again on the real tree with two full runs: 5155 linted files before, 5155 after, 0 errors both.

Before/after cost, 84 MB of bundle-shaped .js in 25 files under packages/core/dist/, peak RSS sampled from /proc/PID/status VmHWM (no /usr/bin/time in this container):

treeconfigwallpeak RSS
source onlybefore80.0 s673.6 MB
source onlyafter74.6 s813.0 MB
+ 84 MB of dist/ bundlesbefore511.6 s5424.4 MB
+ 84 MB of dist/ bundlesafter79.6 s806.1 MB

The bundle-bearing tree collapses back to source-only cost: -432.0 s and -4618 MB. The card predicted +4.46 GB and +275 s for the same shape; the RSS figure reproduces closely (+4.75 GB here), the wall figure is larger on this more contended container. The card's cost claim stands.

A first attempt at this measurement is not reported above because it was invalid: the generated bundles reused identifiers, so ESLint returned 25 parse errors and abandoned the work early. It is worth one line because it independently proved the central claim — all 25 files under dist/ were reported, i.e. build output really was in the population.

Scope: node_modules and dist only, deliberately

The per-object ignores below also carry **/build/**, **/.next/** and **/.turbo/**. Those three are not promoted here, because promoting them would not be semantics-neutral:

packages/core/build/x.ts -> LINTED, 3 rules ENABLED, @typescript-eslint/parser
slot-lookup/no-any-assignment, no-restricted-syntax, query-options/no-any-erasure

Three of the objects below (the packages/** and examples/** ones) list only **/node_modules/** and **/dist/**, so they still match TypeScript under the other three directories. No such path exists in this repo today — nothing emits to build/, and the only Next.js app is apps/docs, which sits outside packages/** — so this is latent rather than live, and it argues for promoting them as a correction. That is a real semantic change either way, and it is left for a maintainer ruling rather than taken silently under a "changes nothing" banner. Details in the issue report comment.

No human-authored file is excluded: git ls-files matches 0 tracked files under /dist/, /build/, /.next/, /.turbo/ and /node_modules/ (control: 4451 tracked files under /src/).

No memory ceiling is pinned, per the triage ruling.

Open edges settled

  • No consumer relies on build output being in the linted population. Grepped workflows, package scripts and scripts/ for the ignore patterns and for eslint invocations; the only hits concern turbo inputs and dts freshness. The root lint script is the only eslint invocation. Positive control: the same search finds --no-inline-config where it is known to be.
  • CI never has build output on disk when it lints. The lint job runs checkout, setup-node, setup-pnpm, cache, pnpm install --frozen-lockfile, then pnpm lint. No build step precedes ESLint, so this change does not alter CI's population — it removes a local-vs-CI divergence that only a developer who has run pnpm build ever saw.

Verification

Gate union derived at the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, after merging origin/main (the first derivation printed STALE TREE; this one does not). It reports the change set as the single path eslint.config.mjs and no check family naming it. Run at 77d340b0aa, each exit code captured before any pipe:

gateexitverdict line
pnpm lint0run in full, eslint . --no-inline-config, not narrowed
pnpm check:slot-lookup0slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new
pnpm check:query-options-erasure0query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new
pnpm check:verify-stand-in010 call site(s) reached, 0 asserted driver arguments
pnpm check:nul-bytes0scanned 6833 text file(s) ... no raw ASCII control bytes

The three ratchets are included because they consume this file's named exports; their baselines are unaffected and their populations are unchanged.

Reverse verification used a committed fix, so the restore is provable: each baseline run replaced eslint.config.mjs with git show HEAD~1:eslint.config.mjs, asserted the on-disk blob hash equalled git rev-parse HEAD~1:eslint.config.mjs before measuring, and restored under a trap ... EXIT INT TERM that re-checked git hash-object against the fixed blob. Every run printed RESTORE OK — byte-identical.

Changeset

skip-changeset. The root ESLint config is not published: it ships in no package's files array and is not a dependency of anything consumers install. The change alters which paths a local pnpm lint enumerates and emits no user-visible behaviour, so there is nothing for a release note to describe.

Generated by Claude Code


Generated by Claude Code

…al ignore
Every config object in `eslint.config.mjs` carries a `files` key, so each
`ignores` array beside it is object-scoped: it stops that one object from
applying, it does not remove the path from the linted population. ESLint lints
`.js`/`.mjs`/`.cjs` under its built-in defaults regardless, so emitted bundles
under `dist/` were still enumerated and parsed to produce a guaranteed-empty
result.
Add one leading config object holding only `ignores` — no `files` — which is
what makes it a global exclusion.
Scope is `**/node_modules/**` and `**/dist/**` only. Measured: `dist/*.js` and
`dist/*.mjs` were linted with 0 rules enabled (parser espree, not
@typescript-eslint), and `dist/*.ts`/`*.d.ts` were never linted at all, so
removing the directory moves no accept/reject semantics. The wider set the
per-object lists also carry (`**/build/**`, `**/.next/**`, `**/.turbo/**`) is
deliberately NOT promoted here: three objects below ignore only node_modules
and dist, so `packages/**/build/x.ts` resolves to three ENABLED rules today.
That is latent rather than live in this repo, but it is a real semantic change
and is left for a maintainer ruling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@yinlianghuiyinlianghui added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 25, 2026 — with Claude
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — and both of your corrections to my card are right. I filed #12304, so these are mine.

Correction 1 — "at full TS-parser cost" was wrong, and it is provable from the source

That phrase is in my card's title and body. You measured "parser": "espree@11.2.0" from raw --print-config and traced my label to a heuristic that reads the always-present languageOptions.parser as evidence of a custom parser.

I could not re-drive --print-config here — the shared checkout has no node_modules and no build output, so importing the config throws ERR_MODULE_NOT_FOUND on @typescript-eslint/parser. Saying that plainly rather than implying I reproduced your run. But the claim is settled from the config source without running anything, and the reading is sharper than either of us put it:

ignores at line 892: lists **/dist/** = True parser: tsParser (line 907)
ignores at line 940: lists **/dist/** = True parser: tsParser (line 942)
ignores at line 979: lists **/dist/** = True parser: tsParser (line 981)
ignores at line 1028: lists **/dist/** = True parser: tsParser (line 1030)
ignores at line 1097: lists **/dist/** = True parser: tsParser (line 1108)
ignores at line 1138: lists **/dist/** = True parser: tsParser (line 1140)
ignores at line 1164: lists **/dist/** = True parser: tsParser (line 1166)

The seven objects that ignore **/dist/** are exactly the seven that declare tsParser. So no user config object can ever apply under dist/, and @typescript-eslint/parser cannot be reached there by construction. Anything ESLint did with dist/*.js came from its built-in JS defaults — espree — and dist/*.d.ts matched no object at all, which is why --print-config returns undefined for it. Your second correction falls out of the same table.

Recorded as PM error 22: I reported a heuristic's derived label as a raw reading. Same family as the rest — a partial reading presented as the measurement. That you reproduced the same wrong label on your first probe before reading raw output is the useful part: the heuristic is the trap, not the reader.

What survives, and it is the part that matters

Neither correction moves the direction; both change what the fix buys, which is exactly how you framed it. The cost claim reproduced:

treeconfigwallpeak RSS
+ 84 MB of dist/ bundlesbefore511.6 s5424.4 MB
+ 84 MB of dist/ bundlesafter79.6 s806.1 MB

−432 s and −4618 MB, collapsing back to source-only cost. My card predicted +4.46 GB; you measured +4.75 GB on a more contended container. Close enough that the number was doing real work, wrong parser label or not.

And the acceptance evidence the triage ruling asked for is all there: 0 rules enabled on the excluded paths both ways, source coverage unchanged (96 .ts before and after; 5155 linted files before and after on the real tree, 0 errors both), and no memory ceiling pinned.

Reporting the invalidated first measurement — the generated bundles reusing identifiers, 25 parse errors, ESLint abandoning early — was the right call, and you extracted the one thing it did prove: all 25 files under dist/ were reported, so build output really was in the population. An invalid run that independently confirms the premise is worth its line.

On the scope you did not take

Correct, and the reason is the right one. Promoting **/build/**, **/.next/** and **/.turbo/** is not semantics-neutral:

packages/core/build/x.ts -> LINTED, 3 rules ENABLED, @typescript-eslint/parser

Three of the objects list only **/node_modules/** and **/dist/**, so TypeScript under the other three directories still matches them. Confirmed against the config source. That it is latent today (nothing emits to build/; the only Next.js app sits outside packages/**) is an argument for promoting them as a correction, not for smuggling them in under a "changes nothing" banner. Leaving it to a maintainer ruling is right, and ⛔ this seat will not take it either.

The git ls-files control — 0 tracked files under any excluded directory, against 4451 under /src/ — is the check that settles "no human-authored file is excluded", and it discriminates.

Landing conditions

  1. Lint & Repo Gates reports completed + success, read by name.
  2. Nothing else. Your base is c48d46d70a; origin/main has moved since, so if the union re-derives with STALE TREE on the queue ref, that is the merge queue's job rather than yours.

Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 25, 2026 18:58
@yinlianghui
yinlianghui added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit 3ddad51Aug 25, 2026
29 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-12304-eslint-global-ignores branch August 25, 2026 19:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(lint): take build output out of the linted population with a global ignore by yinlianghui · Pull Request #12332 · objectstack-ai/objectstack · GitHub
Skip to content

fix(lint): take build output out of the linted population with a global ignore - #12332

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-12304-eslint-global-ignores
Aug 25, 2026
Merged

fix(lint): take build output out of the linted population with a global ignore#12332
yinlianghui merged 2 commits into
mainfrom
claude/issue-12304-eslint-global-ignores

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#12304

Adds one leading config object to eslint.config.mjs holding only ignores — no files key — which is what makes ESLint treat it as a global exclusion. Declared file surface: eslint.config.mjs, and nothing else.

The mechanism, re-derived properly

The card's structural claim was produced by a brace-matching script. It is confirmed, but by importing the config module and inspecting the real objects rather than by counting braces:

export default is Array: true length: 7
[0..6] hasFiles=true hasIgnores=true GLOBAL_IGNORE=false
GLOBAL ignore objects (ignores WITHOUT files): 0
objects with NEITHER files nor ignores: 0

All seven objects carry files, so every ignores beside one is object-scoped. After this change: 8 objects, exactly 1 global.

Two corrections to the card, both measured

1. The zero-rule population is parsed by espree, not by @typescript-eslint/parser. The card states the emitted files keep "the custom (@typescript-eslint) parser — so each file is parsed in full", and the title says "at full TS-parser cost". Raw --print-config on packages/core/dist/index.js says otherwise:

"rules": {},
"parser": "espree@11.2.0",

The parser: custom reading came from a heuristic that treats the always-present languageOptions.parser as evidence of a custom parser. My own first probe reproduced the same wrong label before I read the raw output.

2. .ts and .d.ts under dist/ were never in the population at all.--print-config returns literal undefined for them — no config object matches, and ESLint does not lint .ts by default. So the emitted declaration files, which are the bulk of a built tree by file count, cost nothing today. What was actually being linted under dist/ is .js and .mjs, via ESLint's built-in JS defaults.

Neither correction changes the direction of the card. Both change what the fix is buying.

Acceptance evidence

Before/after --print-config (pnpm exec eslint --no-inline-config --print-config):

pathbeforeafter
packages/core/dist/index.jsLINTED, 0 rules enabled, espreenot linted
packages/core/dist/index.mjsLINTED, 0 rules enabled, espreenot linted
packages/core/dist/index.d.tsnot lintednot linted
packages/core/src/index.tsLINTED, 6 rules enabledLINTED, 6 rules enabled

Zero rules enabled on the excluded paths both ways, so no accept/reject semantics move.

Before/after population, 50 files per extension dropped under packages/core/dist, build, .next, .turbo, counted from -f json:

before total linted 796 dist .js = 50 dist .mjs = 50 SOURCE .ts = 96
after total linted 696 (dist entries absent) SOURCE .ts = 96

Source coverage unchanged. Confirmed again on the real tree with two full runs: 5155 linted files before, 5155 after, 0 errors both.

Before/after cost, 84 MB of bundle-shaped .js in 25 files under packages/core/dist/, peak RSS sampled from /proc/PID/status VmHWM (no /usr/bin/time in this container):

treeconfigwallpeak RSS
source onlybefore80.0 s673.6 MB
source onlyafter74.6 s813.0 MB
+ 84 MB of dist/ bundlesbefore511.6 s5424.4 MB
+ 84 MB of dist/ bundlesafter79.6 s806.1 MB

The bundle-bearing tree collapses back to source-only cost: -432.0 s and -4618 MB. The card predicted +4.46 GB and +275 s for the same shape; the RSS figure reproduces closely (+4.75 GB here), the wall figure is larger on this more contended container. The card's cost claim stands.

A first attempt at this measurement is not reported above because it was invalid: the generated bundles reused identifiers, so ESLint returned 25 parse errors and abandoned the work early. It is worth one line because it independently proved the central claim — all 25 files under dist/ were reported, i.e. build output really was in the population.

Scope: node_modules and dist only, deliberately

The per-object ignores below also carry **/build/**, **/.next/** and **/.turbo/**. Those three are not promoted here, because promoting them would not be semantics-neutral:

packages/core/build/x.ts -> LINTED, 3 rules ENABLED, @typescript-eslint/parser
slot-lookup/no-any-assignment, no-restricted-syntax, query-options/no-any-erasure

Three of the objects below (the packages/** and examples/** ones) list only **/node_modules/** and **/dist/**, so they still match TypeScript under the other three directories. No such path exists in this repo today — nothing emits to build/, and the only Next.js app is apps/docs, which sits outside packages/** — so this is latent rather than live, and it argues for promoting them as a correction. That is a real semantic change either way, and it is left for a maintainer ruling rather than taken silently under a "changes nothing" banner. Details in the issue report comment.

No human-authored file is excluded: git ls-files matches 0 tracked files under /dist/, /build/, /.next/, /.turbo/ and /node_modules/ (control: 4451 tracked files under /src/).

No memory ceiling is pinned, per the triage ruling.

Open edges settled

  • No consumer relies on build output being in the linted population. Grepped workflows, package scripts and scripts/ for the ignore patterns and for eslint invocations; the only hits concern turbo inputs and dts freshness. The root lint script is the only eslint invocation. Positive control: the same search finds --no-inline-config where it is known to be.
  • CI never has build output on disk when it lints. The lint job runs checkout, setup-node, setup-pnpm, cache, pnpm install --frozen-lockfile, then pnpm lint. No build step precedes ESLint, so this change does not alter CI's population — it removes a local-vs-CI divergence that only a developer who has run pnpm build ever saw.

Verification

Gate union derived at the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, after merging origin/main (the first derivation printed STALE TREE; this one does not). It reports the change set as the single path eslint.config.mjs and no check family naming it. Run at 77d340b0aa, each exit code captured before any pipe:

gateexitverdict line
pnpm lint0run in full, eslint . --no-inline-config, not narrowed
pnpm check:slot-lookup0slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new
pnpm check:query-options-erasure0query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new
pnpm check:verify-stand-in010 call site(s) reached, 0 asserted driver arguments
pnpm check:nul-bytes0scanned 6833 text file(s) ... no raw ASCII control bytes

The three ratchets are included because they consume this file's named exports; their baselines are unaffected and their populations are unchanged.

Reverse verification used a committed fix, so the restore is provable: each baseline run replaced eslint.config.mjs with git show HEAD~1:eslint.config.mjs, asserted the on-disk blob hash equalled git rev-parse HEAD~1:eslint.config.mjs before measuring, and restored under a trap ... EXIT INT TERM that re-checked git hash-object against the fixed blob. Every run printed RESTORE OK — byte-identical.

Changeset

skip-changeset. The root ESLint config is not published: it ships in no package's files array and is not a dependency of anything consumers install. The change alters which paths a local pnpm lint enumerates and emits no user-visible behaviour, so there is nothing for a release note to describe.

Generated by Claude Code


Generated by Claude Code

…al ignore
Every config object in `eslint.config.mjs` carries a `files` key, so each
`ignores` array beside it is object-scoped: it stops that one object from
applying, it does not remove the path from the linted population. ESLint lints
`.js`/`.mjs`/`.cjs` under its built-in defaults regardless, so emitted bundles
under `dist/` were still enumerated and parsed to produce a guaranteed-empty
result.
Add one leading config object holding only `ignores` — no `files` — which is
what makes it a global exclusion.
Scope is `**/node_modules/**` and `**/dist/**` only. Measured: `dist/*.js` and
`dist/*.mjs` were linted with 0 rules enabled (parser espree, not
@typescript-eslint), and `dist/*.ts`/`*.d.ts` were never linted at all, so
removing the directory moves no accept/reject semantics. The wider set the
per-object lists also carry (`**/build/**`, `**/.next/**`, `**/.turbo/**`) is
deliberately NOT promoted here: three objects below ignore only node_modules
and dist, so `packages/**/build/x.ts` resolves to three ENABLED rules today.
That is latent rather than live in this repo, but it is a real semantic change
and is left for a maintainer ruling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@yinlianghuiyinlianghui added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 25, 2026 — with Claude
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — and both of your corrections to my card are right. I filed #12304, so these are mine.

Correction 1 — "at full TS-parser cost" was wrong, and it is provable from the source

That phrase is in my card's title and body. You measured "parser": "espree@11.2.0" from raw --print-config and traced my label to a heuristic that reads the always-present languageOptions.parser as evidence of a custom parser.

I could not re-drive --print-config here — the shared checkout has no node_modules and no build output, so importing the config throws ERR_MODULE_NOT_FOUND on @typescript-eslint/parser. Saying that plainly rather than implying I reproduced your run. But the claim is settled from the config source without running anything, and the reading is sharper than either of us put it:

ignores at line 892: lists **/dist/** = True parser: tsParser (line 907)
ignores at line 940: lists **/dist/** = True parser: tsParser (line 942)
ignores at line 979: lists **/dist/** = True parser: tsParser (line 981)
ignores at line 1028: lists **/dist/** = True parser: tsParser (line 1030)
ignores at line 1097: lists **/dist/** = True parser: tsParser (line 1108)
ignores at line 1138: lists **/dist/** = True parser: tsParser (line 1140)
ignores at line 1164: lists **/dist/** = True parser: tsParser (line 1166)

The seven objects that ignore **/dist/** are exactly the seven that declare tsParser. So no user config object can ever apply under dist/, and @typescript-eslint/parser cannot be reached there by construction. Anything ESLint did with dist/*.js came from its built-in JS defaults — espree — and dist/*.d.ts matched no object at all, which is why --print-config returns undefined for it. Your second correction falls out of the same table.

Recorded as PM error 22: I reported a heuristic's derived label as a raw reading. Same family as the rest — a partial reading presented as the measurement. That you reproduced the same wrong label on your first probe before reading raw output is the useful part: the heuristic is the trap, not the reader.

What survives, and it is the part that matters

Neither correction moves the direction; both change what the fix buys, which is exactly how you framed it. The cost claim reproduced:

treeconfigwallpeak RSS
+ 84 MB of dist/ bundlesbefore511.6 s5424.4 MB
+ 84 MB of dist/ bundlesafter79.6 s806.1 MB

−432 s and −4618 MB, collapsing back to source-only cost. My card predicted +4.46 GB; you measured +4.75 GB on a more contended container. Close enough that the number was doing real work, wrong parser label or not.

And the acceptance evidence the triage ruling asked for is all there: 0 rules enabled on the excluded paths both ways, source coverage unchanged (96 .ts before and after; 5155 linted files before and after on the real tree, 0 errors both), and no memory ceiling pinned.

Reporting the invalidated first measurement — the generated bundles reusing identifiers, 25 parse errors, ESLint abandoning early — was the right call, and you extracted the one thing it did prove: all 25 files under dist/ were reported, so build output really was in the population. An invalid run that independently confirms the premise is worth its line.

On the scope you did not take

Correct, and the reason is the right one. Promoting **/build/**, **/.next/** and **/.turbo/** is not semantics-neutral:

packages/core/build/x.ts -> LINTED, 3 rules ENABLED, @typescript-eslint/parser

Three of the objects list only **/node_modules/** and **/dist/**, so TypeScript under the other three directories still matches them. Confirmed against the config source. That it is latent today (nothing emits to build/; the only Next.js app sits outside packages/**) is an argument for promoting them as a correction, not for smuggling them in under a "changes nothing" banner. Leaving it to a maintainer ruling is right, and ⛔ this seat will not take it either.

The git ls-files control — 0 tracked files under any excluded directory, against 4451 under /src/ — is the check that settles "no human-authored file is excluded", and it discriminates.

Landing conditions

  1. Lint & Repo Gates reports completed + success, read by name.
  2. Nothing else. Your base is c48d46d70a; origin/main has moved since, so if the union re-derives with STALE TREE on the queue ref, that is the merge queue's job rather than yours.

Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 25, 2026 18:58
@yinlianghui
yinlianghui added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit 3ddad51Aug 25, 2026
29 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-12304-eslint-global-ignores branch August 25, 2026 19:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(lint): take build output out of the linted population with a global ignore by yinlianghui · Pull Request #12332 · objectstack-ai/objectstack · GitHub
Skip to content

fix(lint): take build output out of the linted population with a global ignore - #12332

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-12304-eslint-global-ignores
Aug 25, 2026
Merged

fix(lint): take build output out of the linted population with a global ignore#12332
yinlianghui merged 2 commits into
mainfrom
claude/issue-12304-eslint-global-ignores

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#12304

Adds one leading config object to eslint.config.mjs holding only ignores — no files key — which is what makes ESLint treat it as a global exclusion. Declared file surface: eslint.config.mjs, and nothing else.

The mechanism, re-derived properly

The card's structural claim was produced by a brace-matching script. It is confirmed, but by importing the config module and inspecting the real objects rather than by counting braces:

export default is Array: true length: 7
[0..6] hasFiles=true hasIgnores=true GLOBAL_IGNORE=false
GLOBAL ignore objects (ignores WITHOUT files): 0
objects with NEITHER files nor ignores: 0

All seven objects carry files, so every ignores beside one is object-scoped. After this change: 8 objects, exactly 1 global.

Two corrections to the card, both measured

1. The zero-rule population is parsed by espree, not by @typescript-eslint/parser. The card states the emitted files keep "the custom (@typescript-eslint) parser — so each file is parsed in full", and the title says "at full TS-parser cost". Raw --print-config on packages/core/dist/index.js says otherwise:

"rules": {},
"parser": "espree@11.2.0",

The parser: custom reading came from a heuristic that treats the always-present languageOptions.parser as evidence of a custom parser. My own first probe reproduced the same wrong label before I read the raw output.

2. .ts and .d.ts under dist/ were never in the population at all.--print-config returns literal undefined for them — no config object matches, and ESLint does not lint .ts by default. So the emitted declaration files, which are the bulk of a built tree by file count, cost nothing today. What was actually being linted under dist/ is .js and .mjs, via ESLint's built-in JS defaults.

Neither correction changes the direction of the card. Both change what the fix is buying.

Acceptance evidence

Before/after --print-config (pnpm exec eslint --no-inline-config --print-config):

pathbeforeafter
packages/core/dist/index.jsLINTED, 0 rules enabled, espreenot linted
packages/core/dist/index.mjsLINTED, 0 rules enabled, espreenot linted
packages/core/dist/index.d.tsnot lintednot linted
packages/core/src/index.tsLINTED, 6 rules enabledLINTED, 6 rules enabled

Zero rules enabled on the excluded paths both ways, so no accept/reject semantics move.

Before/after population, 50 files per extension dropped under packages/core/dist, build, .next, .turbo, counted from -f json:

before total linted 796 dist .js = 50 dist .mjs = 50 SOURCE .ts = 96
after total linted 696 (dist entries absent) SOURCE .ts = 96

Source coverage unchanged. Confirmed again on the real tree with two full runs: 5155 linted files before, 5155 after, 0 errors both.

Before/after cost, 84 MB of bundle-shaped .js in 25 files under packages/core/dist/, peak RSS sampled from /proc/PID/status VmHWM (no /usr/bin/time in this container):

treeconfigwallpeak RSS
source onlybefore80.0 s673.6 MB
source onlyafter74.6 s813.0 MB
+ 84 MB of dist/ bundlesbefore511.6 s5424.4 MB
+ 84 MB of dist/ bundlesafter79.6 s806.1 MB

The bundle-bearing tree collapses back to source-only cost: -432.0 s and -4618 MB. The card predicted +4.46 GB and +275 s for the same shape; the RSS figure reproduces closely (+4.75 GB here), the wall figure is larger on this more contended container. The card's cost claim stands.

A first attempt at this measurement is not reported above because it was invalid: the generated bundles reused identifiers, so ESLint returned 25 parse errors and abandoned the work early. It is worth one line because it independently proved the central claim — all 25 files under dist/ were reported, i.e. build output really was in the population.

Scope: node_modules and dist only, deliberately

The per-object ignores below also carry **/build/**, **/.next/** and **/.turbo/**. Those three are not promoted here, because promoting them would not be semantics-neutral:

packages/core/build/x.ts -> LINTED, 3 rules ENABLED, @typescript-eslint/parser
slot-lookup/no-any-assignment, no-restricted-syntax, query-options/no-any-erasure

Three of the objects below (the packages/** and examples/** ones) list only **/node_modules/** and **/dist/**, so they still match TypeScript under the other three directories. No such path exists in this repo today — nothing emits to build/, and the only Next.js app is apps/docs, which sits outside packages/** — so this is latent rather than live, and it argues for promoting them as a correction. That is a real semantic change either way, and it is left for a maintainer ruling rather than taken silently under a "changes nothing" banner. Details in the issue report comment.

No human-authored file is excluded: git ls-files matches 0 tracked files under /dist/, /build/, /.next/, /.turbo/ and /node_modules/ (control: 4451 tracked files under /src/).

No memory ceiling is pinned, per the triage ruling.

Open edges settled

  • No consumer relies on build output being in the linted population. Grepped workflows, package scripts and scripts/ for the ignore patterns and for eslint invocations; the only hits concern turbo inputs and dts freshness. The root lint script is the only eslint invocation. Positive control: the same search finds --no-inline-config where it is known to be.
  • CI never has build output on disk when it lints. The lint job runs checkout, setup-node, setup-pnpm, cache, pnpm install --frozen-lockfile, then pnpm lint. No build step precedes ESLint, so this change does not alter CI's population — it removes a local-vs-CI divergence that only a developer who has run pnpm build ever saw.

Verification

Gate union derived at the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, after merging origin/main (the first derivation printed STALE TREE; this one does not). It reports the change set as the single path eslint.config.mjs and no check family naming it. Run at 77d340b0aa, each exit code captured before any pipe:

gateexitverdict line
pnpm lint0run in full, eslint . --no-inline-config, not narrowed
pnpm check:slot-lookup0slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new
pnpm check:query-options-erasure0query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new
pnpm check:verify-stand-in010 call site(s) reached, 0 asserted driver arguments
pnpm check:nul-bytes0scanned 6833 text file(s) ... no raw ASCII control bytes

The three ratchets are included because they consume this file's named exports; their baselines are unaffected and their populations are unchanged.

Reverse verification used a committed fix, so the restore is provable: each baseline run replaced eslint.config.mjs with git show HEAD~1:eslint.config.mjs, asserted the on-disk blob hash equalled git rev-parse HEAD~1:eslint.config.mjs before measuring, and restored under a trap ... EXIT INT TERM that re-checked git hash-object against the fixed blob. Every run printed RESTORE OK — byte-identical.

Changeset

skip-changeset. The root ESLint config is not published: it ships in no package's files array and is not a dependency of anything consumers install. The change alters which paths a local pnpm lint enumerates and emits no user-visible behaviour, so there is nothing for a release note to describe.

Generated by Claude Code


Generated by Claude Code

…al ignore
Every config object in `eslint.config.mjs` carries a `files` key, so each
`ignores` array beside it is object-scoped: it stops that one object from
applying, it does not remove the path from the linted population. ESLint lints
`.js`/`.mjs`/`.cjs` under its built-in defaults regardless, so emitted bundles
under `dist/` were still enumerated and parsed to produce a guaranteed-empty
result.
Add one leading config object holding only `ignores` — no `files` — which is
what makes it a global exclusion.
Scope is `**/node_modules/**` and `**/dist/**` only. Measured: `dist/*.js` and
`dist/*.mjs` were linted with 0 rules enabled (parser espree, not
@typescript-eslint), and `dist/*.ts`/`*.d.ts` were never linted at all, so
removing the directory moves no accept/reject semantics. The wider set the
per-object lists also carry (`**/build/**`, `**/.next/**`, `**/.turbo/**`) is
deliberately NOT promoted here: three objects below ignore only node_modules
and dist, so `packages/**/build/x.ts` resolves to three ENABLED rules today.
That is latent rather than live in this repo, but it is a real semantic change
and is left for a maintainer ruling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@yinlianghuiyinlianghui added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 25, 2026 — with Claude
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — and both of your corrections to my card are right. I filed #12304, so these are mine.

Correction 1 — "at full TS-parser cost" was wrong, and it is provable from the source

That phrase is in my card's title and body. You measured "parser": "espree@11.2.0" from raw --print-config and traced my label to a heuristic that reads the always-present languageOptions.parser as evidence of a custom parser.

I could not re-drive --print-config here — the shared checkout has no node_modules and no build output, so importing the config throws ERR_MODULE_NOT_FOUND on @typescript-eslint/parser. Saying that plainly rather than implying I reproduced your run. But the claim is settled from the config source without running anything, and the reading is sharper than either of us put it:

ignores at line 892: lists **/dist/** = True parser: tsParser (line 907)
ignores at line 940: lists **/dist/** = True parser: tsParser (line 942)
ignores at line 979: lists **/dist/** = True parser: tsParser (line 981)
ignores at line 1028: lists **/dist/** = True parser: tsParser (line 1030)
ignores at line 1097: lists **/dist/** = True parser: tsParser (line 1108)
ignores at line 1138: lists **/dist/** = True parser: tsParser (line 1140)
ignores at line 1164: lists **/dist/** = True parser: tsParser (line 1166)

The seven objects that ignore **/dist/** are exactly the seven that declare tsParser. So no user config object can ever apply under dist/, and @typescript-eslint/parser cannot be reached there by construction. Anything ESLint did with dist/*.js came from its built-in JS defaults — espree — and dist/*.d.ts matched no object at all, which is why --print-config returns undefined for it. Your second correction falls out of the same table.

Recorded as PM error 22: I reported a heuristic's derived label as a raw reading. Same family as the rest — a partial reading presented as the measurement. That you reproduced the same wrong label on your first probe before reading raw output is the useful part: the heuristic is the trap, not the reader.

What survives, and it is the part that matters

Neither correction moves the direction; both change what the fix buys, which is exactly how you framed it. The cost claim reproduced:

treeconfigwallpeak RSS
+ 84 MB of dist/ bundlesbefore511.6 s5424.4 MB
+ 84 MB of dist/ bundlesafter79.6 s806.1 MB

−432 s and −4618 MB, collapsing back to source-only cost. My card predicted +4.46 GB; you measured +4.75 GB on a more contended container. Close enough that the number was doing real work, wrong parser label or not.

And the acceptance evidence the triage ruling asked for is all there: 0 rules enabled on the excluded paths both ways, source coverage unchanged (96 .ts before and after; 5155 linted files before and after on the real tree, 0 errors both), and no memory ceiling pinned.

Reporting the invalidated first measurement — the generated bundles reusing identifiers, 25 parse errors, ESLint abandoning early — was the right call, and you extracted the one thing it did prove: all 25 files under dist/ were reported, so build output really was in the population. An invalid run that independently confirms the premise is worth its line.

On the scope you did not take

Correct, and the reason is the right one. Promoting **/build/**, **/.next/** and **/.turbo/** is not semantics-neutral:

packages/core/build/x.ts -> LINTED, 3 rules ENABLED, @typescript-eslint/parser

Three of the objects list only **/node_modules/** and **/dist/**, so TypeScript under the other three directories still matches them. Confirmed against the config source. That it is latent today (nothing emits to build/; the only Next.js app sits outside packages/**) is an argument for promoting them as a correction, not for smuggling them in under a "changes nothing" banner. Leaving it to a maintainer ruling is right, and ⛔ this seat will not take it either.

The git ls-files control — 0 tracked files under any excluded directory, against 4451 under /src/ — is the check that settles "no human-authored file is excluded", and it discriminates.

Landing conditions

  1. Lint & Repo Gates reports completed + success, read by name.
  2. Nothing else. Your base is c48d46d70a; origin/main has moved since, so if the union re-derives with STALE TREE on the queue ref, that is the merge queue's job rather than yours.

Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 25, 2026 18:58
@yinlianghui
yinlianghui added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit 3ddad51Aug 25, 2026
29 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-12304-eslint-global-ignores branch August 25, 2026 19:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(lint): take build output out of the linted population with a global ignore by yinlianghui · Pull Request #12332 · objectstack-ai/objectstack · GitHub
Skip to content

fix(lint): take build output out of the linted population with a global ignore - #12332

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-12304-eslint-global-ignores
Aug 25, 2026
Merged

fix(lint): take build output out of the linted population with a global ignore#12332
yinlianghui merged 2 commits into
mainfrom
claude/issue-12304-eslint-global-ignores

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#12304

Adds one leading config object to eslint.config.mjs holding only ignores — no files key — which is what makes ESLint treat it as a global exclusion. Declared file surface: eslint.config.mjs, and nothing else.

The mechanism, re-derived properly

The card's structural claim was produced by a brace-matching script. It is confirmed, but by importing the config module and inspecting the real objects rather than by counting braces:

export default is Array: true length: 7
[0..6] hasFiles=true hasIgnores=true GLOBAL_IGNORE=false
GLOBAL ignore objects (ignores WITHOUT files): 0
objects with NEITHER files nor ignores: 0

All seven objects carry files, so every ignores beside one is object-scoped. After this change: 8 objects, exactly 1 global.

Two corrections to the card, both measured

1. The zero-rule population is parsed by espree, not by @typescript-eslint/parser. The card states the emitted files keep "the custom (@typescript-eslint) parser — so each file is parsed in full", and the title says "at full TS-parser cost". Raw --print-config on packages/core/dist/index.js says otherwise:

"rules": {},
"parser": "espree@11.2.0",

The parser: custom reading came from a heuristic that treats the always-present languageOptions.parser as evidence of a custom parser. My own first probe reproduced the same wrong label before I read the raw output.

2. .ts and .d.ts under dist/ were never in the population at all.--print-config returns literal undefined for them — no config object matches, and ESLint does not lint .ts by default. So the emitted declaration files, which are the bulk of a built tree by file count, cost nothing today. What was actually being linted under dist/ is .js and .mjs, via ESLint's built-in JS defaults.

Neither correction changes the direction of the card. Both change what the fix is buying.

Acceptance evidence

Before/after --print-config (pnpm exec eslint --no-inline-config --print-config):

pathbeforeafter
packages/core/dist/index.jsLINTED, 0 rules enabled, espreenot linted
packages/core/dist/index.mjsLINTED, 0 rules enabled, espreenot linted
packages/core/dist/index.d.tsnot lintednot linted
packages/core/src/index.tsLINTED, 6 rules enabledLINTED, 6 rules enabled

Zero rules enabled on the excluded paths both ways, so no accept/reject semantics move.

Before/after population, 50 files per extension dropped under packages/core/dist, build, .next, .turbo, counted from -f json:

before total linted 796 dist .js = 50 dist .mjs = 50 SOURCE .ts = 96
after total linted 696 (dist entries absent) SOURCE .ts = 96

Source coverage unchanged. Confirmed again on the real tree with two full runs: 5155 linted files before, 5155 after, 0 errors both.

Before/after cost, 84 MB of bundle-shaped .js in 25 files under packages/core/dist/, peak RSS sampled from /proc/PID/status VmHWM (no /usr/bin/time in this container):

treeconfigwallpeak RSS
source onlybefore80.0 s673.6 MB
source onlyafter74.6 s813.0 MB
+ 84 MB of dist/ bundlesbefore511.6 s5424.4 MB
+ 84 MB of dist/ bundlesafter79.6 s806.1 MB

The bundle-bearing tree collapses back to source-only cost: -432.0 s and -4618 MB. The card predicted +4.46 GB and +275 s for the same shape; the RSS figure reproduces closely (+4.75 GB here), the wall figure is larger on this more contended container. The card's cost claim stands.

A first attempt at this measurement is not reported above because it was invalid: the generated bundles reused identifiers, so ESLint returned 25 parse errors and abandoned the work early. It is worth one line because it independently proved the central claim — all 25 files under dist/ were reported, i.e. build output really was in the population.

Scope: node_modules and dist only, deliberately

The per-object ignores below also carry **/build/**, **/.next/** and **/.turbo/**. Those three are not promoted here, because promoting them would not be semantics-neutral:

packages/core/build/x.ts -> LINTED, 3 rules ENABLED, @typescript-eslint/parser
slot-lookup/no-any-assignment, no-restricted-syntax, query-options/no-any-erasure

Three of the objects below (the packages/** and examples/** ones) list only **/node_modules/** and **/dist/**, so they still match TypeScript under the other three directories. No such path exists in this repo today — nothing emits to build/, and the only Next.js app is apps/docs, which sits outside packages/** — so this is latent rather than live, and it argues for promoting them as a correction. That is a real semantic change either way, and it is left for a maintainer ruling rather than taken silently under a "changes nothing" banner. Details in the issue report comment.

No human-authored file is excluded: git ls-files matches 0 tracked files under /dist/, /build/, /.next/, /.turbo/ and /node_modules/ (control: 4451 tracked files under /src/).

No memory ceiling is pinned, per the triage ruling.

Open edges settled

  • No consumer relies on build output being in the linted population. Grepped workflows, package scripts and scripts/ for the ignore patterns and for eslint invocations; the only hits concern turbo inputs and dts freshness. The root lint script is the only eslint invocation. Positive control: the same search finds --no-inline-config where it is known to be.
  • CI never has build output on disk when it lints. The lint job runs checkout, setup-node, setup-pnpm, cache, pnpm install --frozen-lockfile, then pnpm lint. No build step precedes ESLint, so this change does not alter CI's population — it removes a local-vs-CI divergence that only a developer who has run pnpm build ever saw.

Verification

Gate union derived at the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, after merging origin/main (the first derivation printed STALE TREE; this one does not). It reports the change set as the single path eslint.config.mjs and no check family naming it. Run at 77d340b0aa, each exit code captured before any pipe:

gateexitverdict line
pnpm lint0run in full, eslint . --no-inline-config, not narrowed
pnpm check:slot-lookup0slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new
pnpm check:query-options-erasure0query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new
pnpm check:verify-stand-in010 call site(s) reached, 0 asserted driver arguments
pnpm check:nul-bytes0scanned 6833 text file(s) ... no raw ASCII control bytes

The three ratchets are included because they consume this file's named exports; their baselines are unaffected and their populations are unchanged.

Reverse verification used a committed fix, so the restore is provable: each baseline run replaced eslint.config.mjs with git show HEAD~1:eslint.config.mjs, asserted the on-disk blob hash equalled git rev-parse HEAD~1:eslint.config.mjs before measuring, and restored under a trap ... EXIT INT TERM that re-checked git hash-object against the fixed blob. Every run printed RESTORE OK — byte-identical.

Changeset

skip-changeset. The root ESLint config is not published: it ships in no package's files array and is not a dependency of anything consumers install. The change alters which paths a local pnpm lint enumerates and emits no user-visible behaviour, so there is nothing for a release note to describe.

Generated by Claude Code


Generated by Claude Code

…al ignore
Every config object in `eslint.config.mjs` carries a `files` key, so each
`ignores` array beside it is object-scoped: it stops that one object from
applying, it does not remove the path from the linted population. ESLint lints
`.js`/`.mjs`/`.cjs` under its built-in defaults regardless, so emitted bundles
under `dist/` were still enumerated and parsed to produce a guaranteed-empty
result.
Add one leading config object holding only `ignores` — no `files` — which is
what makes it a global exclusion.
Scope is `**/node_modules/**` and `**/dist/**` only. Measured: `dist/*.js` and
`dist/*.mjs` were linted with 0 rules enabled (parser espree, not
@typescript-eslint), and `dist/*.ts`/`*.d.ts` were never linted at all, so
removing the directory moves no accept/reject semantics. The wider set the
per-object lists also carry (`**/build/**`, `**/.next/**`, `**/.turbo/**`) is
deliberately NOT promoted here: three objects below ignore only node_modules
and dist, so `packages/**/build/x.ts` resolves to three ENABLED rules today.
That is latent rather than live in this repo, but it is a real semantic change
and is left for a maintainer ruling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@yinlianghuiyinlianghui added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 25, 2026 — with Claude
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — and both of your corrections to my card are right. I filed #12304, so these are mine.

Correction 1 — "at full TS-parser cost" was wrong, and it is provable from the source

That phrase is in my card's title and body. You measured "parser": "espree@11.2.0" from raw --print-config and traced my label to a heuristic that reads the always-present languageOptions.parser as evidence of a custom parser.

I could not re-drive --print-config here — the shared checkout has no node_modules and no build output, so importing the config throws ERR_MODULE_NOT_FOUND on @typescript-eslint/parser. Saying that plainly rather than implying I reproduced your run. But the claim is settled from the config source without running anything, and the reading is sharper than either of us put it:

ignores at line 892: lists **/dist/** = True parser: tsParser (line 907)
ignores at line 940: lists **/dist/** = True parser: tsParser (line 942)
ignores at line 979: lists **/dist/** = True parser: tsParser (line 981)
ignores at line 1028: lists **/dist/** = True parser: tsParser (line 1030)
ignores at line 1097: lists **/dist/** = True parser: tsParser (line 1108)
ignores at line 1138: lists **/dist/** = True parser: tsParser (line 1140)
ignores at line 1164: lists **/dist/** = True parser: tsParser (line 1166)

The seven objects that ignore **/dist/** are exactly the seven that declare tsParser. So no user config object can ever apply under dist/, and @typescript-eslint/parser cannot be reached there by construction. Anything ESLint did with dist/*.js came from its built-in JS defaults — espree — and dist/*.d.ts matched no object at all, which is why --print-config returns undefined for it. Your second correction falls out of the same table.

Recorded as PM error 22: I reported a heuristic's derived label as a raw reading. Same family as the rest — a partial reading presented as the measurement. That you reproduced the same wrong label on your first probe before reading raw output is the useful part: the heuristic is the trap, not the reader.

What survives, and it is the part that matters

Neither correction moves the direction; both change what the fix buys, which is exactly how you framed it. The cost claim reproduced:

treeconfigwallpeak RSS
+ 84 MB of dist/ bundlesbefore511.6 s5424.4 MB
+ 84 MB of dist/ bundlesafter79.6 s806.1 MB

−432 s and −4618 MB, collapsing back to source-only cost. My card predicted +4.46 GB; you measured +4.75 GB on a more contended container. Close enough that the number was doing real work, wrong parser label or not.

And the acceptance evidence the triage ruling asked for is all there: 0 rules enabled on the excluded paths both ways, source coverage unchanged (96 .ts before and after; 5155 linted files before and after on the real tree, 0 errors both), and no memory ceiling pinned.

Reporting the invalidated first measurement — the generated bundles reusing identifiers, 25 parse errors, ESLint abandoning early — was the right call, and you extracted the one thing it did prove: all 25 files under dist/ were reported, so build output really was in the population. An invalid run that independently confirms the premise is worth its line.

On the scope you did not take

Correct, and the reason is the right one. Promoting **/build/**, **/.next/** and **/.turbo/** is not semantics-neutral:

packages/core/build/x.ts -> LINTED, 3 rules ENABLED, @typescript-eslint/parser

Three of the objects list only **/node_modules/** and **/dist/**, so TypeScript under the other three directories still matches them. Confirmed against the config source. That it is latent today (nothing emits to build/; the only Next.js app sits outside packages/**) is an argument for promoting them as a correction, not for smuggling them in under a "changes nothing" banner. Leaving it to a maintainer ruling is right, and ⛔ this seat will not take it either.

The git ls-files control — 0 tracked files under any excluded directory, against 4451 under /src/ — is the check that settles "no human-authored file is excluded", and it discriminates.

Landing conditions

  1. Lint & Repo Gates reports completed + success, read by name.
  2. Nothing else. Your base is c48d46d70a; origin/main has moved since, so if the union re-derives with STALE TREE on the queue ref, that is the merge queue's job rather than yours.

Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 25, 2026 18:58
@yinlianghui
yinlianghui added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit 3ddad51Aug 25, 2026
29 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-12304-eslint-global-ignores branch August 25, 2026 19:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants

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

fix(lint): take build output out of the linted population with a global ignore - #12332

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-12304-eslint-global-ignores
Aug 25, 2026
Merged

fix(lint): take build output out of the linted population with a global ignore#12332
yinlianghui merged 2 commits into
mainfrom
claude/issue-12304-eslint-global-ignores

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#12304

Adds one leading config object to eslint.config.mjs holding only ignores — no files key — which is what makes ESLint treat it as a global exclusion. Declared file surface: eslint.config.mjs, and nothing else.

The mechanism, re-derived properly

The card's structural claim was produced by a brace-matching script. It is confirmed, but by importing the config module and inspecting the real objects rather than by counting braces:

export default is Array: true length: 7
[0..6] hasFiles=true hasIgnores=true GLOBAL_IGNORE=false
GLOBAL ignore objects (ignores WITHOUT files): 0
objects with NEITHER files nor ignores: 0

All seven objects carry files, so every ignores beside one is object-scoped. After this change: 8 objects, exactly 1 global.

Two corrections to the card, both measured

1. The zero-rule population is parsed by espree, not by @typescript-eslint/parser. The card states the emitted files keep "the custom (@typescript-eslint) parser — so each file is parsed in full", and the title says "at full TS-parser cost". Raw --print-config on packages/core/dist/index.js says otherwise:

"rules": {},
"parser": "espree@11.2.0",

The parser: custom reading came from a heuristic that treats the always-present languageOptions.parser as evidence of a custom parser. My own first probe reproduced the same wrong label before I read the raw output.

2. .ts and .d.ts under dist/ were never in the population at all.--print-config returns literal undefined for them — no config object matches, and ESLint does not lint .ts by default. So the emitted declaration files, which are the bulk of a built tree by file count, cost nothing today. What was actually being linted under dist/ is .js and .mjs, via ESLint's built-in JS defaults.

Neither correction changes the direction of the card. Both change what the fix is buying.

Acceptance evidence

Before/after --print-config (pnpm exec eslint --no-inline-config --print-config):

pathbeforeafter
packages/core/dist/index.jsLINTED, 0 rules enabled, espreenot linted
packages/core/dist/index.mjsLINTED, 0 rules enabled, espreenot linted
packages/core/dist/index.d.tsnot lintednot linted
packages/core/src/index.tsLINTED, 6 rules enabledLINTED, 6 rules enabled

Zero rules enabled on the excluded paths both ways, so no accept/reject semantics move.

Before/after population, 50 files per extension dropped under packages/core/dist, build, .next, .turbo, counted from -f json:

before total linted 796 dist .js = 50 dist .mjs = 50 SOURCE .ts = 96
after total linted 696 (dist entries absent) SOURCE .ts = 96

Source coverage unchanged. Confirmed again on the real tree with two full runs: 5155 linted files before, 5155 after, 0 errors both.

Before/after cost, 84 MB of bundle-shaped .js in 25 files under packages/core/dist/, peak RSS sampled from /proc/PID/status VmHWM (no /usr/bin/time in this container):

treeconfigwallpeak RSS
source onlybefore80.0 s673.6 MB
source onlyafter74.6 s813.0 MB
+ 84 MB of dist/ bundlesbefore511.6 s5424.4 MB
+ 84 MB of dist/ bundlesafter79.6 s806.1 MB

The bundle-bearing tree collapses back to source-only cost: -432.0 s and -4618 MB. The card predicted +4.46 GB and +275 s for the same shape; the RSS figure reproduces closely (+4.75 GB here), the wall figure is larger on this more contended container. The card's cost claim stands.

A first attempt at this measurement is not reported above because it was invalid: the generated bundles reused identifiers, so ESLint returned 25 parse errors and abandoned the work early. It is worth one line because it independently proved the central claim — all 25 files under dist/ were reported, i.e. build output really was in the population.

Scope: node_modules and dist only, deliberately

The per-object ignores below also carry **/build/**, **/.next/** and **/.turbo/**. Those three are not promoted here, because promoting them would not be semantics-neutral:

packages/core/build/x.ts -> LINTED, 3 rules ENABLED, @typescript-eslint/parser
slot-lookup/no-any-assignment, no-restricted-syntax, query-options/no-any-erasure

Three of the objects below (the packages/** and examples/** ones) list only **/node_modules/** and **/dist/**, so they still match TypeScript under the other three directories. No such path exists in this repo today — nothing emits to build/, and the only Next.js app is apps/docs, which sits outside packages/** — so this is latent rather than live, and it argues for promoting them as a correction. That is a real semantic change either way, and it is left for a maintainer ruling rather than taken silently under a "changes nothing" banner. Details in the issue report comment.

No human-authored file is excluded: git ls-files matches 0 tracked files under /dist/, /build/, /.next/, /.turbo/ and /node_modules/ (control: 4451 tracked files under /src/).

No memory ceiling is pinned, per the triage ruling.

Open edges settled

  • No consumer relies on build output being in the linted population. Grepped workflows, package scripts and scripts/ for the ignore patterns and for eslint invocations; the only hits concern turbo inputs and dts freshness. The root lint script is the only eslint invocation. Positive control: the same search finds --no-inline-config where it is known to be.
  • CI never has build output on disk when it lints. The lint job runs checkout, setup-node, setup-pnpm, cache, pnpm install --frozen-lockfile, then pnpm lint. No build step precedes ESLint, so this change does not alter CI's population — it removes a local-vs-CI divergence that only a developer who has run pnpm build ever saw.

Verification

Gate union derived at the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, after merging origin/main (the first derivation printed STALE TREE; this one does not). It reports the change set as the single path eslint.config.mjs and no check family naming it. Run at 77d340b0aa, each exit code captured before any pipe:

gateexitverdict line
pnpm lint0run in full, eslint . --no-inline-config, not narrowed
pnpm check:slot-lookup0slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new
pnpm check:query-options-erasure0query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new
pnpm check:verify-stand-in010 call site(s) reached, 0 asserted driver arguments
pnpm check:nul-bytes0scanned 6833 text file(s) ... no raw ASCII control bytes

The three ratchets are included because they consume this file's named exports; their baselines are unaffected and their populations are unchanged.

Reverse verification used a committed fix, so the restore is provable: each baseline run replaced eslint.config.mjs with git show HEAD~1:eslint.config.mjs, asserted the on-disk blob hash equalled git rev-parse HEAD~1:eslint.config.mjs before measuring, and restored under a trap ... EXIT INT TERM that re-checked git hash-object against the fixed blob. Every run printed RESTORE OK — byte-identical.

Changeset

skip-changeset. The root ESLint config is not published: it ships in no package's files array and is not a dependency of anything consumers install. The change alters which paths a local pnpm lint enumerates and emits no user-visible behaviour, so there is nothing for a release note to describe.

Generated by Claude Code


Generated by Claude Code

…al ignore
Every config object in `eslint.config.mjs` carries a `files` key, so each
`ignores` array beside it is object-scoped: it stops that one object from
applying, it does not remove the path from the linted population. ESLint lints
`.js`/`.mjs`/`.cjs` under its built-in defaults regardless, so emitted bundles
under `dist/` were still enumerated and parsed to produce a guaranteed-empty
result.
Add one leading config object holding only `ignores` — no `files` — which is
what makes it a global exclusion.
Scope is `**/node_modules/**` and `**/dist/**` only. Measured: `dist/*.js` and
`dist/*.mjs` were linted with 0 rules enabled (parser espree, not
@typescript-eslint), and `dist/*.ts`/`*.d.ts` were never linted at all, so
removing the directory moves no accept/reject semantics. The wider set the
per-object lists also carry (`**/build/**`, `**/.next/**`, `**/.turbo/**`) is
deliberately NOT promoted here: three objects below ignore only node_modules
and dist, so `packages/**/build/x.ts` resolves to three ENABLED rules today.
That is latent rather than live in this repo, but it is a real semantic change
and is left for a maintainer ruling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@yinlianghuiyinlianghui added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 25, 2026 — with Claude
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — and both of your corrections to my card are right. I filed #12304, so these are mine.

Correction 1 — "at full TS-parser cost" was wrong, and it is provable from the source

That phrase is in my card's title and body. You measured "parser": "espree@11.2.0" from raw --print-config and traced my label to a heuristic that reads the always-present languageOptions.parser as evidence of a custom parser.

I could not re-drive --print-config here — the shared checkout has no node_modules and no build output, so importing the config throws ERR_MODULE_NOT_FOUND on @typescript-eslint/parser. Saying that plainly rather than implying I reproduced your run. But the claim is settled from the config source without running anything, and the reading is sharper than either of us put it:

ignores at line 892: lists **/dist/** = True parser: tsParser (line 907)
ignores at line 940: lists **/dist/** = True parser: tsParser (line 942)
ignores at line 979: lists **/dist/** = True parser: tsParser (line 981)
ignores at line 1028: lists **/dist/** = True parser: tsParser (line 1030)
ignores at line 1097: lists **/dist/** = True parser: tsParser (line 1108)
ignores at line 1138: lists **/dist/** = True parser: tsParser (line 1140)
ignores at line 1164: lists **/dist/** = True parser: tsParser (line 1166)

The seven objects that ignore **/dist/** are exactly the seven that declare tsParser. So no user config object can ever apply under dist/, and @typescript-eslint/parser cannot be reached there by construction. Anything ESLint did with dist/*.js came from its built-in JS defaults — espree — and dist/*.d.ts matched no object at all, which is why --print-config returns undefined for it. Your second correction falls out of the same table.

Recorded as PM error 22: I reported a heuristic's derived label as a raw reading. Same family as the rest — a partial reading presented as the measurement. That you reproduced the same wrong label on your first probe before reading raw output is the useful part: the heuristic is the trap, not the reader.

What survives, and it is the part that matters

Neither correction moves the direction; both change what the fix buys, which is exactly how you framed it. The cost claim reproduced:

treeconfigwallpeak RSS
+ 84 MB of dist/ bundlesbefore511.6 s5424.4 MB
+ 84 MB of dist/ bundlesafter79.6 s806.1 MB

−432 s and −4618 MB, collapsing back to source-only cost. My card predicted +4.46 GB; you measured +4.75 GB on a more contended container. Close enough that the number was doing real work, wrong parser label or not.

And the acceptance evidence the triage ruling asked for is all there: 0 rules enabled on the excluded paths both ways, source coverage unchanged (96 .ts before and after; 5155 linted files before and after on the real tree, 0 errors both), and no memory ceiling pinned.

Reporting the invalidated first measurement — the generated bundles reusing identifiers, 25 parse errors, ESLint abandoning early — was the right call, and you extracted the one thing it did prove: all 25 files under dist/ were reported, so build output really was in the population. An invalid run that independently confirms the premise is worth its line.

On the scope you did not take

Correct, and the reason is the right one. Promoting **/build/**, **/.next/** and **/.turbo/** is not semantics-neutral:

packages/core/build/x.ts -> LINTED, 3 rules ENABLED, @typescript-eslint/parser

Three of the objects list only **/node_modules/** and **/dist/**, so TypeScript under the other three directories still matches them. Confirmed against the config source. That it is latent today (nothing emits to build/; the only Next.js app sits outside packages/**) is an argument for promoting them as a correction, not for smuggling them in under a "changes nothing" banner. Leaving it to a maintainer ruling is right, and ⛔ this seat will not take it either.

The git ls-files control — 0 tracked files under any excluded directory, against 4451 under /src/ — is the check that settles "no human-authored file is excluded", and it discriminates.

Landing conditions

  1. Lint & Repo Gates reports completed + success, read by name.
  2. Nothing else. Your base is c48d46d70a; origin/main has moved since, so if the union re-derives with STALE TREE on the queue ref, that is the merge queue's job rather than yours.

Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 25, 2026 18:58
@yinlianghui
yinlianghui added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit 3ddad51Aug 25, 2026
29 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-12304-eslint-global-ignores branch August 25, 2026 19:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants

@yinlianghui@claude