ast-mcp-server 0.6.0 → 0.7.1
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 +38 -0
- package/README.md +47 -15
- package/SECURITY.md +31 -0
- package/dist/cli.js +0 -0
- package/dist/index.js +129 -10
- package/dist/server.js +21 -2
- package/dist/services/agent-setup.js +36 -117
- package/dist/services/agent-targets.js +122 -0
- package/dist/services/context-builder.js +52 -24
- package/dist/services/diagnostics.js +10 -5
- package/dist/services/file-fingerprints.js +104 -0
- package/dist/services/file-snapshot.js +8 -1
- package/dist/services/impact.js +334 -0
- package/dist/services/operations.js +343 -254
- package/dist/services/outline.js +94 -21
- package/dist/services/pagination.js +10 -4
- package/dist/services/project-operation-scheduler.js +475 -0
- package/dist/services/project-status.js +134 -52
- package/dist/services/project-watcher.js +230 -0
- package/dist/services/project.js +646 -66
- package/dist/services/public-errors.js +255 -0
- package/dist/services/references.js +7 -1
- package/dist/services/relationships.js +1198 -0
- package/dist/services/request-context.js +42 -0
- package/dist/services/runtime-activity.js +65 -0
- package/dist/services/runtime-logger.js +50 -0
- package/dist/services/runtime-policy.js +47 -0
- package/dist/services/setup-wizard.js +4 -7
- package/dist/services/shutdown.js +93 -0
- package/dist/services/symbol-index-limits.js +5 -0
- package/dist/services/symbol-index-order.js +13 -0
- package/dist/services/symbol-index-policy.js +91 -0
- package/dist/services/symbol-index-sqlite.js +850 -0
- package/dist/services/symbol-index.js +447 -0
- package/dist/services/symbols.js +403 -79
- package/dist/services/test-candidates.js +209 -0
- package/dist/services/workspace.js +18 -7
- package/dist/tools/apply_operation.js +8 -4
- package/dist/tools/explore.js +8 -6
- package/dist/tools/find_references.js +12 -13
- package/dist/tools/get_diagnostics.js +10 -7
- package/dist/tools/get_file.js +23 -11
- package/dist/tools/get_impact.js +134 -0
- package/dist/tools/get_operation_preview.js +7 -4
- package/dist/tools/get_outline.js +8 -6
- package/dist/tools/get_project_status.js +77 -8
- package/dist/tools/get_symbol_source.js +8 -6
- package/dist/tools/list_files.js +11 -6
- package/dist/tools/rename_symbol.js +6 -4
- package/dist/tools/replace_symbol_body.js +6 -4
- package/dist/tools/result.js +27 -17
- package/dist/tools/scaffold_class.js +6 -4
- package/dist/tools/search_symbols.js +15 -14
- package/docs/support.md +80 -0
- package/package.json +16 -5
- package/skills/structural-code-editing/SKILL.md +37 -10
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export class RequestContextError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(code) {
|
|
4
|
+
super(code === "REQUEST_CANCELLED" ? "Request cancelled." : "Operation deadline exceeded.");
|
|
5
|
+
this.name = "RequestContextError";
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function isCooperativeInterruption(error) {
|
|
10
|
+
const code = error?.code;
|
|
11
|
+
return code === "REQUEST_CANCELLED" || code === "OPERATION_DEADLINE_EXCEEDED";
|
|
12
|
+
}
|
|
13
|
+
const neverAbortedSignal = new AbortController().signal;
|
|
14
|
+
export const NO_REQUEST_CONTEXT = Object.freeze({
|
|
15
|
+
signal: neverAbortedSignal,
|
|
16
|
+
checkpoint() { },
|
|
17
|
+
enterCompletionCritical() { },
|
|
18
|
+
});
|
|
19
|
+
class ActiveRequestContext {
|
|
20
|
+
#completionCritical = false;
|
|
21
|
+
#requestSignal;
|
|
22
|
+
constructor(requestSignal) {
|
|
23
|
+
this.#requestSignal = requestSignal;
|
|
24
|
+
}
|
|
25
|
+
get signal() {
|
|
26
|
+
return this.#completionCritical ? neverAbortedSignal : this.#requestSignal;
|
|
27
|
+
}
|
|
28
|
+
checkpoint() {
|
|
29
|
+
if (!this.#completionCritical && this.#requestSignal.aborted) {
|
|
30
|
+
throw new RequestContextError("REQUEST_CANCELLED");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
enterCompletionCritical() {
|
|
34
|
+
if (this.#completionCritical)
|
|
35
|
+
return;
|
|
36
|
+
this.checkpoint();
|
|
37
|
+
this.#completionCritical = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function createRequestContext(signal) {
|
|
41
|
+
return signal === undefined ? NO_REQUEST_CONTEXT : new ActiveRequestContext(signal);
|
|
42
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export class RuntimeActivityTracker {
|
|
2
|
+
#admission = "open";
|
|
3
|
+
#activeRequests = 0;
|
|
4
|
+
#activeSends = 0;
|
|
5
|
+
#idleWaiters = new Set();
|
|
6
|
+
#idleCheck;
|
|
7
|
+
get admission() {
|
|
8
|
+
return this.#admission;
|
|
9
|
+
}
|
|
10
|
+
prepareForStartup() {
|
|
11
|
+
if (this.#activeRequests !== 0 || this.#activeSends !== 0 || this.#idleWaiters.size !== 0) {
|
|
12
|
+
throw new Error("Cannot start runtime activity tracking while work remains active.");
|
|
13
|
+
}
|
|
14
|
+
this.#admission = "open";
|
|
15
|
+
}
|
|
16
|
+
beginShutdown() {
|
|
17
|
+
this.#admission = "closed";
|
|
18
|
+
}
|
|
19
|
+
async trackRequest(callback) {
|
|
20
|
+
this.#activeRequests += 1;
|
|
21
|
+
try {
|
|
22
|
+
return await callback();
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
this.#activeRequests -= 1;
|
|
26
|
+
this.#scheduleIdleCheck();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async trackSend(callback) {
|
|
30
|
+
this.#activeSends += 1;
|
|
31
|
+
try {
|
|
32
|
+
return await callback();
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
this.#activeSends -= 1;
|
|
36
|
+
this.#scheduleIdleCheck();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
waitForIdle() {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
this.#idleWaiters.add(resolve);
|
|
42
|
+
this.#scheduleIdleCheck();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
snapshot() {
|
|
46
|
+
return Object.freeze({
|
|
47
|
+
admission: this.#admission,
|
|
48
|
+
active_requests: this.#activeRequests,
|
|
49
|
+
active_sends: this.#activeSends,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
#scheduleIdleCheck() {
|
|
53
|
+
if (this.#idleWaiters.size === 0 || this.#idleCheck !== undefined)
|
|
54
|
+
return;
|
|
55
|
+
this.#idleCheck = setImmediate(() => {
|
|
56
|
+
this.#idleCheck = undefined;
|
|
57
|
+
if (this.#activeRequests !== 0 || this.#activeSends !== 0)
|
|
58
|
+
return;
|
|
59
|
+
const waiters = [...this.#idleWaiters];
|
|
60
|
+
this.#idleWaiters.clear();
|
|
61
|
+
for (const resolve of waiters)
|
|
62
|
+
resolve();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { INTERNAL_ERROR_MESSAGE, MAX_PUBLIC_ERROR_RESPONSE_BYTES, sanitizePublicText, } from "./public-errors.js";
|
|
3
|
+
export const MAX_RUNTIME_FAILURE_EVENT_BYTES = 8192;
|
|
4
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
5
|
+
const TOOL_NAME_PATTERN = /^ast_[a-z0-9_]{1,64}$/;
|
|
6
|
+
const PROJECT_ID_PATTERN = /^project_[0-9a-f]{20}$/;
|
|
7
|
+
const CONFIG_ID_PATTERN = /^config_[0-9a-f]{20}$/;
|
|
8
|
+
function normalizeProjectIdentity(identity) {
|
|
9
|
+
if (identity === undefined || !PROJECT_ID_PATTERN.test(identity.project_id))
|
|
10
|
+
return {};
|
|
11
|
+
return {
|
|
12
|
+
project_id: identity.project_id,
|
|
13
|
+
...(identity.config_id !== null && CONFIG_ID_PATTERN.test(identity.config_id)
|
|
14
|
+
? { config_id: identity.config_id }
|
|
15
|
+
: {}),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function renderToolFailureEvent(input) {
|
|
19
|
+
const identity = normalizeProjectIdentity(input.projectIdentity);
|
|
20
|
+
const event = {
|
|
21
|
+
event: "tool_failure",
|
|
22
|
+
version: 1,
|
|
23
|
+
correlation_id: UUID_PATTERN.test(input.correlationId)
|
|
24
|
+
? input.correlationId
|
|
25
|
+
: "invalid-correlation-id",
|
|
26
|
+
tool: TOOL_NAME_PATTERN.test(input.toolName) ? input.toolName : "unknown_tool",
|
|
27
|
+
code: input.code,
|
|
28
|
+
message: sanitizePublicText(input.message),
|
|
29
|
+
...(identity.project_id === undefined ? {} : { project_id: identity.project_id }),
|
|
30
|
+
...(identity.config_id === undefined ? {} : { config_id: identity.config_id }),
|
|
31
|
+
};
|
|
32
|
+
let rendered = JSON.stringify(event);
|
|
33
|
+
if (Buffer.byteLength(rendered, "utf8") + 1 > MAX_RUNTIME_FAILURE_EVENT_BYTES) {
|
|
34
|
+
rendered = JSON.stringify({ ...event, message: INTERNAL_ERROR_MESSAGE });
|
|
35
|
+
}
|
|
36
|
+
if (Buffer.byteLength(rendered, "utf8") + 1 > MAX_RUNTIME_FAILURE_EVENT_BYTES) {
|
|
37
|
+
return JSON.stringify({
|
|
38
|
+
event: "tool_failure",
|
|
39
|
+
version: 1,
|
|
40
|
+
correlation_id: event.correlation_id,
|
|
41
|
+
tool: "unknown_tool",
|
|
42
|
+
code: "INTERNAL_ERROR",
|
|
43
|
+
message: INTERNAL_ERROR_MESSAGE.slice(0, MAX_PUBLIC_ERROR_RESPONSE_BYTES),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return rendered;
|
|
47
|
+
}
|
|
48
|
+
export function emitToolFailureEvent(input) {
|
|
49
|
+
process.stderr.write(`${renderToolFailureEvent(input)}\n`);
|
|
50
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export const RUNTIME_POLICY_ENV_KEYS = Object.freeze([
|
|
2
|
+
"AST_MAX_PROJECT_SESSIONS",
|
|
3
|
+
"AST_MAX_QUEUED_OPERATIONS_PER_PROJECT",
|
|
4
|
+
"AST_QUEUE_WAIT_TIMEOUT_MS",
|
|
5
|
+
"AST_OPERATION_DEADLINE_MS",
|
|
6
|
+
"AST_SHUTDOWN_DRAIN_TIMEOUT_MS",
|
|
7
|
+
]);
|
|
8
|
+
function parseBoundedInteger(rawValue, defaultValue, minimum, maximum) {
|
|
9
|
+
if (rawValue === undefined)
|
|
10
|
+
return { value: defaultValue, reason: "default" };
|
|
11
|
+
if (typeof rawValue !== "string" || !/^-?\d+$/.test(rawValue)) {
|
|
12
|
+
return { value: defaultValue, reason: "invalid_integer" };
|
|
13
|
+
}
|
|
14
|
+
const value = Number(rawValue);
|
|
15
|
+
if (!Number.isSafeInteger(value)) {
|
|
16
|
+
return { value: defaultValue, reason: "invalid_integer" };
|
|
17
|
+
}
|
|
18
|
+
if (value < minimum || value > maximum) {
|
|
19
|
+
return { value: defaultValue, reason: "out_of_range" };
|
|
20
|
+
}
|
|
21
|
+
return { value, reason: "configured" };
|
|
22
|
+
}
|
|
23
|
+
export function parseRuntimePolicy(environment) {
|
|
24
|
+
const maxProjectSessions = parseBoundedInteger(environment.AST_MAX_PROJECT_SESSIONS, 8, 1, 32);
|
|
25
|
+
const maxQueuedOperationsPerProject = parseBoundedInteger(environment.AST_MAX_QUEUED_OPERATIONS_PER_PROJECT, 32, 1, 256);
|
|
26
|
+
const queueWaitTimeoutMs = parseBoundedInteger(environment.AST_QUEUE_WAIT_TIMEOUT_MS, 30_000, 100, 300_000);
|
|
27
|
+
const operationDeadlineMs = parseBoundedInteger(environment.AST_OPERATION_DEADLINE_MS, 120_000, 1_000, 900_000);
|
|
28
|
+
const shutdownDrainTimeoutMs = parseBoundedInteger(environment.AST_SHUTDOWN_DRAIN_TIMEOUT_MS, 10_000, 100, 60_000);
|
|
29
|
+
const reasons = Object.freeze({
|
|
30
|
+
AST_MAX_PROJECT_SESSIONS: maxProjectSessions.reason,
|
|
31
|
+
AST_MAX_QUEUED_OPERATIONS_PER_PROJECT: maxQueuedOperationsPerProject.reason,
|
|
32
|
+
AST_QUEUE_WAIT_TIMEOUT_MS: queueWaitTimeoutMs.reason,
|
|
33
|
+
AST_OPERATION_DEADLINE_MS: operationDeadlineMs.reason,
|
|
34
|
+
AST_SHUTDOWN_DRAIN_TIMEOUT_MS: shutdownDrainTimeoutMs.reason,
|
|
35
|
+
});
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
maxProjectSessions: maxProjectSessions.value,
|
|
38
|
+
maxQueuedOperationsPerProject: maxQueuedOperationsPerProject.value,
|
|
39
|
+
queueWaitTimeoutMs: queueWaitTimeoutMs.value,
|
|
40
|
+
operationDeadlineMs: operationDeadlineMs.value,
|
|
41
|
+
shutdownDrainTimeoutMs: shutdownDrainTimeoutMs.value,
|
|
42
|
+
reasons,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export function readRuntimePolicy(environment = process.env) {
|
|
46
|
+
return parseRuntimePolicy(environment);
|
|
47
|
+
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
claude: "Claude Code",
|
|
4
|
-
hermes: "Hermes",
|
|
5
|
-
};
|
|
1
|
+
import { AGENT_IDS, getAgentTarget } from "./agent-targets.js";
|
|
2
|
+
export { AGENT_IDS } from "./agent-targets.js";
|
|
6
3
|
function isAgentId(value) {
|
|
7
4
|
return AGENT_IDS.includes(value);
|
|
8
5
|
}
|
|
@@ -67,14 +64,14 @@ function renderSelectionQuestion(detections, validationMessage) {
|
|
|
67
64
|
const detail = item.installed
|
|
68
65
|
? [item.version, item.executable].filter(Boolean).join(" · ")
|
|
69
66
|
: "not installed";
|
|
70
|
-
return ` [${marker}] ${index + 1}. ${
|
|
67
|
+
return ` [${marker}] ${index + 1}. ${getAgentTarget(item.id).label} (${detail})`;
|
|
71
68
|
})
|
|
72
69
|
.join("\n");
|
|
73
70
|
const validation = validationMessage === undefined ? "" : `\n${validationMessage}\n`;
|
|
74
71
|
return `\nDetected agents (all installed agents are selected):\n${choices}\n${validation}\nEnter agents to deselect by number or name, 'none' to deselect all, or press Enter to keep all:\n> `;
|
|
75
72
|
}
|
|
76
73
|
function renderConfirmation(agents, validationMessage) {
|
|
77
|
-
const labels = agents.map((agent) =>
|
|
74
|
+
const labels = agents.map((agent) => getAgentTarget(agent).label).join(", ");
|
|
78
75
|
const validation = validationMessage === undefined ? "" : `\n${validationMessage}\n`;
|
|
79
76
|
return `${validation}\nSetup will install the MCP server and skill for: ${labels}.\nContinue? [Y/n] `;
|
|
80
77
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
function assertDrainTimeout(value) {
|
|
2
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 300_000) {
|
|
3
|
+
throw new RangeError("drainTimeoutMs must be an integer in 1..300000.");
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
function boundedCount(value) {
|
|
7
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
8
|
+
return 0;
|
|
9
|
+
return Math.min(value, 2_147_483_647);
|
|
10
|
+
}
|
|
11
|
+
export function renderIncompleteShutdownEvent(trigger, snapshot) {
|
|
12
|
+
const event = {
|
|
13
|
+
event: "shutdown_incomplete",
|
|
14
|
+
version: 1,
|
|
15
|
+
state: "forced_noncritical",
|
|
16
|
+
trigger,
|
|
17
|
+
active_operations: boundedCount(snapshot.active_operations),
|
|
18
|
+
queued_operations: boundedCount(snapshot.queued_operations),
|
|
19
|
+
completion_critical_operations: boundedCount(snapshot.completion_critical_operations),
|
|
20
|
+
};
|
|
21
|
+
return JSON.stringify(event);
|
|
22
|
+
}
|
|
23
|
+
export function emitIncompleteShutdownEvent(trigger, snapshot) {
|
|
24
|
+
process.stderr.write(`${renderIncompleteShutdownEvent(trigger, snapshot)}\n`);
|
|
25
|
+
}
|
|
26
|
+
export class ShutdownCoordinator {
|
|
27
|
+
#options;
|
|
28
|
+
#completion;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
assertDrainTimeout(options.drainTimeoutMs);
|
|
31
|
+
this.#options = options;
|
|
32
|
+
}
|
|
33
|
+
shutdown(trigger) {
|
|
34
|
+
this.#completion ??= this.#execute(trigger);
|
|
35
|
+
return this.#completion;
|
|
36
|
+
}
|
|
37
|
+
async #execute(trigger) {
|
|
38
|
+
this.#options.stopAdmissionAndCancel();
|
|
39
|
+
const drain = this.#options.waitForDrain();
|
|
40
|
+
while (true) {
|
|
41
|
+
const drainedWithinGrace = await this.#waitForBoundedDrain(drain);
|
|
42
|
+
if (drainedWithinGrace)
|
|
43
|
+
break;
|
|
44
|
+
const snapshot = this.#options.getDrainSnapshot();
|
|
45
|
+
if (snapshot.completion_critical_operations === 0) {
|
|
46
|
+
this.#options.emitIncompleteShutdown(trigger, snapshot);
|
|
47
|
+
return Object.freeze({ state: "forced_noncritical", trigger });
|
|
48
|
+
}
|
|
49
|
+
this.#observeCompletionCriticalWait(snapshot);
|
|
50
|
+
await this.#options.waitForCompletionCriticalOperationsToDrain();
|
|
51
|
+
}
|
|
52
|
+
await this.#closeResources();
|
|
53
|
+
return Object.freeze({ state: "complete", trigger });
|
|
54
|
+
}
|
|
55
|
+
#observeCompletionCriticalWait(snapshot) {
|
|
56
|
+
try {
|
|
57
|
+
this.#options.onCompletionCriticalWait?.(snapshot);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Runtime observation is non-authoritative and must never interrupt shutdown.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async #waitForBoundedDrain(drain) {
|
|
64
|
+
let timer;
|
|
65
|
+
const graceExpired = new Promise((resolve) => {
|
|
66
|
+
timer = setTimeout(() => resolve(false), this.#options.drainTimeoutMs);
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
return await Promise.race([drain.then(() => true), graceExpired]);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
if (timer !== undefined)
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async #closeResources() {
|
|
77
|
+
let firstError;
|
|
78
|
+
try {
|
|
79
|
+
await this.#options.closeMcp();
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
firstError = error;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
await this.#options.closeProjects();
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
firstError ??= error;
|
|
89
|
+
}
|
|
90
|
+
if (firstError !== undefined)
|
|
91
|
+
throw firstError;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const MAX_SYMBOL_INDEX_QUERY_CANDIDATES = 10_000;
|
|
2
|
+
export const MAX_SYMBOL_INDEX_SCANNED_SYMBOLS = 50_000;
|
|
3
|
+
export const MAX_SYMBOL_INDEX_FILE_ENTRIES = 10_000;
|
|
4
|
+
export const MAX_SYMBOL_INDEX_ROW_PAYLOAD_BYTES = 4 * 1024 * 1024;
|
|
5
|
+
export const MAX_SYMBOL_INDEX_TOTAL_PAYLOAD_BYTES = 64 * 1024 * 1024;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function compareSymbolIndexText(left, right) {
|
|
2
|
+
let leftIndex = 0;
|
|
3
|
+
let rightIndex = 0;
|
|
4
|
+
while (leftIndex < left.length && rightIndex < right.length) {
|
|
5
|
+
const leftCodePoint = left.codePointAt(leftIndex);
|
|
6
|
+
const rightCodePoint = right.codePointAt(rightIndex);
|
|
7
|
+
if (leftCodePoint !== rightCodePoint)
|
|
8
|
+
return leftCodePoint - rightCodePoint;
|
|
9
|
+
leftIndex += leftCodePoint > 0xffff ? 2 : 1;
|
|
10
|
+
rightIndex += rightCodePoint > 0xffff ? 2 : 1;
|
|
11
|
+
}
|
|
12
|
+
return left.length - right.length;
|
|
13
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const DEFAULT_BUSY_TIMEOUT_MS = 1_000;
|
|
4
|
+
export const MAX_SYMBOL_INDEX_RUNTIME_COUNT = 1_000_000;
|
|
5
|
+
export function addSymbolIndexRuntimeCount(current, increment = 1) {
|
|
6
|
+
if (!Number.isSafeInteger(current) || current < 0)
|
|
7
|
+
return MAX_SYMBOL_INDEX_RUNTIME_COUNT;
|
|
8
|
+
if (!Number.isSafeInteger(increment) || increment < 0)
|
|
9
|
+
return MAX_SYMBOL_INDEX_RUNTIME_COUNT;
|
|
10
|
+
return Math.min(MAX_SYMBOL_INDEX_RUNTIME_COUNT, current + increment);
|
|
11
|
+
}
|
|
12
|
+
export function createInitialSymbolIndexRuntimeObservability(policy = disabledPolicy("default")) {
|
|
13
|
+
return {
|
|
14
|
+
policy: policy.mode,
|
|
15
|
+
policy_reason: policy.reason,
|
|
16
|
+
backend: policy.backend,
|
|
17
|
+
state: "disabled",
|
|
18
|
+
operation: policy.backend === "sqlite" ? "miss" : "disabled",
|
|
19
|
+
last_operation: policy.backend === "sqlite" ? "miss" : "disabled",
|
|
20
|
+
loaded_entries: 0,
|
|
21
|
+
accepted_entries: 0,
|
|
22
|
+
rejected_entries: 0,
|
|
23
|
+
cache_hits: 0,
|
|
24
|
+
cache_misses: 0,
|
|
25
|
+
fallback_count: 0,
|
|
26
|
+
migration_count: 0,
|
|
27
|
+
corruption_count: 0,
|
|
28
|
+
write_failure_count: 0,
|
|
29
|
+
rebuilt_files: 0,
|
|
30
|
+
reused_files: 0,
|
|
31
|
+
removed_files: 0,
|
|
32
|
+
last_error: null,
|
|
33
|
+
last_successful_persistence_at: null,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function disabledPolicy(reason) {
|
|
37
|
+
return {
|
|
38
|
+
mode: "disabled",
|
|
39
|
+
backend: "memory",
|
|
40
|
+
cache_root: null,
|
|
41
|
+
busy_timeout_ms: DEFAULT_BUSY_TIMEOUT_MS,
|
|
42
|
+
reason,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function readBusyTimeout(value) {
|
|
46
|
+
if (!value)
|
|
47
|
+
return DEFAULT_BUSY_TIMEOUT_MS;
|
|
48
|
+
const parsed = Number(value);
|
|
49
|
+
return Number.isSafeInteger(parsed) && parsed > 0 && parsed <= 60_000
|
|
50
|
+
? parsed
|
|
51
|
+
: DEFAULT_BUSY_TIMEOUT_MS;
|
|
52
|
+
}
|
|
53
|
+
export function readSymbolIndexPersistencePolicy(environment = process.env) {
|
|
54
|
+
const mode = environment.AST_SYMBOL_INDEX_PERSISTENCE ?? "disabled";
|
|
55
|
+
if (mode === "disabled")
|
|
56
|
+
return disabledPolicy("default");
|
|
57
|
+
if (mode === "enabled")
|
|
58
|
+
return disabledPolicy("enabled_not_released");
|
|
59
|
+
if (mode !== "canary")
|
|
60
|
+
return disabledPolicy("invalid_mode");
|
|
61
|
+
const cacheRoot = environment.AST_SYMBOL_INDEX_CACHE_ROOT;
|
|
62
|
+
if (!cacheRoot)
|
|
63
|
+
return disabledPolicy("cache_root_missing");
|
|
64
|
+
if (!path.isAbsolute(cacheRoot) || path.normalize(cacheRoot) !== cacheRoot) {
|
|
65
|
+
return disabledPolicy("cache_root_invalid");
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
mode,
|
|
69
|
+
backend: "sqlite",
|
|
70
|
+
cache_root: cacheRoot,
|
|
71
|
+
busy_timeout_ms: readBusyTimeout(environment.AST_SYMBOL_INDEX_BUSY_TIMEOUT_MS),
|
|
72
|
+
reason: "default",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function symbolIndexPolicyKey(policy) {
|
|
76
|
+
return [
|
|
77
|
+
policy.mode,
|
|
78
|
+
policy.backend,
|
|
79
|
+
policy.reason,
|
|
80
|
+
policy.cache_root ?? "",
|
|
81
|
+
policy.busy_timeout_ms,
|
|
82
|
+
].join("\u0000");
|
|
83
|
+
}
|
|
84
|
+
export function symbolIndexCachePath(policy, project) {
|
|
85
|
+
if (policy.backend !== "sqlite" || !policy.cache_root)
|
|
86
|
+
return null;
|
|
87
|
+
const opaqueProjectKey = createHash("sha256")
|
|
88
|
+
.update(`${project.project_id}\u0000${project.config_id ?? ""}`)
|
|
89
|
+
.digest("hex");
|
|
90
|
+
return path.join(policy.cache_root, `symbol-index-${opaqueProjectKey}.sqlite`);
|
|
91
|
+
}
|