ast-mcp-server 0.9.2 → 0.11.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/CHANGELOG.md +33 -0
- package/README.md +46 -14
- package/SECURITY.md +1 -1
- package/dist/batch/schema.js +2 -0
- package/dist/cli.js +67 -1
- package/dist/server.js +2 -0
- package/dist/services/agent-setup.js +11 -5
- package/dist/services/agent-targets.js +1 -0
- package/dist/services/call-spines.js +101 -0
- package/dist/services/cli-project-discovery.js +97 -0
- package/dist/services/context-builder.js +83 -24
- package/dist/services/doctor-continuation.js +37 -0
- package/dist/services/doctor-model.js +77 -0
- package/dist/services/doctor.js +111 -0
- package/dist/services/explore-presentation.js +160 -0
- package/dist/services/managed-bundle.js +145 -0
- package/dist/services/managed-file.js +37 -0
- package/dist/services/project.js +103 -24
- package/dist/services/public-errors.js +7 -1
- package/dist/services/relationships.js +104 -0
- package/dist/services/skill-installer.js +74 -56
- package/dist/services/test-candidates.js +15 -8
- package/dist/services/upgrade-command.js +77 -0
- package/dist/services/upgrade-reconcile.js +41 -0
- package/dist/services/upgrade-runtime.js +157 -0
- package/dist/services/upgrade.js +232 -0
- package/dist/tools/explore.js +73 -2
- package/dist/tools/find_test_candidates.js +124 -0
- package/dist/tools/get_impact.js +3 -30
- package/dist/tools/relationship-schema.js +31 -0
- package/docs/support.md +13 -13
- package/package.json +2 -2
- package/skills/structural-code-editing/SKILL.md +30 -158
- package/skills/structural-code-editing/references/operations.md +53 -0
- package/skills/structural-code-editing/releases.json +22 -3
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import { Buffer } from "node:buffer";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import { buildFileOutline, nodeSourceWithLocation } from "./outline.js";
|
|
4
3
|
import { paginate } from "./pagination.js";
|
|
4
|
+
import { resolveImpactRoot } from "./impact.js";
|
|
5
|
+
import { planCallSpines } from "./call-spines.js";
|
|
5
6
|
import { isCooperativeInterruption, NO_REQUEST_CONTEXT, } from "./request-context.js";
|
|
6
7
|
import { findDeclarationByName, getSourceFileOrThrow, reportSymbolIndexFailure, } from "./project.js";
|
|
7
8
|
import { collectSymbolReferences } from "./references.js";
|
|
9
|
+
import { collectCompilerCallRelationships } from "./relationships.js";
|
|
10
|
+
import { presentExploreClusters } from "./explore-presentation.js";
|
|
8
11
|
import { searchProjectSymbolsPage, searchProjectSymbolsPageWithIndex, sourceFileSymbols, symbolMatchRank, } from "./symbols.js";
|
|
9
12
|
export const EXPLORE_DEFAULT_LIMIT = 20;
|
|
10
13
|
export const EXPLORE_DEFAULT_REFERENCE_LIMIT = 20;
|
|
11
14
|
export const EXPLORE_DEFAULT_MAX_BYTES = 64 * 1024;
|
|
12
15
|
export const EXPLORE_MAX_BYTES = 1024 * 1024;
|
|
16
|
+
export const EXPLORE_DEFAULT_OMISSION_DETAIL_LIMIT = 20;
|
|
17
|
+
export const EXPLORE_MAX_OMISSION_DETAIL_LIMIT = 100;
|
|
13
18
|
function matchesQuery(record, query) {
|
|
14
19
|
const normalizedQuery = query.toLowerCase();
|
|
15
20
|
return (record.name.toLowerCase().includes(normalizedQuery) ||
|
|
@@ -130,9 +135,6 @@ function projectSymbol(detail, record) {
|
|
|
130
135
|
signature: record.signature,
|
|
131
136
|
};
|
|
132
137
|
}
|
|
133
|
-
function jsonBytes(value) {
|
|
134
|
-
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
135
|
-
}
|
|
136
138
|
function effectiveExpansion(request) {
|
|
137
139
|
return {
|
|
138
140
|
source: request.includeSource ?? (request.detail === "context" || request.detail === "full"),
|
|
@@ -148,6 +150,27 @@ export async function buildExploreContext(context, request, requestContext = NO_
|
|
|
148
150
|
const expansion = effectiveExpansion(request);
|
|
149
151
|
const unresolved = [];
|
|
150
152
|
const evidence = [];
|
|
153
|
+
let callSpines;
|
|
154
|
+
if (request.callSpines) {
|
|
155
|
+
if (!request.filePath || !request.symbolPath || routed.route !== "symbol") {
|
|
156
|
+
throw new Error("Call spines require an exact file_path and symbol_path.");
|
|
157
|
+
}
|
|
158
|
+
const freshness = {
|
|
159
|
+
state: effectiveContext.status.state,
|
|
160
|
+
causes: effectiveContext.status.causes,
|
|
161
|
+
checked_at: effectiveContext.status.lastSuccessfulSyncAt,
|
|
162
|
+
};
|
|
163
|
+
const root = resolveImpactRoot(effectiveContext.project, effectiveContext.projectRoot, { file_path: request.filePath, symbol_path: request.symbolPath }, requestContext);
|
|
164
|
+
const projected = collectCompilerCallRelationships(effectiveContext.project, effectiveContext.projectRoot, freshness, { max_edges: request.callSpines.maxEdges }, requestContext);
|
|
165
|
+
callSpines = planCallSpines(root, projected.edges, {
|
|
166
|
+
direction: request.callSpines.direction,
|
|
167
|
+
max_depth: request.callSpines.maxDepth,
|
|
168
|
+
max_nodes: request.callSpines.maxNodes,
|
|
169
|
+
max_edges: request.callSpines.maxEdges,
|
|
170
|
+
discovery_complete: !projected.incomplete,
|
|
171
|
+
freshness,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
151
174
|
for (const record of page.items) {
|
|
152
175
|
requestContext.checkpoint();
|
|
153
176
|
let sourceFile;
|
|
@@ -190,30 +213,66 @@ export async function buildExploreContext(context, request, requestContext = NO_
|
|
|
190
213
|
}
|
|
191
214
|
}
|
|
192
215
|
const symbols = page.items.map((record) => projectSymbol(request.detail, record));
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
216
|
+
const initialResult = createResult(effectiveContext, request, routed.route, routed.file, routed.symbol, symbols, evidence, page.total, unresolved, page.has_more ? "record_limit" : null);
|
|
217
|
+
const evidenceBySelector = new Map(evidence.map((item) => [item.selector, item]));
|
|
218
|
+
const omissions = unresolved.map((item) => ({
|
|
219
|
+
subject: item.selector,
|
|
220
|
+
category: "incomplete",
|
|
221
|
+
component: item.reason === "source_unresolved" ? "source" : "references",
|
|
222
|
+
reason: item.reason,
|
|
223
|
+
}));
|
|
224
|
+
for (const item of evidence) {
|
|
225
|
+
if (item.references?.has_more) {
|
|
226
|
+
omissions.push({
|
|
227
|
+
subject: item.selector,
|
|
228
|
+
category: "budget",
|
|
229
|
+
component: "references",
|
|
230
|
+
reason: "reference_limit",
|
|
231
|
+
});
|
|
203
232
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
233
|
+
}
|
|
234
|
+
if (callSpines?.authority_state === "untrusted") {
|
|
235
|
+
omissions.push({
|
|
236
|
+
subject: callSpines.root.selector,
|
|
237
|
+
category: "untrusted",
|
|
238
|
+
component: "call_spine",
|
|
239
|
+
reason: "non_exact_evidence",
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
else if (callSpines?.authority_state === "incomplete") {
|
|
243
|
+
omissions.push({
|
|
244
|
+
subject: callSpines.root.selector,
|
|
245
|
+
category: "incomplete",
|
|
246
|
+
component: "call_spine",
|
|
247
|
+
reason: "call_discovery_incomplete",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (callSpines) {
|
|
251
|
+
for (const reason of callSpines.truncation_reasons) {
|
|
252
|
+
omissions.push({
|
|
253
|
+
subject: callSpines.root.selector,
|
|
254
|
+
category: "budget",
|
|
255
|
+
component: "call_spine",
|
|
256
|
+
reason,
|
|
257
|
+
});
|
|
208
258
|
}
|
|
209
|
-
result = createResult(effectiveContext, request, routed.route, routed.file, routed.symbol, returnedSymbols, returnedEvidence, page.total, unresolved, reason);
|
|
210
259
|
}
|
|
211
260
|
requestContext.checkpoint();
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
261
|
+
return presentExploreClusters({
|
|
262
|
+
base: initialResult,
|
|
263
|
+
clusters: symbols.map((symbol) => ({
|
|
264
|
+
symbol,
|
|
265
|
+
evidence: evidenceBySelector.get(symbol.selector),
|
|
266
|
+
})),
|
|
267
|
+
offset: request.offset,
|
|
268
|
+
limit: request.limit,
|
|
269
|
+
total: page.total,
|
|
270
|
+
maxBytes: request.maxBytes,
|
|
271
|
+
omissions,
|
|
272
|
+
omissionDetailLimit: request.omissionDetailLimit ?? EXPLORE_DEFAULT_OMISSION_DETAIL_LIMIT,
|
|
273
|
+
callSpines,
|
|
274
|
+
unresolved,
|
|
275
|
+
});
|
|
217
276
|
}
|
|
218
277
|
function createResult(context, request, route, file, symbol, symbols, evidence, total, unresolved, reason) {
|
|
219
278
|
const hasMore = request.offset + symbols.length < total;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
function shellQuote(value) {
|
|
4
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
5
|
+
}
|
|
6
|
+
function relativeArgument(project, cwd) {
|
|
7
|
+
const requested = project ?? ".";
|
|
8
|
+
if (/[\0\r\n]/u.test(requested))
|
|
9
|
+
return ".";
|
|
10
|
+
const relative = path.relative(cwd, path.resolve(cwd, requested)).split(path.sep).join("/") || ".";
|
|
11
|
+
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
12
|
+
}
|
|
13
|
+
function ambiguousArgument(project, cwd) {
|
|
14
|
+
let directory = path.resolve(cwd, project ?? ".");
|
|
15
|
+
while (true) {
|
|
16
|
+
const candidate = path.join(directory, "tsconfig.json");
|
|
17
|
+
if (existsSync(candidate) && existsSync(path.join(directory, "jsconfig.json")))
|
|
18
|
+
return relativeArgument(candidate, cwd);
|
|
19
|
+
if (existsSync(path.join(directory, ".git")))
|
|
20
|
+
break;
|
|
21
|
+
const parent = path.dirname(directory);
|
|
22
|
+
if (parent === directory)
|
|
23
|
+
break;
|
|
24
|
+
directory = parent;
|
|
25
|
+
}
|
|
26
|
+
return relativeArgument(path.join(project ?? ".", "tsconfig.json"), cwd);
|
|
27
|
+
}
|
|
28
|
+
export function buildDoctorConfigContinuation(state, discoveryContinuation, project, cwd) {
|
|
29
|
+
void discoveryContinuation;
|
|
30
|
+
const requested = state === "ambiguous" ? ambiguousArgument(project, cwd) : relativeArgument(project, cwd);
|
|
31
|
+
const command = `ast-tool doctor --project ${shellQuote(requested)}`;
|
|
32
|
+
if (state === "ambiguous")
|
|
33
|
+
return `Run: ${command}`;
|
|
34
|
+
return state === "unsafe"
|
|
35
|
+
? `Replace the requested project location with a direct config, then run: ${command}`
|
|
36
|
+
: `Create the requested project location and a supported config, then run: ${command}`;
|
|
37
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { sanitizePublicText } from "./public-errors.js";
|
|
2
|
+
function item(code, status, summary, reasonCode, continuation, continuationTrusted = false) {
|
|
3
|
+
return {
|
|
4
|
+
code,
|
|
5
|
+
status,
|
|
6
|
+
summary: sanitizePublicText(summary),
|
|
7
|
+
reason_code: reasonCode,
|
|
8
|
+
...(continuation
|
|
9
|
+
? { continuation: continuationTrusted ? continuation : sanitizePublicText(continuation) }
|
|
10
|
+
: {}),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function overall(checks) {
|
|
14
|
+
if (checks.some((check) => check.status === "failed"))
|
|
15
|
+
return "failed";
|
|
16
|
+
return checks.some((check) => check.status === "degraded") ? "degraded" : "healthy";
|
|
17
|
+
}
|
|
18
|
+
export function evaluateDoctor(evidence) {
|
|
19
|
+
const checks = [];
|
|
20
|
+
checks.push(evidence.runtime_invalid
|
|
21
|
+
? item("runtime_policy", "degraded", "Runtime limits contain invalid values and defaults were applied.", "invalid_runtime_policy", "Fix AST runtime limit values, then run: ast-tool doctor")
|
|
22
|
+
: item("runtime_policy", "healthy", "Runtime limits are admitted.", "runtime_policy_ready"));
|
|
23
|
+
checks.push(evidence.config.state === "ready"
|
|
24
|
+
? item("project_config", "healthy", "A unique project config was resolved.", "config_ready")
|
|
25
|
+
: item("project_config", "failed", "Project config discovery did not produce a safe unique identity.", `config_${evidence.config.state}`, evidence.config.continuation, evidence.config.continuation_trusted));
|
|
26
|
+
if (evidence.project) {
|
|
27
|
+
const { compiler, watcher, index, operation_queue: queue } = evidence.project;
|
|
28
|
+
checks.push(compiler.state === "ready"
|
|
29
|
+
? item("compiler", "healthy", "Compiler synchronization is ready.", "compiler_ready")
|
|
30
|
+
: item("compiler", compiler.state === "failed" ? "failed" : "degraded", "Compiler synchronization is not ready.", `compiler_${compiler.state}`, "Run: ast-tool doctor"));
|
|
31
|
+
checks.push(watcher.state === "ready"
|
|
32
|
+
? item("watcher", "healthy", "Project watcher is ready.", "watcher_ready")
|
|
33
|
+
: item("watcher", "degraded", "Project watcher is unavailable; compiler reads may require resynchronization.", `watcher_${watcher.state}`, "Restart MCP clients, then run: ast-tool doctor"));
|
|
34
|
+
checks.push(evidence.project_authority === "isolated" && index.state === "disabled"
|
|
35
|
+
? item("derived_index", "not_run", "Persistent derived-index inspection is disabled for an isolated diagnosis.", "index_not_run_diagnostic_policy")
|
|
36
|
+
: index.state === "ready" || index.state === "disabled"
|
|
37
|
+
? item("derived_index", "healthy", index.state === "ready"
|
|
38
|
+
? "Derived symbol index is ready."
|
|
39
|
+
: "Derived symbol index is disabled by admitted policy.", `index_${index.state}`)
|
|
40
|
+
: item("derived_index", "degraded", compiler.state === "ready"
|
|
41
|
+
? "Derived index is unavailable; compiler-backed file operations remain available."
|
|
42
|
+
: "Derived index is unavailable while compiler synchronization is not ready.", compiler.state === "ready" ? "index_degraded_compiler_ready" : `index_${index.state}`, "Run: ast-tool cache inspect"));
|
|
43
|
+
const queueDegraded = evidence.runtime_admission !== "open" ||
|
|
44
|
+
queue.admission !== "open" ||
|
|
45
|
+
queue.rejected_operations > 0 ||
|
|
46
|
+
queue.queue_timeout_operations > 0 ||
|
|
47
|
+
queue.deadline_exceeded_operations > 0;
|
|
48
|
+
checks.push(evidence.project_authority === "isolated" && evidence.runtime_admission === "open"
|
|
49
|
+
? item("operation_queue", "not_run", "No registered project queue exists to inspect.", "queue_not_run_no_registered_session")
|
|
50
|
+
: queueDegraded
|
|
51
|
+
? item("operation_queue", "degraded", "Operation queue reports admission or timeout pressure.", evidence.runtime_admission === "closed"
|
|
52
|
+
? "runtime_admission_closed"
|
|
53
|
+
: "queue_pressure", "Run: ast-tool doctor")
|
|
54
|
+
: item("operation_queue", "healthy", "Operation queue is accepting work.", "queue_ready"));
|
|
55
|
+
}
|
|
56
|
+
else if (evidence.config.state === "ready" && evidence.project_authority === "unavailable") {
|
|
57
|
+
checks.push(item("compiler", "not_run", "Compiler diagnosis is unavailable while runtime admission is closed.", "compiler_not_run_runtime_closed"), item("watcher", "not_run", "Watcher check requires compiler diagnosis.", "watcher_not_run"), item("derived_index", "not_run", "Derived index check requires compiler diagnosis.", "index_not_run"), item("operation_queue", "degraded", "Project runtime admission is closed.", "runtime_admission_closed", "Restart MCP clients, then run: ast-tool doctor"));
|
|
58
|
+
}
|
|
59
|
+
else if (evidence.config.state === "ready") {
|
|
60
|
+
checks.push(item("compiler", "failed", "Compiler creation failed.", "compiler_creation_failed", "Run: ast-tool doctor"), item("watcher", "not_run", "Watcher check requires a ready compiler.", "watcher_not_run"), item("derived_index", "not_run", "Derived index check requires a ready compiler.", "index_not_run"), item("operation_queue", "not_run", "Queue check requires a diagnostic project.", "queue_not_run"));
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
checks.push(item("compiler", "not_run", "Compiler check requires a project config.", "compiler_not_run"), item("watcher", "not_run", "Watcher check requires a project config.", "watcher_not_run"), item("derived_index", "not_run", "Derived index check requires a project config.", "index_not_run"), item("operation_queue", "not_run", "Queue check requires a project config.", "queue_not_run"));
|
|
64
|
+
}
|
|
65
|
+
checks.push(evidence.cache.state === "failed" || evidence.cache.unsafe_entry_count > 0
|
|
66
|
+
? item("index_cache", "degraded", "Index cache inspection found unavailable or unsafe state.", evidence.cache.unsafe_entry_count > 0 ? "cache_unsafe" : "cache_failed", "Run: ast-tool cache inspect")
|
|
67
|
+
: item("index_cache", "healthy", `Index cache is ${evidence.cache.state}.`, `cache_${evidence.cache.state}`));
|
|
68
|
+
checks.push(evidence.package.state === "ready"
|
|
69
|
+
? item("package", "healthy", `Package version ${evidence.package.version ?? "unknown"} is locally admitted.`, "package_ready")
|
|
70
|
+
: item("package", "degraded", "Running package provenance could not be admitted locally.", `package_${evidence.package.state}`, "Run: ast-tool upgrade --check"));
|
|
71
|
+
checks.push(evidence.setup.state === "ready"
|
|
72
|
+
? item("client_setup", "healthy", "Detected client skills are current.", "setup_ready")
|
|
73
|
+
: item("client_setup", "degraded", "Detected client skill setup requires attention.", `setup_${evidence.setup.state}`, evidence.setup.state === "conflict"
|
|
74
|
+
? "Run: ast-tool setup --agents all --yes --force-skill"
|
|
75
|
+
: "Run: ast-tool setup --agents all --yes"));
|
|
76
|
+
return { schema_version: 1, status: overall(checks), checks: checks.slice(0, 9) };
|
|
77
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { detectInstalledAgents } from "./agent-setup.js";
|
|
4
|
+
import { resolveCliBatchProject, CliProjectDiscoveryError } from "./cli-project-discovery.js";
|
|
5
|
+
import { buildDoctorConfigContinuation } from "./doctor-continuation.js";
|
|
6
|
+
import { evaluateDoctor } from "./doctor-model.js";
|
|
7
|
+
import { getDiagnosticProjectStatus } from "./project.js";
|
|
8
|
+
import { readRuntimePolicy } from "./runtime-policy.js";
|
|
9
|
+
import { planBundledSkillInstallation, resolveBundledSkillAssets, SkillConflictError, } from "./skill-installer.js";
|
|
10
|
+
import { inspectSymbolIndexCache, } from "./symbol-index-cache.js";
|
|
11
|
+
async function resolveProject(project, cwd) {
|
|
12
|
+
const resolved = (await resolveCliBatchProject({ ...(project ? { project_root: project } : {}) }, cwd));
|
|
13
|
+
return resolved.project_root;
|
|
14
|
+
}
|
|
15
|
+
async function inspectPackage(executable) {
|
|
16
|
+
const cli = await realpath(executable);
|
|
17
|
+
const root = path.resolve(path.dirname(cli), "..");
|
|
18
|
+
const metadata = JSON.parse(await readFile(path.join(root, "package.json"), "utf8"));
|
|
19
|
+
const bins = metadata.bin;
|
|
20
|
+
if (metadata.name !== "ast-mcp-server" ||
|
|
21
|
+
typeof metadata.version !== "string" ||
|
|
22
|
+
typeof bins?.["ast-tool"] !== "string" ||
|
|
23
|
+
(await realpath(path.resolve(root, bins["ast-tool"]))) !== cli)
|
|
24
|
+
return { state: "unsupported" };
|
|
25
|
+
return { state: "ready", version: metadata.version };
|
|
26
|
+
}
|
|
27
|
+
async function inspectSetup(executable, cwd, environment) {
|
|
28
|
+
const detections = await detectInstalledAgents({ environment, commandTimeoutMs: 5000 });
|
|
29
|
+
const installed = detections.filter((agent) => agent.installed);
|
|
30
|
+
if (installed.length === 0)
|
|
31
|
+
return { state: "ready" };
|
|
32
|
+
if (installed.some((agent) => agent.compatibility?.status !== "compatible"))
|
|
33
|
+
return { state: "outdated" };
|
|
34
|
+
const assets = await resolveBundledSkillAssets(executable);
|
|
35
|
+
try {
|
|
36
|
+
const plan = await planBundledSkillInstallation({
|
|
37
|
+
target: installed.map((agent) => agent.id),
|
|
38
|
+
scope: "user",
|
|
39
|
+
sourceSkillPath: assets.skillPath,
|
|
40
|
+
releaseManifestPath: assets.releasesPath,
|
|
41
|
+
environment,
|
|
42
|
+
...(environment.HOME ? { homeDirectory: environment.HOME } : {}),
|
|
43
|
+
workingDirectory: cwd,
|
|
44
|
+
});
|
|
45
|
+
if (plan.installations.every((item) => item.status === "unchanged"))
|
|
46
|
+
return { state: "ready" };
|
|
47
|
+
return {
|
|
48
|
+
state: plan.installations.some((item) => item.status === "updated") ? "outdated" : "missing",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error instanceof SkillConflictError)
|
|
53
|
+
return { state: "conflict" };
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const defaults = {
|
|
58
|
+
resolveProject,
|
|
59
|
+
projectStatus: getDiagnosticProjectStatus,
|
|
60
|
+
cache: (environment) => inspectSymbolIndexCache({ environment }),
|
|
61
|
+
package: inspectPackage,
|
|
62
|
+
setup: inspectSetup,
|
|
63
|
+
runtime: readRuntimePolicy,
|
|
64
|
+
};
|
|
65
|
+
export async function runDoctor(options, overrides = {}) {
|
|
66
|
+
const dependencies = { ...defaults, ...overrides };
|
|
67
|
+
const environment = options.environment ?? process.env;
|
|
68
|
+
const policy = dependencies.runtime(environment);
|
|
69
|
+
const runtimeInvalid = Object.values(policy.reasons).some((reason) => reason === "invalid_integer" || reason === "out_of_range");
|
|
70
|
+
let config = { state: "ready" };
|
|
71
|
+
let projectRoot;
|
|
72
|
+
try {
|
|
73
|
+
projectRoot = await dependencies.resolveProject(options.project, options.cwd);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
const discovery = error instanceof CliProjectDiscoveryError ? error : undefined;
|
|
77
|
+
const state = discovery?.code.endsWith("AMBIGUOUS")
|
|
78
|
+
? "ambiguous"
|
|
79
|
+
: discovery?.code.endsWith("UNSAFE")
|
|
80
|
+
? "unsafe"
|
|
81
|
+
: "missing";
|
|
82
|
+
config = {
|
|
83
|
+
state,
|
|
84
|
+
continuation: buildDoctorConfigContinuation(state, discovery?.continuation, options.project, options.cwd),
|
|
85
|
+
continuation_trusted: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
let diagnostic;
|
|
89
|
+
if (projectRoot)
|
|
90
|
+
diagnostic = await dependencies.projectStatus(projectRoot).catch(() => undefined);
|
|
91
|
+
const [cache, packageEvidence, setup] = await Promise.all([
|
|
92
|
+
dependencies
|
|
93
|
+
.cache(environment)
|
|
94
|
+
.then((result) => ({ state: result.state, unsafe_entry_count: result.unsafe_entry_count }))
|
|
95
|
+
.catch(() => ({ state: "failed", unsafe_entry_count: 0 })),
|
|
96
|
+
dependencies.package(options.executable).catch(() => ({ state: "failed" })),
|
|
97
|
+
dependencies
|
|
98
|
+
.setup(options.executable, options.cwd, environment)
|
|
99
|
+
.catch(() => ({ state: "failed" })),
|
|
100
|
+
]);
|
|
101
|
+
return evaluateDoctor({
|
|
102
|
+
runtime_invalid: runtimeInvalid,
|
|
103
|
+
project_authority: diagnostic?.authority ?? (projectRoot ? "failed" : "unavailable"),
|
|
104
|
+
runtime_admission: diagnostic?.runtime_admission ?? "open",
|
|
105
|
+
config,
|
|
106
|
+
...(diagnostic?.status ? { project: diagnostic.status } : {}),
|
|
107
|
+
cache,
|
|
108
|
+
package: packageEvidence,
|
|
109
|
+
setup,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
export const EXPLORE_OMISSION_REASONS = [
|
|
3
|
+
"byte_limit",
|
|
4
|
+
"reference_limit",
|
|
5
|
+
"source_unresolved",
|
|
6
|
+
"references_unresolved",
|
|
7
|
+
"non_exact_evidence",
|
|
8
|
+
"call_discovery_incomplete",
|
|
9
|
+
"depth_limit",
|
|
10
|
+
"node_limit",
|
|
11
|
+
"edge_limit",
|
|
12
|
+
];
|
|
13
|
+
function jsonBytes(value) {
|
|
14
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
15
|
+
}
|
|
16
|
+
function summarize(omissions, detailLimit) {
|
|
17
|
+
const details = [...omissions].sort((left, right) => [left.subject, left.category, left.component, left.reason]
|
|
18
|
+
.join("\0")
|
|
19
|
+
.localeCompare([right.subject, right.category, right.component, right.reason].join("\0")));
|
|
20
|
+
const countMap = new Map();
|
|
21
|
+
for (const item of omissions) {
|
|
22
|
+
const key = `${item.category}\0${item.component}`;
|
|
23
|
+
countMap.set(key, (countMap.get(key) ?? 0) + 1);
|
|
24
|
+
}
|
|
25
|
+
const counts = [...countMap]
|
|
26
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
27
|
+
.map(([key, count]) => {
|
|
28
|
+
const [category, component] = key.split("\0");
|
|
29
|
+
return { category, component, count };
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
counts,
|
|
33
|
+
details: details.slice(0, detailLimit),
|
|
34
|
+
total: omissions.length,
|
|
35
|
+
has_more: details.length > detailLimit,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function selectorOnly(symbol) {
|
|
39
|
+
return { file: symbol.file, selector: symbol.selector, kind: symbol.kind };
|
|
40
|
+
}
|
|
41
|
+
function omittedClusterParts(symbol, evidence) {
|
|
42
|
+
const component = (value) => ({
|
|
43
|
+
subject: symbol.selector,
|
|
44
|
+
category: "budget",
|
|
45
|
+
component: value,
|
|
46
|
+
reason: "byte_limit",
|
|
47
|
+
});
|
|
48
|
+
return [
|
|
49
|
+
...(symbol.signature === undefined ? [] : [component("signature")]),
|
|
50
|
+
...(evidence?.source === undefined ? [] : [component("source")]),
|
|
51
|
+
...(evidence?.references === undefined ? [] : [component("references")]),
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
export function presentExploreClusters(input) {
|
|
55
|
+
const symbols = [];
|
|
56
|
+
const evidence = [];
|
|
57
|
+
let omissions = [...input.omissions];
|
|
58
|
+
let consumed = 0;
|
|
59
|
+
let byteLimited = false;
|
|
60
|
+
const build = (detailLimit, includeCallSpines) => {
|
|
61
|
+
const callSpinesOmitted = input.callSpines !== undefined && !includeCallSpines;
|
|
62
|
+
const effectiveOmissions = callSpinesOmitted
|
|
63
|
+
? [
|
|
64
|
+
...omissions,
|
|
65
|
+
{
|
|
66
|
+
subject: input.callSpines.root.selector,
|
|
67
|
+
category: "budget",
|
|
68
|
+
component: "call_spine",
|
|
69
|
+
reason: "byte_limit",
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
: omissions;
|
|
73
|
+
const spinesComplete = input.callSpines === undefined
|
|
74
|
+
? undefined
|
|
75
|
+
: includeCallSpines && !input.callSpines.incomplete;
|
|
76
|
+
const hasMore = input.offset + consumed < input.total;
|
|
77
|
+
const evidenceComplete = effectiveOmissions.length === 0 &&
|
|
78
|
+
(input.unresolved?.length ?? 0) === 0 &&
|
|
79
|
+
spinesComplete !== false;
|
|
80
|
+
const reason = byteLimited || callSpinesOmitted ? "byte_limit" : hasMore ? "record_limit" : null;
|
|
81
|
+
const result = {
|
|
82
|
+
...input.base,
|
|
83
|
+
...(includeCallSpines && input.callSpines ? { call_spines: input.callSpines } : {}),
|
|
84
|
+
symbols,
|
|
85
|
+
evidence,
|
|
86
|
+
offset: input.offset,
|
|
87
|
+
limit: input.limit,
|
|
88
|
+
total: input.total,
|
|
89
|
+
has_more: hasMore,
|
|
90
|
+
next_offset: hasMore ? input.offset + consumed : null,
|
|
91
|
+
omissions: summarize(effectiveOmissions, detailLimit),
|
|
92
|
+
completeness: {
|
|
93
|
+
complete: !hasMore && evidenceComplete,
|
|
94
|
+
symbols_complete: !hasMore,
|
|
95
|
+
evidence_complete: evidenceComplete,
|
|
96
|
+
...(spinesComplete === undefined ? {} : { spines_complete: spinesComplete }),
|
|
97
|
+
unresolved: input.unresolved ?? [],
|
|
98
|
+
},
|
|
99
|
+
budget: {
|
|
100
|
+
max_records: input.limit,
|
|
101
|
+
max_bytes: input.maxBytes,
|
|
102
|
+
max_depth: null,
|
|
103
|
+
max_edges: null,
|
|
104
|
+
max_invocations: 1,
|
|
105
|
+
used_bytes: 0,
|
|
106
|
+
},
|
|
107
|
+
truncation: { truncated: reason !== null, reason },
|
|
108
|
+
};
|
|
109
|
+
let usedBytes = 0;
|
|
110
|
+
let fixed = result;
|
|
111
|
+
for (let index = 0; index < 10; index += 1) {
|
|
112
|
+
fixed = { ...result, budget: { ...result.budget, used_bytes: usedBytes } };
|
|
113
|
+
const next = jsonBytes(fixed);
|
|
114
|
+
if (next === usedBytes)
|
|
115
|
+
return fixed;
|
|
116
|
+
usedBytes = next;
|
|
117
|
+
}
|
|
118
|
+
return { ...fixed, budget: { ...fixed.budget, used_bytes: jsonBytes(fixed) } };
|
|
119
|
+
};
|
|
120
|
+
const fittingResult = () => {
|
|
121
|
+
for (let detailLimit = input.omissionDetailLimit; detailLimit >= 0; detailLimit -= 1) {
|
|
122
|
+
if (input.callSpines) {
|
|
123
|
+
const included = build(detailLimit, true);
|
|
124
|
+
if (included.budget.used_bytes <= input.maxBytes)
|
|
125
|
+
return included;
|
|
126
|
+
}
|
|
127
|
+
const omitted = build(detailLimit, false);
|
|
128
|
+
if (omitted.budget.used_bytes <= input.maxBytes)
|
|
129
|
+
return omitted;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
};
|
|
133
|
+
for (const cluster of input.clusters) {
|
|
134
|
+
const omissionsBeforeCluster = omissions;
|
|
135
|
+
symbols.push(cluster.symbol);
|
|
136
|
+
if (cluster.evidence)
|
|
137
|
+
evidence.push(cluster.evidence);
|
|
138
|
+
consumed += 1;
|
|
139
|
+
if (fittingResult())
|
|
140
|
+
continue;
|
|
141
|
+
symbols[symbols.length - 1] = selectorOnly(cluster.symbol);
|
|
142
|
+
if (cluster.evidence)
|
|
143
|
+
evidence.pop();
|
|
144
|
+
omissions = [...omissions, ...omittedClusterParts(cluster.symbol, cluster.evidence)];
|
|
145
|
+
byteLimited = true;
|
|
146
|
+
if (fittingResult())
|
|
147
|
+
continue;
|
|
148
|
+
if (symbols.length > 1) {
|
|
149
|
+
symbols.pop();
|
|
150
|
+
consumed -= 1;
|
|
151
|
+
omissions = omissionsBeforeCluster;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
throw new Error(`max_bytes cannot fit selector ${cluster.symbol.selector}.`);
|
|
155
|
+
}
|
|
156
|
+
const result = fittingResult();
|
|
157
|
+
if (!result)
|
|
158
|
+
throw new Error("max_bytes cannot fit the ast_explore result shell.");
|
|
159
|
+
return result;
|
|
160
|
+
}
|