@alint-js/plugin-simplicity 0.0.22
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 +301 -0
- package/dist/index.d.mts +193 -0
- package/dist/index.mjs +1303 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# `@alint-js/plugin-simplicity`
|
|
2
|
+
|
|
3
|
+
Tell your code to **DRY — Don't Repeat Yourself**.
|
|
4
|
+
Finds small helper functions that are defined twice, or that should not exist at all.
|
|
5
|
+
|
|
6
|
+
Both rules work on **helpers**: anything with a name and a body. A function declaration,
|
|
7
|
+
a method, and, in TypeScript, TSX and JavaScript, an arrow function or function
|
|
8
|
+
expression bound to a name (`const parse = (text: string) => JSON.parse(text)`). That
|
|
9
|
+
last shape is how most tiny helpers are actually written, so leaving it out would hide
|
|
10
|
+
exactly the code these rules look for.
|
|
11
|
+
|
|
12
|
+
Languages: TypeScript, TSX, JavaScript, Rust, Go and Python.
|
|
13
|
+
|
|
14
|
+
## How it works
|
|
15
|
+
|
|
16
|
+
One index of the whole workspace, built once per run and shared by both rules. Everything
|
|
17
|
+
a hash can settle is settled by code. Only what is left reaches a model.
|
|
18
|
+
|
|
19
|
+
```mermaid
|
|
20
|
+
flowchart TB
|
|
21
|
+
subgraph idx ["Repo index — Once per run, no token cost"]
|
|
22
|
+
direction TB
|
|
23
|
+
files["Every parseable file in the workspace"]
|
|
24
|
+
files --> parse["Parse and query by tree-sitter"]
|
|
25
|
+
parse --> keep{"Small helper?<br/>within maxLines, at least minTokens"}
|
|
26
|
+
keep -->|no| skip["Not indexed"]
|
|
27
|
+
keep -->|yes| helpers["Indexed with its facts:<br/>exact fingerprint, alpha fingerprint,<br/>usage count, whether exported"]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
helpers --> lint["Files to lint"]
|
|
31
|
+
lint --> dup
|
|
32
|
+
lint --> needless
|
|
33
|
+
|
|
34
|
+
subgraph dup ["no-duplicated-helper"]
|
|
35
|
+
direction TB
|
|
36
|
+
d1{"Identical body<br/>somewhere else?"}
|
|
37
|
+
d1 -->|yes| dr1["Report: also defined at ..."]
|
|
38
|
+
d1 -->|no| d2{"Same body, renamed?<br/>alpha-equivalent"}
|
|
39
|
+
d2 -->|yes| dr2["Report: renamed copy of ..."]
|
|
40
|
+
d2 -->|no| d3["Agent, holding the index as tools:<br/>search bodies, rank similar, read in full"]
|
|
41
|
+
d3 --> dr3["Report: duplicates ..., and why"]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
subgraph needless ["no-needless-helper"]
|
|
45
|
+
direction TB
|
|
46
|
+
n1{"Whole body is<br/>one expression?"}
|
|
47
|
+
n1 -->|no| nx["Never asked about"]
|
|
48
|
+
n1 -->|yes| n2["One model call per file.<br/>Usage count and exported go in<br/>as facts, not filters"]
|
|
49
|
+
n2 --> nr["Report: does not earn its existence"]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
cache[("Cache in .alint/simplicity/<br/>a run that changed nothing costs nothing")]
|
|
53
|
+
d3 <--> cache
|
|
54
|
+
n2 <--> cache
|
|
55
|
+
|
|
56
|
+
classDef free fill:#e8f5e9,stroke:#43a047,color:#1b5e20
|
|
57
|
+
classDef paid fill:#fff3e0,stroke:#fb8c00,color:#e65100
|
|
58
|
+
class files,parse,keep,skip,helpers,d1,d2,dr1,dr2,n1,nx free
|
|
59
|
+
class d3,dr3,n2,nr paid
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Green spends nothing. Orange spends model tokens, and only ever sees the helpers the
|
|
63
|
+
green half could not settle.
|
|
64
|
+
|
|
65
|
+
### The two fingerprints
|
|
66
|
+
|
|
67
|
+
Every indexed helper is hashed twice. Both hashes drop comments and collapse whitespace
|
|
68
|
+
first, so reformatting a copy or rewording its documentation changes neither.
|
|
69
|
+
|
|
70
|
+
**The exact fingerprint** hashes what is left, verbatim. Two helpers that share it are the
|
|
71
|
+
same code, character for character, once layout and comments are set aside.
|
|
72
|
+
|
|
73
|
+
**The alpha fingerprint** goes one step further: every name the helper *declares* — its own
|
|
74
|
+
name, its parameters, its locals — is replaced by a placeholder, in order of first
|
|
75
|
+
appearance. Everything the helper merely *refers to* stays as written: the functions it
|
|
76
|
+
calls, the types it names, the properties it reads.
|
|
77
|
+
|
|
78
|
+
That split is the whole design. A copy can rename what it declares; it cannot rename what
|
|
79
|
+
it refers to and still be a copy. So these two match:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
function hasErrorCode(failure: unknown): failure is NodeJS.ErrnoException {
|
|
83
|
+
return failure instanceof Error && 'code' in failure
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
87
|
+
return error instanceof Error && 'code' in error
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Both normalize to the same thing, so the second is reported as a renamed copy of the first:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
function $0($1: unknown): $1 is NodeJS.ErrnoException { return $1 instanceof Error && 'code' in $1 }
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`Error` and `NodeJS.ErrnoException` survive, because the helper only refers to them. And
|
|
98
|
+
these two do **not** match:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
export function readName(entry: Entry): string {
|
|
102
|
+
return entry.name
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function readSize(entry: Entry): number {
|
|
106
|
+
return entry.size
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`entry` is declared, so it is blinded; `name` and `size` are properties the helpers read,
|
|
111
|
+
so they survive the hash and tell the two apart. A detector that blinded every identifier
|
|
112
|
+
instead — NiCad's blind renaming, PMD's `--ignore-identifiers` — collapses this pair and
|
|
113
|
+
reports two unrelated accessors as copies.
|
|
114
|
+
|
|
115
|
+
Matching alpha fingerprints mean the helpers are **alpha-equivalent**: identical up to
|
|
116
|
+
renaming. It is an equivalence, not a similarity score, so there is no threshold to tune
|
|
117
|
+
and no false-positive rate to trade off. Everything it cannot decide is the agent's
|
|
118
|
+
question.
|
|
119
|
+
|
|
120
|
+
## Rules
|
|
121
|
+
|
|
122
|
+
- **`simplicity/no-duplicated-helper`**: a small helper is defined in two or more places.
|
|
123
|
+
- **`simplicity/no-needless-helper`**: a one-line helper that does not earn its existence.
|
|
124
|
+
|
|
125
|
+
Rule ids carry the prefix you give the plugin in `plugins`, which is `simplicity/` in
|
|
126
|
+
every example here.
|
|
127
|
+
|
|
128
|
+
## `no-duplicated-helper`
|
|
129
|
+
|
|
130
|
+
Two approaches, and the AST one spends nothing.
|
|
131
|
+
|
|
132
|
+
The **AST approach** hashes. An **identical** helper is reported by code alone, and so is a
|
|
133
|
+
**renamed** one: a copy that changed its own name, its parameters and its locals, but not the
|
|
134
|
+
functions it calls, the types it names or the properties it reads. Those are the names a copy
|
|
135
|
+
cannot change and stay a copy, so they are left in the hash, and they are also what keeps
|
|
136
|
+
`return this.name` and `return this.size` from being mistaken for each other. This is
|
|
137
|
+
alpha-equivalence, and because it is an equivalence rather than a score there is no
|
|
138
|
+
threshold to tune.
|
|
139
|
+
|
|
140
|
+
Everything a hash cannot settle goes to the **agentic approach**. The agent is not handed a
|
|
141
|
+
shortlist: it gets the index as tools and searches the way a reviewer would, saying what a
|
|
142
|
+
helper does, searching other bodies for that behaviour (`search_helper_bodies` looks for
|
|
143
|
+
`instanceof Error`, not for names), reading the candidates in full, and deciding.
|
|
144
|
+
|
|
145
|
+
A finding names the twin and the responsibility they share, and stops there:
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
Helper "isNodeErrorCode" duplicates "isNodeError" at ts/store.ts:16:
|
|
149
|
+
Both ask whether an error has a specific Node error code.
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
It does not suggest where the shared copy should live. That decision depends on
|
|
153
|
+
ownership, layering and dependency direction, none of which this rule looks at; see
|
|
154
|
+
`docs/simplicity-architecture.md` for the version that tried and what it produced.
|
|
155
|
+
|
|
156
|
+
## `no-needless-helper`
|
|
157
|
+
|
|
158
|
+
A helper earns its existence when its interface is simpler than its implementation, when
|
|
159
|
+
the name tells a reader something the body would not. This rule reports the ones that do
|
|
160
|
+
not: a body of a single expression that says what the name says, so a reader jumps to the
|
|
161
|
+
declaration to learn nothing.
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
Helper "parse" does not earn its existence: Forwards to JSON.parse unchanged.
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**It has no AST approach, and cannot have one.** A hash can prove a duplicate, because two
|
|
168
|
+
identical bodies are a fact. Nothing can prove a helper *should not exist*, because that
|
|
169
|
+
is a judgement about a reader. So the deterministic half finds the helpers worth asking
|
|
170
|
+
about (one expression, no more) and gathers the facts a judgement needs: how often the
|
|
171
|
+
helper is called, and whether it is somebody's public API. Those go to the model as
|
|
172
|
+
**facts, not filters**. A helper called forty times is not disqualified by code; the model
|
|
173
|
+
is told the number and left to weigh it.
|
|
174
|
+
|
|
175
|
+
What it must *not* report matters as much as what it must:
|
|
176
|
+
|
|
177
|
+
- **`clamp(value, low, high)`** is short, and earns it. The name is the documentation;
|
|
178
|
+
`Math.min(Math.max(x, 0), 1)` is not.
|
|
179
|
+
- **`isNodeError(error): error is ErrnoException`** is a one-line type guard. Inlined, the
|
|
180
|
+
check still runs but the call site loses the type it narrowed to.
|
|
181
|
+
|
|
182
|
+
That second case is the sharpest in the plugin: `no-duplicated-helper` must report
|
|
183
|
+
`isNodeError`, because it is copied, and `no-needless-helper` must not, because it earns
|
|
184
|
+
its keep. The same three lines, two rules, opposite decisions.
|
|
185
|
+
|
|
186
|
+
It costs one model call per file rather than an agent loop, because everything the
|
|
187
|
+
judgement needs is already in hand and there is nothing to search for.
|
|
188
|
+
|
|
189
|
+
## Usage
|
|
190
|
+
|
|
191
|
+
The rules read whole files and parse them internally, so they take plain-text targets:
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
import simplicityPlugin from '@alint-js/plugin-simplicity'
|
|
195
|
+
|
|
196
|
+
import { createApeiraAdapter } from '@alint-js/agent-apeira'
|
|
197
|
+
import { defineConfig } from '@alint-js/cli'
|
|
198
|
+
|
|
199
|
+
export default defineConfig([
|
|
200
|
+
{
|
|
201
|
+
// `no-duplicated-helper`'s agentic approach needs this. Without it, the AST approach
|
|
202
|
+
// still runs.
|
|
203
|
+
agent: createApeiraAdapter(),
|
|
204
|
+
files: ['**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts,rs,go,py}'],
|
|
205
|
+
language: 'text/plain',
|
|
206
|
+
plugins: {
|
|
207
|
+
simplicity: simplicityPlugin,
|
|
208
|
+
},
|
|
209
|
+
rules: {
|
|
210
|
+
'simplicity/no-duplicated-helper': 'warn',
|
|
211
|
+
'simplicity/no-needless-helper': 'warn',
|
|
212
|
+
},
|
|
213
|
+
settings: {
|
|
214
|
+
simplicity: {
|
|
215
|
+
// Test and fixture globs to leave alone.
|
|
216
|
+
ignores: ['**/*.test.ts'],
|
|
217
|
+
// False keeps the AST approach and spends no tokens at all.
|
|
218
|
+
judge: true,
|
|
219
|
+
// The small-helper threshold, in lines. Note that it *selects* small functions;
|
|
220
|
+
// every copy-paste detector uses its threshold to exclude them.
|
|
221
|
+
maxLines: 10,
|
|
222
|
+
// Content tokens a helper needs before it is worth a word. An empty function is
|
|
223
|
+
// 3 in any language; the smallest real helper is 6.
|
|
224
|
+
minTokens: 5,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
])
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`simplicityPlugin.configs.recommended` ships the same rules and file globs, if you would
|
|
232
|
+
rather not spell them out.
|
|
233
|
+
|
|
234
|
+
alint rule entries carry a severity and nothing else, which is why the options live under
|
|
235
|
+
`settings.simplicity` rather than beside the rule.
|
|
236
|
+
|
|
237
|
+
### Cost
|
|
238
|
+
|
|
239
|
+
The agentic approach is **on by default and spends model tokens**. Identical and renamed
|
|
240
|
+
copies never reach it, and a file whose helpers were all settled by a hash costs nothing at
|
|
241
|
+
all, because no agent is started for it. `settings.simplicity.judge: false` turns the plugin
|
|
242
|
+
into a deterministic, zero-token duplicate detector for small helpers.
|
|
243
|
+
|
|
244
|
+
Two things keep the agentic approach affordable.
|
|
245
|
+
|
|
246
|
+
**A review is cached under a fingerprint of every helper in the workspace**, so a run that
|
|
247
|
+
changed nothing costs nothing. A helper added, edited, moved or deleted anywhere throws
|
|
248
|
+
the whole cache away, because a helper added anywhere could be the twin of a helper here,
|
|
249
|
+
and a decision decided against one workspace means nothing against another. The
|
|
250
|
+
invalidation is coarse on purpose and cheap in practice: the index holds only helpers of
|
|
251
|
+
ten lines or fewer, and most commits do not touch one. Turn it off with
|
|
252
|
+
`settings.simplicity.cache: false`.
|
|
253
|
+
|
|
254
|
+
The cache is written to `.alint/simplicity/`. Add it to your `.gitignore`: what a model
|
|
255
|
+
decided is not project configuration.
|
|
256
|
+
|
|
257
|
+
**Only the files you lint are reviewed.** The index always covers the whole workspace, so
|
|
258
|
+
a twin is found wherever it lives, but only the files you pass are handed to an agent.
|
|
259
|
+
That makes the cost proportional to your diff rather than to the repository:
|
|
260
|
+
|
|
261
|
+
```sh
|
|
262
|
+
alint $(git diff --name-only origin/main...)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Agent tokens are metered and reported, so a run says what it cost.
|
|
266
|
+
|
|
267
|
+
### Evaluating a change to a prompt
|
|
268
|
+
|
|
269
|
+
`fixtures/` is a graded corpus: every helper in it is either a finding a run must produce
|
|
270
|
+
or one it must not. The unit tests assert the parts a fingerprint decides, which cost
|
|
271
|
+
nothing. The parts a model decides are measured by a harness that spends real tokens, so
|
|
272
|
+
it is kept outside this repository and CI never runs it. See
|
|
273
|
+
[the architecture notes](./docs/simplicity-architecture.md) for what it measures and why a
|
|
274
|
+
prompt cannot be changed without it.
|
|
275
|
+
|
|
276
|
+
## When to use
|
|
277
|
+
|
|
278
|
+
- A codebase where several people, or several agents, open branches in parallel and the
|
|
279
|
+
same tiny helper keeps being re-invented in each of them. This is the case the plugin
|
|
280
|
+
was built from: the duplication in #28 and #31 entered through parallel branches and was
|
|
281
|
+
found by a human afterwards.
|
|
282
|
+
- Reviews where "search for an existing implementation first" is written down as guidance
|
|
283
|
+
and is not, on its own, working.
|
|
284
|
+
- Codebases that already run a copy-paste detector and want the band it cannot see:
|
|
285
|
+
helpers below its minimum-token floor.
|
|
286
|
+
|
|
287
|
+
## When not to use
|
|
288
|
+
|
|
289
|
+
- **Not a general code-quality plugin.** The scope is function-level simplification smells
|
|
290
|
+
and nothing more. If a check does not answer "does this helper exist twice" or "should
|
|
291
|
+
this helper exist at all", it belongs elsewhere.
|
|
292
|
+
- **Not a cross-language dedupe tool.** Helpers are compared within one language only. A
|
|
293
|
+
Go twin of a TypeScript helper is not shareable code, so reporting it would be advice
|
|
294
|
+
nobody could take, and the tool that records findings refuses a cross-language pair
|
|
295
|
+
outright.
|
|
296
|
+
- **Not a replacement for token-based copy-paste detectors.** jscpd, PMD's CPD and their
|
|
297
|
+
peers cover large duplicated blocks (roughly 50 tokens and up) well, cheaply, and across
|
|
298
|
+
150+ languages. Keep using them, and run this plugin for the small helpers they are
|
|
299
|
+
configured to ignore.
|
|
300
|
+
- **Not free, unless you ask it to be.** The judge spends tokens. `judge: false` keeps the
|
|
301
|
+
AST approach and costs nothing.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { RuleContext, SourceLocation, SourceRange } from "@alint-js/core";
|
|
2
|
+
import { AgentTool } from "@alint-js/core/agent";
|
|
3
|
+
import { InferOutput } from "valibot";
|
|
4
|
+
|
|
5
|
+
//#region src/extract/types.d.ts
|
|
6
|
+
interface CallSite {
|
|
7
|
+
/** The last segment only: `a.b.helper()` and `helper()` both yield `helper`. */
|
|
8
|
+
name: string;
|
|
9
|
+
range: SourceRange;
|
|
10
|
+
}
|
|
11
|
+
interface ExtractedFunction {
|
|
12
|
+
/** The names this function declares. Everything else stays verbatim in the alpha fingerprint; see `queries.ts`. */
|
|
13
|
+
binderNames: string[];
|
|
14
|
+
/** Stricter than one statement: a body that is one `if` and its two returns is one statement and not one expression. */
|
|
15
|
+
bodyIsSingleExpression: boolean;
|
|
16
|
+
bodyStatements: number;
|
|
17
|
+
/** Relative to `text`, not to the source. */
|
|
18
|
+
commentRanges: SourceRange[];
|
|
19
|
+
exported: boolean;
|
|
20
|
+
/** Relative to `text`. Property, field and type names are not here: replacing them would collapse `entry.name` and `entry.size`. */
|
|
21
|
+
identifierRanges: SourceRange[];
|
|
22
|
+
/** Absolute. Lines are 1-based, columns 0-based. */
|
|
23
|
+
loc: SourceLocation;
|
|
24
|
+
name: string;
|
|
25
|
+
/** Absolute offsets into the source. */
|
|
26
|
+
range: SourceRange;
|
|
27
|
+
text: string;
|
|
28
|
+
}
|
|
29
|
+
type ExtractLanguage = 'go' | 'javascript' | 'python' | 'rust' | 'tsx' | 'typescript';
|
|
30
|
+
interface SourceExtract {
|
|
31
|
+
/** Every call in the source, including calls made outside any function. */
|
|
32
|
+
calls: CallSite[];
|
|
33
|
+
functions: ExtractedFunction[];
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/extract/extract.d.ts
|
|
37
|
+
declare function extractSource(source: string, language: ExtractLanguage): Promise<SourceExtract>;
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/extract/language.d.ts
|
|
40
|
+
declare function resolveExtractLanguage(filePath: string): ExtractLanguage | undefined;
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/fingerprint/fingerprint.d.ts
|
|
43
|
+
/**
|
|
44
|
+
* Hashes a function with the names it declares (own name, parameters, locals) replaced by
|
|
45
|
+
* placeholders. Same alpha fingerprint means the same function, renamed.
|
|
46
|
+
*
|
|
47
|
+
* `identifierRanges` must hold renameable identifiers only. Replacing property, field or type
|
|
48
|
+
* names is the mistake this fingerprint exists to avoid.
|
|
49
|
+
*/
|
|
50
|
+
declare function alphaFingerprint(text: string, commentRanges: readonly SourceRange[], identifierRanges: readonly SourceRange[], binderNames: readonly string[]): string;
|
|
51
|
+
/** Hashes a function with comments and formatting removed, so layout and docs do not count. */
|
|
52
|
+
declare function exactFingerprint(text: string, commentRanges: readonly SourceRange[]): string;
|
|
53
|
+
/** Comments and formatting removed, names left alone, so `search_helper_bodies` can search real code. */
|
|
54
|
+
declare function normalizedBody(text: string, commentRanges: readonly SourceRange[]): string;
|
|
55
|
+
/** Content tokens, alpha-normalized. Punctuation is dropped: every function has braces. */
|
|
56
|
+
declare function tokenize(text: string, commentRanges: readonly SourceRange[], identifierRanges: readonly SourceRange[], binderNames: readonly string[]): string[];
|
|
57
|
+
/** Shared token fraction of the larger bag. Ranks candidates, decides nothing. */
|
|
58
|
+
declare function tokenOverlap(left: readonly string[], right: readonly string[]): number;
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/rules/no-duplicated-helper/tools.d.ts
|
|
61
|
+
interface AgentFinding {
|
|
62
|
+
helperId: string;
|
|
63
|
+
reason: string;
|
|
64
|
+
twinId: string;
|
|
65
|
+
}
|
|
66
|
+
interface DuplicateToolsOptions {
|
|
67
|
+
/** Mutated by `report_duplicate`; the caller reads it once the agent stops. */
|
|
68
|
+
findings: AgentFinding[];
|
|
69
|
+
index: RepoIndex;
|
|
70
|
+
/** Helpers of the file under review: the only ones the agent may report on. */
|
|
71
|
+
reviewing: readonly IndexedHelper[];
|
|
72
|
+
}
|
|
73
|
+
declare function createDuplicateTools(options: DuplicateToolsOptions): AgentTool[];
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/repo/cache.d.ts
|
|
76
|
+
interface ReviewCache {
|
|
77
|
+
/** A review is returned only to a run whose `fingerprint` matches the one it was decided against. */
|
|
78
|
+
get: (filePath: string) => AgentFinding[] | undefined;
|
|
79
|
+
set: (filePath: string, findings: AgentFinding[]) => Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
declare function reviewCacheFor(ctx: RuleContext, options: {
|
|
82
|
+
cwd: string;
|
|
83
|
+
enabled: boolean;
|
|
84
|
+
fingerprint: string;
|
|
85
|
+
}): Promise<ReviewCache>;
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/repo/index.d.ts
|
|
88
|
+
interface IndexedHelper {
|
|
89
|
+
alphaFingerprint: string;
|
|
90
|
+
/** Comments and formatting removed, names left alone, so `search_helper_bodies` searches real code. */
|
|
91
|
+
body: string;
|
|
92
|
+
bodyIsSingleExpression: boolean;
|
|
93
|
+
/** Statements in the body, not counting comments. */
|
|
94
|
+
bodyStatements: number;
|
|
95
|
+
exactFingerprint: string;
|
|
96
|
+
exported: boolean;
|
|
97
|
+
filePath: string;
|
|
98
|
+
/** `packages/cli/src/lint.ts:57`. Unique, and a model can quote it back. */
|
|
99
|
+
id: string;
|
|
100
|
+
language: ExtractLanguage;
|
|
101
|
+
line: number;
|
|
102
|
+
lines: number;
|
|
103
|
+
name: string;
|
|
104
|
+
text: string;
|
|
105
|
+
tokens: string[];
|
|
106
|
+
/**
|
|
107
|
+
* How often a function of this name is called across the workspace.
|
|
108
|
+
*
|
|
109
|
+
* Counted by NAME, not by binding, so two `isEmpty` helpers share a count and `x.isEmpty()` counts too.
|
|
110
|
+
* Only ever handed to the judge as an approximate fact.
|
|
111
|
+
*/
|
|
112
|
+
usageCount: number;
|
|
113
|
+
}
|
|
114
|
+
interface RepoIndex {
|
|
115
|
+
byAlpha: Map<string, IndexedHelper[]>;
|
|
116
|
+
byExact: Map<string, IndexedHelper[]>;
|
|
117
|
+
byId: Map<string, IndexedHelper>;
|
|
118
|
+
/** Every helper in the workspace, in one hash. What a cached review is stamped with. */
|
|
119
|
+
fingerprint: string;
|
|
120
|
+
helpers: IndexedHelper[];
|
|
121
|
+
}
|
|
122
|
+
interface RepoIndexOptions {
|
|
123
|
+
cwd: string;
|
|
124
|
+
ignores: readonly string[];
|
|
125
|
+
maxLines: number;
|
|
126
|
+
minTokens: number;
|
|
127
|
+
}
|
|
128
|
+
/** Helpers of one file, in source order. */
|
|
129
|
+
declare function helpersIn(index: RepoIndex, filePath: string): IndexedHelper[];
|
|
130
|
+
declare function repoIndexFor(ctx: RuleContext, options: RepoIndexOptions): Promise<RepoIndex>;
|
|
131
|
+
/** Every other helper sharing a fingerprint. A helper is never its own twin. */
|
|
132
|
+
declare function twinsOf(index: RepoIndex, helper: IndexedHelper, kind: 'alpha' | 'exact'): IndexedHelper[];
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/rules/no-duplicated-helper/prompt.d.ts
|
|
135
|
+
declare const duplicatedHelperInstructions: string;
|
|
136
|
+
/**
|
|
137
|
+
* The nearest helpers are pasted in rather than fetched: measured, the agent spent three of its four steps discovering what to read,
|
|
138
|
+
* and every step re-sends the system prompt and every tool schema.
|
|
139
|
+
*/
|
|
140
|
+
declare function buildDuplicatedHelperPrompt(options: {
|
|
141
|
+
candidates: readonly IndexedHelper[];
|
|
142
|
+
filePath: string;
|
|
143
|
+
helpers: readonly IndexedHelper[];
|
|
144
|
+
}): string;
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/rules/no-duplicated-helper/rule.d.ts
|
|
147
|
+
/**
|
|
148
|
+
* Reports a small helper already implemented somewhere else in the workspace.
|
|
149
|
+
*
|
|
150
|
+
* Identical bodies and renamed-only copies are settled by AST fingerprints, with no model. What
|
|
151
|
+
* a fingerprint cannot settle goes to an agent holding the whole index as tools.
|
|
152
|
+
*/
|
|
153
|
+
declare const duplicatedHelperRule: import("@alint-js/core").RuleDefinition;
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/rules/no-needless-helper/prompt.d.ts
|
|
156
|
+
declare const needlessHelperPrompt: string;
|
|
157
|
+
declare function buildNeedlessHelperPrompt(helpers: readonly IndexedHelper[]): string;
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/rules/no-needless-helper/rule.d.ts
|
|
160
|
+
declare const needlessHelperResponseSchema: import("valibot").ObjectSchema<{
|
|
161
|
+
readonly findings: import("valibot").ArraySchema<import("valibot").ObjectSchema<{
|
|
162
|
+
readonly helper: import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<undefined>, import("valibot").DescriptionAction<number, "The number shown beside the helper, exactly as given.">]>;
|
|
163
|
+
readonly name: import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").DescriptionAction<string, "The helper's name, exactly as shown.">]>;
|
|
164
|
+
readonly reason: import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").DescriptionAction<string, "At most twelve words, naming what the body already says.">]>;
|
|
165
|
+
}, undefined>, undefined>;
|
|
166
|
+
}, undefined>;
|
|
167
|
+
/**
|
|
168
|
+
* Reports a helper whose interface is no simpler than its implementation.
|
|
169
|
+
*
|
|
170
|
+
* No AST approach is possible: a hash can prove a duplicate, but nothing can prove a helper
|
|
171
|
+
* should not exist. The deterministic half only finds candidates and gathers facts (usage
|
|
172
|
+
* count, exported), which are given to the model rather than applied as filters.
|
|
173
|
+
*/
|
|
174
|
+
declare const needlessHelperRule: import("@alint-js/core").RuleDefinition;
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/rules/shared/settings.d.ts
|
|
177
|
+
declare const settingsSchema: import("valibot").ObjectSchema<{
|
|
178
|
+
readonly cache: import("valibot").OptionalSchema<import("valibot").BooleanSchema<undefined>, true>; /** Globs matched against the repository-relative file path. */
|
|
179
|
+
readonly ignores: import("valibot").OptionalSchema<import("valibot").ArraySchema<import("valibot").StringSchema<undefined>, undefined>, readonly []>;
|
|
180
|
+
/**
|
|
181
|
+
* When false, no model is called: `no-duplicated-helper` still reports what a fingerprint settles,
|
|
182
|
+
* `no-needless-helper` reports nothing.
|
|
183
|
+
*/
|
|
184
|
+
readonly judge: import("valibot").OptionalSchema<import("valibot").BooleanSchema<undefined>, true>;
|
|
185
|
+
readonly maxLines: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<undefined>, import("valibot").IntegerAction<number, undefined>, import("valibot").MinValueAction<number, 1, undefined>]>, 10>;
|
|
186
|
+
readonly minTokens: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<undefined>, import("valibot").IntegerAction<number, undefined>, import("valibot").MinValueAction<number, 1, undefined>]>, 5>;
|
|
187
|
+
}, undefined>;
|
|
188
|
+
type SimplicitySettings = InferOutput<typeof settingsSchema>;
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/index.d.ts
|
|
191
|
+
declare const simplicityPlugin: import("@alint-js/core").PluginDefinition;
|
|
192
|
+
//#endregion
|
|
193
|
+
export { type AgentFinding, type CallSite, type DuplicateToolsOptions, type ExtractLanguage, type ExtractedFunction, type IndexedHelper, type RepoIndex, type RepoIndexOptions, type ReviewCache, type SimplicitySettings, type SourceExtract, alphaFingerprint, buildDuplicatedHelperPrompt, buildNeedlessHelperPrompt, createDuplicateTools, simplicityPlugin as default, simplicityPlugin, duplicatedHelperInstructions, duplicatedHelperRule, exactFingerprint, extractSource, helpersIn, needlessHelperPrompt, needlessHelperResponseSchema, needlessHelperRule, normalizedBody, repoIndexFor, resolveExtractLanguage, reviewCacheFor, tokenOverlap, tokenize, twinsOf };
|