@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
@@ -1,5 +1,6 @@
1
+ import { randomBytes } from "node:crypto";
1
2
  import { existsSync } from "node:fs";
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
4
  import { dirname, join } from "node:path";
4
5
  import { createError, ERROR_CODES } from "./errors.js";
5
6
  import { log } from "./logger.js";
@@ -339,7 +340,9 @@ export async function resolveMojangTinyFile(version, config, deps = {}) {
339
340
  const membersByOwner = normalizeMemberMappings(parsedMappings, classMap, warnings);
340
341
  const tinyContent = renderTinyV2(classMap, membersByOwner);
341
342
  await mkdir(dirname(cachedTiny), { recursive: true });
342
- await writeFile(cachedTiny, tinyContent, "utf8");
343
+ const tempTiny = `${cachedTiny}.${randomBytes(4).toString("hex")}.tmp`;
344
+ await writeFile(tempTiny, tinyContent, "utf8");
345
+ await rename(tempTiny, cachedTiny);
343
346
  log("info", "mojang-tiny.generated", {
344
347
  version,
345
348
  path: cachedTiny,
@@ -98,6 +98,9 @@ function decodeMutf8(buffer, start, end) {
98
98
  throw parseError("Truncated MUTF-8 2-byte sequence.", { offset: i - 1 });
99
99
  }
100
100
  const b1 = buffer[i++];
101
+ if ((b1 & 0xc0) !== 0x80) {
102
+ throw parseError("Invalid MUTF-8 continuation byte.", { byte: b1, offset: i - 1 });
103
+ }
101
104
  codeUnits.push(((b0 & 0x1f) << 6) | (b1 & 0x3f));
102
105
  }
103
106
  else if ((b0 & 0xf0) === 0xe0) {
@@ -106,6 +109,9 @@ function decodeMutf8(buffer, start, end) {
106
109
  }
107
110
  const b1 = buffer[i++];
108
111
  const b2 = buffer[i++];
112
+ if ((b1 & 0xc0) !== 0x80 || (b2 & 0xc0) !== 0x80) {
113
+ throw parseError("Invalid MUTF-8 continuation byte.", { offset: i - 2 });
114
+ }
109
115
  codeUnits.push(((b0 & 0x0f) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f));
110
116
  }
111
117
  else {
@@ -219,12 +225,12 @@ class NbtWriter {
219
225
  }
220
226
  writeFloat32(value) {
221
227
  const chunk = Buffer.allocUnsafe(4);
222
- chunk.writeFloatBE(value, 0);
228
+ chunk.writeFloatBE(Number(value), 0);
223
229
  this.chunks.push(chunk);
224
230
  }
225
231
  writeFloat64(value) {
226
232
  const chunk = Buffer.allocUnsafe(8);
227
- chunk.writeDoubleBE(value, 0);
233
+ chunk.writeDoubleBE(Number(value), 0);
228
234
  this.chunks.push(chunk);
229
235
  }
230
236
  writeInt64(value) {
@@ -232,6 +238,27 @@ class NbtWriter {
232
238
  chunk.writeBigInt64BE(value, 0);
233
239
  this.chunks.push(chunk);
234
240
  }
241
+ writeInt8Array(values) {
242
+ const chunk = Buffer.allocUnsafe(values.length);
243
+ for (let i = 0; i < values.length; i += 1) {
244
+ chunk.writeInt8(values[i], i);
245
+ }
246
+ this.chunks.push(chunk);
247
+ }
248
+ writeInt32Array(values) {
249
+ const chunk = Buffer.allocUnsafe(values.length * 4);
250
+ for (let i = 0; i < values.length; i += 1) {
251
+ chunk.writeInt32BE(values[i], i * 4);
252
+ }
253
+ this.chunks.push(chunk);
254
+ }
255
+ writeInt64Array(values) {
256
+ const chunk = Buffer.allocUnsafe(values.length * 8);
257
+ for (let i = 0; i < values.length; i += 1) {
258
+ chunk.writeBigInt64BE(values[i], i * 8);
259
+ }
260
+ this.chunks.push(chunk);
261
+ }
235
262
  writeString(value) {
236
263
  const encoded = encodeMutf8(value);
237
264
  if (encoded.length > 0xffff) {
@@ -255,10 +282,14 @@ function readPayload(reader, tagId, pointer) {
255
282
  return { type: "int", value: reader.readInt32() };
256
283
  case "long":
257
284
  return { type: "long", value: reader.readInt64().toString() };
258
- case "float":
259
- return { type: "float", value: reader.readFloat32() };
260
- case "double":
261
- return { type: "double", value: reader.readFloat64() };
285
+ case "float": {
286
+ const value = reader.readFloat32();
287
+ return { type: "float", value: Number.isFinite(value) ? value : Number.isNaN(value) ? "NaN" : value > 0 ? "Infinity" : "-Infinity" };
288
+ }
289
+ case "double": {
290
+ const value = reader.readFloat64();
291
+ return { type: "double", value: Number.isFinite(value) ? value : Number.isNaN(value) ? "NaN" : value > 0 ? "Infinity" : "-Infinity" };
292
+ }
262
293
  case "byteArray": {
263
294
  const length = reader.readInt32();
264
295
  if (length < 0) {
@@ -309,7 +340,12 @@ function readPayload(reader, tagId, pointer) {
309
340
  break;
310
341
  }
311
342
  const childName = reader.readString();
312
- value[childName] = readPayload(reader, childType, `${pointer}/value/${childName}`);
343
+ Object.defineProperty(value, childName, {
344
+ value: readPayload(reader, childType, `${pointer}/value/${childName}`),
345
+ enumerable: true,
346
+ writable: true,
347
+ configurable: true
348
+ });
313
349
  }
314
350
  return { type: "compound", value };
315
351
  }
@@ -374,9 +410,7 @@ function writePayload(writer, node, pointer) {
374
410
  return;
375
411
  case "byteArray":
376
412
  writer.writeInt32(node.value.length);
377
- for (const value of node.value) {
378
- writer.writeInt8(value);
379
- }
413
+ writer.writeInt8Array(node.value);
380
414
  return;
381
415
  case "string":
382
416
  writer.writeString(node.value);
@@ -398,15 +432,11 @@ function writePayload(writer, node, pointer) {
398
432
  return;
399
433
  case "intArray":
400
434
  writer.writeInt32(node.value.length);
401
- for (const value of node.value) {
402
- writer.writeInt32(value);
403
- }
435
+ writer.writeInt32Array(node.value);
404
436
  return;
405
437
  case "longArray":
406
438
  writer.writeInt32(node.value.length);
407
- for (const value of node.value) {
408
- writer.writeInt64(BigInt(value));
409
- }
439
+ writer.writeInt64Array(node.value.map((value) => BigInt(value)));
410
440
  return;
411
441
  default:
412
442
  throw encodeError("Unsupported typed NBT node for encoding.", {
@@ -1,6 +1,6 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
2
  import { createError, ERROR_CODES } from "../errors.js";
3
- import { assertValidTypedNbtDocument, validateTypedNbtDocument } from "./typed-json.js";
3
+ import { assertValidTypedNbtDocument, validateTypedNbtDocument, validateTypedNbtNode } from "./typed-json.js";
4
4
  function isRecord(value) {
5
5
  return (typeof value === "object" &&
6
6
  value !== null &&
@@ -136,6 +136,9 @@ function parseArrayIndex(token, length, options) {
136
136
  }
137
137
  return index;
138
138
  }
139
+ function isTypedNbtNode(value) {
140
+ return isRecord(value) && typeof value.type === "string";
141
+ }
139
142
  function resolveParent(root, tokens, opIndex, path) {
140
143
  if (tokens.length === 0) {
141
144
  patchConflict("Operation path does not reference a child location.", {
@@ -144,6 +147,8 @@ function resolveParent(root, tokens, opIndex, path) {
144
147
  });
145
148
  }
146
149
  let cursor = root;
150
+ let enclosingNode;
151
+ let enclosingPointer = "";
147
152
  for (let i = 0; i < tokens.length - 1; i += 1) {
148
153
  const token = tokens[i];
149
154
  const pointer = `/${tokens.slice(0, i + 1).join("/")}`;
@@ -154,6 +159,10 @@ function resolveParent(root, tokens, opIndex, path) {
154
159
  path: pointer
155
160
  });
156
161
  cursor = cursor[index];
162
+ if (isTypedNbtNode(cursor)) {
163
+ enclosingNode = cursor;
164
+ enclosingPointer = pointer;
165
+ }
157
166
  continue;
158
167
  }
159
168
  if (!isRecord(cursor)) {
@@ -169,8 +178,12 @@ function resolveParent(root, tokens, opIndex, path) {
169
178
  });
170
179
  }
171
180
  cursor = cursor[token];
181
+ if (isTypedNbtNode(cursor)) {
182
+ enclosingNode = cursor;
183
+ enclosingPointer = pointer;
184
+ }
172
185
  }
173
- return { parent: cursor, key: tokens[tokens.length - 1] };
186
+ return { parent: cursor, key: tokens[tokens.length - 1], enclosingNode, enclosingPointer };
174
187
  }
175
188
  function readValueAtPath(root, tokens, opIndex, path) {
176
189
  if (tokens.length === 0) {
@@ -205,8 +218,10 @@ function readValueAtPath(root, tokens, opIndex, path) {
205
218
  }
206
219
  return cursor;
207
220
  }
208
- function assertTypedNbtInvariant(root, opIndex, path) {
209
- const validation = validateTypedNbtDocument(root);
221
+ function assertTypedNbtInvariant(subtree, opIndex, path) {
222
+ const validation = "root" in subtree
223
+ ? validateTypedNbtDocument(subtree.root)
224
+ : validateTypedNbtNode(subtree.node, subtree.pointer);
210
225
  if (!validation.ok) {
211
226
  patchConflict("JSON Patch operation produced invalid typed NBT JSON.", {
212
227
  opIndex,
@@ -241,11 +256,13 @@ export function applyJsonPatch(document, patch) {
241
256
  }
242
257
  if (operation.op === "add") {
243
258
  const nextValue = structuredClone(operation.value);
259
+ let subtree = { root: working };
244
260
  if (tokens.length === 0) {
245
261
  working = nextValue;
262
+ subtree = { root: working };
246
263
  }
247
264
  else {
248
- const { parent, key } = resolveParent(working, tokens, i, operation.path);
265
+ const { parent, key, enclosingNode, enclosingPointer } = resolveParent(working, tokens, i, operation.path);
249
266
  if (Array.isArray(parent)) {
250
267
  const index = parseArrayIndex(key, parent.length, {
251
268
  allowAppend: true,
@@ -255,7 +272,12 @@ export function applyJsonPatch(document, patch) {
255
272
  parent.splice(index, 0, nextValue);
256
273
  }
257
274
  else if (isRecord(parent)) {
258
- parent[key] = nextValue;
275
+ Object.defineProperty(parent, key, {
276
+ value: nextValue,
277
+ enumerable: true,
278
+ writable: true,
279
+ configurable: true
280
+ });
259
281
  }
260
282
  else {
261
283
  patchConflict("Add target parent is not a container.", {
@@ -263,9 +285,13 @@ export function applyJsonPatch(document, patch) {
263
285
  jsonPointer: operation.path
264
286
  });
265
287
  }
288
+ subtree =
289
+ enclosingNode === undefined
290
+ ? { root: working }
291
+ : { node: enclosingNode, pointer: enclosingPointer };
266
292
  }
267
293
  changed = true;
268
- assertTypedNbtInvariant(working, i, operation.path);
294
+ assertTypedNbtInvariant(subtree, i, operation.path);
269
295
  continue;
270
296
  }
271
297
  if (operation.op === "remove") {
@@ -275,7 +301,7 @@ export function applyJsonPatch(document, patch) {
275
301
  jsonPointer: operation.path
276
302
  });
277
303
  }
278
- const { parent, key } = resolveParent(working, tokens, i, operation.path);
304
+ const { parent, key, enclosingNode, enclosingPointer } = resolveParent(working, tokens, i, operation.path);
279
305
  if (Array.isArray(parent)) {
280
306
  const index = parseArrayIndex(key, parent.length, {
281
307
  allowAppend: false,
@@ -300,16 +326,20 @@ export function applyJsonPatch(document, patch) {
300
326
  });
301
327
  }
302
328
  changed = true;
303
- assertTypedNbtInvariant(working, i, operation.path);
329
+ assertTypedNbtInvariant(enclosingNode === undefined
330
+ ? { root: working }
331
+ : { node: enclosingNode, pointer: enclosingPointer }, i, operation.path);
304
332
  continue;
305
333
  }
306
334
  if (operation.op === "replace") {
307
335
  const nextValue = structuredClone(operation.value);
336
+ let subtree = { root: working };
308
337
  if (tokens.length === 0) {
309
338
  working = nextValue;
339
+ subtree = { root: working };
310
340
  }
311
341
  else {
312
- const { parent, key } = resolveParent(working, tokens, i, operation.path);
342
+ const { parent, key, enclosingNode, enclosingPointer } = resolveParent(working, tokens, i, operation.path);
313
343
  if (Array.isArray(parent)) {
314
344
  const index = parseArrayIndex(key, parent.length, {
315
345
  allowAppend: false,
@@ -325,7 +355,12 @@ export function applyJsonPatch(document, patch) {
325
355
  jsonPointer: operation.path
326
356
  });
327
357
  }
328
- parent[key] = nextValue;
358
+ Object.defineProperty(parent, key, {
359
+ value: nextValue,
360
+ enumerable: true,
361
+ writable: true,
362
+ configurable: true
363
+ });
329
364
  }
330
365
  else {
331
366
  patchConflict("Replace target parent is not a container.", {
@@ -333,9 +368,13 @@ export function applyJsonPatch(document, patch) {
333
368
  jsonPointer: operation.path
334
369
  });
335
370
  }
371
+ subtree =
372
+ enclosingNode === undefined
373
+ ? { root: working }
374
+ : { node: enclosingNode, pointer: enclosingPointer };
336
375
  }
337
376
  changed = true;
338
- assertTypedNbtInvariant(working, i, operation.path);
377
+ assertTypedNbtInvariant(subtree, i, operation.path);
339
378
  continue;
340
379
  }
341
380
  }
@@ -5,6 +5,7 @@ type ValidationResult = {
5
5
  ok: false;
6
6
  issue: TypedNbtValidationIssue;
7
7
  };
8
+ export declare function validateTypedNbtNode(value: unknown, pointer: string): ValidationResult;
8
9
  export declare function validateTypedNbtDocument(value: unknown): ValidationResult;
9
10
  export declare function assertValidTypedNbtDocument(value: unknown): asserts value is TypedNbtDocument;
10
11
  export type { TypedNbtDocument, TypedNbtValidationIssue } from "./types.js";
@@ -16,6 +16,7 @@ const NODE_TYPES = [
16
16
  const LIST_ELEMENT_TYPES = ["end", ...NODE_TYPES];
17
17
  const NODE_TYPE_SET = new Set(NODE_TYPES);
18
18
  const LIST_ELEMENT_TYPE_SET = new Set(LIST_ELEMENT_TYPES);
19
+ const NON_FINITE_FLOAT_SENTINELS = new Set(["NaN", "Infinity", "-Infinity"]);
19
20
  const INT64_MIN = -9223372036854775808n;
20
21
  const INT64_MAX = 9223372036854775807n;
21
22
  function isRecord(value) {
@@ -119,8 +120,14 @@ function validateNode(value, pointer) {
119
120
  return validateLongString(node.value, `${pointer}/value`);
120
121
  case "float":
121
122
  case "double":
122
- if (typeof node.value !== "number" || !Number.isFinite(node.value)) {
123
- return fail(`${pointer}/value`, "finite-number", node.value);
123
+ if (typeof node.value === "string") {
124
+ if (!NON_FINITE_FLOAT_SENTINELS.has(node.value)) {
125
+ return fail(`${pointer}/value`, "number-or-non-finite-sentinel", node.value);
126
+ }
127
+ return { ok: true };
128
+ }
129
+ if (typeof node.value !== "number") {
130
+ return fail(`${pointer}/value`, "number-or-non-finite-sentinel", node.value);
124
131
  }
125
132
  return { ok: true };
126
133
  case "string":
@@ -180,6 +187,9 @@ function validateNode(value, pointer) {
180
187
  return fail(`${pointer}/type`, "nbt-node-type", nodeType);
181
188
  }
182
189
  }
190
+ export function validateTypedNbtNode(value, pointer) {
191
+ return validateNode(value, pointer);
192
+ }
183
193
  export function validateTypedNbtDocument(value) {
184
194
  if (!isRecord(value)) {
185
195
  return fail("", "typed-nbt-document", value);
@@ -15,10 +15,10 @@ export type NbtNode = {
15
15
  value: string;
16
16
  } | {
17
17
  type: "float";
18
- value: number;
18
+ value: number | "NaN" | "Infinity" | "-Infinity";
19
19
  } | {
20
20
  type: "double";
21
- value: number;
21
+ value: number | "NaN" | "Infinity" | "-Infinity";
22
22
  } | {
23
23
  type: "byteArray";
24
24
  value: number[];
@@ -40,6 +40,8 @@ export interface RuntimeMetricSnapshot {
40
40
  search_rows_scanned: number;
41
41
  search_rows_returned: number;
42
42
  search_indexed_zero_shortcircuit_count: number;
43
+ search_like_prefilter_count: number;
44
+ search_scan_truncated_count: number;
43
45
  reindex_count: number;
44
46
  reindex_skip_count: number;
45
47
  cache_evictions: number;
@@ -74,6 +76,8 @@ export declare class RuntimeMetrics {
74
76
  private searchRowsScanned;
75
77
  private searchRowsReturned;
76
78
  private searchIndexedZeroShortcircuitCount;
79
+ private searchLikePrefilterCount;
80
+ private searchScanTruncatedCount;
77
81
  private reindexCount;
78
82
  private reindexSkipCount;
79
83
  private cacheEvictions;
@@ -102,6 +106,8 @@ export declare class RuntimeMetrics {
102
106
  recordSearchRowsScanned(count: number): void;
103
107
  recordSearchRowsReturned(count: number): void;
104
108
  recordSearchIndexedZeroShortcircuit(): void;
109
+ recordSearchLikePrefilter(): void;
110
+ recordSearchScanTruncated(): void;
105
111
  recordReindex(): void;
106
112
  recordReindexSkip(): void;
107
113
  recordCacheEviction(count?: number): void;
@@ -26,6 +26,8 @@ export class RuntimeMetrics {
26
26
  searchRowsScanned = 0;
27
27
  searchRowsReturned = 0;
28
28
  searchIndexedZeroShortcircuitCount = 0;
29
+ searchLikePrefilterCount = 0;
30
+ searchScanTruncatedCount = 0;
29
31
  reindexCount = 0;
30
32
  reindexSkipCount = 0;
31
33
  cacheEvictions = 0;
@@ -128,6 +130,12 @@ export class RuntimeMetrics {
128
130
  recordSearchIndexedZeroShortcircuit() {
129
131
  this.searchIndexedZeroShortcircuitCount += 1;
130
132
  }
133
+ recordSearchLikePrefilter() {
134
+ this.searchLikePrefilterCount += 1;
135
+ }
136
+ recordSearchScanTruncated() {
137
+ this.searchScanTruncatedCount += 1;
138
+ }
131
139
  recordReindex() {
132
140
  this.reindexCount += 1;
133
141
  }
@@ -192,6 +200,8 @@ export class RuntimeMetrics {
192
200
  search_rows_scanned: this.searchRowsScanned,
193
201
  search_rows_returned: this.searchRowsReturned,
194
202
  search_indexed_zero_shortcircuit_count: this.searchIndexedZeroShortcircuitCount,
203
+ search_like_prefilter_count: this.searchLikePrefilterCount,
204
+ search_scan_truncated_count: this.searchScanTruncatedCount,
195
205
  reindex_count: this.reindexCount,
196
206
  reindex_skip_count: this.reindexSkipCount,
197
207
  cache_evictions: this.cacheEvictions,
@@ -0,0 +1,17 @@
1
+ export type OffsetCursor = {
2
+ offset: number;
3
+ contextKey: string;
4
+ };
5
+ /** Build a stable context key from the query parameters a cursor belongs to. */
6
+ export declare function buildPageContextKey(parts: Array<string | number | boolean | undefined>): string;
7
+ export declare function encodeOffsetCursor(offset: number, contextKey: string): string;
8
+ export declare function decodeOffsetCursor(cursor: string | undefined): OffsetCursor | undefined;
9
+ /**
10
+ * Resolve the starting offset from an incoming cursor. Returns offset 0 when the
11
+ * cursor is absent, malformed, or belongs to a different query context, and
12
+ * reports whether a provided cursor was ignored.
13
+ */
14
+ export declare function resolveCursorOffset(cursor: string | undefined, contextKey: string): {
15
+ offset: number;
16
+ cursorIgnored: boolean;
17
+ };
Binary file
@@ -3,7 +3,7 @@ import { createError, ERROR_CODES } from "./errors.js";
3
3
  const WINDOWS_DRIVE_PATH = /^[A-Za-z]:[\\/]/;
4
4
  const MALFORMED_WINDOWS_DRIVE_PATH = /^[A-Za-z]:(?![\\/])/;
5
5
  const WSL_MOUNT_PATH = /^\/mnt\/[a-z](?:\/|$)/i;
6
- const UNC_WSL_PATH = /^(?:\\\\wsl\$\\|\/\/wsl\$\/)/i;
6
+ const UNC_WSL_PATH = /^(?:\\\\(?:wsl\$|wsl\.localhost)\\|\/\/(?:wsl\$|wsl\.localhost)\/)/i;
7
7
  function normalizeToUnixSlashes(value) {
8
8
  return value.replace(/\\/g, "/");
9
9
  }
@@ -12,10 +12,11 @@ function normalizeToWindowsSlashes(value) {
12
12
  }
13
13
  function parseUncWslPath(pathValue) {
14
14
  const normalized = normalizeToUnixSlashes(pathValue);
15
- if (!normalized.toLowerCase().startsWith("//wsl$/")) {
15
+ const prefixMatch = normalized.match(/^\/\/(?:wsl\$|wsl\.localhost)\//i);
16
+ if (!prefixMatch) {
16
17
  return undefined;
17
18
  }
18
- const remainder = normalized.slice("//wsl$/".length).replace(/^\/+/, "");
19
+ const remainder = normalized.slice(prefixMatch[0].length).replace(/^\/+/, "");
19
20
  const slashIndex = remainder.indexOf("/");
20
21
  if (slashIndex < 0) {
21
22
  return {
@@ -99,6 +100,12 @@ function toWslPath(pathValue, runtime, field) {
99
100
  }
100
101
  const parsed = parseUncWslPath(pathValue);
101
102
  if (parsed) {
103
+ const distro = runtime.wslDistro?.trim();
104
+ if (!distro || parsed.distro.toLowerCase() !== distro.toLowerCase()) {
105
+ throwInvalidPath(pathValue, distro
106
+ ? `UNC WSL path targets distro "${parsed.distro}", but this server runs in distro "${distro}".`
107
+ : `Cannot resolve UNC WSL path for distro "${parsed.distro}" without a known WSL distro name.`, runtime, field);
108
+ }
102
109
  const rest = parsed.innerPath.replace(/^\/+/, "");
103
110
  return rest ? `/${rest}` : "/";
104
111
  }
@@ -28,9 +28,11 @@ export declare class RegistryService {
28
28
  private readonly config;
29
29
  private readonly versionService;
30
30
  private readonly registryCache;
31
+ private readonly loadLocks;
31
32
  constructor(config: Config, versionService: VersionService);
32
33
  getRegistryData(input: GetRegistryDataInput): Promise<GetRegistryDataOutput>;
33
34
  private loadRegistries;
35
+ private loadRegistriesInternal;
34
36
  private loadExistingRegistries;
35
37
  private readRegistryFileOrThrow;
36
38
  private isInvalidRegistrySnapshot;
@@ -82,6 +82,25 @@ function parseRegistrySnapshot(raw, version, registryFile) {
82
82
  details: { version, registryFile }
83
83
  });
84
84
  }
85
+ // Every registry value must carry an `entries` object. Without this guard a
86
+ // malformed value slips through and later throws a raw TypeError on `.entries`
87
+ // (registry-service.ts:268/305, version-diff-service.ts), bypassing the
88
+ // corrupt-snapshot regeneration path that keys off REGISTRY_GENERATION_FAILED.
89
+ for (const [registryName, value] of Object.entries(parsed)) {
90
+ const entries = value?.entries;
91
+ if (typeof value !== "object" ||
92
+ value === null ||
93
+ Array.isArray(value) ||
94
+ typeof entries !== "object" ||
95
+ entries === null ||
96
+ Array.isArray(entries)) {
97
+ throw createError({
98
+ code: ERROR_CODES.REGISTRY_GENERATION_FAILED,
99
+ message: `registries.json for version "${version}" has a malformed "${registryName}" registry (missing entries).`,
100
+ details: { version, registryFile, registryName }
101
+ });
102
+ }
103
+ }
85
104
  return parsed;
86
105
  }
87
106
  function runDataGen(serverJarPath, outputDir, version) {
@@ -107,7 +126,7 @@ function runDataGen(serverJarPath, outputDir, version) {
107
126
  ];
108
127
  log("info", "registry.datagen.start", { version, isLegacy, serverJarPath, outputDir });
109
128
  const proc = spawn("java", args, {
110
- stdio: ["ignore", "pipe", "pipe"],
129
+ stdio: ["ignore", "ignore", "pipe"],
111
130
  cwd: outputDir
112
131
  });
113
132
  let stderr = "";
@@ -175,6 +194,7 @@ export class RegistryService {
175
194
  config;
176
195
  versionService;
177
196
  registryCache = new Map();
197
+ loadLocks = new Map();
178
198
  constructor(config, versionService) {
179
199
  this.config = config;
180
200
  this.versionService = versionService;
@@ -251,9 +271,28 @@ export class RegistryService {
251
271
  };
252
272
  }
253
273
  async loadRegistries(version, warnings) {
274
+ const existingLock = this.loadLocks.get(version);
275
+ if (existingLock) {
276
+ return existingLock;
277
+ }
278
+ const loadPromise = this.loadRegistriesInternal(version, warnings);
279
+ this.loadLocks.set(version, loadPromise);
280
+ try {
281
+ return await loadPromise;
282
+ }
283
+ finally {
284
+ this.loadLocks.delete(version);
285
+ }
286
+ }
287
+ async loadRegistriesInternal(version, warnings) {
254
288
  const cached = this.registryCache.get(version);
255
- if (cached)
289
+ if (cached) {
290
+ // LRU touch: move to most-recently-used so the bound evicts the true
291
+ // coldest entry (Map preserves insertion order).
292
+ this.registryCache.delete(version);
293
+ this.registryCache.set(version, cached);
256
294
  return cached;
295
+ }
257
296
  const registryDir = join(this.config.cacheDir, "registries", version);
258
297
  const cachedRegistries = this.loadExistingRegistries(registryDir, version, warnings);
259
298
  if (cachedRegistries) {
@@ -40,7 +40,6 @@ export async function downloadToCache(url, destinationPath, opts = {}) {
40
40
  const timer = setTimeout(() => timeout.abort(), timeoutMs);
41
41
  try {
42
42
  const response = await fetchFn(url, { signal: timeout.signal });
43
- clearTimeout(timer);
44
43
  const status = response.status;
45
44
  if (status === 404) {
46
45
  return { ok: false, statusCode: status };
@@ -71,7 +70,7 @@ export async function downloadToCache(url, destinationPath, opts = {}) {
71
70
  }
72
71
  else {
73
72
  const readable = Readable.fromWeb(response.body);
74
- await pipeline(readable, createWriteStream(tempPath));
73
+ await pipeline(readable, createWriteStream(tempPath), { signal: timeout.signal });
75
74
  }
76
75
  const contentLength = statSync(tempPath).size;
77
76
  renameSync(tempPath, destinationPath);
@@ -95,13 +94,15 @@ export async function downloadToCache(url, destinationPath, opts = {}) {
95
94
  }
96
95
  }
97
96
  catch (caughtError) {
98
- clearTimeout(timer);
99
97
  if (attempt >= maxRetries) {
100
98
  throw caughtError instanceof Error ? caughtError : new Error(String(caughtError));
101
99
  }
102
100
  await sleep(retryDelay(200, attempt));
103
101
  attempt += 1;
104
102
  }
103
+ finally {
104
+ clearTimeout(timer);
105
+ }
105
106
  }
106
107
  }
107
108
  export function defaultDownloadPath(cacheDir, url) {