ast-mcp-server 0.5.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/CHANGELOG.md +28 -0
- package/README.md +20 -3
- package/dist/server.js +6 -0
- package/dist/services/context-builder.js +234 -0
- package/dist/services/file-snapshot.js +96 -0
- package/dist/services/project-status.js +736 -0
- package/dist/services/project.js +87 -13
- package/dist/services/read-contracts.js +159 -0
- package/dist/services/references.js +48 -0
- package/dist/services/symbols.js +59 -0
- package/dist/services/workspace.js +12 -6
- package/dist/tools/explore.js +174 -0
- package/dist/tools/find_references.js +7 -44
- package/dist/tools/get_file.js +95 -0
- package/dist/tools/get_project_status.js +68 -0
- package/dist/tools/search_symbols.js +5 -54
- package/package.json +2 -1
- package/skills/structural-code-editing/SKILL.md +12 -5
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createTruncationMetadata, isFreshnessCause, isSnapshotState, isTruncationReason, } from "./read-contracts.js";
|
|
3
|
+
export const MAX_PENDING_FILES = 64;
|
|
4
|
+
export const MAX_PENDING_FILENAME_LENGTH = 256;
|
|
5
|
+
export const MAX_DEGRADED_ERRORS = 8;
|
|
6
|
+
export const MAX_STATUS_TEXT_LENGTH = 512;
|
|
7
|
+
const TRUNCATION_SUFFIX = "... [truncated]";
|
|
8
|
+
const MAX_STATUS_CAUSES = 5;
|
|
9
|
+
const MAX_PENDING_INPUT_SCAN = MAX_PENDING_FILES * 4;
|
|
10
|
+
const MAX_ERROR_INPUT_SCAN = MAX_DEGRADED_ERRORS * 4;
|
|
11
|
+
function isCompilerStatusState(value) {
|
|
12
|
+
return value === "ready" || value === "rebuilding" || value === "failed";
|
|
13
|
+
}
|
|
14
|
+
function isIndexStatusState(value) {
|
|
15
|
+
return value === "disabled" || value === "ready" || value === "rebuilding" || value === "failed";
|
|
16
|
+
}
|
|
17
|
+
function isWatcherStatusState(value) {
|
|
18
|
+
return value === "disabled" || value === "ready" || value === "failed";
|
|
19
|
+
}
|
|
20
|
+
function isSynchronizationCause(value) {
|
|
21
|
+
return value === "source_change" || value === "config_change" || value === "compiler_rebuild";
|
|
22
|
+
}
|
|
23
|
+
function canonicalIdentityPath(value) {
|
|
24
|
+
const normalized = value.trim().replaceAll("\\", "/");
|
|
25
|
+
const hasLeadingSlash = normalized.startsWith("/");
|
|
26
|
+
const windowsDrive = /^[A-Za-z]:\//.test(normalized);
|
|
27
|
+
const segments = normalized.split("/");
|
|
28
|
+
const result = [];
|
|
29
|
+
for (const segment of segments) {
|
|
30
|
+
if (!segment || segment === ".")
|
|
31
|
+
continue;
|
|
32
|
+
if (segment === ".." && result.length > 0 && result[result.length - 1] !== "..") {
|
|
33
|
+
result.pop();
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
result.push(segment);
|
|
37
|
+
}
|
|
38
|
+
const joined = result.join("/");
|
|
39
|
+
if (hasLeadingSlash)
|
|
40
|
+
return `/${joined}`;
|
|
41
|
+
if (windowsDrive)
|
|
42
|
+
return joined;
|
|
43
|
+
return joined || ".";
|
|
44
|
+
}
|
|
45
|
+
function digest(prefix, value) {
|
|
46
|
+
return `${prefix}_${createHash("sha256").update(value, "utf8").digest("hex").slice(0, 20)}`;
|
|
47
|
+
}
|
|
48
|
+
function fingerprintDigest(prefix, value) {
|
|
49
|
+
return `${prefix}_${createHash("sha256").update(value, "utf8").digest("hex")}`;
|
|
50
|
+
}
|
|
51
|
+
function normalizeIdentityToken(value, prefix) {
|
|
52
|
+
const expected = new RegExp(`^${prefix}_[0-9a-f]{20}$`);
|
|
53
|
+
return typeof value === "string" && expected.test(value) ? value : digest(prefix, "[redacted]");
|
|
54
|
+
}
|
|
55
|
+
function normalizeProjectIdentity(value) {
|
|
56
|
+
const record = typeof value === "object" && value !== null
|
|
57
|
+
? value
|
|
58
|
+
: {};
|
|
59
|
+
return {
|
|
60
|
+
project_id: normalizeIdentityToken(record.project_id, "project"),
|
|
61
|
+
config_id: record.config_id === null || record.config_id === undefined
|
|
62
|
+
? null
|
|
63
|
+
: normalizeIdentityToken(record.config_id, "config"),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function createProjectIdentity(input) {
|
|
67
|
+
if (typeof input !== "object" ||
|
|
68
|
+
input === null ||
|
|
69
|
+
typeof input.projectRoot !== "string" ||
|
|
70
|
+
(input.configPath !== undefined && typeof input.configPath !== "string")) {
|
|
71
|
+
throw new Error("Project identity input must contain string paths.");
|
|
72
|
+
}
|
|
73
|
+
const projectRoot = canonicalIdentityPath(input.projectRoot);
|
|
74
|
+
const configPath = input.configPath === undefined ? null : canonicalIdentityPath(input.configPath);
|
|
75
|
+
return {
|
|
76
|
+
project_id: digest("project", projectRoot),
|
|
77
|
+
config_id: configPath === null ? null : digest("config", configPath),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function isFiniteCount(value) {
|
|
81
|
+
return (typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0);
|
|
82
|
+
}
|
|
83
|
+
function boundedCount(value) {
|
|
84
|
+
if (!isFiniteCount(value))
|
|
85
|
+
return 0;
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
function redactAbsolutePaths(value) {
|
|
89
|
+
const traversalMarker = "__TRAVERSAL_PATH_REDACTED__";
|
|
90
|
+
const quotedTraversalPath = /(?<=["'`])(?:[^"'`\n\r]*[\\/])?\.\.[\\/][^"'`\n\r]*?(?=["'`])/g;
|
|
91
|
+
const labelledTraversalPath = /([=:]\s+)([^"'`:\n\r;,]*[ \t]+[^"'`:\n\r;,/\\]+[\\/]\.\.[\\/][^"'`:\n\r]*?)(?=\s*[:;,\n\r"'`)\]}]|(?:\.(?=\s|$))|$)/g;
|
|
92
|
+
const diagnosticTraversalPath = /(\b(?:read|open|resolve|watch|parse)\s+)([^"'`:\n\r;,]*[\\/]\.\.[\\/][^"'`:\n\r]*?)(?=\s*[:;,\n\r"'`)\]}]|(?:\.(?=\s|$))|$)/gi;
|
|
93
|
+
const spacedTraversalPath = /(?<=\s)([^"'`:\n\r;,]*[ \t]+[^"'`:\n\r;,/\\]+[\\/]\.\.[\\/][^"'`:\n\r]*?)(?=\s*[:;,\n\r"'`)\]}]|(?:\.(?=\s|$))|$)/g;
|
|
94
|
+
const relativeTraversalPath = /(?:[^\s"'`:/${}\\]+[\\/])+\.\.[\\/][^"'`:\n\r]*?(?=\s*[:;,\n\r"'`)\]}]|(?:\.(?=\s|$))|$)/g;
|
|
95
|
+
const directTraversalPath = /(?:^|(?<=[\s(]))\.\.[\\/][^"'`:\n\r]*?(?=\s*[:;,\n\r"'`)\]}]|(?:\.(?=\s|$))|$)/g;
|
|
96
|
+
const uncPath = /(?:\\\\|\/{2})[^"'`\n\r]*?(?=\s*(?::(?=\s|$)|[;,\n\r"'`)\]}])|(?:\.(?=\s|$))|$)/g;
|
|
97
|
+
const drivePath = /\b[A-Za-z]:[^"'`\n\r]*?(?=\s*(?::(?=\s|$)|[;,\n\r"'`)\]}])|(?:\.(?=\s|$))|$)/g;
|
|
98
|
+
const posixPath = /(?<![A-Za-z0-9_./-])\/(?!\/)[^"'`\n\r]*?(?=\s*(?::(?=\s|$)|[;,\n\r"'`)\]}])|(?:\.(?=\s|$))|$)/g;
|
|
99
|
+
const redacted = value
|
|
100
|
+
.replace(quotedTraversalPath, traversalMarker)
|
|
101
|
+
.replace(labelledTraversalPath, `$1${traversalMarker}`)
|
|
102
|
+
.replace(diagnosticTraversalPath, `$1${traversalMarker}`)
|
|
103
|
+
.replace(spacedTraversalPath, traversalMarker)
|
|
104
|
+
.replace(relativeTraversalPath, traversalMarker)
|
|
105
|
+
.replace(directTraversalPath, traversalMarker);
|
|
106
|
+
return redacted
|
|
107
|
+
.replace(drivePath, "[path-redacted]")
|
|
108
|
+
.replace(uncPath, "[path-redacted]")
|
|
109
|
+
.replace(posixPath, "[path-redacted]")
|
|
110
|
+
.replaceAll(traversalMarker, "[path-redacted]");
|
|
111
|
+
}
|
|
112
|
+
function redactSensitiveText(value) {
|
|
113
|
+
return value
|
|
114
|
+
.replace(/(?!(?:\bauthorization[ \t]*[:=])[ \t]*\[REDACTED\])(\bauthorization[ \t]*[:=])([ \t]*)[^\r\n;]+/gi, "$1$2[REDACTED]")
|
|
115
|
+
.replace(/(?!(?:\bbearer)[ \t]+\[REDACTED\])\bbearer[ \t]+[^\r\n;]+/gi, "Bearer [REDACTED]")
|
|
116
|
+
.replace(/\b(api[-_ ]?key|access[-_ ]?token|refresh[-_ ]?token|token|password|secret|credential|connection[-_ ]?string)\b\s*[:=]\s*(?!\[REDACTED\])(?:"[^"]*"|'[^']*'|[^\s,;}\]]+)/gi, "$1=[REDACTED]")
|
|
117
|
+
.replace(/\b([A-Za-z][A-Za-z0-9+.-]*:\/\/)[^\s"'`]+@/g, "$1[REDACTED]@")
|
|
118
|
+
.replace(/\b(?:sk|ghp|github_pat|xox[baprs]-|AIza)[A-Za-z0-9_-]{8,}\b/g, "[REDACTED]");
|
|
119
|
+
}
|
|
120
|
+
function boundedText(value) {
|
|
121
|
+
const text = typeof value === "string" ? value : "[invalid-error]";
|
|
122
|
+
const redacted = redactSensitiveText(redactAbsolutePaths(text));
|
|
123
|
+
return redacted.length <= MAX_STATUS_TEXT_LENGTH
|
|
124
|
+
? { value: redacted, truncated: false }
|
|
125
|
+
: {
|
|
126
|
+
value: `${redacted.slice(0, MAX_STATUS_TEXT_LENGTH - TRUNCATION_SUFFIX.length)}${TRUNCATION_SUFFIX}`,
|
|
127
|
+
truncated: true,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function projectRelativePath(value) {
|
|
131
|
+
let normalized = value.trim().replaceAll("\\", "/");
|
|
132
|
+
const quote = normalized[0];
|
|
133
|
+
if (normalized.length >= 2 &&
|
|
134
|
+
(quote === '"' || quote === "'" || quote === "`") &&
|
|
135
|
+
normalized.endsWith(quote)) {
|
|
136
|
+
normalized = normalized.slice(1, -1).trim();
|
|
137
|
+
}
|
|
138
|
+
if (normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized)) {
|
|
139
|
+
return "[absolute-path-redacted]";
|
|
140
|
+
}
|
|
141
|
+
const segments = normalized.split("/");
|
|
142
|
+
if (segments.some((segment) => segment === "..")) {
|
|
143
|
+
return "[path-redacted]";
|
|
144
|
+
}
|
|
145
|
+
return segments.filter((segment) => segment.length > 0 && segment !== ".").join("/") || ".";
|
|
146
|
+
}
|
|
147
|
+
function validateArrayShape(value, limit) {
|
|
148
|
+
if (!Array.isArray(value))
|
|
149
|
+
return { valid: false, truncated: false };
|
|
150
|
+
if (Object.getPrototypeOf(value) !== Array.prototype)
|
|
151
|
+
return { valid: false, truncated: false };
|
|
152
|
+
if (Object.getOwnPropertySymbols(value).length > 0)
|
|
153
|
+
return { valid: false, truncated: false };
|
|
154
|
+
const end = Math.min(value.length, limit);
|
|
155
|
+
for (let index = 0; index < end; index += 1) {
|
|
156
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
157
|
+
if (descriptor === undefined ||
|
|
158
|
+
!("value" in descriptor) ||
|
|
159
|
+
typeof descriptor.value !== "string") {
|
|
160
|
+
return { valid: false, truncated: value.length > end };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { valid: true, truncated: value.length > end };
|
|
164
|
+
}
|
|
165
|
+
function collectStringPrefix(value, limit) {
|
|
166
|
+
const shape = validateArrayShape(value, limit);
|
|
167
|
+
if (!Array.isArray(value))
|
|
168
|
+
return { values: [], ...shape };
|
|
169
|
+
const end = Math.min(value.length, limit);
|
|
170
|
+
const values = [];
|
|
171
|
+
for (let index = 0; index < end; index += 1) {
|
|
172
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
173
|
+
if (descriptor !== undefined && "value" in descriptor && typeof descriptor.value === "string") {
|
|
174
|
+
values.push(descriptor.value);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { values, ...shape };
|
|
178
|
+
}
|
|
179
|
+
function boundedPendingFilename(value) {
|
|
180
|
+
if (value.length <= MAX_PENDING_FILENAME_LENGTH) {
|
|
181
|
+
return { value, truncated: false };
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
value: `${value.slice(0, MAX_PENDING_FILENAME_LENGTH - TRUNCATION_SUFFIX.length)}${TRUNCATION_SUFFIX}`,
|
|
185
|
+
truncated: true,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function boundedPendingFiles(files) {
|
|
189
|
+
const collected = collectStringPrefix(files, MAX_PENDING_INPUT_SCAN);
|
|
190
|
+
const unique = [];
|
|
191
|
+
const seen = new Set();
|
|
192
|
+
for (const file of collected.values) {
|
|
193
|
+
const normalized = projectRelativePath(file);
|
|
194
|
+
if (seen.has(normalized))
|
|
195
|
+
continue;
|
|
196
|
+
seen.add(normalized);
|
|
197
|
+
unique.push(normalized);
|
|
198
|
+
if (unique.length > MAX_PENDING_FILES)
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
const bounded = unique.slice(0, MAX_PENDING_FILES).map(boundedPendingFilename);
|
|
202
|
+
const recordTruncated = collected.truncated || unique.length > MAX_PENDING_FILES;
|
|
203
|
+
const filenameTruncated = bounded.some((file) => file.truncated);
|
|
204
|
+
return {
|
|
205
|
+
files: bounded.slice(0, MAX_PENDING_FILES).map((file) => file.value),
|
|
206
|
+
recordTruncation: createTruncationMetadata(recordTruncated, recordTruncated ? "record_limit" : null),
|
|
207
|
+
filenameTruncation: createTruncationMetadata(filenameTruncated, filenameTruncated ? "serialization_limit" : null),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function mergePendingFiles(current, incoming) {
|
|
211
|
+
const currentPrefix = collectStringPrefix(current, MAX_PENDING_INPUT_SCAN);
|
|
212
|
+
const incomingPrefix = collectStringPrefix(incoming, MAX_PENDING_INPUT_SCAN);
|
|
213
|
+
const merged = boundedPendingFiles([...currentPrefix.values, ...incomingPrefix.values]);
|
|
214
|
+
const recordTruncated = merged.recordTruncation.truncated || currentPrefix.truncated || incomingPrefix.truncated;
|
|
215
|
+
return {
|
|
216
|
+
...merged,
|
|
217
|
+
recordTruncation: createTruncationMetadata(recordTruncated, recordTruncated ? "record_limit" : null),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function appendCause(current, cause) {
|
|
221
|
+
if (current.includes(cause))
|
|
222
|
+
return current;
|
|
223
|
+
if (current.length >= MAX_STATUS_CAUSES)
|
|
224
|
+
return current;
|
|
225
|
+
return [...current, cause];
|
|
226
|
+
}
|
|
227
|
+
function removeCause(current, cause) {
|
|
228
|
+
return current.filter((currentCause) => currentCause !== cause);
|
|
229
|
+
}
|
|
230
|
+
function appendError(current, error) {
|
|
231
|
+
const bounded = boundedText(error);
|
|
232
|
+
const currentErrors = collectStringPrefix(current, MAX_DEGRADED_ERRORS).values;
|
|
233
|
+
const next = currentErrors.includes(bounded.value)
|
|
234
|
+
? currentErrors
|
|
235
|
+
: [...currentErrors, bounded.value];
|
|
236
|
+
return {
|
|
237
|
+
errors: next.slice(-MAX_DEGRADED_ERRORS),
|
|
238
|
+
countTruncated: next.length > MAX_DEGRADED_ERRORS,
|
|
239
|
+
textTruncated: bounded.truncated,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function boundedErrors(errors) {
|
|
243
|
+
let current = [];
|
|
244
|
+
let countTruncated = false;
|
|
245
|
+
let textTruncated = false;
|
|
246
|
+
const collected = collectStringPrefix(errors, MAX_ERROR_INPUT_SCAN);
|
|
247
|
+
for (const error of collected.values) {
|
|
248
|
+
const appended = appendError(current, error);
|
|
249
|
+
current = appended.errors;
|
|
250
|
+
countTruncated ||= appended.countTruncated;
|
|
251
|
+
textTruncated ||= appended.textTruncated;
|
|
252
|
+
}
|
|
253
|
+
countTruncated ||= collected.truncated;
|
|
254
|
+
return {
|
|
255
|
+
errors: current,
|
|
256
|
+
collectionValid: collected.valid,
|
|
257
|
+
countTruncated,
|
|
258
|
+
textTruncated,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function emptyTruncationMetadata() {
|
|
262
|
+
return createTruncationMetadata(false);
|
|
263
|
+
}
|
|
264
|
+
function mergeTruncation(current, incoming, reason) {
|
|
265
|
+
const truncated = current.truncated || incoming.truncated;
|
|
266
|
+
return createTruncationMetadata(truncated, truncated ? reason : null);
|
|
267
|
+
}
|
|
268
|
+
function errorTruncation(countTruncated, textTruncated) {
|
|
269
|
+
if (countTruncated)
|
|
270
|
+
return createTruncationMetadata(true, "record_limit");
|
|
271
|
+
if (textTruncated)
|
|
272
|
+
return createTruncationMetadata(true, "serialization_limit");
|
|
273
|
+
return emptyTruncationMetadata();
|
|
274
|
+
}
|
|
275
|
+
function nonEmptyFingerprint(value, prefix) {
|
|
276
|
+
if (typeof value !== "string")
|
|
277
|
+
return null;
|
|
278
|
+
const normalized = value.trim();
|
|
279
|
+
if (normalized.length === 0)
|
|
280
|
+
return null;
|
|
281
|
+
const canonical = new RegExp(`^${prefix}_[0-9a-f]{64}$`);
|
|
282
|
+
return canonical.test(value) ? value : fingerprintDigest(prefix, value);
|
|
283
|
+
}
|
|
284
|
+
function normalizedTimestamp(value) {
|
|
285
|
+
if (typeof value !== "string")
|
|
286
|
+
return null;
|
|
287
|
+
const normalized = value.trim();
|
|
288
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{3}))?Z$/.exec(normalized);
|
|
289
|
+
if (match === null)
|
|
290
|
+
return null;
|
|
291
|
+
const year = Number(match[1]);
|
|
292
|
+
const month = Number(match[2]);
|
|
293
|
+
const day = Number(match[3]);
|
|
294
|
+
const hour = Number(match[4]);
|
|
295
|
+
const minute = Number(match[5]);
|
|
296
|
+
const second = Number(match[6]);
|
|
297
|
+
const millisecond = match[7] === undefined ? 0 : Number(match[7]);
|
|
298
|
+
const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
299
|
+
const daysInMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
300
|
+
return month >= 1 &&
|
|
301
|
+
month <= 12 &&
|
|
302
|
+
day >= 1 &&
|
|
303
|
+
day <= daysInMonth[month - 1] &&
|
|
304
|
+
hour <= 23 &&
|
|
305
|
+
minute <= 59 &&
|
|
306
|
+
second <= 59 &&
|
|
307
|
+
millisecond <= 999
|
|
308
|
+
? normalized
|
|
309
|
+
: null;
|
|
310
|
+
}
|
|
311
|
+
function statusWith(status, changes) {
|
|
312
|
+
return { ...status, ...changes };
|
|
313
|
+
}
|
|
314
|
+
function normalizeTruncation(current, derived, fallbackReason, acceptedReasons = [fallbackReason]) {
|
|
315
|
+
const record = typeof current === "object" && current !== null
|
|
316
|
+
? current
|
|
317
|
+
: {};
|
|
318
|
+
const currentTruncated = record.truncated === true;
|
|
319
|
+
const currentReason = isTruncationReason(record.reason) && acceptedReasons.includes(record.reason)
|
|
320
|
+
? record.reason
|
|
321
|
+
: null;
|
|
322
|
+
const truncated = currentTruncated || derived.truncated;
|
|
323
|
+
if (!truncated)
|
|
324
|
+
return emptyTruncationMetadata();
|
|
325
|
+
return createTruncationMetadata(true, currentReason ?? derived.reason ?? fallbackReason);
|
|
326
|
+
}
|
|
327
|
+
function isValidTruncationMetadata(value) {
|
|
328
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
329
|
+
return false;
|
|
330
|
+
const record = value;
|
|
331
|
+
if (typeof record.truncated !== "boolean")
|
|
332
|
+
return false;
|
|
333
|
+
return record.truncated ? isTruncationReason(record.reason) : record.reason === null;
|
|
334
|
+
}
|
|
335
|
+
function invalidProjectStatus() {
|
|
336
|
+
return {
|
|
337
|
+
project: normalizeProjectIdentity(null),
|
|
338
|
+
state: "degraded",
|
|
339
|
+
causes: [],
|
|
340
|
+
sourceCount: 0,
|
|
341
|
+
indexedCount: 0,
|
|
342
|
+
pendingFiles: [],
|
|
343
|
+
pendingFilesTruncated: false,
|
|
344
|
+
pendingFilesTruncation: emptyTruncationMetadata(),
|
|
345
|
+
pendingFilesFilenameTruncation: emptyTruncationMetadata(),
|
|
346
|
+
compiler: { state: "failed" },
|
|
347
|
+
index: { state: "disabled" },
|
|
348
|
+
watcher: { state: "disabled" },
|
|
349
|
+
lastSuccessfulSyncAt: null,
|
|
350
|
+
lastSuccessfulIndexAt: null,
|
|
351
|
+
sourceSnapshotFingerprint: null,
|
|
352
|
+
configSnapshotFingerprint: null,
|
|
353
|
+
canonicalSnapshotFingerprint: null,
|
|
354
|
+
degradedErrors: ["[invalid-status]"],
|
|
355
|
+
degradedErrorsTruncation: emptyTruncationMetadata(),
|
|
356
|
+
degradedErrorsTextTruncation: emptyTruncationMetadata(),
|
|
357
|
+
boundaryInvalid: true,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
function normalizeProjectStatus(status) {
|
|
361
|
+
if (typeof status !== "object" || status === null || Array.isArray(status)) {
|
|
362
|
+
return invalidProjectStatus();
|
|
363
|
+
}
|
|
364
|
+
const causeCollection = collectStringPrefix(status.causes, MAX_STATUS_CAUSES * 4);
|
|
365
|
+
const causesFromInput = causeCollection.values
|
|
366
|
+
.filter(isFreshnessCause)
|
|
367
|
+
.filter((cause, index, causes) => causes.indexOf(cause) === index);
|
|
368
|
+
const hasMalformedCause = !causeCollection.valid ||
|
|
369
|
+
causeCollection.truncated ||
|
|
370
|
+
causeCollection.values.some((cause) => !isFreshnessCause(cause));
|
|
371
|
+
const rawIndexState = status.index?.state;
|
|
372
|
+
const rawWatcherState = status.watcher?.state;
|
|
373
|
+
const rawCompilerState = status.compiler?.state;
|
|
374
|
+
const malformedCount = !isFiniteCount(status.sourceCount) || !isFiniteCount(status.indexedCount);
|
|
375
|
+
const indexFailed = !isIndexStatusState(rawIndexState) || rawIndexState === "failed";
|
|
376
|
+
const watcherFailed = !isWatcherStatusState(rawWatcherState) || rawWatcherState === "failed";
|
|
377
|
+
const malformedComponentState = !isIndexStatusState(rawIndexState) || !isWatcherStatusState(rawWatcherState);
|
|
378
|
+
const compilerValid = isCompilerStatusState(rawCompilerState);
|
|
379
|
+
const compilerFailed = rawCompilerState === "failed";
|
|
380
|
+
const compilerRebuilding = rawCompilerState === "rebuilding";
|
|
381
|
+
const sourceSnapshotFingerprint = nonEmptyFingerprint(status.sourceSnapshotFingerprint, "source");
|
|
382
|
+
const configSnapshotFingerprint = nonEmptyFingerprint(status.configSnapshotFingerprint, "config");
|
|
383
|
+
const canonicalSnapshotFingerprint = nonEmptyFingerprint(status.canonicalSnapshotFingerprint, "snapshot");
|
|
384
|
+
const lastSuccessfulSyncAt = normalizedTimestamp(status.lastSuccessfulSyncAt);
|
|
385
|
+
const hasCompleteSnapshotEvidence = canonicalSnapshotFingerprint !== null ||
|
|
386
|
+
(sourceSnapshotFingerprint !== null && configSnapshotFingerprint !== null);
|
|
387
|
+
const hasActiveInvalidationCause = causesFromInput.some((cause) => cause === "source_change" || cause === "config_change" || cause === "compiler_rebuild");
|
|
388
|
+
let causes = causesFromInput;
|
|
389
|
+
if (indexFailed)
|
|
390
|
+
causes = appendCause(causes, "index_failure");
|
|
391
|
+
if (watcherFailed)
|
|
392
|
+
causes = appendCause(causes, "watcher_failure");
|
|
393
|
+
const hasComponentFailure = causes.includes("index_failure") ||
|
|
394
|
+
causes.includes("watcher_failure") ||
|
|
395
|
+
indexFailed ||
|
|
396
|
+
watcherFailed ||
|
|
397
|
+
(compilerFailed && status.state === "fresh");
|
|
398
|
+
const pendingCollection = collectStringPrefix(status.pendingFiles, MAX_PENDING_INPUT_SCAN);
|
|
399
|
+
const pending = boundedPendingFiles(status.pendingFiles);
|
|
400
|
+
const bounded = boundedErrors(status.degradedErrors);
|
|
401
|
+
const malformedTruncation = [
|
|
402
|
+
status.pendingFilesTruncation,
|
|
403
|
+
status.pendingFilesFilenameTruncation,
|
|
404
|
+
status.degradedErrorsTruncation,
|
|
405
|
+
status.degradedErrorsTextTruncation,
|
|
406
|
+
].some((metadata) => !isValidTruncationMetadata(metadata));
|
|
407
|
+
const boundaryInvalid = status.boundaryInvalid === true ||
|
|
408
|
+
hasMalformedCause ||
|
|
409
|
+
malformedCount ||
|
|
410
|
+
malformedComponentState ||
|
|
411
|
+
!pendingCollection.valid ||
|
|
412
|
+
!bounded.collectionValid ||
|
|
413
|
+
malformedTruncation ||
|
|
414
|
+
!isSnapshotState(status.state) ||
|
|
415
|
+
!compilerValid;
|
|
416
|
+
const pendingFilesTruncation = normalizeTruncation(status.pendingFilesTruncation, pending.recordTruncation, "record_limit");
|
|
417
|
+
const pendingFilesFilenameTruncation = normalizeTruncation(status.pendingFilesFilenameTruncation, pending.filenameTruncation, "serialization_limit");
|
|
418
|
+
const degradedErrorsTruncation = normalizeTruncation(status.degradedErrorsTruncation, errorTruncation(bounded.countTruncated, bounded.textTruncated), "record_limit", ["record_limit", "serialization_limit"]);
|
|
419
|
+
const degradedErrorsTextTruncation = normalizeTruncation(status.degradedErrorsTextTruncation, createTruncationMetadata(bounded.textTruncated, bounded.textTruncated ? "serialization_limit" : null), "serialization_limit");
|
|
420
|
+
const hasPendingEvidence = pending.files.length > 0 ||
|
|
421
|
+
pendingFilesTruncation.truncated ||
|
|
422
|
+
pendingFilesFilenameTruncation.truncated;
|
|
423
|
+
return {
|
|
424
|
+
...status,
|
|
425
|
+
project: normalizeProjectIdentity(status.project),
|
|
426
|
+
state: hasComponentFailure || boundaryInvalid
|
|
427
|
+
? "degraded"
|
|
428
|
+
: status.state === "fresh" &&
|
|
429
|
+
(!hasCompleteSnapshotEvidence ||
|
|
430
|
+
lastSuccessfulSyncAt === null ||
|
|
431
|
+
hasActiveInvalidationCause ||
|
|
432
|
+
hasPendingEvidence ||
|
|
433
|
+
compilerRebuilding ||
|
|
434
|
+
bounded.errors.length > 0)
|
|
435
|
+
? "pending"
|
|
436
|
+
: status.state,
|
|
437
|
+
causes,
|
|
438
|
+
boundaryInvalid,
|
|
439
|
+
sourceCount: boundedCount(status.sourceCount),
|
|
440
|
+
indexedCount: 0,
|
|
441
|
+
index: { state: "disabled" },
|
|
442
|
+
compiler: { state: compilerValid ? rawCompilerState : "failed" },
|
|
443
|
+
watcher: { state: watcherFailed ? "failed" : rawWatcherState },
|
|
444
|
+
lastSuccessfulSyncAt,
|
|
445
|
+
lastSuccessfulIndexAt: null,
|
|
446
|
+
sourceSnapshotFingerprint,
|
|
447
|
+
configSnapshotFingerprint,
|
|
448
|
+
canonicalSnapshotFingerprint,
|
|
449
|
+
pendingFiles: pending.files,
|
|
450
|
+
pendingFilesTruncated: pendingFilesTruncation.truncated || pendingFilesFilenameTruncation.truncated,
|
|
451
|
+
pendingFilesTruncation,
|
|
452
|
+
pendingFilesFilenameTruncation,
|
|
453
|
+
degradedErrors: bounded.errors,
|
|
454
|
+
degradedErrorsTruncation,
|
|
455
|
+
degradedErrorsTextTruncation,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function hasActiveIndexFailure(status) {
|
|
459
|
+
return status.causes.includes("index_failure");
|
|
460
|
+
}
|
|
461
|
+
function hasActiveWatcherFailure(status) {
|
|
462
|
+
return status.causes.includes("watcher_failure");
|
|
463
|
+
}
|
|
464
|
+
function hasActiveComponentFailure(status) {
|
|
465
|
+
return (hasActiveIndexFailure(status) ||
|
|
466
|
+
hasActiveWatcherFailure(status) ||
|
|
467
|
+
status.boundaryInvalid === true ||
|
|
468
|
+
status.index.state === "failed" ||
|
|
469
|
+
status.watcher.state === "failed");
|
|
470
|
+
}
|
|
471
|
+
function stateAfterRecovery(status) {
|
|
472
|
+
return hasActiveComponentFailure(status) ? "degraded" : "pending";
|
|
473
|
+
}
|
|
474
|
+
function markInvalidTransition(status, error) {
|
|
475
|
+
const appended = appendError(status.degradedErrors, error);
|
|
476
|
+
const textTruncated = status.degradedErrorsTextTruncation.truncated || appended.textTruncated;
|
|
477
|
+
const countTruncated = status.degradedErrorsTruncation.reason === "record_limit" || appended.countTruncated;
|
|
478
|
+
return statusWith(status, {
|
|
479
|
+
state: "degraded",
|
|
480
|
+
boundaryInvalid: true,
|
|
481
|
+
degradedErrors: appended.errors,
|
|
482
|
+
degradedErrorsTruncation: errorTruncation(countTruncated, textTruncated),
|
|
483
|
+
degradedErrorsTextTruncation: createTruncationMetadata(textTruncated, textTruncated ? "serialization_limit" : null),
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
export function createInitialProjectStatus(project, options = {}) {
|
|
487
|
+
const pendingInput = options.pendingFiles === undefined ? [] : options.pendingFiles;
|
|
488
|
+
const pendingInputCollection = collectStringPrefix(pendingInput, MAX_PENDING_INPUT_SCAN);
|
|
489
|
+
const pending = boundedPendingFiles(pendingInput);
|
|
490
|
+
const watcherState = options.watcherState === undefined ? "disabled" : options.watcherState;
|
|
491
|
+
const initial = {
|
|
492
|
+
project: { ...project },
|
|
493
|
+
state: "pending",
|
|
494
|
+
causes: pending.files.length > 0 ? ["source_change"] : [],
|
|
495
|
+
sourceCount: boundedCount(options.sourceCount),
|
|
496
|
+
indexedCount: 0,
|
|
497
|
+
pendingFiles: pending.files,
|
|
498
|
+
pendingFilesTruncated: pending.recordTruncation.truncated || pending.filenameTruncation.truncated,
|
|
499
|
+
pendingFilesTruncation: pending.recordTruncation,
|
|
500
|
+
pendingFilesFilenameTruncation: pending.filenameTruncation,
|
|
501
|
+
compiler: { state: "ready" },
|
|
502
|
+
index: { state: "disabled" },
|
|
503
|
+
watcher: { state: watcherState },
|
|
504
|
+
lastSuccessfulSyncAt: null,
|
|
505
|
+
lastSuccessfulIndexAt: null,
|
|
506
|
+
sourceSnapshotFingerprint: null,
|
|
507
|
+
configSnapshotFingerprint: null,
|
|
508
|
+
canonicalSnapshotFingerprint: null,
|
|
509
|
+
degradedErrors: [],
|
|
510
|
+
degradedErrorsTruncation: emptyTruncationMetadata(),
|
|
511
|
+
degradedErrorsTextTruncation: emptyTruncationMetadata(),
|
|
512
|
+
boundaryInvalid: !pendingInputCollection.valid ||
|
|
513
|
+
(options.sourceCount !== undefined && !isFiniteCount(options.sourceCount)) ||
|
|
514
|
+
(options.indexedCount !== undefined && !isFiniteCount(options.indexedCount)),
|
|
515
|
+
};
|
|
516
|
+
return normalizeProjectStatus(initial);
|
|
517
|
+
}
|
|
518
|
+
export function transitionProjectStatus(inputStatus, event) {
|
|
519
|
+
const status = normalizeProjectStatus(inputStatus);
|
|
520
|
+
const runtimeEvent = event;
|
|
521
|
+
if (typeof runtimeEvent !== "object" ||
|
|
522
|
+
runtimeEvent === null ||
|
|
523
|
+
typeof runtimeEvent.type !== "string") {
|
|
524
|
+
return markInvalidTransition(status, "[invalid-event]");
|
|
525
|
+
}
|
|
526
|
+
switch (event.type) {
|
|
527
|
+
case "source_changed": {
|
|
528
|
+
const eventFiles = collectStringPrefix(event.files, MAX_PENDING_INPUT_SCAN);
|
|
529
|
+
if (!eventFiles.valid)
|
|
530
|
+
return markInvalidTransition(status, "[invalid-files]");
|
|
531
|
+
const pending = mergePendingFiles(status.pendingFiles, event.files);
|
|
532
|
+
const recordTruncation = mergeTruncation(status.pendingFilesTruncation, pending.recordTruncation, "record_limit");
|
|
533
|
+
const filenameTruncation = mergeTruncation(status.pendingFilesFilenameTruncation, pending.filenameTruncation, "serialization_limit");
|
|
534
|
+
return statusWith(status, {
|
|
535
|
+
state: status.state === "degraded" ? "degraded" : "pending",
|
|
536
|
+
causes: appendCause(status.causes, "source_change"),
|
|
537
|
+
pendingFiles: pending.files,
|
|
538
|
+
pendingFilesTruncated: recordTruncation.truncated || filenameTruncation.truncated,
|
|
539
|
+
pendingFilesTruncation: recordTruncation,
|
|
540
|
+
pendingFilesFilenameTruncation: filenameTruncation,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
case "config_changed":
|
|
544
|
+
return statusWith(status, {
|
|
545
|
+
state: status.state === "degraded" ? "degraded" : "pending",
|
|
546
|
+
causes: appendCause(status.causes, "config_change"),
|
|
547
|
+
});
|
|
548
|
+
case "compiler_rebuild_started":
|
|
549
|
+
return statusWith(status, {
|
|
550
|
+
state: status.state === "degraded" ? "degraded" : "rebuilding",
|
|
551
|
+
causes: appendCause(status.causes, "compiler_rebuild"),
|
|
552
|
+
compiler: { state: "rebuilding" },
|
|
553
|
+
index: {
|
|
554
|
+
state: hasActiveIndexFailure(status) || status.index.state === "disabled"
|
|
555
|
+
? "disabled"
|
|
556
|
+
: "rebuilding",
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
case "sync_succeeded": {
|
|
560
|
+
if (!isFiniteCount(event.sourceCount) || !isFiniteCount(event.indexedCount)) {
|
|
561
|
+
return markInvalidTransition(status, "[invalid-count]");
|
|
562
|
+
}
|
|
563
|
+
const hasFailure = hasActiveComponentFailure(status);
|
|
564
|
+
const indexDisabled = hasActiveIndexFailure(status) || status.index.state === "disabled";
|
|
565
|
+
const sourceSnapshotFingerprint = nonEmptyFingerprint(event.sourceSnapshotFingerprint, "source");
|
|
566
|
+
const configSnapshotFingerprint = nonEmptyFingerprint(event.configSnapshotFingerprint, "config");
|
|
567
|
+
const canonicalSnapshotFingerprint = nonEmptyFingerprint(event.canonicalSnapshotFingerprint, "snapshot");
|
|
568
|
+
const hasSnapshotEvidence = sourceSnapshotFingerprint !== null ||
|
|
569
|
+
configSnapshotFingerprint !== null ||
|
|
570
|
+
canonicalSnapshotFingerprint !== null;
|
|
571
|
+
const hasCompleteSnapshotEvidence = canonicalSnapshotFingerprint !== null ||
|
|
572
|
+
(sourceSnapshotFingerprint !== null && configSnapshotFingerprint !== null);
|
|
573
|
+
const eventSyncAt = event.at === undefined ? status.lastSuccessfulSyncAt : normalizedTimestamp(event.at);
|
|
574
|
+
const nextSyncAt = event.at === undefined
|
|
575
|
+
? status.lastSuccessfulSyncAt
|
|
576
|
+
: (normalizedTimestamp(event.at) ?? status.lastSuccessfulSyncAt);
|
|
577
|
+
const hasFreshEvidence = hasCompleteSnapshotEvidence && eventSyncAt !== null;
|
|
578
|
+
const canClearInvalidation = !hasFailure && hasFreshEvidence;
|
|
579
|
+
const pending = canClearInvalidation
|
|
580
|
+
? boundedPendingFiles([])
|
|
581
|
+
: {
|
|
582
|
+
files: status.pendingFiles,
|
|
583
|
+
recordTruncation: status.pendingFilesTruncation,
|
|
584
|
+
filenameTruncation: status.pendingFilesFilenameTruncation,
|
|
585
|
+
};
|
|
586
|
+
const nextState = hasFailure ? "degraded" : canClearInvalidation ? "fresh" : "pending";
|
|
587
|
+
return statusWith(status, {
|
|
588
|
+
state: nextState,
|
|
589
|
+
causes: canClearInvalidation ? [] : status.causes,
|
|
590
|
+
sourceCount: boundedCount(event.sourceCount),
|
|
591
|
+
indexedCount: indexDisabled ? 0 : boundedCount(event.indexedCount),
|
|
592
|
+
pendingFiles: pending.files,
|
|
593
|
+
pendingFilesTruncated: pending.recordTruncation.truncated || pending.filenameTruncation.truncated,
|
|
594
|
+
pendingFilesTruncation: pending.recordTruncation,
|
|
595
|
+
pendingFilesFilenameTruncation: pending.filenameTruncation,
|
|
596
|
+
compiler: { state: "ready" },
|
|
597
|
+
index: {
|
|
598
|
+
state: indexDisabled
|
|
599
|
+
? "disabled"
|
|
600
|
+
: hasFailure && status.index.state === "failed"
|
|
601
|
+
? "failed"
|
|
602
|
+
: "ready",
|
|
603
|
+
},
|
|
604
|
+
lastSuccessfulSyncAt: canClearInvalidation ? nextSyncAt : status.lastSuccessfulSyncAt,
|
|
605
|
+
lastSuccessfulIndexAt: !hasFreshEvidence || indexDisabled || (hasFailure && status.index.state === "failed")
|
|
606
|
+
? status.lastSuccessfulIndexAt
|
|
607
|
+
: (nextSyncAt ?? status.lastSuccessfulIndexAt),
|
|
608
|
+
sourceSnapshotFingerprint: hasSnapshotEvidence
|
|
609
|
+
? (sourceSnapshotFingerprint ?? status.sourceSnapshotFingerprint)
|
|
610
|
+
: status.sourceSnapshotFingerprint,
|
|
611
|
+
configSnapshotFingerprint: hasSnapshotEvidence
|
|
612
|
+
? (configSnapshotFingerprint ?? status.configSnapshotFingerprint)
|
|
613
|
+
: status.configSnapshotFingerprint,
|
|
614
|
+
canonicalSnapshotFingerprint: hasSnapshotEvidence
|
|
615
|
+
? (canonicalSnapshotFingerprint ?? status.canonicalSnapshotFingerprint)
|
|
616
|
+
: status.canonicalSnapshotFingerprint,
|
|
617
|
+
degradedErrors: canClearInvalidation ? [] : status.degradedErrors,
|
|
618
|
+
degradedErrorsTruncation: canClearInvalidation
|
|
619
|
+
? emptyTruncationMetadata()
|
|
620
|
+
: status.degradedErrorsTruncation,
|
|
621
|
+
degradedErrorsTextTruncation: canClearInvalidation
|
|
622
|
+
? emptyTruncationMetadata()
|
|
623
|
+
: status.degradedErrorsTextTruncation,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
case "sync_failed":
|
|
627
|
+
if (!isSynchronizationCause(event.cause)) {
|
|
628
|
+
return markInvalidTransition(status, event.error);
|
|
629
|
+
}
|
|
630
|
+
return statusWith(status, {
|
|
631
|
+
state: hasActiveComponentFailure(status) ? "degraded" : "stale",
|
|
632
|
+
causes: appendCause(status.causes, event.cause),
|
|
633
|
+
compiler: { state: "failed" },
|
|
634
|
+
});
|
|
635
|
+
case "stale_detected": {
|
|
636
|
+
const cause = event.cause === undefined ? "source_change" : event.cause;
|
|
637
|
+
const eventFiles = event.files === undefined
|
|
638
|
+
? { valid: true }
|
|
639
|
+
: collectStringPrefix(event.files, MAX_PENDING_INPUT_SCAN);
|
|
640
|
+
if (!eventFiles.valid)
|
|
641
|
+
return markInvalidTransition(status, "[invalid-files]");
|
|
642
|
+
const pending = mergePendingFiles(status.pendingFiles, event.files);
|
|
643
|
+
const recordTruncation = mergeTruncation(status.pendingFilesTruncation, pending.recordTruncation, "record_limit");
|
|
644
|
+
const filenameTruncation = mergeTruncation(status.pendingFilesFilenameTruncation, pending.filenameTruncation, "serialization_limit");
|
|
645
|
+
const next = statusWith(status, {
|
|
646
|
+
state: hasActiveComponentFailure(status) ? "degraded" : "stale",
|
|
647
|
+
causes: isSynchronizationCause(cause) ? appendCause(status.causes, cause) : status.causes,
|
|
648
|
+
pendingFiles: pending.files,
|
|
649
|
+
pendingFilesTruncated: recordTruncation.truncated || filenameTruncation.truncated,
|
|
650
|
+
pendingFilesTruncation: recordTruncation,
|
|
651
|
+
pendingFilesFilenameTruncation: filenameTruncation,
|
|
652
|
+
});
|
|
653
|
+
return isSynchronizationCause(cause) ? next : markInvalidTransition(next, "[invalid-cause]");
|
|
654
|
+
}
|
|
655
|
+
case "index_failed": {
|
|
656
|
+
const appended = appendError(status.degradedErrors, event.error);
|
|
657
|
+
const textTruncated = status.degradedErrorsTextTruncation.truncated || appended.textTruncated;
|
|
658
|
+
const countTruncated = status.degradedErrorsTruncation.reason === "record_limit" || appended.countTruncated;
|
|
659
|
+
return statusWith(status, {
|
|
660
|
+
state: "degraded",
|
|
661
|
+
causes: appendCause(status.causes, "index_failure"),
|
|
662
|
+
index: { state: "disabled" },
|
|
663
|
+
degradedErrors: appended.errors,
|
|
664
|
+
degradedErrorsTruncation: errorTruncation(countTruncated, textTruncated),
|
|
665
|
+
degradedErrorsTextTruncation: createTruncationMetadata(textTruncated, textTruncated ? "serialization_limit" : null),
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
case "watcher_failed": {
|
|
669
|
+
const appended = appendError(status.degradedErrors, event.error);
|
|
670
|
+
const textTruncated = status.degradedErrorsTextTruncation.truncated || appended.textTruncated;
|
|
671
|
+
const countTruncated = status.degradedErrorsTruncation.reason === "record_limit" || appended.countTruncated;
|
|
672
|
+
return statusWith(status, {
|
|
673
|
+
state: "degraded",
|
|
674
|
+
causes: appendCause(status.causes, "watcher_failure"),
|
|
675
|
+
watcher: { state: "failed" },
|
|
676
|
+
degradedErrors: appended.errors,
|
|
677
|
+
degradedErrorsTruncation: errorTruncation(countTruncated, textTruncated),
|
|
678
|
+
degradedErrorsTextTruncation: createTruncationMetadata(textTruncated, textTruncated ? "serialization_limit" : null),
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
case "component_recovered":
|
|
682
|
+
if (event.component !== "index" && event.component !== "watcher") {
|
|
683
|
+
return markInvalidTransition(status, "[invalid-component]");
|
|
684
|
+
}
|
|
685
|
+
return recoverComponent(status, event.component);
|
|
686
|
+
case "index_recovered":
|
|
687
|
+
return recoverComponent(status, "index");
|
|
688
|
+
case "watcher_recovered":
|
|
689
|
+
return recoverComponent(status, "watcher");
|
|
690
|
+
default:
|
|
691
|
+
return markInvalidTransition(status, "[invalid-event]");
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
function recoverComponent(status, component) {
|
|
695
|
+
const recovered = component === "index"
|
|
696
|
+
? statusWith(status, {
|
|
697
|
+
index: { state: "disabled" },
|
|
698
|
+
causes: removeCause(status.causes, "index_failure"),
|
|
699
|
+
})
|
|
700
|
+
: statusWith(status, {
|
|
701
|
+
watcher: { state: "ready" },
|
|
702
|
+
causes: removeCause(status.causes, "watcher_failure"),
|
|
703
|
+
});
|
|
704
|
+
const state = stateAfterRecovery(recovered);
|
|
705
|
+
return statusWith(recovered, {
|
|
706
|
+
state,
|
|
707
|
+
degradedErrors: state === "degraded" ? recovered.degradedErrors : [],
|
|
708
|
+
degradedErrorsTruncation: state === "degraded" ? recovered.degradedErrorsTruncation : emptyTruncationMetadata(),
|
|
709
|
+
degradedErrorsTextTruncation: state === "degraded" ? recovered.degradedErrorsTextTruncation : emptyTruncationMetadata(),
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
export function projectStatusToProjection(status) {
|
|
713
|
+
const normalized = normalizeProjectStatus(status);
|
|
714
|
+
return {
|
|
715
|
+
project: { ...normalized.project },
|
|
716
|
+
state: normalized.state,
|
|
717
|
+
causes: [...normalized.causes],
|
|
718
|
+
source_count: normalized.sourceCount,
|
|
719
|
+
indexed_count: 0,
|
|
720
|
+
pending_files: [...normalized.pendingFiles],
|
|
721
|
+
pending_files_truncated: normalized.pendingFilesTruncated,
|
|
722
|
+
pending_files_truncation: { ...normalized.pendingFilesTruncation },
|
|
723
|
+
pending_files_filename_truncation: { ...normalized.pendingFilesFilenameTruncation },
|
|
724
|
+
compiler: { ...normalized.compiler },
|
|
725
|
+
index: { state: "disabled" },
|
|
726
|
+
watcher: { ...normalized.watcher },
|
|
727
|
+
last_successful_sync_at: normalized.lastSuccessfulSyncAt,
|
|
728
|
+
last_successful_index_at: null,
|
|
729
|
+
source_snapshot_fingerprint: normalized.sourceSnapshotFingerprint,
|
|
730
|
+
config_snapshot_fingerprint: normalized.configSnapshotFingerprint,
|
|
731
|
+
canonical_snapshot_fingerprint: normalized.canonicalSnapshotFingerprint,
|
|
732
|
+
degraded_errors: [...normalized.degradedErrors],
|
|
733
|
+
degraded_errors_truncation: { ...normalized.degradedErrorsTruncation },
|
|
734
|
+
degraded_errors_text_truncation: { ...normalized.degradedErrorsTextTruncation },
|
|
735
|
+
};
|
|
736
|
+
}
|