@skaleagents/swarm 0.4.0 → 0.6.0
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 +95 -3
- package/dist/application/graph.d.ts +45 -0
- package/dist/application/graph.js +264 -0
- package/dist/application/intake.d.ts +63 -0
- package/dist/application/intake.js +182 -0
- package/dist/application/review.d.ts +71 -0
- package/dist/application/review.js +360 -0
- package/dist/application/tools.d.ts +2 -0
- package/dist/application/tools.js +94 -0
- package/dist/iac/parse.d.ts +25 -0
- package/dist/iac/parse.js +239 -0
- package/dist/iac/rules.d.ts +16 -0
- package/dist/iac/rules.js +450 -0
- package/dist/iac/scan.d.ts +43 -0
- package/dist/iac/scan.js +60 -0
- package/dist/review.d.ts +11 -1
- package/dist/review.js +72 -11
- package/dist/server.js +118 -68
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +6 -4
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type Reference } from "./graph.js";
|
|
2
|
+
import { type Area, type ReviewContext, type SourceFile } from "./intake.js";
|
|
3
|
+
type Finding = {
|
|
4
|
+
ruleId: string;
|
|
5
|
+
area: Area;
|
|
6
|
+
severity: "high" | "medium";
|
|
7
|
+
confidence: "high" | "medium";
|
|
8
|
+
title: string;
|
|
9
|
+
detail: string;
|
|
10
|
+
remediation: string;
|
|
11
|
+
evidence: Reference[];
|
|
12
|
+
importChain?: string[];
|
|
13
|
+
};
|
|
14
|
+
export declare function reviewApplication(inputFiles: SourceFile[], context: ReviewContext): {
|
|
15
|
+
status: string;
|
|
16
|
+
engineVersion: string;
|
|
17
|
+
reviewScope: string;
|
|
18
|
+
verdict: string;
|
|
19
|
+
framework: {
|
|
20
|
+
name: string;
|
|
21
|
+
declaredVersion: string | null;
|
|
22
|
+
appRouter: boolean;
|
|
23
|
+
pagesRouter: boolean;
|
|
24
|
+
};
|
|
25
|
+
filesReviewed: number;
|
|
26
|
+
modulesParsed: number;
|
|
27
|
+
architecture: {
|
|
28
|
+
routes: {
|
|
29
|
+
file: string;
|
|
30
|
+
clientBoundary: boolean;
|
|
31
|
+
}[];
|
|
32
|
+
clientEntries: string[];
|
|
33
|
+
clientReachableModules: string[];
|
|
34
|
+
serverActionModules: string[];
|
|
35
|
+
mutationEntryPoints: string[];
|
|
36
|
+
dataAccessCandidates: string[];
|
|
37
|
+
importEdges: {
|
|
38
|
+
from: string;
|
|
39
|
+
to: string | undefined;
|
|
40
|
+
line: number;
|
|
41
|
+
}[];
|
|
42
|
+
};
|
|
43
|
+
findings: Finding[];
|
|
44
|
+
assessments: {
|
|
45
|
+
area: "reliability" | "purpose" | "routing_rendering" | "module_boundaries" | "data_access" | "authentication" | "testing_delivery" | "deployment_cost";
|
|
46
|
+
status: string;
|
|
47
|
+
evidence: Reference[];
|
|
48
|
+
findingIds: string[];
|
|
49
|
+
reviewTask: string;
|
|
50
|
+
}[];
|
|
51
|
+
rulesEvaluated: string[];
|
|
52
|
+
evidenceGaps: {
|
|
53
|
+
kind: string;
|
|
54
|
+
evidence: Reference[];
|
|
55
|
+
request: string;
|
|
56
|
+
}[];
|
|
57
|
+
nextQuestions: {
|
|
58
|
+
answerKey: string;
|
|
59
|
+
id: keyof ReviewContext;
|
|
60
|
+
area: Area;
|
|
61
|
+
question: string;
|
|
62
|
+
why: string;
|
|
63
|
+
}[];
|
|
64
|
+
remainingQuestions: number;
|
|
65
|
+
answeredTopics: string[];
|
|
66
|
+
contextSource: string;
|
|
67
|
+
nextStep: string;
|
|
68
|
+
instructions: string;
|
|
69
|
+
limitations: string[];
|
|
70
|
+
};
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { builtinModules } from "node:module";
|
|
3
|
+
import { buildGraph, reference } from "./graph.js";
|
|
4
|
+
import { areas, prepareFiles, unanswered, consultationInstructions, } from "./intake.js";
|
|
5
|
+
import { ScanInputError } from "../iac/parse.js";
|
|
6
|
+
import { VERSION } from "../version.js";
|
|
7
|
+
const serverPackages = new Set([
|
|
8
|
+
"server-only",
|
|
9
|
+
"next/headers",
|
|
10
|
+
"next/server",
|
|
11
|
+
"@prisma/client",
|
|
12
|
+
"prisma",
|
|
13
|
+
"pg",
|
|
14
|
+
"mysql2",
|
|
15
|
+
"better-sqlite3",
|
|
16
|
+
"mongodb",
|
|
17
|
+
"mongoose",
|
|
18
|
+
"redis",
|
|
19
|
+
"ioredis",
|
|
20
|
+
"drizzle-orm/node-postgres",
|
|
21
|
+
"drizzle-orm/postgres-js",
|
|
22
|
+
"firebase-admin",
|
|
23
|
+
]);
|
|
24
|
+
const builtins = new Set(builtinModules.map((name) => name.replace(/^node:/, "")));
|
|
25
|
+
const hookNames = new Set([
|
|
26
|
+
"useState",
|
|
27
|
+
"useEffect",
|
|
28
|
+
"useLayoutEffect",
|
|
29
|
+
"useReducer",
|
|
30
|
+
"useContext",
|
|
31
|
+
"useRef",
|
|
32
|
+
"useSyncExternalStore",
|
|
33
|
+
]);
|
|
34
|
+
const appFile = (path) => /^(?:src\/)?app\//.test(path);
|
|
35
|
+
const entryFile = (path) => appFile(path) &&
|
|
36
|
+
/\/(?:page|layout|route|error|global-error|not-found|loading|template)\.[cm]?[jt]sx?$/.test(path);
|
|
37
|
+
const testFile = (path) => /(?:\.(?:test|spec)\.[cm]?[jt]sx?$|(?:^|\/)(?:tests?|e2e|__tests__)\/)/.test(path);
|
|
38
|
+
function serverDependency(name) {
|
|
39
|
+
return (builtins.has(name.replace(/^node:/, "")) ||
|
|
40
|
+
[...serverPackages].some((pkg) => name === pkg || name.startsWith(pkg + "/")));
|
|
41
|
+
}
|
|
42
|
+
function collectServerPaths(modules) {
|
|
43
|
+
const paths = new Set();
|
|
44
|
+
const queue = [...modules.values()]
|
|
45
|
+
.filter((m) => (entryFile(m.path) || m.server) && !m.client)
|
|
46
|
+
.map((m) => m.path);
|
|
47
|
+
while (queue.length) {
|
|
48
|
+
const path = queue.shift();
|
|
49
|
+
if (paths.has(path))
|
|
50
|
+
continue;
|
|
51
|
+
paths.add(path);
|
|
52
|
+
for (const dependency of modules.get(path).imports)
|
|
53
|
+
if (dependency.resolved && !modules.get(dependency.resolved).client)
|
|
54
|
+
queue.push(dependency.resolved);
|
|
55
|
+
}
|
|
56
|
+
return paths;
|
|
57
|
+
}
|
|
58
|
+
function containsJsx(node) {
|
|
59
|
+
if (ts.isJsxElement(node) ||
|
|
60
|
+
ts.isJsxSelfClosingElement(node) ||
|
|
61
|
+
ts.isJsxFragment(node))
|
|
62
|
+
return true;
|
|
63
|
+
return ts.forEachChild(node, containsJsx) ?? false;
|
|
64
|
+
}
|
|
65
|
+
export function reviewApplication(inputFiles, context) {
|
|
66
|
+
const files = prepareFiles(inputFiles);
|
|
67
|
+
const manifestFile = files.find((file) => file.path === "package.json");
|
|
68
|
+
let manifest = {};
|
|
69
|
+
if (manifestFile) {
|
|
70
|
+
try {
|
|
71
|
+
manifest = JSON.parse(manifestFile.content);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new ScanInputError("Cannot parse package.json.");
|
|
75
|
+
}
|
|
76
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
|
|
77
|
+
throw new ScanInputError("package.json must contain an object.");
|
|
78
|
+
}
|
|
79
|
+
const dependencies = {
|
|
80
|
+
...(manifest.dependencies ?? {}),
|
|
81
|
+
...(manifest.devDependencies ?? {}),
|
|
82
|
+
};
|
|
83
|
+
const nextVersion = typeof dependencies.next === "string" ? dependencies.next : null;
|
|
84
|
+
const graph = buildGraph(files);
|
|
85
|
+
const { modules, clientPaths } = graph;
|
|
86
|
+
const isNext = !!nextVersion || [...modules.keys()].some(entryFile);
|
|
87
|
+
const serverPaths = collectServerPaths(modules);
|
|
88
|
+
const findings = [];
|
|
89
|
+
const checked = new Set();
|
|
90
|
+
const add = (ruleId, area, severity, title, detail, remediation, evidence, importChain, confidence = "high") => {
|
|
91
|
+
findings.push({
|
|
92
|
+
ruleId,
|
|
93
|
+
area,
|
|
94
|
+
severity,
|
|
95
|
+
confidence,
|
|
96
|
+
title,
|
|
97
|
+
detail,
|
|
98
|
+
remediation,
|
|
99
|
+
evidence,
|
|
100
|
+
...(importChain ? { importChain } : {}),
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
for (const module of modules.values()) {
|
|
104
|
+
if (testFile(module.path))
|
|
105
|
+
continue;
|
|
106
|
+
const client = clientPaths.has(module.path);
|
|
107
|
+
if (isNext && client) {
|
|
108
|
+
checked.add("NEXT001");
|
|
109
|
+
for (const dependency of module.imports) {
|
|
110
|
+
if (!dependency.resolved && serverDependency(dependency.specifier)) {
|
|
111
|
+
add("NEXT001", "module_boundaries", "high", "Server-only dependency in the client module graph", "A Client Component imports a module that depends on a server-only API or database driver.", "Move this dependency behind a server-only data-access module, Route Handler, or authenticated Server Action. Keep the interactive component's imports browser-compatible.", [dependency.reference], clientPaths.get(module.path));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
checked.add("NEXT002");
|
|
115
|
+
for (const variable of module.env) {
|
|
116
|
+
if (!variable.name.startsWith("NEXT_PUBLIC_") &&
|
|
117
|
+
variable.name !== "NODE_ENV")
|
|
118
|
+
add("NEXT002", "data_access", "medium", "Server environment variable referenced in client code", "A client-reachable module reads an environment variable that Next.js does not normally expose to the browser. This does not establish that its value has leaked.", "Read private configuration on the server and pass only the public data the UI needs. Do not rename a secret with NEXT_PUBLIC_.", [variable.reference], clientPaths.get(module.path));
|
|
119
|
+
}
|
|
120
|
+
checked.add("NEXT003");
|
|
121
|
+
const defaultExport = module.exports.find((e) => e.name === "default" && e.function && e.async);
|
|
122
|
+
if (defaultExport && containsJsx(module.source))
|
|
123
|
+
add("NEXT003", "routing_rendering", "high", "Async Client Component", "An async default component is part of the client module graph.", "Keep the async component on the server and move interactive controls into a synchronous Client Component.", [reference(module, defaultExport.node)], clientPaths.get(module.path));
|
|
124
|
+
}
|
|
125
|
+
if (isNext && appFile(module.path)) {
|
|
126
|
+
checked.add("NEXT004");
|
|
127
|
+
for (const exported of module.exports) {
|
|
128
|
+
if (module.client &&
|
|
129
|
+
["metadata", "generateMetadata"].includes(exported.name))
|
|
130
|
+
add("NEXT004", "routing_rendering", "high", "Metadata exported from a Client Component", "App Router metadata exports belong in Server Components.", "Keep page or layout metadata in a server file and render a separate interactive Client Component beneath it.", [reference(module, exported.node)]);
|
|
131
|
+
if (["getServerSideProps", "getStaticProps", "getStaticPaths"].includes(exported.name))
|
|
132
|
+
add("NEXT005", "routing_rendering", "high", "Pages Router data API used in the App Router", "This App Router module exports a Pages Router-only data function.", "Use App Router server data fetching and generateStaticParams where appropriate, following the installed Next.js version's documentation.", [reference(module, exported.node)]);
|
|
133
|
+
}
|
|
134
|
+
checked.add("NEXT005");
|
|
135
|
+
checked.add("NEXT006");
|
|
136
|
+
if (/\/(?:error|global-error)\.[jt]sx?$/.test(module.path) &&
|
|
137
|
+
!module.client)
|
|
138
|
+
add("NEXT006", "reliability", "high", "Error boundary lacks a client directive", "Next.js error boundaries must be Client Components.", "Add a use client directive to the error boundary and keep its dependencies browser-compatible.", [{ file: module.path, line: 1, column: 1 }]);
|
|
139
|
+
}
|
|
140
|
+
if (isNext && serverPaths.has(module.path)) {
|
|
141
|
+
checked.add("NEXT007");
|
|
142
|
+
for (const dependency of module.imports) {
|
|
143
|
+
if (dependency.specifier === "client-only" ||
|
|
144
|
+
(dependency.specifier === "react" &&
|
|
145
|
+
dependency.symbols.some((name) => hookNames.has(name))))
|
|
146
|
+
add("NEXT007", "module_boundaries", "high", "Client-only module used by a server entry point", "A module reachable from a Server Component imports client-only hooks or the client-only marker without a client boundary.", "Put interactive behavior behind a use client boundary. Keep server fetching and private data outside that module graph.", [dependency.reference]);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (isNext && module.server) {
|
|
150
|
+
checked.add("NEXT008");
|
|
151
|
+
for (const exported of module.exports) {
|
|
152
|
+
const literal = ts.isVariableDeclaration(exported.node) &&
|
|
153
|
+
exported.node.initializer &&
|
|
154
|
+
[
|
|
155
|
+
ts.SyntaxKind.StringLiteral,
|
|
156
|
+
ts.SyntaxKind.NumericLiteral,
|
|
157
|
+
ts.SyntaxKind.TrueKeyword,
|
|
158
|
+
ts.SyntaxKind.FalseKeyword,
|
|
159
|
+
ts.SyntaxKind.ObjectLiteralExpression,
|
|
160
|
+
ts.SyntaxKind.ArrayLiteralExpression,
|
|
161
|
+
].includes(exported.node.initializer.kind);
|
|
162
|
+
if ((exported.function && !exported.async) || literal)
|
|
163
|
+
add("NEXT008", "module_boundaries", "high", "Non-async value exported from a use server module", "A module-level use server directive makes its runtime exports Server Functions; a known export is not an async function.", "Keep constants and synchronous utilities in a separate module. Export only async Server Functions from this module.", [reference(module, exported.node)]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (isNext && module.edge) {
|
|
167
|
+
checked.add("NEXT009");
|
|
168
|
+
const queue = [module.path];
|
|
169
|
+
const visited = new Set();
|
|
170
|
+
while (queue.length) {
|
|
171
|
+
const current = queue.shift();
|
|
172
|
+
if (visited.has(current))
|
|
173
|
+
continue;
|
|
174
|
+
visited.add(current);
|
|
175
|
+
for (const dependency of modules.get(current).imports) {
|
|
176
|
+
if (dependency.resolved)
|
|
177
|
+
queue.push(dependency.resolved);
|
|
178
|
+
else if (builtins.has(dependency.specifier.replace(/^node:/, "")))
|
|
179
|
+
add("NEXT009", "deployment_cost", "high", "Node.js API imported by an Edge runtime entry", "The entry explicitly selects the Edge runtime and imports a Node.js builtin through its dependency graph.", "Use the Node.js runtime or replace the dependency with an Edge-compatible implementation. Verify support against the target deployment platform.", [{ file: module.path, line: 1, column: 1 }, dependency.reference]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Detect cycles without assuming that every legal JavaScript cycle is a defect.
|
|
185
|
+
checked.add("MOD001");
|
|
186
|
+
const visited = new Set();
|
|
187
|
+
const active = new Set();
|
|
188
|
+
const stack = [];
|
|
189
|
+
const cycleKeys = new Set();
|
|
190
|
+
const walk = (path) => {
|
|
191
|
+
if (visited.has(path))
|
|
192
|
+
return;
|
|
193
|
+
visited.add(path);
|
|
194
|
+
active.add(path);
|
|
195
|
+
stack.push(path);
|
|
196
|
+
for (const dependency of modules.get(path).imports) {
|
|
197
|
+
if (!dependency.resolved ||
|
|
198
|
+
(clientPaths.has(path) && modules.get(dependency.resolved).server))
|
|
199
|
+
continue;
|
|
200
|
+
if (active.has(dependency.resolved)) {
|
|
201
|
+
const cycle = [
|
|
202
|
+
...stack.slice(stack.indexOf(dependency.resolved)),
|
|
203
|
+
dependency.resolved,
|
|
204
|
+
];
|
|
205
|
+
const key = [...new Set(cycle)].sort().join("|");
|
|
206
|
+
if (!cycleKeys.has(key)) {
|
|
207
|
+
cycleKeys.add(key);
|
|
208
|
+
add("MOD001", "module_boundaries", "medium", "Circular module dependency needs review", "A runtime import cycle crosses the submitted modules. It may be intentional, but can make initialization and responsibility boundaries harder to reason about.", "Check the cycle's initialization behavior and ownership. Extract a shared contract or invert a dependency if the modules cannot be understood independently.", [dependency.reference], cycle, "medium");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else
|
|
212
|
+
walk(dependency.resolved);
|
|
213
|
+
}
|
|
214
|
+
stack.pop();
|
|
215
|
+
active.delete(path);
|
|
216
|
+
};
|
|
217
|
+
for (const path of modules.keys())
|
|
218
|
+
if (!testFile(path))
|
|
219
|
+
walk(path);
|
|
220
|
+
const pending = unanswered(context);
|
|
221
|
+
const evidenceGaps = [
|
|
222
|
+
...graph.unresolved.map((i) => ({
|
|
223
|
+
kind: "missing_import",
|
|
224
|
+
evidence: [i.reference],
|
|
225
|
+
request: `Include the source for the unresolved local import ${i.specifier}.`,
|
|
226
|
+
})),
|
|
227
|
+
...(!manifestFile
|
|
228
|
+
? [
|
|
229
|
+
{
|
|
230
|
+
kind: "framework_version",
|
|
231
|
+
evidence: [],
|
|
232
|
+
request: "Include package.json to establish the framework version and build/test scripts.",
|
|
233
|
+
},
|
|
234
|
+
]
|
|
235
|
+
: []),
|
|
236
|
+
...(!files.some((f) => testFile(f.path))
|
|
237
|
+
? [
|
|
238
|
+
{
|
|
239
|
+
kind: "tests_not_submitted",
|
|
240
|
+
evidence: [],
|
|
241
|
+
request: "Include tests for a critical flow, or describe the tests maintained in another repository.",
|
|
242
|
+
},
|
|
243
|
+
]
|
|
244
|
+
: []),
|
|
245
|
+
];
|
|
246
|
+
const routeModules = [...modules.values()].filter((m) => entryFile(m.path) || /^(?:src\/)?pages\//.test(m.path));
|
|
247
|
+
const mutationModules = [...modules.values()].filter((m) => m.server ||
|
|
248
|
+
m.exports.some((e) => ["POST", "PUT", "PATCH", "DELETE"].includes(e.name)));
|
|
249
|
+
const dataModules = [...modules.values()].filter((m) => m.imports.some((i) => serverDependency(i.specifier) &&
|
|
250
|
+
!["server-only", "next/headers", "next/server"].includes(i.specifier)) || m.calls.some((c) => c.name === "fetch"));
|
|
251
|
+
const areaEvidence = {
|
|
252
|
+
purpose: [],
|
|
253
|
+
routing_rendering: routeModules.map((m) => ({
|
|
254
|
+
file: m.path,
|
|
255
|
+
line: 1,
|
|
256
|
+
column: 1,
|
|
257
|
+
})),
|
|
258
|
+
module_boundaries: [...modules.values()]
|
|
259
|
+
.flatMap((m) => m.imports.map((i) => i.reference))
|
|
260
|
+
.slice(0, 30),
|
|
261
|
+
data_access: dataModules.map((m) => ({ file: m.path, line: 1, column: 1 })),
|
|
262
|
+
authentication: mutationModules.map((m) => ({
|
|
263
|
+
file: m.path,
|
|
264
|
+
line: 1,
|
|
265
|
+
column: 1,
|
|
266
|
+
})),
|
|
267
|
+
reliability: files
|
|
268
|
+
.filter((f) => /(?:error|loading)\.[jt]sx?$/.test(f.path))
|
|
269
|
+
.map((f) => ({ file: f.path, line: 1, column: 1 })),
|
|
270
|
+
testing_delivery: files
|
|
271
|
+
.filter((f) => testFile(f.path) || f.path.includes(".github/workflows/"))
|
|
272
|
+
.map((f) => ({ file: f.path, line: 1, column: 1 })),
|
|
273
|
+
deployment_cost: files
|
|
274
|
+
.filter((f) => /(?:next\.config\.|Dockerfile|railway\.toml|vercel\.json)/.test(f.path))
|
|
275
|
+
.map((f) => ({ file: f.path, line: 1, column: 1 })),
|
|
276
|
+
};
|
|
277
|
+
const assessments = areas.map((area) => {
|
|
278
|
+
const areaFindings = findings.filter((f) => f.area === area);
|
|
279
|
+
return {
|
|
280
|
+
area,
|
|
281
|
+
status: areaFindings.some((f) => f.confidence === "high" && f.severity === "high")
|
|
282
|
+
? "needs_changes"
|
|
283
|
+
: "needs_contextual_review",
|
|
284
|
+
evidence: areaEvidence[area],
|
|
285
|
+
findingIds: [...new Set(areaFindings.map((f) => f.ruleId))],
|
|
286
|
+
reviewTask: {
|
|
287
|
+
purpose: "Trace the critical flows and judge the design against the product requirements.",
|
|
288
|
+
routing_rendering: "Explain which routes render on the server or client and whether caching, SEO, and freshness match the requirements.",
|
|
289
|
+
module_boundaries: "Check responsibility boundaries and coupling, including unresolved imports and shared contracts.",
|
|
290
|
+
data_access: "Trace reads and writes through the API or data-access layer. Check ownership, validation, DTOs, transactions, and caching.",
|
|
291
|
+
authentication: "Verify identity and object/tenant authorization at every protected read or mutation, including external backend services. Do not infer security from function names.",
|
|
292
|
+
reliability: "Trace dependency failures and timeouts. Evaluate observability, recovery, and rollback against the stated targets.",
|
|
293
|
+
testing_delivery: "Review test assertions for critical flows and inspect actual build/test outcomes. File presence is not a passing result.",
|
|
294
|
+
deployment_cost: "Check runtime and hosting compatibility and compare complexity and cost against team and budget constraints.",
|
|
295
|
+
}[area],
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
findings.sort((a, b) => (a.severity === "high" ? 0 : 1) - (b.severity === "high" ? 0 : 1) ||
|
|
299
|
+
a.evidence[0].file.localeCompare(b.evidence[0].file) ||
|
|
300
|
+
a.evidence[0].line - b.evidence[0].line);
|
|
301
|
+
return {
|
|
302
|
+
status: "reviewed",
|
|
303
|
+
engineVersion: VERSION,
|
|
304
|
+
reviewScope: "whole_application",
|
|
305
|
+
verdict: findings.some((f) => f.severity === "high" && f.confidence === "high")
|
|
306
|
+
? "needs_changes"
|
|
307
|
+
: pending.length || evidenceGaps.length
|
|
308
|
+
? "needs_more_evidence"
|
|
309
|
+
: "ready_for_contextual_assessment",
|
|
310
|
+
framework: {
|
|
311
|
+
name: isNext ? "nextjs" : "javascript_typescript",
|
|
312
|
+
declaredVersion: nextVersion,
|
|
313
|
+
appRouter: [...modules.keys()].some(entryFile),
|
|
314
|
+
pagesRouter: [...modules.keys()].some((path) => /^(?:src\/)?pages\//.test(path)),
|
|
315
|
+
},
|
|
316
|
+
filesReviewed: files.length,
|
|
317
|
+
modulesParsed: modules.size,
|
|
318
|
+
architecture: {
|
|
319
|
+
routes: routeModules.map((m) => ({
|
|
320
|
+
file: m.path,
|
|
321
|
+
clientBoundary: m.client,
|
|
322
|
+
})),
|
|
323
|
+
clientEntries: [...modules.values()]
|
|
324
|
+
.filter((m) => m.client)
|
|
325
|
+
.map((m) => m.path),
|
|
326
|
+
clientReachableModules: [...clientPaths.keys()],
|
|
327
|
+
serverActionModules: [...modules.values()]
|
|
328
|
+
.filter((m) => m.server)
|
|
329
|
+
.map((m) => m.path),
|
|
330
|
+
mutationEntryPoints: mutationModules.map((m) => m.path),
|
|
331
|
+
dataAccessCandidates: dataModules.map((m) => m.path),
|
|
332
|
+
importEdges: [...modules.values()].flatMap((m) => m.imports
|
|
333
|
+
.filter((i) => i.resolved)
|
|
334
|
+
.map((i) => ({
|
|
335
|
+
from: m.path,
|
|
336
|
+
to: i.resolved,
|
|
337
|
+
line: i.reference.line,
|
|
338
|
+
}))),
|
|
339
|
+
},
|
|
340
|
+
findings,
|
|
341
|
+
assessments,
|
|
342
|
+
rulesEvaluated: [...checked].sort(),
|
|
343
|
+
evidenceGaps,
|
|
344
|
+
nextQuestions: pending.slice(0, 3),
|
|
345
|
+
remainingQuestions: pending.length,
|
|
346
|
+
answeredTopics: Object.keys(context).filter((key) => !!context[key]),
|
|
347
|
+
contextSource: "Answers supplied by the caller; not independently verified.",
|
|
348
|
+
nextStep: pending.length
|
|
349
|
+
? "Ask the next questions, inspect requested evidence, and call this tool again with the updated context and relevant files."
|
|
350
|
+
: "Use the evidence and area review tasks to assess end-to-end flows, compare tradeoffs, and produce a prioritized consultant report with file references.",
|
|
351
|
+
instructions: consultationInstructions,
|
|
352
|
+
limitations: [
|
|
353
|
+
"This result combines static syntax/import checks with a contextual review agenda. The connected assistant supplies the architecture reasoning; no separate hosted model is invoked.",
|
|
354
|
+
"Only submitted files are inspected. No repository, URL, dependency source, cloud account, build, or test is fetched or executed by this tool.",
|
|
355
|
+
"Import resolution covers submitted relative modules and root tsconfig/jsconfig paths. Runtime module loading, bundler plugins, external packages, and inherited configs can leave gaps.",
|
|
356
|
+
"Next.js-specific checks target App Router conventions. Pages Router and other frameworks require contextual review; framework-version-specific behavior must be checked against the project's installed documentation.",
|
|
357
|
+
...graph.warnings,
|
|
358
|
+
],
|
|
359
|
+
};
|
|
360
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { requireApiAuth, unauthorizedContent } from "../auth.js";
|
|
3
|
+
import { ScanInputError } from "../iac/parse.js";
|
|
4
|
+
import { contextSchema, fileSchema, planReview, consultationInstructions, } from "./intake.js";
|
|
5
|
+
import { reviewApplication } from "./review.js";
|
|
6
|
+
async function run(remote, compute) {
|
|
7
|
+
if (!remote) {
|
|
8
|
+
const auth = await requireApiAuth();
|
|
9
|
+
if (!auth.ok)
|
|
10
|
+
return unauthorizedContent(auth);
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const output = compute();
|
|
14
|
+
return {
|
|
15
|
+
content: [
|
|
16
|
+
{ type: "text", text: JSON.stringify(output, null, 2) },
|
|
17
|
+
],
|
|
18
|
+
structuredContent: output,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (!(error instanceof ScanInputError))
|
|
23
|
+
throw error;
|
|
24
|
+
return {
|
|
25
|
+
isError: true,
|
|
26
|
+
content: [
|
|
27
|
+
{
|
|
28
|
+
type: "text",
|
|
29
|
+
text: JSON.stringify({
|
|
30
|
+
error: "invalid_input",
|
|
31
|
+
message: error.message,
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function registerConsultation(server, remote) {
|
|
39
|
+
const annotations = {
|
|
40
|
+
readOnlyHint: true,
|
|
41
|
+
destructiveHint: false,
|
|
42
|
+
openWorldHint: false,
|
|
43
|
+
};
|
|
44
|
+
const security = { securitySchemes: [{ type: "oauth2", scopes: ["mcp"] }] };
|
|
45
|
+
server.registerTool("plan_architecture_review", {
|
|
46
|
+
title: "Plan a whole-application architecture review",
|
|
47
|
+
description: "Start an interactive architecture consultation. Supply repository-relative file paths and known project context. Returns prioritized files to read, review areas, and up to three follow-up questions. Covers the whole application, including Next.js apps, not only infrastructure or scaling.",
|
|
48
|
+
annotations,
|
|
49
|
+
_meta: security,
|
|
50
|
+
inputSchema: {
|
|
51
|
+
filePaths: z
|
|
52
|
+
.array(z.string().min(1).max(300))
|
|
53
|
+
.max(3000)
|
|
54
|
+
.describe("Repository file inventory, relative to the application package root; use the client's workspace tools to list files"),
|
|
55
|
+
context: contextSchema,
|
|
56
|
+
},
|
|
57
|
+
}, ({ filePaths, context }) => run(remote, () => planReview(filePaths, context)));
|
|
58
|
+
server.registerTool("review_application_architecture", {
|
|
59
|
+
title: "Review whole-application architecture",
|
|
60
|
+
description: "Review related source files as an application. Builds a JS/TS import graph, checks Next.js server/client and runtime boundaries, maps routes and mutations, and returns evidence-backed findings plus questions covering data, auth, reliability, tests, deployment and product fit. Pass answers in context on subsequent calls. No repository fetch or execution; the connected assistant reasons about system-level tradeoffs using the returned evidence.",
|
|
61
|
+
annotations,
|
|
62
|
+
_meta: security,
|
|
63
|
+
inputSchema: {
|
|
64
|
+
files: z
|
|
65
|
+
.array(fileSchema)
|
|
66
|
+
.min(1)
|
|
67
|
+
.max(80)
|
|
68
|
+
.describe("Related source/config/test files with repository-relative paths; include package.json and tsconfig.json. At most 500,000 combined content characters"),
|
|
69
|
+
context: contextSchema,
|
|
70
|
+
},
|
|
71
|
+
}, ({ files, context }) => run(remote, () => reviewApplication(files, context)));
|
|
72
|
+
server.registerPrompt("architecture_consultation", {
|
|
73
|
+
title: "Independent application architecture consultation",
|
|
74
|
+
description: "Guide a multi-turn whole-application review with source evidence, follow-up questions, and design tradeoffs.",
|
|
75
|
+
argsSchema: {
|
|
76
|
+
goal: z
|
|
77
|
+
.string()
|
|
78
|
+
.max(3000)
|
|
79
|
+
.optional()
|
|
80
|
+
.describe("The architecture question or business goal to assess"),
|
|
81
|
+
},
|
|
82
|
+
}, ({ goal }) => ({
|
|
83
|
+
description: "Evidence-led architecture consultation",
|
|
84
|
+
messages: [
|
|
85
|
+
{
|
|
86
|
+
role: "user",
|
|
87
|
+
content: {
|
|
88
|
+
type: "text",
|
|
89
|
+
text: `${consultationInstructions}\n\nReview goal: ${goal || "Assess the whole application's architecture and explain the highest-priority improvements."}`,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type IacFormat = "terraform" | "cloudformation" | "kubernetes";
|
|
2
|
+
export type Path = (string | number)[];
|
|
3
|
+
export type Location = {
|
|
4
|
+
line: number;
|
|
5
|
+
column: number;
|
|
6
|
+
path: string;
|
|
7
|
+
};
|
|
8
|
+
export type Resource = {
|
|
9
|
+
id: string;
|
|
10
|
+
type: string;
|
|
11
|
+
value: Record<string, unknown>;
|
|
12
|
+
locate: (path?: Path) => Location;
|
|
13
|
+
};
|
|
14
|
+
export type ParsedIac = {
|
|
15
|
+
format: IacFormat;
|
|
16
|
+
resources: Resource[];
|
|
17
|
+
warnings: string[];
|
|
18
|
+
};
|
|
19
|
+
export declare class ScanInputError extends Error {
|
|
20
|
+
constructor(message: string);
|
|
21
|
+
}
|
|
22
|
+
export declare function object(value: unknown): Record<string, unknown>;
|
|
23
|
+
export declare function array(value: unknown): unknown[];
|
|
24
|
+
export declare function looksLikeIac(content: string): boolean;
|
|
25
|
+
export declare function parseIac(content: string, requested?: IacFormat | "auto"): ParsedIac;
|