Repository navigation
Expand file tree
/
Copy patheslint.config.js
More file actions
160 lines (155 loc) · 7.49 KB
/
Copy patheslint.config.js
File metadata and controls
160 lines (155 loc) · 7.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
// Lint gate for the-loop's own code (the self-hosting lint-gate binding): strictest
// presets as the floor, explicit adds on top, complexity budgets, architecture-as-lint.
// Suppressions follow the build-agent rule — a standing relaxation carries its
// justification here or it doesn't land.
import js from '@eslint/js';
import { defineConfig, globalIgnores } from 'eslint/config';
import importX from 'eslint-plugin-import-x';
import nodePlugin from 'eslint-plugin-n';
import simpleImportSort from 'eslint-plugin-simple-import-sort';
import unicorn from 'eslint-plugin-unicorn';
import globals from 'globals';
export default defineConfig([
// eval/results + fixtures-cache hold materialized repo copies (linting them would
// re-lint the whole codebase per fixture); oracle files execute inside a fixture
// at <fixture>/eval-oracle/, so their relative imports only resolve there.
globalIgnores(['node_modules', 'docs', 'eval/results', 'eval/fixtures-cache', 'eval/**/oracle']),
{
files: ['**/*.js'],
extends: [
js.configs.recommended,
nodePlugin.configs['flat/recommended-module'],
unicorn.configs.recommended,
],
languageOptions: {
ecmaVersion: 2024,
globals: globals.node,
},
plugins: {
'import-x': importX,
'simple-import-sort': simpleImportSort,
},
rules: {
// ── Structural budgets ─────────────────────────────────────
'max-lines': ['error', { max: 350, skipBlankLines: true, skipComments: true }],
'max-lines-per-function': ['error', { max: 50, skipBlankLines: true, skipComments: true }],
'max-depth': ['error', 3],
'max-params': ['error', 3],
complexity: ['error', 10],
'max-nested-callbacks': ['error', 2],
// ── Architecture as lint: import direction is downward-only ─
// plugin/bin (CLI surface) → plugin/src (library). src imports nothing above it.
'import-x/no-restricted-paths': [
'error',
{
zones: [
{ target: './plugin/src', from: './plugin/bin', message: 'src is the library layer; it must not reach up into the CLI surface.' },
{ target: './plugin/src', from: './test', message: 'src must not import test code.' },
{ target: './plugin/bin', from: './test', message: 'bin must not import test code.' },
],
},
],
'import-x/no-cycle': ['error', { maxDepth: 4 }],
'import-x/no-duplicates': 'error',
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error',
// ── Code quality ───────────────────────────────────────────
'no-console': 'error',
'no-debugger': 'error',
'prefer-const': 'error',
'no-var': 'error',
eqeqeq: ['error', 'always', { null: 'ignore' }],
curly: ['error', 'all'],
'no-else-return': ['error', { allowElseIf: false }],
'no-lonely-if': 'error',
'no-unneeded-ternary': 'error',
'no-nested-ternary': 'error',
'object-shorthand': 'error',
'prefer-template': 'error',
// Destructure-and-drop ({ _blocks, ...rest }) and _-prefixed placeholders are
// the house omit idiom.
'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', ignoreRestSiblings: true }],
// ── Unicorn overrides ──────────────────────────────────────
// The codebase's JSDoc'd contracts use null deliberately ({doc, span} | null);
// short names (doc, opts, idx) are the house idiom, not abbreviation drift.
'unicorn/no-null': 'off',
'unicorn/prevent-abbreviations': 'off',
'unicorn/name-replacements': 'off',
},
},
{
// The CLI surface owns exit codes; process.exit at this boundary is the contract.
// plugin/bin is the shipped CLI; the repo-root bin holds the dev fixture generator.
files: ['bin/**/*.js', 'plugin/bin/**/*.js'],
rules: {
'n/no-process-exit': 'off',
'unicorn/no-process-exit': 'off',
},
},
{
// The eval harness is an operator-facing CLI surface like bin/: its product IS
// console output (progress lines, comparison tables) and it owns its exit codes.
files: ['eval/**/*.js'],
rules: {
'no-console': 'off',
'n/no-process-exit': 'off',
'unicorn/no-process-exit': 'off',
},
},
{
// Test files: a node:test body is one narrative — fixture + acts + asserts —
// and callbacks nest one level deeper by construction (test(() => throws(() => …))).
files: ['test/**/*.js'],
rules: {
'max-lines-per-function': 'off',
'max-nested-callbacks': ['error', 3],
},
},
{
// Workflow scripts run inside a Claude Code Workflow (ADR-0029): no imports, no
// filesystem — agent/parallel/pipeline/log/args/budget arrive as harness globals —
// and the file's only export, `meta`, sits above a bare top-level `return`. Neither
// shape parses on its own terms: a top-level `await` needs module scope, a top-level
// `return` needs script/commonjs scope with `globalReturn`, and no sourceType grants
// both at once (checked directly against espree — there is no parserOptions
// combination that does). The harness resolves this by running the file's body as an
// async function; test/execution-pipeline-harness.js mirrors that at runtime, and this processor
// mirrors it at parse time only, so every other rule below still runs against the
// real code, just shifted one function deeper (postprocess below un-shifts line
// numbers by the one line the wrapper adds).
files: ['cli/assets/**/*.js'],
processor: {
preprocess: (text) => [
// `void meta` gives the neutralized binding a use, so the harness-mandated
// export doesn't read as a dead local under this file's own lint pass.
`(async () => {\n${text.replace(/^(\s*)export const meta\b(.*)$/m, '$1const meta$2 void meta;')}\n})();\n`,
],
postprocess: (messagesList) => messagesList.flat().map((m) => ({
...m,
line: m.line - 1,
...(m.endLine != null && { endLine: m.endLine - 1 }),
})),
},
languageOptions: {
globals: {
agent: 'readonly', parallel: 'readonly', pipeline: 'readonly',
log: 'readonly', args: 'readonly', budget: 'readonly',
},
},
rules: {
// The processor's wrapping makes the whole file look like one function's body to
// this rule; every genuine nested function below is still checked on its own.
'max-lines-per-function': 'off',
// Ratchet above the shared 350 floor for this directory only: the harness runs a
// workflow script's whole body as one no-import AsyncFunction (ES imports are
// impossible — ADR-0038), so payload assembly and serialization that would live in
// a src/ module elsewhere must sit inline in the engine file. It legitimately runs
// longer than a modular file; 430 is the honest budget (architecture-as-lint,
// 2026-07-09 — raised to admit calibration-capture's inline Record phase).
'max-lines': ['error', { max: 430, skipBlankLines: true, skipComments: true }],
// The wrapper itself is an async IIFE by construction (see the processor above) —
// this rule would ask us to "prefer" the very shape we can't parse without it.
'unicorn/prefer-top-level-await': 'off',
},
},
]);