@aexol/spectral 0.9.128 → 0.9.130
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/dist/agent/index.js +2 -2
- package/dist/extensions/code-order/analyzer.d.ts +47 -0
- package/dist/extensions/code-order/analyzer.d.ts.map +1 -0
- package/dist/extensions/code-order/analyzer.js +315 -0
- package/dist/extensions/code-order/heuristics.d.ts +37 -0
- package/dist/extensions/code-order/heuristics.d.ts.map +1 -0
- package/dist/extensions/code-order/heuristics.js +188 -0
- package/dist/extensions/code-order/index.d.ts +15 -0
- package/dist/extensions/code-order/index.d.ts.map +1 -0
- package/dist/extensions/code-order/index.js +76 -0
- package/dist/extensions/code-order/report.d.ts +14 -0
- package/dist/extensions/code-order/report.d.ts.map +1 -0
- package/dist/extensions/code-order/report.js +140 -0
- package/dist/extensions/code-order/types.d.ts +63 -0
- package/dist/extensions/code-order/types.d.ts.map +1 -0
- package/dist/extensions/code-order/types.js +7 -0
- package/dist/mcp/ui-stream-types.d.ts +2 -2
- package/dist/relay/dispatcher.d.ts.map +1 -1
- package/dist/relay/dispatcher.js +5 -4
- package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/native-extensions.js +11 -0
- package/dist/sdk/coding-agent/core/system-prompt.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/system-prompt.js +2 -0
- package/dist/sdk/coding-agent/core/tools/bash-blocklist.d.ts +42 -0
- package/dist/sdk/coding-agent/core/tools/bash-blocklist.d.ts.map +1 -0
- package/dist/sdk/coding-agent/core/tools/bash-blocklist.js +141 -0
- package/dist/sdk/coding-agent/core/tools/bash.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/bash.js +5 -0
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +6 -3
- package/dist/server/error-humanizer.d.ts +31 -0
- package/dist/server/error-humanizer.d.ts.map +1 -0
- package/dist/server/error-humanizer.js +77 -0
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +7 -6
- package/package.json +1 -1
package/dist/agent/index.js
CHANGED
|
@@ -228,8 +228,8 @@ export function resolveSubagentTools(toolNames, cwd, ext, ctx) {
|
|
|
228
228
|
const toolDef = richApi.getToolDefinition(name);
|
|
229
229
|
if (toolDef) {
|
|
230
230
|
const isolatedContext = name === RECALL_OBSERVATION_TOOL_NAME
|
|
231
|
-
? { cwd, sessionManager: createSubagentSessionManagerSnapshot(ctx.sessionManager) }
|
|
232
|
-
: { cwd };
|
|
231
|
+
? { cwd, sessionManager: createSubagentSessionManagerSnapshot(ctx.sessionManager), getSystemPrompt: ctx.getSystemPrompt }
|
|
232
|
+
: { cwd, getSystemPrompt: ctx.getSystemPrompt };
|
|
233
233
|
const isolatedContextWithNoParent = isolatedContext;
|
|
234
234
|
tools.push(wrapToolDefinition(toolDef, () => isolatedContextWithNoParent));
|
|
235
235
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Order — directory walker, size aggregation and metrics.
|
|
3
|
+
*
|
|
4
|
+
* Iterative (stack-based) traversal — never recurses so deep trees cannot blow
|
|
5
|
+
* the call stack. Honors a hardcoded ignore list (node_modules, .git, dist…),
|
|
6
|
+
* a hard cap on scanned files, and a max depth.
|
|
7
|
+
*
|
|
8
|
+
* The walker builds the full folder tree in one pass, aggregating size,
|
|
9
|
+
* file/folder counts and max depth into each FolderNode as it unwinds.
|
|
10
|
+
*/
|
|
11
|
+
import type { AnalysisResult, FileNode, FolderNode, ProjectMetrics, TreeNode, Issue } from "./types.js";
|
|
12
|
+
/** Returns true for media/binary extensions that should not be flagged as oversized. */
|
|
13
|
+
declare function isBinaryExtension(ext: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Walk the directory tree under `root` and return the full folder tree plus
|
|
16
|
+
* flat lists of every file/folder encountered (used for metrics + heuristics).
|
|
17
|
+
*/
|
|
18
|
+
export declare function walkTree(root: string): Promise<{
|
|
19
|
+
tree: FolderNode;
|
|
20
|
+
allFiles: FileNode[];
|
|
21
|
+
allFolders: string[];
|
|
22
|
+
maxDepth: number;
|
|
23
|
+
fileCount: number;
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* Compute project-wide metrics from the flat file list.
|
|
27
|
+
*/
|
|
28
|
+
export declare function computeMetrics(allFiles: FileNode[], allFolders: string[], maxDepth: number): ProjectMetrics;
|
|
29
|
+
/** How many top items to keep in ranked lists. Exposed for tests. */
|
|
30
|
+
export declare const TOP_N: 15;
|
|
31
|
+
/** Exposed so heuristics.ts can use the same thresholds. */
|
|
32
|
+
export declare const HEURISTIC_LIMITS: {
|
|
33
|
+
/** Stop scanning after this many files regardless of depth. */
|
|
34
|
+
readonly maxFiles: 50000;
|
|
35
|
+
/** Maximum directory depth the walker descends into. */
|
|
36
|
+
readonly maxDepth: 20;
|
|
37
|
+
/** A file larger than this (bytes) is flagged as oversized. */
|
|
38
|
+
readonly largeFileThreshold: 100000;
|
|
39
|
+
/** A directory deeper than this is flagged as deep nesting. */
|
|
40
|
+
readonly deepNestingThreshold: 8;
|
|
41
|
+
/** How many items to keep in "top N" lists. */
|
|
42
|
+
readonly topN: 15;
|
|
43
|
+
};
|
|
44
|
+
export { isBinaryExtension };
|
|
45
|
+
/** Re-exported for the analyzer entry point. */
|
|
46
|
+
export type { AnalysisResult, Issue, TreeNode, FolderNode, FileNode };
|
|
47
|
+
//# sourceMappingURL=analyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../../../src/extensions/code-order/analyzer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EACX,cAAc,EAEd,QAAQ,EACR,UAAU,EACV,cAAc,EACd,QAAQ,EACR,KAAK,EACL,MAAM,YAAY,CAAC;AAmDpB,wFAAwF;AACxF,iBAAS,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE/C;AAkDD;;;GAGG;AACH,wBAAsB,QAAQ,CAC7B,IAAI,EAAE,MAAM,GACV,OAAO,CAAC;IACV,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACrB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CAClB,CAAC,CAiKD;AAWD;;GAEG;AACH,wBAAgB,cAAc,CAC7B,QAAQ,EAAE,QAAQ,EAAE,EACpB,UAAU,EAAE,MAAM,EAAE,EACpB,QAAQ,EAAE,MAAM,GACd,cAAc,CAwChB;AAED,qEAAqE;AACrE,eAAO,MAAM,KAAK,IAAc,CAAC;AAEjC,4DAA4D;AAC5D,eAAO,MAAM,gBAAgB;IA3R5B,+DAA+D;;IAE/D,wDAAwD;;IAExD,+DAA+D;;IAE/D,+DAA+D;;IAE/D,+CAA+C;;CAmRV,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAE7B,gDAAgD;AAChD,YAAY,EAAE,cAAc,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC"}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Order — directory walker, size aggregation and metrics.
|
|
3
|
+
*
|
|
4
|
+
* Iterative (stack-based) traversal — never recurses so deep trees cannot blow
|
|
5
|
+
* the call stack. Honors a hardcoded ignore list (node_modules, .git, dist…),
|
|
6
|
+
* a hard cap on scanned files, and a max depth.
|
|
7
|
+
*
|
|
8
|
+
* The walker builds the full folder tree in one pass, aggregating size,
|
|
9
|
+
* file/folder counts and max depth into each FolderNode as it unwinds.
|
|
10
|
+
*/
|
|
11
|
+
import { stat } from "node:fs/promises";
|
|
12
|
+
import { join, relative, sep, basename, extname } from "node:path";
|
|
13
|
+
import { readdir } from "node:fs/promises";
|
|
14
|
+
/** Directory names that are skipped outright (matched against the basename). */
|
|
15
|
+
const IGNORED_DIRS = new Set([
|
|
16
|
+
"node_modules",
|
|
17
|
+
".git",
|
|
18
|
+
"dist",
|
|
19
|
+
"dist-release",
|
|
20
|
+
"build",
|
|
21
|
+
".next",
|
|
22
|
+
".turbo",
|
|
23
|
+
".cache",
|
|
24
|
+
".deno",
|
|
25
|
+
"coverage",
|
|
26
|
+
".nyc_output",
|
|
27
|
+
"vendor",
|
|
28
|
+
"__pycache__",
|
|
29
|
+
".pytest_cache",
|
|
30
|
+
".venv",
|
|
31
|
+
"venv",
|
|
32
|
+
"env",
|
|
33
|
+
"target",
|
|
34
|
+
"out",
|
|
35
|
+
"release",
|
|
36
|
+
"releases",
|
|
37
|
+
".gradle",
|
|
38
|
+
".idea",
|
|
39
|
+
".vscode",
|
|
40
|
+
".sst",
|
|
41
|
+
".serverless",
|
|
42
|
+
".terraform",
|
|
43
|
+
".logs",
|
|
44
|
+
"logs",
|
|
45
|
+
".svelte-kit",
|
|
46
|
+
".parcel-cache",
|
|
47
|
+
]);
|
|
48
|
+
/** File basenames that are skipped outright. */
|
|
49
|
+
const IGNORED_FILES = new Set([".DS_Store", "Thumbs.db"]);
|
|
50
|
+
/** Binary/media extensions whose large size is expected and unactionable. */
|
|
51
|
+
const BINARY_EXTENSIONS = new Set([
|
|
52
|
+
"png", "jpg", "jpeg", "gif", "webp", "bmp", "ico", "tiff", "svg",
|
|
53
|
+
"mp4", "webm", "mov", "avi", "mkv", "mp3", "wav", "flac", "ogg",
|
|
54
|
+
"pdf", "zip", "tar", "gz", "tgz", "bz2", "7z", "rar",
|
|
55
|
+
"woff", "woff2", "ttf", "otf", "eot",
|
|
56
|
+
"dylib", "so", "dll", "exe", "bin", "class", "jar",
|
|
57
|
+
"sqlite", "db", "lockb",
|
|
58
|
+
"pak", "dat", "dat1", "index",
|
|
59
|
+
]);
|
|
60
|
+
/** Returns true for media/binary extensions that should not be flagged as oversized. */
|
|
61
|
+
function isBinaryExtension(ext) {
|
|
62
|
+
return BINARY_EXTENSIONS.has(ext);
|
|
63
|
+
}
|
|
64
|
+
/** Hard safety limits — no configuration exposed to the user. */
|
|
65
|
+
const LIMITS = {
|
|
66
|
+
/** Stop scanning after this many files regardless of depth. */
|
|
67
|
+
maxFiles: 50_000,
|
|
68
|
+
/** Maximum directory depth the walker descends into. */
|
|
69
|
+
maxDepth: 20,
|
|
70
|
+
/** A file larger than this (bytes) is flagged as oversized. */
|
|
71
|
+
largeFileThreshold: 100_000,
|
|
72
|
+
/** A directory deeper than this is flagged as deep nesting. */
|
|
73
|
+
deepNestingThreshold: 8,
|
|
74
|
+
/** How many items to keep in "top N" lists. */
|
|
75
|
+
topN: 15,
|
|
76
|
+
};
|
|
77
|
+
function isIgnoredDir(name) {
|
|
78
|
+
return IGNORED_DIRS.has(name);
|
|
79
|
+
}
|
|
80
|
+
function isIgnoredFile(name) {
|
|
81
|
+
if (IGNORED_FILES.has(name))
|
|
82
|
+
return true;
|
|
83
|
+
// Skip lock files (package-lock.json, yarn.lock, Cargo.lock, …) — they are
|
|
84
|
+
// generated and would dominate the size metrics.
|
|
85
|
+
return /(?:^|\.)(?:lock)$/i.test(name) || name.endsWith(".lock");
|
|
86
|
+
}
|
|
87
|
+
function shouldIgnoreEntry(name, isDir) {
|
|
88
|
+
return isDir ? isIgnoredDir(name) : isIgnoredFile(name);
|
|
89
|
+
}
|
|
90
|
+
async function safeStat(p) {
|
|
91
|
+
try {
|
|
92
|
+
const s = await stat(p);
|
|
93
|
+
return { size: s.size, mtime: s.mtimeMs, isDir: s.isDirectory() };
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Walk the directory tree under `root` and return the full folder tree plus
|
|
101
|
+
* flat lists of every file/folder encountered (used for metrics + heuristics).
|
|
102
|
+
*/
|
|
103
|
+
export async function walkTree(root) {
|
|
104
|
+
const allFiles = [];
|
|
105
|
+
const allFolders = [];
|
|
106
|
+
let fileCount = 0;
|
|
107
|
+
let maxDepth = 0;
|
|
108
|
+
// Stack of pending directories to process.
|
|
109
|
+
const stack = [{ dir: root, depth: 0 }];
|
|
110
|
+
// Map from directory path → built node metadata (children, depth).
|
|
111
|
+
// We process directories bottom-up: a folder is finalized after all of
|
|
112
|
+
// its descendants have been finalized.
|
|
113
|
+
const pending = new Map();
|
|
114
|
+
// Seed the root folder node.
|
|
115
|
+
const rootName = basename(root) || root;
|
|
116
|
+
pending.set(root, {
|
|
117
|
+
node: {
|
|
118
|
+
path: ".",
|
|
119
|
+
name: rootName,
|
|
120
|
+
isFile: false,
|
|
121
|
+
totalSize: 0,
|
|
122
|
+
fileCount: 0,
|
|
123
|
+
folderCount: 0,
|
|
124
|
+
maxDepth: 0,
|
|
125
|
+
children: [],
|
|
126
|
+
},
|
|
127
|
+
depth: 0,
|
|
128
|
+
});
|
|
129
|
+
allFolders.push(".");
|
|
130
|
+
while (stack.length > 0) {
|
|
131
|
+
const item = stack.pop();
|
|
132
|
+
const { dir, depth } = item;
|
|
133
|
+
if (depth > maxDepth)
|
|
134
|
+
maxDepth = depth;
|
|
135
|
+
if (depth >= LIMITS.maxDepth)
|
|
136
|
+
continue;
|
|
137
|
+
if (fileCount >= LIMITS.maxFiles)
|
|
138
|
+
break;
|
|
139
|
+
let entries;
|
|
140
|
+
try {
|
|
141
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
continue; // permission error or vanished — skip silently
|
|
145
|
+
}
|
|
146
|
+
// Stable order: folders first, then files, alphabetical within.
|
|
147
|
+
const dirs = [];
|
|
148
|
+
const files = [];
|
|
149
|
+
for (const e of entries) {
|
|
150
|
+
if (shouldIgnoreEntry(e.name, e.isDirectory()))
|
|
151
|
+
continue;
|
|
152
|
+
if (e.isDirectory())
|
|
153
|
+
dirs.push(e.name);
|
|
154
|
+
else if (e.isFile())
|
|
155
|
+
files.push(e.name);
|
|
156
|
+
}
|
|
157
|
+
dirs.sort((a, b) => a.localeCompare(b));
|
|
158
|
+
files.sort((a, b) => a.localeCompare(b));
|
|
159
|
+
// Queue subdirectories for processing.
|
|
160
|
+
for (const d of dirs) {
|
|
161
|
+
const child = join(dir, d);
|
|
162
|
+
const rel = relative(root, child).split(sep).join("/");
|
|
163
|
+
const parentBuilt = pending.get(dir);
|
|
164
|
+
const childDepth = depth + 1;
|
|
165
|
+
pending.set(child, {
|
|
166
|
+
node: {
|
|
167
|
+
path: rel,
|
|
168
|
+
name: d,
|
|
169
|
+
isFile: false,
|
|
170
|
+
totalSize: 0,
|
|
171
|
+
fileCount: 0,
|
|
172
|
+
folderCount: 0,
|
|
173
|
+
maxDepth: 0,
|
|
174
|
+
children: [],
|
|
175
|
+
},
|
|
176
|
+
depth: childDepth,
|
|
177
|
+
});
|
|
178
|
+
allFolders.push(rel);
|
|
179
|
+
if (parentBuilt) {
|
|
180
|
+
// placeholder child; size updated when finalized
|
|
181
|
+
parentBuilt.node.children.push({
|
|
182
|
+
path: rel,
|
|
183
|
+
name: d,
|
|
184
|
+
isFile: false,
|
|
185
|
+
totalSize: 0,
|
|
186
|
+
fileCount: 0,
|
|
187
|
+
folderCount: 0,
|
|
188
|
+
maxDepth: 0,
|
|
189
|
+
children: [],
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
stack.push({ dir: child, depth: childDepth });
|
|
193
|
+
}
|
|
194
|
+
// Process files in this directory immediately.
|
|
195
|
+
const built = pending.get(dir);
|
|
196
|
+
if (!built)
|
|
197
|
+
continue;
|
|
198
|
+
for (const f of files) {
|
|
199
|
+
if (fileCount >= LIMITS.maxFiles)
|
|
200
|
+
break;
|
|
201
|
+
const fullPath = join(dir, f);
|
|
202
|
+
const rel = relative(root, fullPath).split(sep).join("/");
|
|
203
|
+
const info = await safeStat(fullPath);
|
|
204
|
+
if (!info || info.isDir)
|
|
205
|
+
continue;
|
|
206
|
+
fileCount++;
|
|
207
|
+
const node = {
|
|
208
|
+
path: rel,
|
|
209
|
+
name: f,
|
|
210
|
+
size: info.size,
|
|
211
|
+
extension: extname(f).slice(1).toLowerCase(),
|
|
212
|
+
isFile: true,
|
|
213
|
+
mtime: info.mtime,
|
|
214
|
+
};
|
|
215
|
+
allFiles.push(node);
|
|
216
|
+
built.node.children.push(node);
|
|
217
|
+
built.node.totalSize += info.size;
|
|
218
|
+
built.node.fileCount += 1;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Finalize folders bottom-up: aggregate child totals into each parent.
|
|
222
|
+
// Sort by descending depth so children are processed before parents.
|
|
223
|
+
const foldersByDepth = Array.from(pending.entries()).sort((a, b) => b[1].depth - a[1].depth);
|
|
224
|
+
for (const [absPath, built] of foldersByDepth) {
|
|
225
|
+
// Recompute aggregates from finalized children.
|
|
226
|
+
let size = 0;
|
|
227
|
+
let fc = 0;
|
|
228
|
+
let foc = 0;
|
|
229
|
+
let md = built.depth;
|
|
230
|
+
for (const child of built.node.children) {
|
|
231
|
+
if (child.isFile) {
|
|
232
|
+
size += child.size;
|
|
233
|
+
fc += 1;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
size += child.totalSize;
|
|
237
|
+
fc += child.fileCount;
|
|
238
|
+
foc += 1 + child.folderCount;
|
|
239
|
+
if (child.maxDepth > md)
|
|
240
|
+
md = child.maxDepth;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
built.node.totalSize = size;
|
|
244
|
+
built.node.fileCount = fc;
|
|
245
|
+
built.node.folderCount = foc;
|
|
246
|
+
built.node.maxDepth = md;
|
|
247
|
+
// Propagate up into the parent's child placeholder (if any).
|
|
248
|
+
const parentPath = absPath === root ? null : join(absPath, "..");
|
|
249
|
+
if (parentPath && parentPath !== absPath) {
|
|
250
|
+
const parentBuilt = pending.get(parentPath);
|
|
251
|
+
if (parentBuilt) {
|
|
252
|
+
// Replace the placeholder child we pushed earlier.
|
|
253
|
+
const idx = parentBuilt.node.children.findIndex((c) => !c.isFile && c.path === built.node.path);
|
|
254
|
+
if (idx >= 0) {
|
|
255
|
+
parentBuilt.node.children[idx] = built.node;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const tree = pending.get(root).node;
|
|
261
|
+
return { tree, allFiles, allFolders, maxDepth, fileCount };
|
|
262
|
+
}
|
|
263
|
+
function median(values) {
|
|
264
|
+
if (values.length === 0)
|
|
265
|
+
return 0;
|
|
266
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
267
|
+
const mid = Math.floor(sorted.length / 2);
|
|
268
|
+
return sorted.length % 2 === 0
|
|
269
|
+
? Math.round((sorted[mid - 1] + sorted[mid]) / 2)
|
|
270
|
+
: sorted[mid];
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Compute project-wide metrics from the flat file list.
|
|
274
|
+
*/
|
|
275
|
+
export function computeMetrics(allFiles, allFolders, maxDepth) {
|
|
276
|
+
const totalFiles = allFiles.length;
|
|
277
|
+
const totalSize = allFiles.reduce((s, f) => s + f.size, 0);
|
|
278
|
+
const avgFileSize = totalFiles > 0 ? Math.round(totalSize / totalFiles) : 0;
|
|
279
|
+
const medianFileSize = median(allFiles.map((f) => f.size));
|
|
280
|
+
const extMap = new Map();
|
|
281
|
+
for (const f of allFiles) {
|
|
282
|
+
const ext = f.extension || "(none)";
|
|
283
|
+
const entry = extMap.get(ext) ?? { extension: ext, count: 0, size: 0 };
|
|
284
|
+
entry.count += 1;
|
|
285
|
+
entry.size += f.size;
|
|
286
|
+
extMap.set(ext, entry);
|
|
287
|
+
}
|
|
288
|
+
const extensions = [...extMap.values()]
|
|
289
|
+
.sort((a, b) => b.size - a.size || b.count - a.count);
|
|
290
|
+
const topLargestFiles = [...allFiles]
|
|
291
|
+
.sort((a, b) => b.size - a.size)
|
|
292
|
+
.slice(0, LIMITS.topN);
|
|
293
|
+
const emptyFiles = allFiles.filter((f) => f.size === 0).map((f) => f.path);
|
|
294
|
+
// Empty folders: folders whose path never appears as a parent of any file.
|
|
295
|
+
// (We can't easily tell from the flat folder list, so we derive from the
|
|
296
|
+
// tree during heuristics instead.)
|
|
297
|
+
const emptyFolders = [];
|
|
298
|
+
return {
|
|
299
|
+
totalFiles,
|
|
300
|
+
totalFolders: allFolders.length,
|
|
301
|
+
totalSize,
|
|
302
|
+
maxDepth,
|
|
303
|
+
avgFileSize,
|
|
304
|
+
medianFileSize,
|
|
305
|
+
extensions,
|
|
306
|
+
topLargestFiles,
|
|
307
|
+
emptyFolders,
|
|
308
|
+
emptyFiles,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/** How many top items to keep in ranked lists. Exposed for tests. */
|
|
312
|
+
export const TOP_N = LIMITS.topN;
|
|
313
|
+
/** Exposed so heuristics.ts can use the same thresholds. */
|
|
314
|
+
export const HEURISTIC_LIMITS = LIMITS;
|
|
315
|
+
export { isBinaryExtension };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Order — structural heuristics.
|
|
3
|
+
*
|
|
4
|
+
* Each heuristic scans the flat file/folder lists (and the tree) and emits
|
|
5
|
+
* {@link Issue} entries. Issues are the actionable findings the final report
|
|
6
|
+
* surfaces to the agent: oversized files, deep nesting, empty folders, etc.
|
|
7
|
+
*
|
|
8
|
+
* Thresholds live in `analyzer.ts` (HEURISTIC_LIMITS) and are intentionally
|
|
9
|
+
* not user-configurable — tree-like structure is always better.
|
|
10
|
+
*/
|
|
11
|
+
import type { FileNode, FolderNode, Issue, ProjectMetrics } from "./types.js";
|
|
12
|
+
/** Detect source files exceeding the oversized threshold. Binary/media files are skipped. */
|
|
13
|
+
export declare function findOversizedFiles(files: FileNode[]): Issue[];
|
|
14
|
+
/**
|
|
15
|
+
* Detect folders whose depth exceeds the nesting threshold.
|
|
16
|
+
* `folderDepths` maps folder path → depth.
|
|
17
|
+
*/
|
|
18
|
+
export declare function findDeepNesting(folderDepths: Map<string, number>): Issue[];
|
|
19
|
+
/** Detect folders that contain no files (directly or in any descendant). */
|
|
20
|
+
export declare function findEmptyFolders(tree: FolderNode): Issue[];
|
|
21
|
+
/** Detect zero-byte files. */
|
|
22
|
+
export declare function findEmptyFiles(files: FileNode[]): Issue[];
|
|
23
|
+
/**
|
|
24
|
+
* Detect folders that contain exactly one child (file or folder).
|
|
25
|
+
* Single-child folders are candidates for flattening into their parent.
|
|
26
|
+
*/
|
|
27
|
+
export declare function findSingleChildFolders(tree: FolderNode): Issue[];
|
|
28
|
+
/**
|
|
29
|
+
* Detect sibling folders with duplicate names within the same parent.
|
|
30
|
+
* Highlights accidental parallel structure (e.g. two `utils/` folders).
|
|
31
|
+
*/
|
|
32
|
+
export declare function findDuplicateFolderNames(tree: FolderNode): Issue[];
|
|
33
|
+
/** Map of folder path → depth, derived from the flat folder list. */
|
|
34
|
+
export declare function computeFolderDepths(allFolders: string[]): Map<string, number>;
|
|
35
|
+
/** Convenience: run every heuristic and return the combined, sorted issue list. */
|
|
36
|
+
export declare function runAllHeuristics(tree: FolderNode, allFiles: FileNode[], allFolders: string[], metrics: ProjectMetrics): Issue[];
|
|
37
|
+
//# sourceMappingURL=heuristics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heuristics.d.ts","sourceRoot":"","sources":["../../../src/extensions/code-order/heuristics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAU9E,6FAA6F;AAC7F,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,CAiB7D;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,KAAK,EAAE,CAa1E;AAED,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,UAAU,GAAG,KAAK,EAAE,CAyB1D;AAED,8BAA8B;AAC9B,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,CAWzD;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,KAAK,EAAE,CAmBhE;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,UAAU,GAAG,KAAK,EAAE,CA2BlE;AAED,qEAAqE;AACrE,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAM7E;AAED,mFAAmF;AACnF,wBAAgB,gBAAgB,CAC/B,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,QAAQ,EAAE,EACpB,UAAU,EAAE,MAAM,EAAE,EACpB,OAAO,EAAE,cAAc,GACrB,KAAK,EAAE,CAoBT"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Order — structural heuristics.
|
|
3
|
+
*
|
|
4
|
+
* Each heuristic scans the flat file/folder lists (and the tree) and emits
|
|
5
|
+
* {@link Issue} entries. Issues are the actionable findings the final report
|
|
6
|
+
* surfaces to the agent: oversized files, deep nesting, empty folders, etc.
|
|
7
|
+
*
|
|
8
|
+
* Thresholds live in `analyzer.ts` (HEURISTIC_LIMITS) and are intentionally
|
|
9
|
+
* not user-configurable — tree-like structure is always better.
|
|
10
|
+
*/
|
|
11
|
+
import { dirname } from "node:path";
|
|
12
|
+
import { HEURISTIC_LIMITS, isBinaryExtension } from "./analyzer.js";
|
|
13
|
+
function formatBytes(n) {
|
|
14
|
+
if (n < 1024)
|
|
15
|
+
return `${n} B`;
|
|
16
|
+
if (n < 1024 * 1024)
|
|
17
|
+
return `${(n / 1024).toFixed(1)} KB`;
|
|
18
|
+
if (n < 1024 * 1024 * 1024)
|
|
19
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
20
|
+
return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
21
|
+
}
|
|
22
|
+
/** Detect source files exceeding the oversized threshold. Binary/media files are skipped. */
|
|
23
|
+
export function findOversizedFiles(files) {
|
|
24
|
+
const threshold = HEURISTIC_LIMITS.largeFileThreshold;
|
|
25
|
+
return files
|
|
26
|
+
.filter((f) => f.size > threshold && !isBinaryExtension(f.extension))
|
|
27
|
+
.sort((a, b) => b.size - a.size)
|
|
28
|
+
.slice(0, HEURISTIC_LIMITS.topN)
|
|
29
|
+
.map((f) => {
|
|
30
|
+
const sev = f.size > threshold * 10 ? "critical" : "warning";
|
|
31
|
+
return {
|
|
32
|
+
type: "oversized_file",
|
|
33
|
+
severity: sev,
|
|
34
|
+
path: f.path,
|
|
35
|
+
message: `File is ${formatBytes(f.size)} (threshold ${formatBytes(threshold)})`,
|
|
36
|
+
suggestion: "Consider splitting into smaller modules or moving large generated content out of source.",
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Detect folders whose depth exceeds the nesting threshold.
|
|
42
|
+
* `folderDepths` maps folder path → depth.
|
|
43
|
+
*/
|
|
44
|
+
export function findDeepNesting(folderDepths) {
|
|
45
|
+
const threshold = HEURISTIC_LIMITS.deepNestingThreshold;
|
|
46
|
+
return [...folderDepths.entries()]
|
|
47
|
+
.filter(([, d]) => d > threshold)
|
|
48
|
+
.sort((a, b) => b[1] - a[1])
|
|
49
|
+
.slice(0, HEURISTIC_LIMITS.topN)
|
|
50
|
+
.map(([path, d]) => ({
|
|
51
|
+
type: "deep_nesting",
|
|
52
|
+
severity: "warning",
|
|
53
|
+
path,
|
|
54
|
+
message: `Directory at depth ${d} (threshold ${threshold})`,
|
|
55
|
+
suggestion: "Consider flattening the hierarchy — deep nesting hurts LLM navigability.",
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
/** Detect folders that contain no files (directly or in any descendant). */
|
|
59
|
+
export function findEmptyFolders(tree) {
|
|
60
|
+
const out = [];
|
|
61
|
+
const visit = (node) => {
|
|
62
|
+
let hasFile = false;
|
|
63
|
+
for (const child of node.children) {
|
|
64
|
+
if (child.isFile) {
|
|
65
|
+
hasFile = true;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const sub = visit(child);
|
|
69
|
+
if (sub.hasFile)
|
|
70
|
+
hasFile = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!hasFile && node.path !== ".") {
|
|
74
|
+
out.push({
|
|
75
|
+
type: "empty_folder",
|
|
76
|
+
severity: "info",
|
|
77
|
+
path: node.path,
|
|
78
|
+
message: "Folder contains no files (recursively)",
|
|
79
|
+
suggestion: "Safe to remove if not intentionally empty for conventions.",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return { hasFile };
|
|
83
|
+
};
|
|
84
|
+
visit(tree);
|
|
85
|
+
return out.slice(0, HEURISTIC_LIMITS.topN);
|
|
86
|
+
}
|
|
87
|
+
/** Detect zero-byte files. */
|
|
88
|
+
export function findEmptyFiles(files) {
|
|
89
|
+
return files
|
|
90
|
+
.filter((f) => f.size === 0)
|
|
91
|
+
.slice(0, HEURISTIC_LIMITS.topN)
|
|
92
|
+
.map((f) => ({
|
|
93
|
+
type: "empty_file",
|
|
94
|
+
severity: "info",
|
|
95
|
+
path: f.path,
|
|
96
|
+
message: "File is empty (0 bytes)",
|
|
97
|
+
suggestion: "Remove if unused, or add a placeholder comment.",
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Detect folders that contain exactly one child (file or folder).
|
|
102
|
+
* Single-child folders are candidates for flattening into their parent.
|
|
103
|
+
*/
|
|
104
|
+
export function findSingleChildFolders(tree) {
|
|
105
|
+
const out = [];
|
|
106
|
+
const visit = (node) => {
|
|
107
|
+
if (node.path !== "." && node.children.length === 1) {
|
|
108
|
+
const only = node.children[0];
|
|
109
|
+
out.push({
|
|
110
|
+
type: "single_child_folder",
|
|
111
|
+
severity: "info",
|
|
112
|
+
path: node.path,
|
|
113
|
+
message: `Folder contains a single child: ${only.name}`,
|
|
114
|
+
suggestion: "Consider merging into the parent to reduce nesting.",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
for (const child of node.children) {
|
|
118
|
+
if (!child.isFile)
|
|
119
|
+
visit(child);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
visit(tree);
|
|
123
|
+
return out.slice(0, HEURISTIC_LIMITS.topN);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Detect sibling folders with duplicate names within the same parent.
|
|
127
|
+
* Highlights accidental parallel structure (e.g. two `utils/` folders).
|
|
128
|
+
*/
|
|
129
|
+
export function findDuplicateFolderNames(tree) {
|
|
130
|
+
const out = [];
|
|
131
|
+
const visit = (node) => {
|
|
132
|
+
const byName = new Map();
|
|
133
|
+
for (const child of node.children) {
|
|
134
|
+
if (child.isFile)
|
|
135
|
+
continue;
|
|
136
|
+
const arr = byName.get(child.name) ?? [];
|
|
137
|
+
arr.push(child);
|
|
138
|
+
byName.set(child.name, arr);
|
|
139
|
+
}
|
|
140
|
+
for (const [, arr] of byName) {
|
|
141
|
+
if (arr.length > 1) {
|
|
142
|
+
out.push({
|
|
143
|
+
type: "duplicate_names",
|
|
144
|
+
severity: "warning",
|
|
145
|
+
path: node.path,
|
|
146
|
+
message: `${arr.length} sibling folders share the name "${arr[0].name}"`,
|
|
147
|
+
suggestion: "Consolidate duplicate sibling folders.",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const child of node.children) {
|
|
152
|
+
if (!child.isFile)
|
|
153
|
+
visit(child);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
visit(tree);
|
|
157
|
+
return out.slice(0, HEURISTIC_LIMITS.topN);
|
|
158
|
+
}
|
|
159
|
+
/** Map of folder path → depth, derived from the flat folder list. */
|
|
160
|
+
export function computeFolderDepths(allFolders) {
|
|
161
|
+
const depths = new Map();
|
|
162
|
+
for (const p of allFolders) {
|
|
163
|
+
depths.set(p, p === "." ? 0 : p.split("/").length);
|
|
164
|
+
}
|
|
165
|
+
return depths;
|
|
166
|
+
}
|
|
167
|
+
/** Convenience: run every heuristic and return the combined, sorted issue list. */
|
|
168
|
+
export function runAllHeuristics(tree, allFiles, allFolders, metrics) {
|
|
169
|
+
const folderDepths = computeFolderDepths(allFolders);
|
|
170
|
+
const issues = [
|
|
171
|
+
...findOversizedFiles(allFiles),
|
|
172
|
+
...findDeepNesting(folderDepths),
|
|
173
|
+
...findEmptyFolders(tree),
|
|
174
|
+
...findEmptyFiles(allFiles),
|
|
175
|
+
...findSingleChildFolders(tree),
|
|
176
|
+
...findDuplicateFolderNames(tree),
|
|
177
|
+
];
|
|
178
|
+
// Severity sort: critical > warning > info, then by path.
|
|
179
|
+
const order = { critical: 0, warning: 1, info: 2 };
|
|
180
|
+
issues.sort((a, b) => order[a.severity] - order[b.severity] || a.path.localeCompare(b.path));
|
|
181
|
+
// Patch emptyFolders into metrics (computed here, not in computeMetrics).
|
|
182
|
+
metrics.emptyFolders = issues
|
|
183
|
+
.filter((i) => i.type === "empty_folder")
|
|
184
|
+
.map((i) => i.path);
|
|
185
|
+
// Suppress unused-import lint for dirname (kept for future path math).
|
|
186
|
+
void dirname;
|
|
187
|
+
return issues;
|
|
188
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Order — native spectral extension.
|
|
3
|
+
*
|
|
4
|
+
* Registers a single tool, `code_order_report`, that scans the current
|
|
5
|
+
* project's directory tree and returns a Markdown cleanup report: oversized
|
|
6
|
+
* files, deep nesting, empty folders/files, duplicate sibling names and
|
|
7
|
+
* single-child folders. Language-agnostic, read-only, zero configuration.
|
|
8
|
+
*
|
|
9
|
+
* The report is input for the agent — it deduces what to split or flatten
|
|
10
|
+
* outside of this extension. Tree-like structure is enforced separately via
|
|
11
|
+
* the system prompt guidelines (see system-prompt.ts).
|
|
12
|
+
*/
|
|
13
|
+
import type { ExtensionAPI } from "../../sdk/coding-agent/index.js";
|
|
14
|
+
export default function codeOrderExtension(ext: ExtensionAPI): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/code-order/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAEX,YAAY,EAEZ,MAAM,iCAAiC,CAAC;AAMzC,wBAA8B,kBAAkB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAgEjF"}
|