@vibe-agent-toolkit/utils 0.2.0-rc.7 → 0.2.0-rc.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -3
- package/dist/asset-reference.js +3 -11
- package/dist/asset-reference.js.map +1 -1
- package/dist/crawl-timing.d.ts +3 -3
- package/dist/crawl-timing.js +3 -3
- package/dist/dirent-kind.d.ts +83 -0
- package/dist/dirent-kind.d.ts.map +1 -0
- package/dist/dirent-kind.js +129 -0
- package/dist/dirent-kind.js.map +1 -0
- package/dist/errors/errno.d.ts +51 -0
- package/dist/errors/errno.d.ts.map +1 -0
- package/dist/errors/errno.js +112 -0
- package/dist/errors/errno.js.map +1 -0
- package/dist/errors/vat-error.d.ts +60 -0
- package/dist/errors/vat-error.d.ts.map +1 -0
- package/dist/errors/vat-error.js +86 -0
- package/dist/errors/vat-error.js.map +1 -0
- package/dist/file-crawler.js +9 -9
- package/dist/file-crawler.js.map +1 -1
- package/dist/fs/file-hash.d.ts.map +1 -1
- package/dist/fs/file-hash.js +0 -1
- package/dist/fs/file-hash.js.map +1 -1
- package/dist/fs-utils.d.ts +16 -18
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +56 -80
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +2 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +2 -1
- package/dist/fs.js.map +1 -1
- package/dist/git-run.d.ts +1 -1
- package/dist/git-run.js +1 -1
- package/dist/git-tracker.d.ts +1 -1
- package/dist/git-tracker.d.ts.map +1 -1
- package/dist/git-tracker.js +1 -2
- package/dist/git-tracker.js.map +1 -1
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +0 -1
- package/dist/git-utils.js.map +1 -1
- package/dist/gitignore-checker.d.ts.map +1 -1
- package/dist/gitignore-checker.js +9 -3
- package/dist/gitignore-checker.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -4
- package/dist/index.js.map +1 -1
- package/dist/listing-refusal.d.ts +2 -1
- package/dist/listing-refusal.d.ts.map +1 -1
- package/dist/listing-refusal.js +3 -3
- package/dist/listing-refusal.js.map +1 -1
- package/dist/path-containment.d.ts +64 -0
- package/dist/path-containment.d.ts.map +1 -0
- package/dist/path-containment.js +111 -0
- package/dist/path-containment.js.map +1 -0
- package/dist/path-core.d.ts +77 -3
- package/dist/path-core.d.ts.map +1 -1
- package/dist/path-core.js +96 -6
- package/dist/path-core.js.map +1 -1
- package/dist/path-utils.d.ts.map +1 -1
- package/dist/path-utils.js +42 -47
- package/dist/path-utils.js.map +1 -1
- package/dist/path.d.ts +1 -1
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +1 -1
- package/dist/path.js.map +1 -1
- package/dist/project-utils.d.ts.map +1 -1
- package/dist/project-utils.js +9 -6
- package/dist/project-utils.js.map +1 -1
- package/dist/safe-exec.d.ts +2 -1
- package/dist/safe-exec.d.ts.map +1 -1
- package/dist/safe-exec.js +25 -7
- package/dist/safe-exec.js.map +1 -1
- package/dist/skill-targets.d.ts +2 -2
- package/dist/skill-targets.js +2 -2
- package/dist/skill-test/auth-resolver.d.ts +10 -2
- package/dist/skill-test/auth-resolver.d.ts.map +1 -1
- package/dist/skill-test/auth-resolver.js +26 -11
- package/dist/skill-test/auth-resolver.js.map +1 -1
- package/dist/skill-test/spawn-claude.d.ts +2 -1
- package/dist/skill-test/spawn-claude.d.ts.map +1 -1
- package/dist/skill-test/spawn-claude.js +20 -3
- package/dist/skill-test/spawn-claude.js.map +1 -1
- package/dist/skill-test/transcript.d.ts.map +1 -1
- package/dist/skill-test/transcript.js +18 -17
- package/dist/skill-test/transcript.js.map +1 -1
- package/dist/stdio-blocking.js +13 -14
- package/dist/stdio-blocking.js.map +1 -1
- package/dist/test-helpers.d.ts +47 -204
- package/dist/test-helpers.d.ts.map +1 -1
- package/dist/test-helpers.js +105 -310
- package/dist/test-helpers.js.map +1 -1
- package/dist/testing/executables.d.ts +32 -0
- package/dist/testing/executables.d.ts.map +1 -0
- package/dist/testing/executables.js +70 -0
- package/dist/testing/executables.js.map +1 -0
- package/dist/testing/hostile-tree.d.ts +79 -0
- package/dist/testing/hostile-tree.d.ts.map +1 -0
- package/dist/testing/hostile-tree.js +164 -0
- package/dist/testing/hostile-tree.js.map +1 -0
- package/dist/testing/platform-gates.d.ts +31 -0
- package/dist/testing/platform-gates.d.ts.map +1 -0
- package/dist/testing/platform-gates.js +31 -0
- package/dist/testing/platform-gates.js.map +1 -0
- package/dist/testing/temp-dir.d.ts +255 -0
- package/dist/testing/temp-dir.d.ts.map +1 -0
- package/dist/testing/temp-dir.js +363 -0
- package/dist/testing/temp-dir.js.map +1 -0
- package/dist/testing.d.ts +5 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +10 -1
- package/dist/testing.js.map +1 -1
- package/dist/text-content.d.ts.map +1 -1
- package/dist/text-content.js +7 -1
- package/dist/text-content.js.map +1 -1
- package/dist/text-file.d.ts.map +1 -1
- package/dist/text-file.js +0 -2
- package/dist/text-file.js.map +1 -1
- package/dist/timing-dump.d.ts +1 -1
- package/dist/timing-dump.d.ts.map +1 -1
- package/dist/timing-dump.js +1 -2
- package/dist/timing-dump.js.map +1 -1
- package/dist/windows-shell.d.ts +0 -1
- package/dist/windows-shell.d.ts.map +1 -1
- package/dist/windows-shell.js +0 -1
- package/dist/windows-shell.js.map +1 -1
- package/dist/zod-introspection.js +2 -2
- package/dist/zod-introspection.js.map +1 -1
- package/eslint/README.md +125 -67
- package/eslint/index.cjs +111 -162
- package/eslint/index.d.cts +14 -17
- package/eslint/rules/callee-name.cjs +23 -0
- package/eslint/rules/commands-import-boundary.cjs +148 -0
- package/eslint/rules/dead-import.cjs +1 -1
- package/eslint/rules/dirent-type-needs-symlink-check.cjs +282 -0
- package/eslint/rules/eslint-rule-factory.cjs +5 -2
- package/eslint/rules/exempt-path-matcher.cjs +1 -1
- package/eslint/rules/explicit-zod-strictness.cjs +120 -0
- package/eslint/rules/no-bare-dynamic-import-path.cjs +5 -1
- package/eslint/rules/no-bare-executable-spawn.cjs +70 -0
- package/eslint/rules/no-bare-symlink-in-tests.cjs +16 -3
- package/eslint/rules/no-blind-catch.cjs +171 -0
- package/eslint/rules/no-child-process-execSync.cjs +8 -0
- package/eslint/rules/no-decaying-referent.cjs +216 -0
- package/eslint/rules/no-dotdot-containment.cjs +160 -0
- package/eslint/rules/no-file-url-string-concat.cjs +4 -1
- package/eslint/rules/no-fragile-entrypoint-guard.cjs +14 -1
- package/eslint/rules/no-fs-mkdirSync.cjs +8 -0
- package/eslint/rules/no-fs-promises-cp.cjs +7 -0
- package/eslint/rules/no-fs-realpathSync.cjs +8 -0
- package/eslint/rules/no-hardcoded-path-split.cjs +7 -2
- package/eslint/rules/no-io-in-unit-tier.cjs +128 -0
- package/eslint/rules/no-literal-process-exit.cjs +129 -0
- package/eslint/rules/no-manual-path-normalize.cjs +7 -2
- package/eslint/rules/no-os-tmpdir.cjs +8 -0
- package/eslint/rules/no-path-operations-in-comparisons.cjs +7 -2
- package/eslint/rules/no-path-sep-in-strings.cjs +7 -2
- package/eslint/rules/no-path-startswith.cjs +7 -2
- package/eslint/rules/no-process-exit-in-phase.cjs +12 -2
- package/eslint/rules/no-raw-node-path.cjs +563 -0
- package/eslint/rules/no-raw-text-decode.cjs +10 -1
- package/eslint/rules/no-registry-count-pin.cjs +255 -0
- package/eslint/rules/no-self-package-import.cjs +13 -2
- package/eslint/rules/no-test-scoped-functions.cjs +10 -3
- package/eslint/rules/no-unix-shell-commands.cjs +6 -2
- package/eslint/rules/no-unsafe-root-join.cjs +23 -2
- package/eslint/rules/no-url-pathname-for-fs.cjs +5 -1
- package/eslint/rules/no-version-literal.cjs +151 -0
- package/eslint/rules/prefer-startswith-over-regex.cjs +9 -0
- package/eslint/rules/require-justified-skip.cjs +8 -2
- package/eslint/rules/safe-import.cjs +1 -1
- package/package.json +4 -9
- package/eslint/rules/no-path-join.cjs +0 -14
- package/eslint/rules/no-path-relative.cjs +0 -14
- package/eslint/rules/no-path-resolve.cjs +0 -14
- package/eslint/rules/path-function-rule-factory.cjs +0 -496
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-registry-count-pin
|
|
3
|
+
*
|
|
4
|
+
* In a test file, flags `expect(<x>).toHaveLength(<n>)` and
|
|
5
|
+
* `expect(<x>).toBe(<n>)` when `n` is a literal at or above `minLiteral` and
|
|
6
|
+
* `x` is derived from something the file IMPORTED — a registry, a manifest, a
|
|
7
|
+
* rule table pulled from src — rather than from a fixture the test built.
|
|
8
|
+
*
|
|
9
|
+
* Such a pin is a change detector and only a change detector: it fails on
|
|
10
|
+
* every legitimate addition, is fixed by retyping the number, and proves
|
|
11
|
+
* nothing about the set it stands for. The audit found the same four numbers
|
|
12
|
+
* retyped in two files and a prose sentence, all three wrong at once. The
|
|
13
|
+
* assertion that means something is the SET — `toEqual([...names].sort())` —
|
|
14
|
+
* or a derivation from the same source the registry is built from.
|
|
15
|
+
*
|
|
16
|
+
* ## The heuristic, and what it deliberately misses
|
|
17
|
+
*
|
|
18
|
+
* `x` is registry-derived when its value chain reaches an import binding used
|
|
19
|
+
* as a VALUE: `REGISTRY`, `REGISTRY.length`, `Object.keys(REGISTRY)`,
|
|
20
|
+
* `[...REGISTRY]`, `REGISTRY.filter(…)`, a local `const` that holds one of
|
|
21
|
+
* those, or a call to an imported function whose every argument is itself
|
|
22
|
+
* registry-derived (so a zero-argument accessor like `allSpecs()` counts).
|
|
23
|
+
*
|
|
24
|
+
* An imported function applied to LOCAL data — `parse(fixtureText).links` —
|
|
25
|
+
* is NOT registry-derived: the count is the fixture's. That includes a literal
|
|
26
|
+
* argument, so `loadModule('../index.cjs')` is missed on purpose; a literal
|
|
27
|
+
* module path and a literal fixture are indistinguishable syntactically, and a
|
|
28
|
+
* rule that fired on `parse('# heading')` would be switched off within a week.
|
|
29
|
+
* The same holds for a method on an imported receiver: `safePath.join(tmp,
|
|
30
|
+
* 'x')` is an imported namespace of FUNCTIONS fed local data, so the result is
|
|
31
|
+
* local — only a method whose arguments are all derived (or callbacks) keeps
|
|
32
|
+
* the receiver's derivation. Measured on this tree: the receiver-only reading
|
|
33
|
+
* flagged `hash.toHaveLength(64)` on a hash of a local file.
|
|
34
|
+
*
|
|
35
|
+
* Option `minLiteral: number` (default 5) — the smallest literal that reads as
|
|
36
|
+
* a count of a registry rather than a shape assertion on a small fixture.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // BAD — fails on every addition, fixed by retyping 27 → 28
|
|
40
|
+
* expect(Object.keys(plugin.rules)).toHaveLength(27);
|
|
41
|
+
*
|
|
42
|
+
* // GOOD — says WHICH, and fails only when the set actually changes
|
|
43
|
+
* expect(Object.keys(plugin.rules).sort()).toEqual(readdirSync(rulesDir).map(stripExt).sort());
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
'use strict';
|
|
47
|
+
|
|
48
|
+
const { isTestFile } = require('./exempt-path-matcher.cjs');
|
|
49
|
+
|
|
50
|
+
const COUNT_MATCHERS = new Set(['toHaveLength', 'toBe']);
|
|
51
|
+
const DEFAULT_MIN_LITERAL = 5;
|
|
52
|
+
const FUNCTION_ARGUMENT_TYPES = new Set(['ArrowFunctionExpression', 'FunctionExpression']);
|
|
53
|
+
|
|
54
|
+
/** The variable an identifier resolves to, walking up the scope chain, or null. */
|
|
55
|
+
function resolveVariable(sourceCode, identifier) {
|
|
56
|
+
let scope = sourceCode.getScope(identifier);
|
|
57
|
+
while (scope) {
|
|
58
|
+
const variable = scope.set.get(identifier.name);
|
|
59
|
+
if (variable) {
|
|
60
|
+
return variable;
|
|
61
|
+
}
|
|
62
|
+
scope = scope.upper;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Whether `expr` is `expect(...)` or `expect.soft(...)`. */
|
|
68
|
+
function isExpectCall(expr) {
|
|
69
|
+
if (expr.type !== 'CallExpression') {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const { callee } = expr;
|
|
73
|
+
if (callee.type === 'Identifier') {
|
|
74
|
+
return callee.name === 'expect';
|
|
75
|
+
}
|
|
76
|
+
return (
|
|
77
|
+
callee.type === 'MemberExpression' &&
|
|
78
|
+
callee.object.type === 'Identifier' &&
|
|
79
|
+
callee.object.name === 'expect' &&
|
|
80
|
+
callee.property.type === 'Identifier' &&
|
|
81
|
+
callee.property.name === 'soft'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The subject of the `expect(...)` a matcher chain hangs off, descending
|
|
87
|
+
* through modifiers like `.not` / `.resolves`, or null when the chain does not
|
|
88
|
+
* start at `expect`.
|
|
89
|
+
*/
|
|
90
|
+
function expectSubject(matcherCallee) {
|
|
91
|
+
let current = matcherCallee.object;
|
|
92
|
+
while (current.type === 'MemberExpression') {
|
|
93
|
+
current = current.object;
|
|
94
|
+
}
|
|
95
|
+
return isExpectCall(current) ? (current.arguments[0] ?? null) : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Whether a numeric literal argument at or above `min` was passed. */
|
|
99
|
+
function countLiteral(node, min) {
|
|
100
|
+
const arg = node.arguments[0];
|
|
101
|
+
if (node.arguments.length !== 1 || arg.type !== 'Literal' || typeof arg.value !== 'number') {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return arg.value >= min ? arg : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Whether every element of a call/array/new argument list is registry-derived. */
|
|
108
|
+
function createDerivationCheck(sourceCode) {
|
|
109
|
+
const visiting = new Set();
|
|
110
|
+
|
|
111
|
+
function variableIsDerived(variable) {
|
|
112
|
+
const def = variable.defs[0];
|
|
113
|
+
if (!def) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (def.type === 'ImportBinding') {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if (def.type !== 'Variable' || !def.node.init || visiting.has(variable)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
visiting.add(variable);
|
|
123
|
+
const result = isDerived(def.node.init);
|
|
124
|
+
visiting.delete(variable);
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function elementIsDerived(element) {
|
|
129
|
+
return element?.type === 'SpreadElement' ? isDerived(element.argument) : isDerived(element);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Whether no LOCAL data enters a call: every argument is registry-derived,
|
|
134
|
+
* a callback (`.filter((r) => …)` transforms, it does not supply data), or
|
|
135
|
+
* absent. A literal counts as local data — see the module comment.
|
|
136
|
+
*/
|
|
137
|
+
function argumentsCarryNoLocalData(node) {
|
|
138
|
+
return node.arguments.every(
|
|
139
|
+
(arg) => FUNCTION_ARGUMENT_TYPES.has(arg.type) || elementIsDerived(arg),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function callIsDerived(node) {
|
|
144
|
+
const { callee } = node;
|
|
145
|
+
if (callee.type === 'MemberExpression') {
|
|
146
|
+
// `Object.keys(x)` — a global transform: look at what went in.
|
|
147
|
+
// `x.filter(cb)` / `safePath.join(tmp, 'x')` — a method on a value: the
|
|
148
|
+
// receiver must be derived AND nothing local may enter, or `safePath`
|
|
149
|
+
// (an imported namespace of FUNCTIONS) would make every path derived.
|
|
150
|
+
const receiver = callee.object;
|
|
151
|
+
const receiverVar = receiver.type === 'Identifier' ? resolveVariable(sourceCode, receiver) : null;
|
|
152
|
+
const receiverIsGlobal = receiver.type === 'Identifier' && (receiverVar?.defs.length ?? 0) === 0;
|
|
153
|
+
if (receiverIsGlobal) {
|
|
154
|
+
return node.arguments.some(elementIsDerived);
|
|
155
|
+
}
|
|
156
|
+
return isDerived(receiver) && argumentsCarryNoLocalData(node);
|
|
157
|
+
}
|
|
158
|
+
if (callee.type !== 'Identifier') {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
const variable = resolveVariable(sourceCode, callee);
|
|
162
|
+
const isImportedFn = variable?.defs[0]?.type === 'ImportBinding';
|
|
163
|
+
return isImportedFn && argumentsCarryNoLocalData(node);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isDerived(expr) {
|
|
167
|
+
if (!expr) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
switch (expr.type) {
|
|
171
|
+
case 'Identifier': {
|
|
172
|
+
const variable = resolveVariable(sourceCode, expr);
|
|
173
|
+
return variable !== null && variableIsDerived(variable);
|
|
174
|
+
}
|
|
175
|
+
case 'MemberExpression':
|
|
176
|
+
return isDerived(expr.object);
|
|
177
|
+
case 'CallExpression':
|
|
178
|
+
return callIsDerived(expr);
|
|
179
|
+
case 'NewExpression':
|
|
180
|
+
return expr.arguments.some(elementIsDerived);
|
|
181
|
+
case 'ArrayExpression':
|
|
182
|
+
return expr.elements.some(elementIsDerived);
|
|
183
|
+
case 'AwaitExpression':
|
|
184
|
+
return isDerived(expr.argument);
|
|
185
|
+
case 'ChainExpression':
|
|
186
|
+
case 'TSAsExpression':
|
|
187
|
+
case 'TSNonNullExpression':
|
|
188
|
+
return isDerived(expr.expression);
|
|
189
|
+
default:
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return isDerived;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
meta: {
|
|
199
|
+
type: 'suggestion',
|
|
200
|
+
docs: {
|
|
201
|
+
description:
|
|
202
|
+
'Disallow pinning the size of an imported registry with a literal in tests — ' +
|
|
203
|
+
'toHaveLength(27) on something pulled from src is a change detector fixed by retyping',
|
|
204
|
+
recommended: false,
|
|
205
|
+
recommendedSeverity: 'warn',
|
|
206
|
+
},
|
|
207
|
+
schema: [
|
|
208
|
+
{
|
|
209
|
+
type: 'object',
|
|
210
|
+
properties: {
|
|
211
|
+
minLiteral: { type: 'integer', minimum: 0 },
|
|
212
|
+
},
|
|
213
|
+
additionalProperties: false,
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
messages: {
|
|
217
|
+
registryCountPin:
|
|
218
|
+
'This pins the size of something imported from src to the literal {{literal}}. It fails on ' +
|
|
219
|
+
'every legitimate addition and is fixed by retyping the number, so it detects change ' +
|
|
220
|
+
'without checking anything. Assert the SET (toEqual([...names].sort())) or derive the ' +
|
|
221
|
+
'expected count from the same source the registry is built from.',
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
create(context) {
|
|
226
|
+
const filename = context.filename ?? context.getFilename();
|
|
227
|
+
if (!isTestFile(filename)) {
|
|
228
|
+
return {};
|
|
229
|
+
}
|
|
230
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
231
|
+
const min = context.options?.[0]?.minLiteral ?? DEFAULT_MIN_LITERAL;
|
|
232
|
+
const isDerived = createDerivationCheck(sourceCode);
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
CallExpression(node) {
|
|
236
|
+
const { callee } = node;
|
|
237
|
+
if (
|
|
238
|
+
callee.type !== 'MemberExpression' ||
|
|
239
|
+
callee.property.type !== 'Identifier' ||
|
|
240
|
+
!COUNT_MATCHERS.has(callee.property.name)
|
|
241
|
+
) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const literal = countLiteral(node, min);
|
|
245
|
+
if (literal === null) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const subject = expectSubject(callee);
|
|
249
|
+
if (subject !== null && isDerived(subject)) {
|
|
250
|
+
context.report({ node, messageId: 'registryCountPin', data: { literal: String(literal.raw) } });
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
},
|
|
255
|
+
};
|
|
@@ -94,7 +94,18 @@ module.exports = {
|
|
|
94
94
|
description:
|
|
95
95
|
"Disallow importing the enclosing package by its own name; use a relative path so the import does not depend on the package's built `dist/`.",
|
|
96
96
|
category: 'Build correctness',
|
|
97
|
-
|
|
97
|
+
bans: 'importing the enclosing package by its own name',
|
|
98
|
+
useInstead: 'a relative path to the defining module',
|
|
99
|
+
// Not in `recommended`, for two reasons either of which would be enough. It
|
|
100
|
+
// REQUIRES a `packageName` option — it will not read a `package.json` to find
|
|
101
|
+
// out, because a rule module must require nothing (see `index.cjs`) — and a
|
|
102
|
+
// rule with a required option cannot ride in a config that supplies none.
|
|
103
|
+
// And the directories it must not fire in are a property of the adopter's
|
|
104
|
+
// `tsconfig`: a self-import only breaks the build in files the package
|
|
105
|
+
// COMPILES, while test and example trees import their own package by name on
|
|
106
|
+
// purpose. The consuming config knows both; see the README.
|
|
107
|
+
recommended: false,
|
|
108
|
+
recommendedSeverity: 'error',
|
|
98
109
|
},
|
|
99
110
|
messages: {
|
|
100
111
|
useRelativeImport:
|
|
@@ -143,7 +154,7 @@ module.exports = {
|
|
|
143
154
|
// The specifier hangs off `source`; typescript-eslint has called this
|
|
144
155
|
// property `parameter` and `argument` in earlier majors, and reading the
|
|
145
156
|
// wrong one costs nothing at lint time — the visitor simply never fires.
|
|
146
|
-
// The `import() in TYPE position` case in `rules.test.ts` is what turns
|
|
157
|
+
// The `import() in TYPE position` case in `test/eslint/rules/no-self-package-import.test.ts` is what turns
|
|
147
158
|
// that silence into a red test if a future parser renames it again.
|
|
148
159
|
TSImportType: (node) => check(node.source),
|
|
149
160
|
// `require('x')` in the `.cts`/`.cjs` files this pack also lints.
|
|
@@ -68,9 +68,16 @@ module.exports = {
|
|
|
68
68
|
meta: {
|
|
69
69
|
type: 'suggestion',
|
|
70
70
|
docs: {
|
|
71
|
-
description:
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
description:
|
|
72
|
+
'Disallow function declarations inside test blocks (SonarQube S1515)',
|
|
73
|
+
category: 'Code and test hygiene',
|
|
74
|
+
bans: 'helper functions declared inside `describe`/`it`',
|
|
75
|
+
useInstead: 'module scope',
|
|
76
|
+
// Not in `recommended`: a position on TEST STYLE, not a portability or
|
|
77
|
+
// correctness fact. Someone installing this package for `safePath.join()`
|
|
78
|
+
// should not silently inherit a test convention. Enable it by name.
|
|
79
|
+
recommended: false,
|
|
80
|
+
recommendedSeverity: 'error',
|
|
74
81
|
},
|
|
75
82
|
messages: {
|
|
76
83
|
moveToModuleScope:
|
|
@@ -85,9 +85,13 @@ module.exports = {
|
|
|
85
85
|
meta: {
|
|
86
86
|
type: 'problem',
|
|
87
87
|
docs: {
|
|
88
|
-
description:
|
|
89
|
-
|
|
88
|
+
description:
|
|
89
|
+
'Prevent Unix-specific commands that break Windows compatibility',
|
|
90
|
+
category: 'Filesystem and process',
|
|
91
|
+
bans: '`tar`, `grep`, `rm`, `echo`, … spawned directly',
|
|
92
|
+
useInstead: 'Node APIs, or a portable script fixture',
|
|
90
93
|
recommended: true,
|
|
94
|
+
recommendedSeverity: 'error',
|
|
91
95
|
},
|
|
92
96
|
fixable: null, // No auto-fix - requires manual refactoring
|
|
93
97
|
schema: [],
|
|
@@ -41,8 +41,29 @@ module.exports = {
|
|
|
41
41
|
docs: {
|
|
42
42
|
description:
|
|
43
43
|
'Enforce safePath.joinUnderRoot() for joins whose first arg is a security root variable (name ends in "Root").',
|
|
44
|
-
category: '
|
|
45
|
-
|
|
44
|
+
category: 'Path handling',
|
|
45
|
+
bans: '`safePath.join(someRoot, x)` where `x` can escape',
|
|
46
|
+
useInstead: '`safePath.joinUnderRoot()`',
|
|
47
|
+
subpath: '/path',
|
|
48
|
+
// Not in `recommended`: an unsound heuristic, pending a rewrite. It keys on
|
|
49
|
+
// whether an identifier's NAME ends in `root` rather than on whether any
|
|
50
|
+
// segment is caller-controlled, so it is noisy and blind at once — measured
|
|
51
|
+
// on a 4,670-file adopter tree: 108 findings, 0 autofixable, and every one
|
|
52
|
+
// of these verified by execution:
|
|
53
|
+
//
|
|
54
|
+
// FIRES safePath.join(repoRoot, 'docs', 'product') <- all literals, cannot escape
|
|
55
|
+
// FIRES safePath.resolve(packageRoot, '..', '..') <- escaping IS the intent
|
|
56
|
+
// FIRES safePath.join(repoRoot) <- one argument, no segment
|
|
57
|
+
// silent safePath.join(base, userInput) <- THE dangerous shape, missed
|
|
58
|
+
//
|
|
59
|
+
// A rule that misses the case it exists to catch must not ride in a config
|
|
60
|
+
// named `recommended` at any severity — a safety core that cries wolf
|
|
61
|
+
// teaches people to ignore it, which costs the true positives too. It still
|
|
62
|
+
// earns `error` where scoped to directories in which a path escape is a
|
|
63
|
+
// security boundary (VAT scopes it to the skill-test staging code).
|
|
64
|
+
// Re-include it when it keys on taint rather than on naming.
|
|
65
|
+
recommended: false,
|
|
66
|
+
recommendedSeverity: 'error',
|
|
46
67
|
},
|
|
47
68
|
messages: {
|
|
48
69
|
useJoinUnderRoot:
|
|
@@ -77,8 +77,12 @@ module.exports = {
|
|
|
77
77
|
docs: {
|
|
78
78
|
description:
|
|
79
79
|
'Disallow `.pathname` on a URL built from `import.meta.url`; use `fileURLToPath()` instead.',
|
|
80
|
-
category: '
|
|
80
|
+
category: 'URLs and dynamic imports',
|
|
81
|
+
bans: '`new URL(x, import.meta.url).pathname` as a filesystem path',
|
|
82
|
+
useInstead: '`resolveFromImportMeta()` / `fileURLToPath()`',
|
|
83
|
+
subpath: '/fs',
|
|
81
84
|
recommended: true,
|
|
85
|
+
recommendedSeverity: 'error',
|
|
82
86
|
},
|
|
83
87
|
messages: {
|
|
84
88
|
useFileURLToPath:
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-version-literal
|
|
3
|
+
*
|
|
4
|
+
* The mechanical half of CLAUDE.md's "NO VERSIONS" rule. Flags the two shapes
|
|
5
|
+
* a hand-maintained version integer takes:
|
|
6
|
+
*
|
|
7
|
+
* 1. `z.literal(<number>)` on a schema field named like a version
|
|
8
|
+
* (`version`, `schemaVersion`, `formatVersion`, `*_VERSION`, …), with or
|
|
9
|
+
* without a trailing `.optional()` / `.describe()`;
|
|
10
|
+
* 2. a variable, `let`, or class field named `<X>_VERSION`, `<X>_REVISION`,
|
|
11
|
+
* `VERSION`, `fooVersion`, … initialised to a numeric literal.
|
|
12
|
+
*
|
|
13
|
+
* Both are a number a human must remember to bump for stored data to be
|
|
14
|
+
* judged valid, and nothing fails when they forget: the number and the shape
|
|
15
|
+
* it claims to describe drift apart silently, and the failure surfaces later
|
|
16
|
+
* as a reader confidently mis-parsing data it should have refused. A strict
|
|
17
|
+
* schema already answers "can I read this?" and moves when the shape moves,
|
|
18
|
+
* for whoever made the edit; a derived digest answers "did these come from
|
|
19
|
+
* the same shape?". The integer answers neither and costs a permanent human
|
|
20
|
+
* obligation.
|
|
21
|
+
*
|
|
22
|
+
* ## Not offenders, and why the rule already leaves them alone
|
|
23
|
+
*
|
|
24
|
+
* - `ANTHROPIC_VERSION = '2023-06-01'` — a STRING header value, an external
|
|
25
|
+
* fact; the rule only fires on a NUMERIC initialiser.
|
|
26
|
+
* - `SUPPORTED_PYTHON_VERSIONS = [...]` — a real list; the name ends in
|
|
27
|
+
* `VERSIONS`, not `VERSION`, and the value is an array.
|
|
28
|
+
* - a regex that PARSES versions; a `const VERSION = '…'` in a test fixture.
|
|
29
|
+
* - `version: z.string()` / `z.number()` — a field that CARRIES a version is
|
|
30
|
+
* fine; a field pinned to one integer is the offender.
|
|
31
|
+
*
|
|
32
|
+
* Option `allowNames: string[]` — identifier / property names exempt from the
|
|
33
|
+
* rule. Ship it empty: the documented non-offenders need no entry, so any
|
|
34
|
+
* entry here is a version constant somebody decided to keep, and CLAUDE.md
|
|
35
|
+
* says there is no such case.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // BAD — bumped from memory, or not
|
|
39
|
+
* const CACHE_VERSION = 3;
|
|
40
|
+
* const Cached = z.object({ version: z.literal(3), … });
|
|
41
|
+
*
|
|
42
|
+
* // GOOD — the strict schema IS the validity check
|
|
43
|
+
* const Cached = z.object({ … }).strict();
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
'use strict';
|
|
47
|
+
|
|
48
|
+
/** A name that reads as a version or revision: `X_VERSION`, `VERSION`, `fooVersion`, `version`. */
|
|
49
|
+
const VERSION_NAME = /(?:^|_)(?:VERSION|REVISION)$|(?:^|[a-z])(?:Version|Revision)$|^(?:version|revision)$/u;
|
|
50
|
+
|
|
51
|
+
/** The numeric literal an initialiser reduces to, seeing through `as const`, or null. */
|
|
52
|
+
function numericInitialiser(init) {
|
|
53
|
+
if (!init) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
if (init.type === 'TSAsExpression' || init.type === 'TSSatisfiesExpression') {
|
|
57
|
+
return numericInitialiser(init.expression);
|
|
58
|
+
}
|
|
59
|
+
return init.type === 'Literal' && typeof init.value === 'number' ? init : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The plain name of a declarator id, property key, or class field key, or null. */
|
|
63
|
+
function keyName(key) {
|
|
64
|
+
if (key.type === 'Identifier') {
|
|
65
|
+
return key.name;
|
|
66
|
+
}
|
|
67
|
+
return key.type === 'Literal' && typeof key.value === 'string' ? key.value : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Whether a property value's chain roots at `z.literal(<number>)`. */
|
|
71
|
+
function rootsAtNumericZodLiteral(value) {
|
|
72
|
+
let current = value;
|
|
73
|
+
while (current.type === 'CallExpression' && current.callee.type === 'MemberExpression') {
|
|
74
|
+
const { callee } = current;
|
|
75
|
+
const isZodLiteral =
|
|
76
|
+
callee.object.type === 'Identifier' &&
|
|
77
|
+
callee.object.name === 'z' &&
|
|
78
|
+
callee.property.type === 'Identifier' &&
|
|
79
|
+
callee.property.name === 'literal';
|
|
80
|
+
if (isZodLiteral) {
|
|
81
|
+
return numericInitialiser(current.arguments[0]) !== null;
|
|
82
|
+
}
|
|
83
|
+
current = callee.object;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
meta: {
|
|
90
|
+
type: 'problem',
|
|
91
|
+
docs: {
|
|
92
|
+
description:
|
|
93
|
+
'Disallow z.literal(<number>) on a version-named field and <X>_VERSION = <number> ' +
|
|
94
|
+
'constants — a hand-bumped integer deciding data validity is the shape CLAUDE.md bans',
|
|
95
|
+
recommended: false,
|
|
96
|
+
recommendedSeverity: 'warn',
|
|
97
|
+
},
|
|
98
|
+
schema: [
|
|
99
|
+
{
|
|
100
|
+
type: 'object',
|
|
101
|
+
properties: {
|
|
102
|
+
allowNames: { type: 'array', items: { type: 'string' }, uniqueItems: true },
|
|
103
|
+
},
|
|
104
|
+
additionalProperties: false,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
messages: {
|
|
108
|
+
versionConstant:
|
|
109
|
+
'{{name}} is a hand-maintained version integer: nothing fails when it is not bumped, so ' +
|
|
110
|
+
'the number and the shape drift apart silently. Delete it — a .strict() schema already ' +
|
|
111
|
+
'refuses data it cannot read, and a digest of the shape answers "same shape?" without ' +
|
|
112
|
+
'anyone remembering a number.',
|
|
113
|
+
versionLiteralField:
|
|
114
|
+
'{{name}}: z.literal(<number>) pins stored data to an integer a human must remember to ' +
|
|
115
|
+
'bump. Delete the field and let the .strict() schema decide validity, or derive a digest ' +
|
|
116
|
+
'of the shape instead.',
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
create(context) {
|
|
121
|
+
const allow = new Set(context.options?.[0]?.allowNames ?? []);
|
|
122
|
+
|
|
123
|
+
function checkNamedInitialiser(node, key, init) {
|
|
124
|
+
const name = keyName(key);
|
|
125
|
+
if (name === null || allow.has(name) || !VERSION_NAME.test(name)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (numericInitialiser(init) !== null) {
|
|
129
|
+
context.report({ node, messageId: 'versionConstant', data: { name } });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
VariableDeclarator(node) {
|
|
135
|
+
checkNamedInitialiser(node, node.id, node.init);
|
|
136
|
+
},
|
|
137
|
+
PropertyDefinition(node) {
|
|
138
|
+
checkNamedInitialiser(node, node.key, node.value);
|
|
139
|
+
},
|
|
140
|
+
Property(node) {
|
|
141
|
+
const name = keyName(node.key);
|
|
142
|
+
if (name === null || allow.has(name) || !VERSION_NAME.test(name)) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (rootsAtNumericZodLiteral(node.value)) {
|
|
146
|
+
context.report({ node, messageId: 'versionLiteralField', data: { name } });
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
};
|
|
@@ -195,7 +195,16 @@ module.exports = {
|
|
|
195
195
|
docs: {
|
|
196
196
|
description:
|
|
197
197
|
String.raw`Prefer String#startsWith / String#endsWith over /^literal/.test() — including escaped literals such as \/ and \*, and regexes held in a const`,
|
|
198
|
+
category: 'Code and test hygiene',
|
|
199
|
+
bans: '`/^foo/.test(s)`, `` /^\\*glob/.test(s) ``, `const RE = /^foo/; RE.test(s)`',
|
|
200
|
+
useInstead: "`s.startsWith('foo')`",
|
|
201
|
+
// `error`, deliberately. This rule was briefly graded on a different axis
|
|
202
|
+
// ("style, not a defect") and demoted to `warn` — wrong twice over: avoiding
|
|
203
|
+
// a SonarQube S6557 at lint time instead of at merge time is a real saving,
|
|
204
|
+
// and the matcher rejects any regex containing a metacharacter, so it only
|
|
205
|
+
// fires on true literal prefixes and has near-zero churn.
|
|
198
206
|
recommended: true,
|
|
207
|
+
recommendedSeverity: 'error',
|
|
199
208
|
},
|
|
200
209
|
messages: {
|
|
201
210
|
// `{{pattern}}` carries its FLAGS. Rendering `/^abc/` for a source
|
|
@@ -253,8 +253,14 @@ module.exports = {
|
|
|
253
253
|
docs: {
|
|
254
254
|
description:
|
|
255
255
|
'Require a tracking-issue justification for skipped tests, and ban tautological assertions',
|
|
256
|
-
category: '
|
|
257
|
-
|
|
256
|
+
category: 'Code and test hygiene',
|
|
257
|
+
bans: 'unannotated `it.skip`/`it.todo`, tautological assertions, empty test bodies',
|
|
258
|
+
useInstead: 'a `SKIP(#123): reason` annotation, or a real assertion',
|
|
259
|
+
// Not in `recommended`: a position on TEST STYLE, not a portability or
|
|
260
|
+
// correctness fact. Someone installing this package for `safePath.join()`
|
|
261
|
+
// should not silently inherit a test convention. Enable it by name.
|
|
262
|
+
recommended: false,
|
|
263
|
+
recommendedSeverity: 'error',
|
|
258
264
|
},
|
|
259
265
|
schema: [],
|
|
260
266
|
messages: {
|
|
@@ -106,7 +106,7 @@ function insertAboveWithComments(fixer, sourceCode, node, text) {
|
|
|
106
106
|
* PER-RULE rather than one shared `settings` key, because a seam does not have
|
|
107
107
|
* to split its symbols the way this package does. The same adopter's narrow
|
|
108
108
|
* entry carries `normalizedTmpdir`/`mkdirSyncReal` but NOT `safePath`, so their
|
|
109
|
-
* `no-os-tmpdir` and `no-path
|
|
109
|
+
* `no-os-tmpdir` and `no-raw-node-path` need different targets — the exact "path
|
|
110
110
|
* rules here, fs rules there" case a single key cannot express.
|
|
111
111
|
*/
|
|
112
112
|
const SAFE_MODULE_PROPERTY = Object.freeze({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibe-agent-toolkit/utils",
|
|
3
|
-
"version": "0.2.0-rc.
|
|
3
|
+
"version": "0.2.0-rc.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Core utility functions shared across the vibe-agent-toolkit packages",
|
|
6
6
|
"sideEffects": [
|
|
@@ -90,19 +90,19 @@
|
|
|
90
90
|
"README.md"
|
|
91
91
|
],
|
|
92
92
|
"scripts": {
|
|
93
|
-
"build": "
|
|
93
|
+
"build": "tsx ../dev-tools/src/tsc-clean-build.ts",
|
|
94
94
|
"test:unit": "vitest run",
|
|
95
95
|
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
96
96
|
"test:watch": "vitest",
|
|
97
97
|
"typecheck": "tsc --noEmit",
|
|
98
|
-
"clean": "rimraf dist *.tsbuildinfo"
|
|
98
|
+
"clean": "rimraf --glob dist \"*.tsbuildinfo\" .tsc-staging"
|
|
99
99
|
},
|
|
100
100
|
"dependencies": {
|
|
101
101
|
"@vibe-validate/git": "0.20.1",
|
|
102
102
|
"ignore": "^7.0.5",
|
|
103
103
|
"picomatch": "^4.0.3",
|
|
104
104
|
"which": "^5.0.0",
|
|
105
|
-
"yaml": "^2.
|
|
105
|
+
"yaml": "^2.9.0"
|
|
106
106
|
},
|
|
107
107
|
"peerDependencies": {
|
|
108
108
|
"eslint": ">=9.0.0"
|
|
@@ -113,12 +113,7 @@
|
|
|
113
113
|
}
|
|
114
114
|
},
|
|
115
115
|
"devDependencies": {
|
|
116
|
-
"@types/picomatch": "^4.0.2",
|
|
117
116
|
"@types/which": "^3.0.4",
|
|
118
|
-
"eslint": "^9.39.5",
|
|
119
|
-
"rimraf": "^6.0.1",
|
|
120
|
-
"typescript": "^5.9.3",
|
|
121
|
-
"vitest": "^4.1.11",
|
|
122
117
|
"zod": "^3.25.0"
|
|
123
118
|
},
|
|
124
119
|
"publishConfig": {
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ESLint rule: no-path-join
|
|
3
|
-
*
|
|
4
|
-
* Bans path.join() from node:path. Use safePath.join() from `@vibe-agent-toolkit/utils/path`.
|
|
5
|
-
* safePath.join() wraps path.join() + toForwardSlash() to prevent Windows backslash bugs.
|
|
6
|
-
*/
|
|
7
|
-
const factory = require('./path-function-rule-factory.cjs');
|
|
8
|
-
|
|
9
|
-
module.exports = factory({
|
|
10
|
-
unsafeFn: 'join',
|
|
11
|
-
message:
|
|
12
|
-
'Use safePath.join() from {{safeModule}} instead of path.join(). ' +
|
|
13
|
-
'path.join() returns backslashes on Windows, causing Map key mismatches and path comparison bugs.',
|
|
14
|
-
});
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ESLint rule: no-path-relative
|
|
3
|
-
*
|
|
4
|
-
* Bans path.relative() from node:path. Use safePath.relative() from `@vibe-agent-toolkit/utils/path`.
|
|
5
|
-
* safePath.relative() wraps path.relative() + toForwardSlash() to prevent Windows backslash bugs.
|
|
6
|
-
*/
|
|
7
|
-
const factory = require('./path-function-rule-factory.cjs');
|
|
8
|
-
|
|
9
|
-
module.exports = factory({
|
|
10
|
-
unsafeFn: 'relative',
|
|
11
|
-
message:
|
|
12
|
-
'Use safePath.relative() from {{safeModule}} instead of path.relative(). ' +
|
|
13
|
-
'path.relative() returns backslashes on Windows, causing Map key mismatches and path comparison bugs.',
|
|
14
|
-
});
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ESLint rule: no-path-resolve
|
|
3
|
-
*
|
|
4
|
-
* Bans path.resolve() from node:path. Use safePath.resolve() from `@vibe-agent-toolkit/utils/path`.
|
|
5
|
-
* safePath.resolve() wraps path.resolve() + toForwardSlash() to prevent Windows backslash bugs.
|
|
6
|
-
*/
|
|
7
|
-
const factory = require('./path-function-rule-factory.cjs');
|
|
8
|
-
|
|
9
|
-
module.exports = factory({
|
|
10
|
-
unsafeFn: 'resolve',
|
|
11
|
-
message:
|
|
12
|
-
'Use safePath.resolve() from {{safeModule}} instead of path.resolve(). ' +
|
|
13
|
-
'path.resolve() returns backslashes on Windows, causing Map key mismatches and path comparison bugs.',
|
|
14
|
-
});
|