@adhisang/minecraft-modding-mcp 4.2.1 → 6.0.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.
Files changed (144) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +5 -4
  3. package/dist/access-transformer-parser.d.ts +2 -1
  4. package/dist/access-transformer-parser.js +10 -1
  5. package/dist/access-widener-parser.d.ts +1 -0
  6. package/dist/access-widener-parser.js +6 -3
  7. package/dist/build-suggested-call.js +31 -10
  8. package/dist/cache-registry.d.ts +8 -0
  9. package/dist/cache-registry.js +50 -32
  10. package/dist/config.d.ts +1 -0
  11. package/dist/config.js +4 -0
  12. package/dist/decompiler/vineflower.js +8 -2
  13. package/dist/entry-tools/analyze-mod-service.d.ts +24 -24
  14. package/dist/entry-tools/analyze-mod-service.js +2 -1
  15. package/dist/entry-tools/analyze-symbol-service.d.ts +57 -22
  16. package/dist/entry-tools/analyze-symbol-service.js +139 -34
  17. package/dist/entry-tools/batch-class-members-service.d.ts +3 -1
  18. package/dist/entry-tools/batch-class-members-service.js +6 -6
  19. package/dist/entry-tools/batch-class-source-service.d.ts +3 -1
  20. package/dist/entry-tools/batch-class-source-service.js +5 -6
  21. package/dist/entry-tools/batch-mappings-service.d.ts +3 -1
  22. package/dist/entry-tools/batch-mappings-service.js +4 -5
  23. package/dist/entry-tools/batch-symbol-exists-service.d.ts +3 -1
  24. package/dist/entry-tools/batch-symbol-exists-service.js +4 -5
  25. package/dist/entry-tools/compare-minecraft-service.d.ts +10 -10
  26. package/dist/entry-tools/compare-minecraft-service.js +1 -1
  27. package/dist/entry-tools/entry-tool-schema.d.ts +1 -1
  28. package/dist/entry-tools/inspect-minecraft/handlers/artifact.js +1 -3
  29. package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +4 -3
  30. package/dist/entry-tools/inspect-minecraft/handlers/class-overview.js +0 -5
  31. package/dist/entry-tools/inspect-minecraft/handlers/class-source.js +0 -1
  32. package/dist/entry-tools/inspect-minecraft/handlers/file.js +26 -2
  33. package/dist/entry-tools/inspect-minecraft/handlers/list-files.js +0 -1
  34. package/dist/entry-tools/inspect-minecraft/handlers/search.d.ts +8 -0
  35. package/dist/entry-tools/inspect-minecraft/handlers/search.js +13 -2
  36. package/dist/entry-tools/inspect-minecraft/handlers/versions.d.ts +1 -1
  37. package/dist/entry-tools/inspect-minecraft/handlers/versions.js +3 -2
  38. package/dist/entry-tools/inspect-minecraft/internal.d.ts +90 -89
  39. package/dist/entry-tools/inspect-minecraft/internal.js +32 -14
  40. package/dist/entry-tools/inspect-minecraft-service.d.ts +218 -218
  41. package/dist/entry-tools/inspect-minecraft-service.js +1 -1
  42. package/dist/entry-tools/manage-cache-service.d.ts +4 -4
  43. package/dist/entry-tools/response-contract.d.ts +3 -1
  44. package/dist/entry-tools/response-contract.js +3 -2
  45. package/dist/entry-tools/validate-project/cases/mixin.js +2 -1
  46. package/dist/entry-tools/validate-project/cases/project-summary.js +5 -3
  47. package/dist/entry-tools/validate-project-service.d.ts +42 -36
  48. package/dist/entry-tools/validate-project-service.js +3 -1
  49. package/dist/entry-tools/verify-mixin-target-service.d.ts +30 -0
  50. package/dist/entry-tools/verify-mixin-target-service.js +120 -29
  51. package/dist/error-mapping.d.ts +51 -0
  52. package/dist/error-mapping.js +157 -2
  53. package/dist/index.js +147 -76
  54. package/dist/java-process.js +1 -1
  55. package/dist/json-rpc-framing.d.ts +4 -0
  56. package/dist/json-rpc-framing.js +23 -1
  57. package/dist/mapping/internal-types.d.ts +24 -0
  58. package/dist/mapping/loaders/tiny-maven.js +46 -20
  59. package/dist/mapping/lookup.d.ts +13 -2
  60. package/dist/mapping/lookup.js +60 -2
  61. package/dist/mapping/types.d.ts +6 -0
  62. package/dist/mapping-service.d.ts +26 -2
  63. package/dist/mapping-service.js +212 -55
  64. package/dist/mcp-helpers.d.ts +1 -0
  65. package/dist/mcp-helpers.js +25 -3
  66. package/dist/minecraft-explorer-service.js +24 -11
  67. package/dist/mixin/access-validators.js +33 -1
  68. package/dist/mixin/annotation-validators.js +73 -9
  69. package/dist/mixin/parsed-validator.js +3 -2
  70. package/dist/mixin-parser.js +119 -7
  71. package/dist/mod-decompile-service.d.ts +2 -0
  72. package/dist/mod-decompile-service.js +21 -3
  73. package/dist/mod-remap-service.d.ts +20 -1
  74. package/dist/mod-remap-service.js +101 -45
  75. package/dist/mojang-tiny-mapping-service.js +5 -2
  76. package/dist/nbt/java-nbt-codec.js +46 -16
  77. package/dist/nbt/json-patch.js +51 -12
  78. package/dist/nbt/typed-json.d.ts +1 -0
  79. package/dist/nbt/typed-json.js +12 -2
  80. package/dist/nbt/types.d.ts +2 -2
  81. package/dist/observability.d.ts +6 -0
  82. package/dist/observability.js +10 -0
  83. package/dist/page-cursor.d.ts +17 -0
  84. package/dist/page-cursor.js +0 -0
  85. package/dist/path-converter.js +10 -3
  86. package/dist/registry-service.d.ts +2 -0
  87. package/dist/registry-service.js +41 -2
  88. package/dist/repo-downloader.js +4 -3
  89. package/dist/resources.js +45 -9
  90. package/dist/response-utils.d.ts +29 -4
  91. package/dist/response-utils.js +95 -5
  92. package/dist/source/access-validate.js +12 -2
  93. package/dist/source/artifact-resolver.js +2 -2
  94. package/dist/source/class-source/members-builder.d.ts +44 -1
  95. package/dist/source/class-source/members-builder.js +51 -4
  96. package/dist/source/class-source/snippet-builder.d.ts +13 -0
  97. package/dist/source/class-source/snippet-builder.js +54 -4
  98. package/dist/source/class-source-helpers.d.ts +4 -4
  99. package/dist/source/class-source-helpers.js +77 -20
  100. package/dist/source/class-source.d.ts +19 -0
  101. package/dist/source/class-source.js +144 -40
  102. package/dist/source/file-access.js +3 -2
  103. package/dist/source/indexer.js +29 -9
  104. package/dist/source/lifecycle/mapping-helpers.js +28 -3
  105. package/dist/source/lifecycle/runtime-check.js +20 -6
  106. package/dist/source/search.d.ts +1 -1
  107. package/dist/source/search.js +117 -30
  108. package/dist/source/validate-mixin/pipeline/resolve.js +23 -3
  109. package/dist/source/validate-mixin.d.ts +2 -1
  110. package/dist/source/validate-mixin.js +45 -4
  111. package/dist/source/workspace-target.js +17 -10
  112. package/dist/source-jar-reader.d.ts +54 -1
  113. package/dist/source-jar-reader.js +121 -10
  114. package/dist/source-resolver.js +2 -2
  115. package/dist/source-service.d.ts +28 -7
  116. package/dist/source-service.js +9 -1
  117. package/dist/stdio-supervisor.d.ts +2 -2
  118. package/dist/stdio-supervisor.js +29 -9
  119. package/dist/storage/db.js +24 -1
  120. package/dist/storage/files-repo.d.ts +15 -0
  121. package/dist/storage/files-repo.js +67 -37
  122. package/dist/storage/symbols-repo.d.ts +15 -0
  123. package/dist/storage/symbols-repo.js +53 -19
  124. package/dist/symbols/symbol-extractor.js +35 -6
  125. package/dist/text-truncate.d.ts +7 -0
  126. package/dist/text-truncate.js +21 -0
  127. package/dist/tiny-remapper-resolver.js +10 -0
  128. package/dist/tool-contract-manifest.js +2 -2
  129. package/dist/tool-execution-gate.js +12 -1
  130. package/dist/tool-guidance.d.ts +6 -2
  131. package/dist/tool-guidance.js +63 -13
  132. package/dist/tool-schemas.d.ts +561 -496
  133. package/dist/tool-schemas.js +175 -139
  134. package/dist/types.d.ts +1 -0
  135. package/dist/version-diff-service.js +5 -0
  136. package/dist/version-service.d.ts +3 -0
  137. package/dist/version-service.js +18 -2
  138. package/dist/warning-details.d.ts +30 -0
  139. package/dist/warning-details.js +80 -0
  140. package/dist/workspace-mapping-service.js +9 -0
  141. package/docs/README-ja.md +2 -2
  142. package/docs/examples.md +26 -2
  143. package/docs/tool-reference.md +22 -17
  144. package/package.json +1 -1
@@ -3,6 +3,15 @@ import { createError, ERROR_CODES } from "./errors.js";
3
3
  import { log } from "./logger.js";
4
4
  import { isSecureJarEntryPath } from "./path-resolver.js";
5
5
  const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
6
+ // Test-only instrumentation: counts real jar opens at the single openZipFile
7
+ // chokepoint so perf tests can assert N-opens-per-call collapses to 1.
8
+ let zipOpenCount = 0;
9
+ export function __getZipOpenCount() {
10
+ return zipOpenCount;
11
+ }
12
+ export function __resetZipOpenCount() {
13
+ zipOpenCount = 0;
14
+ }
6
15
  function toErrorMessage(value) {
7
16
  if (value instanceof Error) {
8
17
  return value.message;
@@ -23,7 +32,7 @@ export function hasJavaSourceExtension(entryPath) {
23
32
  }
24
33
  return true;
25
34
  }
26
- function openZipFile(jarPath) {
35
+ export function openZipFile(jarPath) {
27
36
  return new Promise((resolve, reject) => {
28
37
  yauzl.open(jarPath, {
29
38
  lazyEntries: true,
@@ -33,6 +42,7 @@ function openZipFile(jarPath) {
33
42
  reject(new Error(`Failed to read jar "${jarPath}": ${toErrorMessage(error)}`));
34
43
  return;
35
44
  }
45
+ zipOpenCount += 1;
36
46
  resolve(zipFile);
37
47
  });
38
48
  });
@@ -193,6 +203,66 @@ export async function readJarEntryAsBuffer(jarPath, entryPath) {
193
203
  }
194
204
  });
195
205
  }
206
+ /**
207
+ * Opens a jar ONCE, drains its central directory into a name->entry index, and
208
+ * serves repeated entry reads via O(1) lookup + openReadStream. Use this when a
209
+ * single logical operation reads many entries from the same jar (e.g. walking a
210
+ * class hierarchy) instead of calling {@link readJarEntryAsBuffer} per entry,
211
+ * which re-opens and re-scans the jar each time. Callers MUST close() the reader
212
+ * (in a finally) to release the file descriptor. getEntryBuffer throws the same
213
+ * INVALID_INPUT (unsafe path) / SOURCE_NOT_FOUND (missing entry) errors as
214
+ * readJarEntryAsBuffer; duplicate entry names resolve to the first occurrence.
215
+ */
216
+ export async function createJarEntryReader(jarPath) {
217
+ const zipFile = await openZipFile(jarPath);
218
+ let closed = false;
219
+ const index = new Map();
220
+ try {
221
+ while (true) {
222
+ const entry = await readNextEntry(zipFile);
223
+ if (!entry) {
224
+ break;
225
+ }
226
+ if (!isSecureJarEntryPath(entry.fileName)) {
227
+ continue;
228
+ }
229
+ if (!index.has(entry.fileName)) {
230
+ index.set(entry.fileName, entry);
231
+ }
232
+ }
233
+ }
234
+ catch (error) {
235
+ zipFile.close();
236
+ throw error;
237
+ }
238
+ return {
239
+ async getEntryBuffer(entryPath) {
240
+ const normalizedTargetPath = entryPath.replaceAll("\\", "/");
241
+ if (!isSecureJarEntryPath(normalizedTargetPath)) {
242
+ throw createError({
243
+ code: ERROR_CODES.INVALID_INPUT,
244
+ message: `Entry path "${normalizedTargetPath}" is not allowed.`,
245
+ details: { jarPath, entryPath: normalizedTargetPath }
246
+ });
247
+ }
248
+ const entry = index.get(normalizedTargetPath);
249
+ if (!entry) {
250
+ throw createError({
251
+ code: ERROR_CODES.SOURCE_NOT_FOUND,
252
+ message: `Entry "${normalizedTargetPath}" was not found in "${jarPath}".`,
253
+ details: { jarPath, entryPath: normalizedTargetPath }
254
+ });
255
+ }
256
+ return readEntryStream(zipFile, entry, jarPath);
257
+ },
258
+ close() {
259
+ if (!closed) {
260
+ closed = true;
261
+ zipFile.close();
262
+ }
263
+ }
264
+ };
265
+ }
196
266
  export async function collectMatchedJarEntriesAsUtf8(jarPath, predicate, options = {}) {
197
267
  return withZipFile(jarPath, async (zipFile) => {
198
268
  const entries = [];
@@ -226,6 +296,48 @@ export async function collectMatchedJarEntriesAsUtf8(jarPath, predicate, options
226
296
  }
227
297
  });
228
298
  }
299
+ /**
300
+ * Raw-buffer counterpart of {@link collectMatchedJarEntriesAsUtf8}: opens the jar
301
+ * ONCE and returns the matched entries as raw Buffers (no UTF-8 decode), so binary
302
+ * inputs like `.class` bytes survive intact. Used to avoid re-opening the jar per
303
+ * sampled entry. Mirrors the sibling's maxBytes/maxEntries/continueOnError semantics
304
+ * and its finally-close, but takes an injectable open seam for open-count assertions.
305
+ */
306
+ export async function collectMatchedJarEntriesAsBuffers(jarPath, predicate, options = {}, deps = {}) {
307
+ const open = deps.openZipFile ?? openZipFile;
308
+ const zipFile = await open(jarPath);
309
+ try {
310
+ const entries = [];
311
+ const maxEntries = options.maxEntries == null ? undefined : Math.max(1, Math.trunc(options.maxEntries));
312
+ while (true) {
313
+ const entry = await readNextEntry(zipFile);
314
+ if (!entry) {
315
+ return entries;
316
+ }
317
+ if (!isSecureJarEntryPath(entry.fileName)) {
318
+ continue;
319
+ }
320
+ if (!predicate(entry.fileName)) {
321
+ continue;
322
+ }
323
+ try {
324
+ const contentBuffer = await readEntryStream(zipFile, entry, jarPath, options.maxBytes);
325
+ entries.push({ filePath: entry.fileName, content: contentBuffer });
326
+ if (maxEntries != null && entries.length >= maxEntries) {
327
+ return entries;
328
+ }
329
+ }
330
+ catch (error) {
331
+ if (!options.continueOnError) {
332
+ throw error;
333
+ }
334
+ }
335
+ }
336
+ }
337
+ finally {
338
+ zipFile.close();
339
+ }
340
+ }
229
341
  export async function* iterateJavaEntriesAsUtf8(jarPath, maxBytes) {
230
342
  const zipFile = await openZipFile(jarPath);
231
343
  try {
@@ -285,7 +397,7 @@ function countMatches(input, pattern) {
285
397
  }
286
398
  return count;
287
399
  }
288
- export async function detectFabricLikeInputNamespace(inputJar) {
400
+ export async function detectFabricLikeInputNamespace(inputJar, deps = {}) {
289
401
  const warnings = [];
290
402
  const classEntries = (await listJarEntries(inputJar))
291
403
  .filter((entry) => entry.endsWith(".class"))
@@ -297,16 +409,15 @@ export async function detectFabricLikeInputNamespace(inputJar) {
297
409
  warnings
298
410
  };
299
411
  }
412
+ // Read all sampled class entries in a SINGLE jar open instead of re-opening the
413
+ // jar once per entry. The sample set and latin1 decode are unchanged, so scores —
414
+ // and therefore the detected namespace — are identical.
415
+ const sampleSet = new Set(classEntries);
416
+ const matched = await collectMatchedJarEntriesAsBuffers(inputJar, (name) => sampleSet.has(name), { continueOnError: true }, deps);
300
417
  let mojangScore = 0;
301
418
  let intermediaryScore = 0;
302
- for (const entry of classEntries) {
303
- let text = "";
304
- try {
305
- text = (await readJarEntryAsBuffer(inputJar, entry)).toString("latin1");
306
- }
307
- catch {
308
- continue;
309
- }
419
+ for (const { content } of matched) {
420
+ const text = content.toString("latin1");
310
421
  mojangScore += countMatches(text, /net\/minecraft\/(?:advancements|client|commands|core|data|gametest|nbt|network|recipe|resources|server|sounds|stats|tags|util|world)\//g) * 3;
311
422
  intermediaryScore += countMatches(text, /net\/minecraft\/class_\d+/g) * 3;
312
423
  intermediaryScore += countMatches(text, /\b(?:method|field)_\d+\b/g);
@@ -260,7 +260,7 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
260
260
  ? false
261
261
  : download.statusCode !== 404 && isTransientFailure(download.statusCode);
262
262
  sawRemoteRepoFailure = sawRemoteRepoFailure || transient;
263
- if (hasNextAttempt && transient) {
263
+ if (hasNextAttempt && (transient || download.ok)) {
264
264
  options.onRepoFailover?.({
265
265
  stage: "source",
266
266
  repoUrl: sourceUrl,
@@ -320,7 +320,7 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
320
320
  ? false
321
321
  : downloaded.statusCode !== 404 && isTransientFailure(downloaded.statusCode);
322
322
  sawRemoteRepoFailure = sawRemoteRepoFailure || transient;
323
- if (hasNextAttempt && transient) {
323
+ if (hasNextAttempt && (transient || downloaded.ok)) {
324
324
  options.onRepoFailover?.({
325
325
  stage: "binary",
326
326
  repoUrl: binaryUrl,
@@ -11,6 +11,7 @@ import * as indexer from "./source/indexer.js";
11
11
  import * as workspaceTarget from "./source/workspace-target.js";
12
12
  import * as validateMixinModule from "./source/validate-mixin.js";
13
13
  import * as artifactResolver from "./source/artifact-resolver.js";
14
+ import type { WireMembersBlock } from "./source/class-source/members-builder.js";
14
15
  import { type StageEmitter } from "./stage-emitter.js";
15
16
  import { WorkspaceMappingService, type WorkspaceCompileMappingOutput } from "./workspace-mapping-service.js";
16
17
  import { type WorkspaceContextCache } from "./workspace-context-cache.js";
@@ -30,7 +31,8 @@ export type ResolveArtifactInput = {
30
31
  scope?: ArtifactScope;
31
32
  preferProjectVersion?: boolean;
32
33
  strictVersion?: boolean;
33
- compact?: boolean;
34
+ /** When true, compute the optional `sampleEntries` listing (detail=full / include=samples). */
35
+ includeSampleEntries?: boolean;
34
36
  };
35
37
  export type ResolveArtifactOutput = {
36
38
  artifactId: string;
@@ -112,6 +114,8 @@ export type SearchClassSourceInput = {
112
114
  export type SearchClassSourceOutput = {
113
115
  hits: SearchSourceHit[];
114
116
  nextCursor?: string;
117
+ /** True when a provided cursor was malformed or belonged to a different query and was ignored (results restart from page one). */
118
+ cursorIgnored?: boolean;
115
119
  mappingApplied: SourceMapping;
116
120
  returnedNamespace: SourceMapping;
117
121
  artifactContents: ArtifactContentsSummary;
@@ -205,6 +209,10 @@ export type GetClassSourceOutput = {
205
209
  };
206
210
  truncated: boolean;
207
211
  charsTruncated?: boolean;
212
+ /** True when startLine begins past EOF; the returned range is empty (not clamped). */
213
+ outOfRange?: boolean;
214
+ /** First not-yet-returned line when truncated; pass back as startLine to continue. */
215
+ nextStartLine?: number;
208
216
  origin: ResolvedSourceArtifact["origin"];
209
217
  artifactId: string;
210
218
  requestedMapping: SourceMapping;
@@ -213,6 +221,11 @@ export type GetClassSourceOutput = {
213
221
  provenance: ArtifactProvenance;
214
222
  qualityFlags: string[];
215
223
  artifactContents: ArtifactContentsSummary;
224
+ /** Replayable get-class-source call that reads from nextStartLine (truncated, non-file output only). */
225
+ suggestedCall?: {
226
+ tool: string;
227
+ params: Record<string, unknown>;
228
+ };
216
229
  outputFile?: string;
217
230
  warnings: string[];
218
231
  };
@@ -245,11 +258,14 @@ export type GetClassMembersInput = {
245
258
  includeInherited?: boolean;
246
259
  memberPattern?: string;
247
260
  maxMembers?: number;
261
+ cursor?: string;
248
262
  projectPath?: string;
249
263
  gradleUserHome?: string;
250
264
  scope?: ArtifactScope;
251
265
  preferProjectVersion?: boolean;
252
266
  strictVersion?: boolean;
267
+ /** When true, keep jvmDescriptor on FIELD members (always kept on methods/constructors). */
268
+ includeDescriptors?: boolean;
253
269
  };
254
270
  export type DecompiledMember = {
255
271
  name: string;
@@ -265,11 +281,7 @@ export type DecompiledFallback = {
265
281
  export type GetClassMembersStatus = "ok" | "members_unavailable" | "partial";
266
282
  export type GetClassMembersOutput = {
267
283
  className: string;
268
- members: {
269
- constructors: SignatureMember[];
270
- fields: SignatureMember[];
271
- methods: SignatureMember[];
272
- };
284
+ members: WireMembersBlock;
273
285
  counts: {
274
286
  constructors: number;
275
287
  fields: number;
@@ -277,6 +289,10 @@ export type GetClassMembersOutput = {
277
289
  total: number;
278
290
  };
279
291
  truncated: boolean;
292
+ /** Continuation cursor when more members remain; pass back as cursor to fetch the next page. */
293
+ nextCursor?: string;
294
+ /** True when a provided cursor was malformed or belonged to a different query and was ignored. */
295
+ cursorIgnored?: boolean;
280
296
  context: ExplorerResponseContext;
281
297
  origin: ResolvedSourceArtifact["origin"];
282
298
  artifactId: string;
@@ -420,7 +436,12 @@ export type ValidateMixinInput = {
420
436
  path: string;
421
437
  };
422
438
  sourceRoots?: string[];
423
- version: string;
439
+ /**
440
+ * Optional when input.mode='project', or preferProjectVersion=true with a
441
+ * projectPath set: the dispatcher detects the Minecraft version from
442
+ * gradle.properties before any version-dependent stage runs.
443
+ */
444
+ version?: string;
424
445
  mapping?: SourceMapping;
425
446
  sourcePriority?: MappingSourcePriority;
426
447
  scope?: ArtifactScope;
@@ -141,7 +141,15 @@ export class SourceService {
141
141
  }
142
142
  async checkSymbolExists(input) {
143
143
  const result = await this.mappingService.checkSymbolExists(input);
144
- if (result.status !== "mapping_unavailable" ||
144
+ // On unobfuscated versions the mapping graph is empty, so the mapping service can
145
+ // only ever report symbols as missing. We then validate against runtime bytecode.
146
+ // Classes/fields surface as `mapping_unavailable` when the graph yields nothing, but
147
+ // a method query with a non-empty graph returns `not_found` (empty member lookup) —
148
+ // which previously skipped the runtime fallback and produced false negatives for
149
+ // methods that genuinely exist (B1). Treat `not_found` methods as fallback-eligible.
150
+ const fallbackEligible = result.status === "mapping_unavailable" ||
151
+ (result.status === "not_found" && input.kind === "method");
152
+ if (!fallbackEligible ||
145
153
  !isUnobfuscatedVersion(input.version) ||
146
154
  (input.sourceMapping !== "mojang" && input.sourceMapping !== "obfuscated")) {
147
155
  return result;
@@ -90,8 +90,8 @@ export declare class StdioSupervisor {
90
90
  private readonly handleWorkerData;
91
91
  private readonly handleWorkerStdinError;
92
92
  private readonly handleWorkerStderr;
93
- private readonly handleWorkerProcessError;
94
- private readonly handleWorkerExit;
93
+ private handleWorkerProcessError;
94
+ private handleWorkerExit;
95
95
  private handleWorkerMessage;
96
96
  private applyStageUpdate;
97
97
  private handleWorkerReady;
@@ -408,6 +408,13 @@ export class StdioSupervisor {
408
408
  if (this.shuttingDown) {
409
409
  return;
410
410
  }
411
+ const stale = this.child;
412
+ if (stale) {
413
+ this.detachChild();
414
+ if (stale.exitCode === null) {
415
+ stale.kill("SIGTERM");
416
+ }
417
+ }
411
418
  const child = spawn(process.execPath, [...process.execArgv, this.entryFile], {
412
419
  env: {
413
420
  ...process.env,
@@ -423,8 +430,8 @@ export class StdioSupervisor {
423
430
  child.stdout.on("data", this.handleWorkerData);
424
431
  child.stderr.on("data", this.handleWorkerStderr);
425
432
  child.stdin.on("error", this.handleWorkerStdinError);
426
- child.once("error", this.handleWorkerProcessError);
427
- child.once("exit", this.handleWorkerExit);
433
+ child.once("error", (error) => this.handleWorkerProcessError(child, error));
434
+ child.once("exit", (code, signal) => this.handleWorkerExit(child, code, signal));
428
435
  log("info", "supervisor.worker_spawn", { pid: child.pid });
429
436
  }
430
437
  handleWorkerData = (chunk) => {
@@ -455,10 +462,23 @@ export class StdioSupervisor {
455
462
  process.stderr.write(`${line}\n`);
456
463
  }
457
464
  };
458
- handleWorkerProcessError = (error) => {
465
+ handleWorkerProcessError(child, error) {
459
466
  log("error", "supervisor.worker_process_error", { message: error.message });
460
- };
461
- handleWorkerExit = (code, signal) => {
467
+ if (child !== this.child) {
468
+ return;
469
+ }
470
+ if (child.exitCode === null && child.signalCode === null) {
471
+ child.kill("SIGTERM");
472
+ }
473
+ this.handleWorkerExit(child, null, null);
474
+ }
475
+ handleWorkerExit(child, code, signal) {
476
+ if (child !== this.child) {
477
+ if (child.exitCode === null && child.signalCode === null) {
478
+ child.kill("SIGTERM");
479
+ }
480
+ return;
481
+ }
462
482
  const childPid = this.child?.pid;
463
483
  this.detachChild();
464
484
  if (this.shuttingDown) {
@@ -472,7 +492,7 @@ export class StdioSupervisor {
472
492
  });
473
493
  this.failPendingRequestsOnWorkerExit({ code, signal });
474
494
  this.scheduleRestart();
475
- };
495
+ }
476
496
  handleWorkerMessage(message) {
477
497
  debugSupervisor("worker_message", {
478
498
  hasMethod: "method" in message,
@@ -580,7 +600,7 @@ export class StdioSupervisor {
580
600
  }
581
601
  }
582
602
  failPendingRequestsOnWorkerExit(exit) {
583
- const preservedInitializeKey = this.initializeRequest && !this.clientInitialized
603
+ const preservedInitializeKey = this.initializeRequest
584
604
  ? requestKey(this.initializeRequest.id)
585
605
  : undefined;
586
606
  const now = performance.now();
@@ -635,8 +655,8 @@ export class StdioSupervisor {
635
655
  child.stdout.off("data", this.handleWorkerData);
636
656
  child.stderr.off("data", this.handleWorkerStderr);
637
657
  child.stdin.off("error", this.handleWorkerStdinError);
638
- child.off("error", this.handleWorkerProcessError);
639
- child.off("exit", this.handleWorkerExit);
658
+ child.removeAllListeners("error");
659
+ child.removeAllListeners("exit");
640
660
  this.child = undefined;
641
661
  this.childReady = false;
642
662
  }
@@ -14,7 +14,7 @@ function runIntegrityCheck(db) {
14
14
  throw createError({
15
15
  code: ERROR_CODES.DB_FAILURE,
16
16
  message: "SQLite integrity check failed.",
17
- details: { integrityCheck: result }
17
+ details: { reason: "integrity_check_failed", integrityCheck: result }
18
18
  });
19
19
  }
20
20
  }
@@ -47,6 +47,22 @@ function isSchemaVersionMismatchError(error) {
47
47
  return (error.details?.reason === "schema_version_unsupported" ||
48
48
  error.details?.reason === "schema_version_invalid");
49
49
  }
50
+ const SQLITE_CORRUPT_ERRCODE = 11;
51
+ const SQLITE_NOTADB_ERRCODE = 26;
52
+ function isCorruptionError(error) {
53
+ if (isAppError(error)) {
54
+ return (error.code === ERROR_CODES.DB_FAILURE && error.details?.reason === "integrity_check_failed");
55
+ }
56
+ const sqliteError = error;
57
+ if (sqliteError?.code === "SQLITE_CORRUPT" || sqliteError?.code === "SQLITE_NOTADB") {
58
+ return true;
59
+ }
60
+ if (typeof sqliteError?.errcode !== "number") {
61
+ return false;
62
+ }
63
+ const primaryErrcode = sqliteError.errcode & 0xff;
64
+ return primaryErrcode === SQLITE_CORRUPT_ERRCODE || primaryErrcode === SQLITE_NOTADB_ERRCODE;
65
+ }
50
66
  function buildDefaultLogger() {
51
67
  return {
52
68
  warn: (message, details) => {
@@ -104,6 +120,13 @@ export function openDatabase(config, logger = buildDefaultLogger()) {
104
120
  throw caughtError;
105
121
  }
106
122
  if (!isMissingPath(config.sqlitePath)) {
123
+ if (!isCorruptionError(caughtError)) {
124
+ logger.error("SQLite initialization failed", {
125
+ path: config.sqlitePath,
126
+ reason: errorMessage
127
+ });
128
+ throw caughtError;
129
+ }
107
130
  const backupPath = backupCorruptedDb(config.sqlitePath);
108
131
  logger.warn("SQLite database integrity check failed. Recreated database after backup", {
109
132
  sqlitePath: config.sqlitePath,
@@ -47,10 +47,14 @@ export declare class FilesRepo {
47
47
  private readonly listStmt;
48
48
  private readonly listRowsStmt;
49
49
  private readonly searchPathStmt;
50
+ private readonly searchContentLikePathsStmt;
50
51
  private readonly searchFtsStmt;
51
52
  private readonly getByPathsStmtCache;
52
53
  private readonly classLookupStmtCache;
54
+ private readonly dynamicStmtCache;
55
+ private static readonly DYNAMIC_STMT_CACHE_MAX;
53
56
  constructor(db: SqliteDatabase);
57
+ private prepareCached;
54
58
  clearFilesForArtifact(artifactId: string): void;
55
59
  insertFilesForArtifact(artifactId: string, files: IndexedFile[]): void;
56
60
  replaceFilesForArtifact(artifactId: string, files: IndexedFile[]): void;
@@ -58,6 +62,17 @@ export declare class FilesRepo {
58
62
  getFileContent(artifactId: string, filePath: string): FileRow | undefined;
59
63
  listFiles(artifactId: string, options: ListFilesOptions): PagedResult<string>;
60
64
  listFileRows(artifactId: string, options: ListFileRowsOptions): PagedResult<FileRow>;
65
+ /**
66
+ * Narrow the literal/contains text-scan candidate set without hydrating content:
67
+ * returns the file_paths whose content matches `content LIKE %needle%` (ASCII
68
+ * case-insensitive). The result is a SUPERSET of the JS contains/exact match set
69
+ * for an ASCII needle, so callers MUST still run the exact JS post-verify. Only
70
+ * file_path is selected — content stays in the DB until getFileContentsByPaths.
71
+ */
72
+ searchContentLikeCandidatePaths(artifactId: string, needle: string, limit: number): {
73
+ filePaths: string[];
74
+ scannedRows: number;
75
+ };
61
76
  getFileContentsByPaths(artifactId: string, filePaths: string[]): FileRow[];
62
77
  searchFileCandidates(artifactId: string, options: SearchFilesOptions): PagedResult<SearchFileCandidateResult> & {
63
78
  scannedRows: number;