@adhisang/minecraft-modding-mcp 4.0.0 → 4.1.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 (174) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +36 -23
  3. package/dist/build-suggested-call.d.ts +29 -0
  4. package/dist/build-suggested-call.js +58 -0
  5. package/dist/cache-registry.d.ts +3 -1
  6. package/dist/cache-registry.js +50 -6
  7. package/dist/entry-tools/analyze-symbol-service.d.ts +16 -16
  8. package/dist/entry-tools/batch-class-members-service.d.ts +34 -0
  9. package/dist/entry-tools/batch-class-members-service.js +97 -0
  10. package/dist/entry-tools/batch-class-source-service.d.ts +37 -0
  11. package/dist/entry-tools/batch-class-source-service.js +100 -0
  12. package/dist/entry-tools/batch-mappings-service.d.ts +36 -0
  13. package/dist/entry-tools/batch-mappings-service.js +66 -0
  14. package/dist/entry-tools/batch-runner.d.ts +72 -0
  15. package/dist/entry-tools/batch-runner.js +90 -0
  16. package/dist/entry-tools/batch-symbol-exists-service.d.ts +46 -0
  17. package/dist/entry-tools/batch-symbol-exists-service.js +113 -0
  18. package/dist/entry-tools/compare-minecraft-service.d.ts +6 -6
  19. package/dist/entry-tools/inspect-minecraft/handlers/artifact.d.ts +5 -0
  20. package/dist/entry-tools/inspect-minecraft/handlers/artifact.js +83 -0
  21. package/dist/entry-tools/inspect-minecraft/handlers/class-members.d.ts +6 -0
  22. package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +80 -0
  23. package/dist/entry-tools/inspect-minecraft/handlers/class-overview.d.ts +5 -0
  24. package/dist/entry-tools/inspect-minecraft/handlers/class-overview.js +248 -0
  25. package/dist/entry-tools/inspect-minecraft/handlers/class-source.d.ts +5 -0
  26. package/dist/entry-tools/inspect-minecraft/handlers/class-source.js +60 -0
  27. package/dist/entry-tools/inspect-minecraft/handlers/file.d.ts +5 -0
  28. package/dist/entry-tools/inspect-minecraft/handlers/file.js +54 -0
  29. package/dist/entry-tools/inspect-minecraft/handlers/list-files.d.ts +5 -0
  30. package/dist/entry-tools/inspect-minecraft/handlers/list-files.js +100 -0
  31. package/dist/entry-tools/inspect-minecraft/handlers/search.d.ts +5 -0
  32. package/dist/entry-tools/inspect-minecraft/handlers/search.js +155 -0
  33. package/dist/entry-tools/inspect-minecraft/handlers/versions.d.ts +6 -0
  34. package/dist/entry-tools/inspect-minecraft/handlers/versions.js +49 -0
  35. package/dist/entry-tools/inspect-minecraft/internal.d.ts +1042 -0
  36. package/dist/entry-tools/inspect-minecraft/internal.js +448 -0
  37. package/dist/entry-tools/inspect-minecraft-service.d.ts +193 -308
  38. package/dist/entry-tools/inspect-minecraft-service.js +20 -1244
  39. package/dist/entry-tools/manage-cache-service.d.ts +16 -16
  40. package/dist/entry-tools/validate-project/cases/access-transformer.d.ts +6 -0
  41. package/dist/entry-tools/validate-project/cases/access-transformer.js +106 -0
  42. package/dist/entry-tools/validate-project/cases/access-widener.d.ts +6 -0
  43. package/dist/entry-tools/validate-project/cases/access-widener.js +86 -0
  44. package/dist/entry-tools/validate-project/cases/mixin.d.ts +6 -0
  45. package/dist/entry-tools/validate-project/cases/mixin.js +90 -0
  46. package/dist/entry-tools/validate-project/cases/project-summary.d.ts +97 -0
  47. package/dist/entry-tools/validate-project/cases/project-summary.js +346 -0
  48. package/dist/entry-tools/validate-project/internal.d.ts +135 -0
  49. package/dist/entry-tools/validate-project/internal.js +287 -0
  50. package/dist/entry-tools/validate-project-service.d.ts +63 -47
  51. package/dist/entry-tools/validate-project-service.js +12 -562
  52. package/dist/entry-tools/verify-mixin-target-service.d.ts +133 -0
  53. package/dist/entry-tools/verify-mixin-target-service.js +323 -0
  54. package/dist/error-mapping.d.ts +40 -0
  55. package/dist/error-mapping.js +139 -0
  56. package/dist/errors.d.ts +6 -0
  57. package/dist/errors.js +6 -0
  58. package/dist/index.d.ts +2 -0
  59. package/dist/index.js +142 -1352
  60. package/dist/mapping/internal-types.d.ts +54 -0
  61. package/dist/mapping/internal-types.js +14 -0
  62. package/dist/mapping/loaders/mojang.d.ts +2 -0
  63. package/dist/mapping/loaders/mojang.js +64 -0
  64. package/dist/mapping/loaders/tiny-loom.d.ts +2 -0
  65. package/dist/mapping/loaders/tiny-loom.js +73 -0
  66. package/dist/mapping/loaders/tiny-maven.d.ts +2 -0
  67. package/dist/mapping/loaders/tiny-maven.js +104 -0
  68. package/dist/mapping/loaders/types.d.ts +14 -0
  69. package/dist/mapping/loaders/types.js +2 -0
  70. package/dist/mapping/lookup.d.ts +52 -0
  71. package/dist/mapping/lookup.js +496 -0
  72. package/dist/mapping/parsers/normalize.d.ts +10 -0
  73. package/dist/mapping/parsers/normalize.js +52 -0
  74. package/dist/mapping/parsers/proguard.d.ts +20 -0
  75. package/dist/mapping/parsers/proguard.js +138 -0
  76. package/dist/mapping/parsers/symbol-records.d.ts +27 -0
  77. package/dist/mapping/parsers/symbol-records.js +216 -0
  78. package/dist/mapping/parsers/tiny.d.ts +9 -0
  79. package/dist/mapping/parsers/tiny.js +96 -0
  80. package/dist/mapping/types.d.ts +147 -0
  81. package/dist/mapping/types.js +2 -0
  82. package/dist/mapping-pipeline-service.js +3 -2
  83. package/dist/mapping-service.d.ts +3 -144
  84. package/dist/mapping-service.js +19 -1201
  85. package/dist/mixin/access-validators.d.ts +9 -0
  86. package/dist/mixin/access-validators.js +257 -0
  87. package/dist/mixin/annotation-validators.d.ts +5 -0
  88. package/dist/mixin/annotation-validators.js +162 -0
  89. package/dist/mixin/helpers.d.ts +28 -0
  90. package/dist/mixin/helpers.js +315 -0
  91. package/dist/mixin/parsed-validator.d.ts +8 -0
  92. package/dist/mixin/parsed-validator.js +337 -0
  93. package/dist/mixin/types.d.ts +208 -0
  94. package/dist/mixin/types.js +28 -0
  95. package/dist/mixin-validator.d.ts +9 -201
  96. package/dist/mixin-validator.js +8 -1020
  97. package/dist/source/access-validate.d.ts +4 -0
  98. package/dist/source/access-validate.js +254 -0
  99. package/dist/source/artifact-resolver.d.ts +110 -0
  100. package/dist/source/artifact-resolver.js +1174 -0
  101. package/dist/source/cache-metrics.d.ts +26 -0
  102. package/dist/source/cache-metrics.js +172 -0
  103. package/dist/source/class-source/members-builder.d.ts +34 -0
  104. package/dist/source/class-source/members-builder.js +46 -0
  105. package/dist/source/class-source/snippet-builder.d.ts +19 -0
  106. package/dist/source/class-source/snippet-builder.js +46 -0
  107. package/dist/source/class-source-helpers.d.ts +34 -0
  108. package/dist/source/class-source-helpers.js +140 -0
  109. package/dist/source/class-source.d.ts +42 -0
  110. package/dist/source/class-source.js +883 -0
  111. package/dist/source/descriptor-utils.d.ts +6 -0
  112. package/dist/source/descriptor-utils.js +37 -0
  113. package/dist/source/file-access.d.ts +4 -0
  114. package/dist/source/file-access.js +102 -0
  115. package/dist/source/indexer.d.ts +82 -0
  116. package/dist/source/indexer.js +505 -0
  117. package/dist/source/lifecycle/diff-utils.d.ts +9 -0
  118. package/dist/source/lifecycle/diff-utils.js +107 -0
  119. package/dist/source/lifecycle/diff.d.ts +2 -0
  120. package/dist/source/lifecycle/diff.js +265 -0
  121. package/dist/source/lifecycle/mapping-helpers.d.ts +22 -0
  122. package/dist/source/lifecycle/mapping-helpers.js +327 -0
  123. package/dist/source/lifecycle/runtime-check.d.ts +2 -0
  124. package/dist/source/lifecycle/runtime-check.js +142 -0
  125. package/dist/source/lifecycle/trace.d.ts +2 -0
  126. package/dist/source/lifecycle/trace.js +231 -0
  127. package/dist/source/lifecycle.d.ts +4 -0
  128. package/dist/source/lifecycle.js +5 -0
  129. package/dist/source/search.d.ts +51 -0
  130. package/dist/source/search.js +676 -0
  131. package/dist/source/shared-utils.d.ts +6 -0
  132. package/dist/source/shared-utils.js +55 -0
  133. package/dist/source/state.d.ts +21 -0
  134. package/dist/source/state.js +19 -0
  135. package/dist/source/symbol-resolver.d.ts +3 -0
  136. package/dist/source/symbol-resolver.js +212 -0
  137. package/dist/source/validate-mixin/pipeline/mapping-health.d.ts +3 -0
  138. package/dist/source/validate-mixin/pipeline/mapping-health.js +41 -0
  139. package/dist/source/validate-mixin/pipeline/parse.d.ts +2 -0
  140. package/dist/source/validate-mixin/pipeline/parse.js +10 -0
  141. package/dist/source/validate-mixin/pipeline/resolve.d.ts +3 -0
  142. package/dist/source/validate-mixin/pipeline/resolve.js +78 -0
  143. package/dist/source/validate-mixin/pipeline/target-lookup.d.ts +6 -0
  144. package/dist/source/validate-mixin/pipeline/target-lookup.js +260 -0
  145. package/dist/source/validate-mixin/pipeline-context.d.ts +72 -0
  146. package/dist/source/validate-mixin/pipeline-context.js +93 -0
  147. package/dist/source/validate-mixin.d.ts +22 -0
  148. package/dist/source/validate-mixin.js +799 -0
  149. package/dist/source/workspace-target.d.ts +18 -0
  150. package/dist/source/workspace-target.js +305 -0
  151. package/dist/source-service.d.ts +147 -170
  152. package/dist/source-service.js +67 -6116
  153. package/dist/stage-emitter.d.ts +13 -0
  154. package/dist/stage-emitter.js +30 -0
  155. package/dist/stdio-supervisor.d.ts +61 -0
  156. package/dist/stdio-supervisor.js +326 -9
  157. package/dist/tool-contract-manifest.d.ts +1 -1
  158. package/dist/tool-contract-manifest.js +23 -6
  159. package/dist/tool-guidance.d.ts +82 -0
  160. package/dist/tool-guidance.js +734 -0
  161. package/dist/tool-schema-registry.d.ts +16 -0
  162. package/dist/tool-schema-registry.js +37 -0
  163. package/dist/tool-schemas.d.ts +3518 -0
  164. package/dist/tool-schemas.js +813 -0
  165. package/dist/types.d.ts +36 -0
  166. package/dist/version-service.js +7 -6
  167. package/dist/workspace-context-cache.d.ts +32 -0
  168. package/dist/workspace-context-cache.js +66 -0
  169. package/dist/workspace-mapping-service.d.ts +16 -0
  170. package/dist/workspace-mapping-service.js +173 -1
  171. package/docs/README-ja.md +414 -0
  172. package/docs/examples.md +483 -0
  173. package/docs/tool-reference.md +459 -0
  174. package/package.json +3 -2
@@ -0,0 +1,505 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, open, rename, stat, unlink } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { buildSuggestedCall } from "../build-suggested-call.js";
6
+ import { decompileBinaryJar } from "../decompiler/vineflower.js";
7
+ import { ERROR_CODES, createError, isAppError } from "../errors.js";
8
+ import { log } from "../logger.js";
9
+ import { resolveMojangTinyFile } from "../mojang-tiny-mapping-service.js";
10
+ import { iterateJavaEntriesAsUtf8 } from "../source-jar-reader.js";
11
+ import { extractSymbolsFromSource } from "../symbols/symbol-extractor.js";
12
+ import { remapJar } from "../tiny-remapper-service.js";
13
+ import { resolveTinyRemapperJar } from "../tiny-remapper-resolver.js";
14
+ import { resolveVineflowerJar } from "../vineflower-resolver.js";
15
+ import { enforceCacheLimits, recordRemappedJarBytes, releaseRemappedJarBytes, touchCacheMetrics, upsertCacheMetrics } from "./cache-metrics.js";
16
+ import { normalizePathStyle } from "./shared-utils.js";
17
+ export const INDEX_SCHEMA_VERSION = 1;
18
+ function chunkArray(items, chunkSize) {
19
+ const size = Math.max(1, Math.trunc(chunkSize));
20
+ if (items.length === 0) {
21
+ return [];
22
+ }
23
+ const chunks = [];
24
+ for (let index = 0; index < items.length; index += size) {
25
+ chunks.push(items.slice(index, index + size));
26
+ }
27
+ return chunks;
28
+ }
29
+ export async function indexArtifact(svc, input) {
30
+ const artifactId = input.artifactId?.trim();
31
+ if (!artifactId) {
32
+ throw createError({
33
+ code: ERROR_CODES.INVALID_INPUT,
34
+ message: "artifactId must be non-empty."
35
+ });
36
+ }
37
+ const artifact = svc.getArtifact(artifactId);
38
+ const force = input.force ?? false;
39
+ const hasFiles = svc.filesRepo.listFiles(artifact.artifactId, { limit: 1 }).items.length > 0;
40
+ const meta = svc.indexMetaRepo.get(artifact.artifactId);
41
+ const expectedSignature = artifact.artifactSignature ?? fallbackArtifactSignature(artifact.artifactId);
42
+ const reason = resolveIndexRebuildReason({
43
+ force,
44
+ expectedSignature,
45
+ hasFiles,
46
+ meta
47
+ });
48
+ if (reason === "already_current") {
49
+ svc.metrics.recordReindexSkip();
50
+ const currentMeta = meta;
51
+ return {
52
+ artifactId: artifact.artifactId,
53
+ reindexed: false,
54
+ reason,
55
+ counts: {
56
+ files: currentMeta.filesCount,
57
+ symbols: currentMeta.symbolsCount,
58
+ ftsRows: currentMeta.ftsRowsCount
59
+ },
60
+ indexedAt: currentMeta.indexedAt,
61
+ durationMs: 0,
62
+ mappingApplied: artifact.mappingApplied ?? "obfuscated"
63
+ };
64
+ }
65
+ const resolved = toResolvedArtifact(svc, artifact);
66
+ const rebuilt = await rebuildAndPersistArtifactIndex(svc, resolved, reason);
67
+ svc.metrics.recordReindex();
68
+ return {
69
+ artifactId: artifact.artifactId,
70
+ reindexed: true,
71
+ reason,
72
+ counts: {
73
+ files: rebuilt.files.length,
74
+ symbols: rebuilt.symbols.length,
75
+ ftsRows: rebuilt.files.length
76
+ },
77
+ indexedAt: rebuilt.indexedAt,
78
+ durationMs: rebuilt.indexDurationMs,
79
+ mappingApplied: artifact.mappingApplied ?? "obfuscated"
80
+ };
81
+ }
82
+ export function fallbackArtifactSignature(artifactId) {
83
+ return createHash("sha256").update(artifactId).digest("hex");
84
+ }
85
+ export function resolveIndexRebuildReason(input) {
86
+ if (input.force) {
87
+ return "force";
88
+ }
89
+ if (!input.hasFiles || !input.meta) {
90
+ return "missing_meta";
91
+ }
92
+ if (input.meta.indexSchemaVersion !== INDEX_SCHEMA_VERSION) {
93
+ return "schema_mismatch";
94
+ }
95
+ if (input.meta.artifactSignature !== input.expectedSignature) {
96
+ return "signature_mismatch";
97
+ }
98
+ return "already_current";
99
+ }
100
+ export function toResolvedArtifact(svc, artifact) {
101
+ return {
102
+ artifactId: artifact.artifactId,
103
+ artifactAlias: artifact.alias,
104
+ artifactSignature: artifact.artifactSignature ?? fallbackArtifactSignature(artifact.artifactId),
105
+ origin: artifact.origin,
106
+ binaryJarPath: artifact.binaryJarPath,
107
+ sourceJarPath: artifact.sourceJarPath,
108
+ coordinate: artifact.coordinate,
109
+ version: artifact.version,
110
+ requestedMapping: artifact.requestedMapping,
111
+ mappingApplied: artifact.mappingApplied,
112
+ repoUrl: artifact.repoUrl,
113
+ provenance: artifact.provenance,
114
+ qualityFlags: artifact.qualityFlags,
115
+ isDecompiled: artifact.isDecompiled,
116
+ resolvedAt: new Date().toISOString()
117
+ };
118
+ }
119
+ export async function rebuildAndPersistArtifactIndex(svc, resolved, reason) {
120
+ const rebuilt = await buildRebuiltArtifactData(svc, resolved);
121
+ const timestamp = new Date().toISOString();
122
+ const chunkSize = Math.max(1, svc.config.indexInsertChunkSize ?? 200);
123
+ const tx = svc.db.transaction(() => {
124
+ svc.artifactsRepo.upsertArtifact({
125
+ artifactId: resolved.artifactId,
126
+ alias: resolved.artifactAlias,
127
+ origin: resolved.origin,
128
+ coordinate: resolved.coordinate,
129
+ version: resolved.version,
130
+ binaryJarPath: resolved.binaryJarPath,
131
+ sourceJarPath: resolved.sourceJarPath,
132
+ repoUrl: resolved.repoUrl,
133
+ requestedMapping: resolved.requestedMapping,
134
+ mappingApplied: resolved.mappingApplied,
135
+ provenance: resolved.provenance,
136
+ qualityFlags: resolved.qualityFlags,
137
+ artifactSignature: resolved.artifactSignature,
138
+ isDecompiled: resolved.isDecompiled,
139
+ timestamp
140
+ });
141
+ svc.filesRepo.clearFilesForArtifact(resolved.artifactId);
142
+ for (const chunk of chunkArray(rebuilt.files, chunkSize)) {
143
+ svc.filesRepo.insertFilesForArtifact(resolved.artifactId, chunk);
144
+ }
145
+ svc.symbolsRepo.clearSymbolsForArtifact(resolved.artifactId);
146
+ for (const chunk of chunkArray(rebuilt.symbols, chunkSize)) {
147
+ svc.symbolsRepo.insertSymbolsForArtifact(resolved.artifactId, chunk);
148
+ }
149
+ svc.indexMetaRepo.upsert({
150
+ artifactId: resolved.artifactId,
151
+ artifactSignature: resolved.artifactSignature,
152
+ indexSchemaVersion: INDEX_SCHEMA_VERSION,
153
+ filesCount: rebuilt.files.length,
154
+ symbolsCount: rebuilt.symbols.length,
155
+ ftsRowsCount: rebuilt.files.length,
156
+ indexedAt: rebuilt.indexedAt,
157
+ indexDurationMs: rebuilt.indexDurationMs
158
+ });
159
+ });
160
+ tx();
161
+ upsertCacheMetrics(svc, resolved.artifactId, rebuilt.totalContentBytes, timestamp);
162
+ log("info", "index.rebuild.done", {
163
+ artifactId: resolved.artifactId,
164
+ reason,
165
+ files: rebuilt.files.length,
166
+ symbols: rebuilt.symbols.length,
167
+ indexDurationMs: rebuilt.indexDurationMs
168
+ });
169
+ return rebuilt;
170
+ }
171
+ export async function buildRebuiltArtifactData(svc, resolved) {
172
+ const indexStartedAt = Date.now();
173
+ let files = [];
174
+ if (resolved.sourceJarPath) {
175
+ files = await loadFromSourceJar(svc, resolved.sourceJarPath);
176
+ }
177
+ else if (resolved.binaryJarPath) {
178
+ const decompileInputJarPath = await maybeRemapBinaryForMojang(svc, resolved);
179
+ // When the binary jar was remapped from obfuscated to mojang, swap the resolved
180
+ // artifact's binaryJarPath to the remapped jar so downstream bytecode consumers
181
+ // (getClassMembers, validateMixin) look up mojang names in the mojang jar — not
182
+ // the original obfuscated jar. Persistence in upsertArtifact happens after this
183
+ // function returns, so the swap reaches both the database row and the
184
+ // resolveArtifact response.
185
+ if (decompileInputJarPath !== resolved.binaryJarPath) {
186
+ resolved.binaryJarPath = decompileInputJarPath;
187
+ }
188
+ const vineflowerPath = await resolveVineflowerJar(svc.config.cacheDir, svc.config.vineflowerJarPath);
189
+ const decompileStartedAt = Date.now();
190
+ try {
191
+ const decompileResult = await decompileBinaryJar(decompileInputJarPath, svc.config.cacheDir, {
192
+ vineflowerJarPath: vineflowerPath,
193
+ artifactIdCandidate: resolved.artifactId,
194
+ timeoutMs: 120_000,
195
+ signature: resolved.artifactId
196
+ });
197
+ files = decompileResult.javaFiles.map((entry) => ({
198
+ filePath: normalizePathStyle(entry.filePath),
199
+ content: entry.content,
200
+ contentBytes: Buffer.byteLength(entry.content, "utf8"),
201
+ contentHash: createHash("sha256").update(entry.content).digest("hex")
202
+ }));
203
+ }
204
+ catch (caughtError) {
205
+ if (isAppError(caughtError) && caughtError.code === ERROR_CODES.DECOMPILER_FAILED) {
206
+ throw createError({
207
+ code: ERROR_CODES.DECOMPILER_FAILED,
208
+ message: caughtError.message,
209
+ details: {
210
+ ...(caughtError.details ?? {}),
211
+ artifactId: resolved.artifactId,
212
+ binaryJarPath: resolved.binaryJarPath,
213
+ producedJavaCount: typeof caughtError.details?.producedJavaCount === "number"
214
+ ? caughtError.details.producedJavaCount
215
+ : 0,
216
+ nextAction: "Verify Java runtime and Vineflower availability, then retry. If available, prefer source-backed artifacts.",
217
+ recommendedCommand: "echo $MCP_VINEFLOWER_JAR_PATH"
218
+ }
219
+ });
220
+ }
221
+ throw caughtError;
222
+ }
223
+ finally {
224
+ svc.metrics.recordDuration("decompile_duration_ms", Date.now() - decompileStartedAt);
225
+ }
226
+ }
227
+ else {
228
+ throw createError({
229
+ code: ERROR_CODES.SOURCE_NOT_FOUND,
230
+ message: "No source artifact available.",
231
+ details: {
232
+ artifactId: resolved.artifactId,
233
+ nextAction: "Use list-artifact-files to inspect the artifact's contents.",
234
+ ...buildSuggestedCall({
235
+ tool: "list-artifact-files",
236
+ params: { artifactId: resolved.artifactId }
237
+ })
238
+ }
239
+ });
240
+ }
241
+ const symbols = [];
242
+ for (const file of files) {
243
+ const extracted = extractSymbolsFromSource(file.filePath, file.content);
244
+ for (const symbol of extracted) {
245
+ symbols.push({
246
+ filePath: file.filePath,
247
+ ...symbol
248
+ });
249
+ }
250
+ }
251
+ return {
252
+ files,
253
+ symbols,
254
+ indexedAt: new Date().toISOString(),
255
+ indexDurationMs: Date.now() - indexStartedAt,
256
+ totalContentBytes: files.reduce((sum, file) => sum + file.contentBytes, 0)
257
+ };
258
+ }
259
+ export function getArtifact(svc, artifactId) {
260
+ if (artifactId.includes("..") || artifactId.includes("/")) {
261
+ // intentionally reject suspicious IDs that are not artifact hashes
262
+ throw createError({
263
+ code: ERROR_CODES.INVALID_INPUT,
264
+ message: "artifactId contains invalid characters.",
265
+ details: { artifactId }
266
+ });
267
+ }
268
+ const artifact = svc.artifactsRepo.getArtifact(artifactId);
269
+ if (!artifact) {
270
+ throw createError({
271
+ code: ERROR_CODES.SOURCE_NOT_FOUND,
272
+ message: "Artifact not found. Resolve context first.",
273
+ details: {
274
+ artifactId,
275
+ nextAction: "Use resolve-artifact to resolve a source artifact first.",
276
+ ...buildSuggestedCall({
277
+ tool: "resolve-artifact",
278
+ params: { target: { kind: "version", value: "latest" } }
279
+ })
280
+ }
281
+ });
282
+ }
283
+ return artifact;
284
+ }
285
+ export async function ingestIfNeeded(svc, resolved) {
286
+ const existing = svc.artifactsRepo.getArtifact(resolved.artifactId);
287
+ const hasFiles = svc.filesRepo.listFiles(resolved.artifactId, { limit: 1 }).items.length > 0;
288
+ const meta = svc.indexMetaRepo.get(resolved.artifactId);
289
+ const reason = resolveIndexRebuildReason({
290
+ force: false,
291
+ expectedSignature: resolved.artifactSignature,
292
+ hasFiles,
293
+ meta
294
+ });
295
+ if (existing && reason === "already_current") {
296
+ // Mojang binary-remap reconciliation on the warm cache hit path:
297
+ // resolveSourceTargetInternal always returns the original binary jar
298
+ // (resolver does not know about prior remap output), so without this
299
+ // step a warm-cache resolve would return mappingApplied="mojang"
300
+ // alongside binaryJarPath pointing at the obfuscated client jar.
301
+ // maybeRemapBinaryForMojang short-circuits on a healthy cache hit
302
+ // (existsSync + ZIP magic) and re-remaps when the cache is missing
303
+ // or corrupted, so this also recovers from out-of-band cache loss.
304
+ const transformChain = resolved.provenance?.transformChain ?? [];
305
+ if (transformChain.includes("binary-remap:obf->mojang") && resolved.binaryJarPath) {
306
+ const reconciledBinaryJarPath = await maybeRemapBinaryForMojang(svc, resolved);
307
+ if (reconciledBinaryJarPath !== resolved.binaryJarPath) {
308
+ resolved.binaryJarPath = reconciledBinaryJarPath;
309
+ }
310
+ }
311
+ // Backfill / rotate alias on the warm-cache path. Without this, schema-v4
312
+ // migrated rows (alias=NULL) and rows whose alias parameters changed since
313
+ // the last upsert would return an artifactAlias from resolveArtifact that
314
+ // does not resolve back via getArtifact(alias), breaking the 3.1b lookup
315
+ // contract. UNIQUE conflicts here are caller bugs (two distinct artifactIds
316
+ // colliding on alias) and surface as DB errors rather than silent drift.
317
+ if (resolved.artifactAlias && existing.alias !== resolved.artifactAlias) {
318
+ svc.artifactsRepo.setAlias(resolved.artifactId, resolved.artifactAlias);
319
+ }
320
+ svc.metrics.recordArtifactCacheHit();
321
+ const touchedAt = new Date().toISOString();
322
+ svc.artifactsRepo.touchArtifact(resolved.artifactId, touchedAt);
323
+ touchCacheMetrics(svc, resolved.artifactId, touchedAt);
324
+ return;
325
+ }
326
+ svc.metrics.recordArtifactCacheMiss();
327
+ svc.metrics.recordReindex();
328
+ log("info", "index.rebuild.start", {
329
+ artifactId: resolved.artifactId,
330
+ reason
331
+ });
332
+ await rebuildAndPersistArtifactIndex(svc, resolved, reason === "already_current" ? "missing_meta" : reason);
333
+ enforceCacheLimits(svc);
334
+ }
335
+ /**
336
+ * If the resolved artifact's transformChain promised an "obf -> mojang"
337
+ * binary remap, run tiny-remapper now and return the remapped jar path.
338
+ * Otherwise return the original binaryJarPath unchanged.
339
+ *
340
+ * Cache safety: writes to a per-attempt temp file then atomic-renames into
341
+ * <cacheDir>/remapped/<artifactId>.jar. A per-target inflight Promise map
342
+ * collapses concurrent calls so two simultaneous resolveArtifact calls for
343
+ * the same artifactId share one tiny-remapper run instead of racing on the
344
+ * same output path.
345
+ */
346
+ export async function maybeRemapBinaryForMojang(svc, resolved) {
347
+ const binaryJarPath = resolved.binaryJarPath;
348
+ if (!binaryJarPath) {
349
+ throw createError({
350
+ code: ERROR_CODES.SOURCE_NOT_FOUND,
351
+ message: "Cannot run binary remap: resolved artifact has no binary jar path.",
352
+ details: { artifactId: resolved.artifactId }
353
+ });
354
+ }
355
+ const transformChain = resolved.provenance?.transformChain ?? [];
356
+ if (!transformChain.includes("binary-remap:obf->mojang")) {
357
+ return binaryJarPath;
358
+ }
359
+ if (!resolved.version) {
360
+ throw createError({
361
+ code: ERROR_CODES.MAPPING_NOT_APPLIED,
362
+ message: "Binary remap promised but artifact has no resolved Minecraft version.",
363
+ details: {
364
+ artifactId: resolved.artifactId,
365
+ binaryJarPath,
366
+ nextAction: "Use target.kind=\"version\" so the remap pipeline can locate Mojang mappings."
367
+ }
368
+ });
369
+ }
370
+ const remappedDir = join(svc.config.cacheDir, "remapped");
371
+ const remappedJarPath = join(remappedDir, `${resolved.artifactId}.jar`);
372
+ if (existsSync(remappedJarPath)) {
373
+ // Validate the cached jar is at least structurally a ZIP (`PK\x03\x04`) and
374
+ // non-empty before reusing. If a prior atomic-rename window was interrupted
375
+ // or the cache file was hand-edited, drop it and re-remap rather than
376
+ // silently feeding a corrupt jar into Vineflower.
377
+ if (await isUsableJarFile(remappedJarPath)) {
378
+ await recordRemappedJarBytesFromDisk(svc, resolved.artifactId, remappedJarPath);
379
+ return remappedJarPath;
380
+ }
381
+ log("warn", "binary-remap.cache.evict-corrupt", {
382
+ artifactId: resolved.artifactId,
383
+ remappedJarPath
384
+ });
385
+ try {
386
+ await unlink(remappedJarPath);
387
+ }
388
+ catch {
389
+ // ignore: race with another process or already-deleted file.
390
+ }
391
+ releaseRemappedJarBytes(svc, resolved.artifactId);
392
+ }
393
+ const inflight = svc.state.inflightRemaps.get(remappedJarPath);
394
+ if (inflight) {
395
+ return inflight;
396
+ }
397
+ const remapPromise = runBinaryRemap(svc, {
398
+ version: resolved.version,
399
+ inputJar: binaryJarPath,
400
+ remappedDir,
401
+ remappedJarPath
402
+ });
403
+ svc.state.inflightRemaps.set(remappedJarPath, remapPromise);
404
+ try {
405
+ const path = await remapPromise;
406
+ await recordRemappedJarBytesFromDisk(svc, resolved.artifactId, path);
407
+ return path;
408
+ }
409
+ finally {
410
+ svc.state.inflightRemaps.delete(remappedJarPath);
411
+ }
412
+ }
413
+ export async function recordRemappedJarBytesFromDisk(svc, artifactId, path) {
414
+ try {
415
+ const fileStat = await stat(path);
416
+ recordRemappedJarBytes(svc, artifactId, fileStat.size);
417
+ }
418
+ catch {
419
+ // best-effort: accounting will be rebuilt on the next refreshCacheMetrics.
420
+ }
421
+ }
422
+ /**
423
+ * Best-effort structural check that `path` is a non-empty file beginning with
424
+ * the ZIP local-file-header magic (`50 4B 03 04`). Used to drop partial /
425
+ * corrupt remap-cache entries before they reach Vineflower. False positives
426
+ * are acceptable (Vineflower will surface a clearer error); false negatives
427
+ * are not (a corrupt cache hit must be evicted).
428
+ */
429
+ export async function isUsableJarFile(path) {
430
+ try {
431
+ const stats = await stat(path);
432
+ if (!stats.isFile() || stats.size < 4) {
433
+ return false;
434
+ }
435
+ }
436
+ catch {
437
+ return false;
438
+ }
439
+ let handle;
440
+ try {
441
+ handle = await open(path, "r");
442
+ const header = Buffer.alloc(4);
443
+ const { bytesRead } = await handle.read(header, 0, 4, 0);
444
+ return bytesRead === 4 && header[0] === 0x50 && header[1] === 0x4b && header[2] === 0x03 && header[3] === 0x04;
445
+ }
446
+ catch {
447
+ return false;
448
+ }
449
+ finally {
450
+ await handle?.close().catch(() => undefined);
451
+ }
452
+ }
453
+ export async function runBinaryRemap(svc, input) {
454
+ const tinyRemapperJarPath = await resolveTinyRemapperJar(svc.config.cacheDir, svc.config.tinyRemapperJarPath);
455
+ const mojangTiny = await resolveMojangTinyFile(input.version, svc.config);
456
+ await mkdir(input.remappedDir, { recursive: true });
457
+ const tempPath = `${input.remappedJarPath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
458
+ const remapStartedAt = Date.now();
459
+ try {
460
+ await remapJar(tinyRemapperJarPath, {
461
+ inputJar: input.inputJar,
462
+ outputJar: tempPath,
463
+ mappingsFile: mojangTiny.path,
464
+ fromNamespace: "obfuscated",
465
+ toNamespace: "mojang",
466
+ timeoutMs: svc.config.remapTimeoutMs,
467
+ maxMemoryMb: svc.config.remapMaxMemoryMb
468
+ });
469
+ const tempStats = await stat(tempPath);
470
+ if (tempStats.size === 0) {
471
+ throw createError({
472
+ code: ERROR_CODES.REMAP_FAILED,
473
+ message: "tiny-remapper produced an empty output jar.",
474
+ details: { inputJar: input.inputJar, tempPath }
475
+ });
476
+ }
477
+ await rename(tempPath, input.remappedJarPath);
478
+ return input.remappedJarPath;
479
+ }
480
+ catch (caughtError) {
481
+ try {
482
+ await unlink(tempPath);
483
+ }
484
+ catch {
485
+ // tempPath may not exist if remapJar failed before writing anything; ignore.
486
+ }
487
+ throw caughtError;
488
+ }
489
+ finally {
490
+ svc.metrics.recordDuration("binary_remap_duration_ms", Date.now() - remapStartedAt);
491
+ }
492
+ }
493
+ export async function loadFromSourceJar(svc, sourceJarPath) {
494
+ const files = [];
495
+ for await (const entry of iterateJavaEntriesAsUtf8(sourceJarPath, svc.config.maxContentBytes)) {
496
+ files.push({
497
+ filePath: normalizePathStyle(entry.filePath),
498
+ content: entry.content,
499
+ contentBytes: Buffer.byteLength(entry.content, "utf8"),
500
+ contentHash: createHash("sha256").update(entry.content).digest("hex")
501
+ });
502
+ }
503
+ return files;
504
+ }
505
+ //# sourceMappingURL=indexer.js.map
@@ -0,0 +1,9 @@
1
+ import type { DiffClassMemberDelta, DiffMember, DiffMemberChange } from "../../source-service.js";
2
+ type DiffMemberChangedField = "accessFlags" | "isSynthetic" | "javaSignature" | "jvmDescriptor";
3
+ export declare function sortDiffMembers(members: DiffMember[]): DiffMember[];
4
+ export declare function sortDiffMemberChanges(changes: DiffMemberChange[]): DiffMemberChange[];
5
+ export declare function changedMemberFields(fromMember: DiffMember, toMember: DiffMember, includeDescriptor: boolean): DiffMemberChangedField[];
6
+ export declare function diffMembersByKey(fromMembersInput: DiffMember[], toMembersInput: DiffMember[], buildKey: (member: DiffMember) => string, includeDescriptorInModified: boolean): DiffClassMemberDelta;
7
+ export declare function emptyDiffDelta(): DiffClassMemberDelta;
8
+ export declare function compactDiffDelta(delta: DiffClassMemberDelta): DiffClassMemberDelta;
9
+ export {};
@@ -0,0 +1,107 @@
1
+ export function sortDiffMembers(members) {
2
+ return [...members].sort((left, right) => {
3
+ const nameCompare = left.name.localeCompare(right.name);
4
+ if (nameCompare !== 0) {
5
+ return nameCompare;
6
+ }
7
+ const descriptorCompare = left.jvmDescriptor.localeCompare(right.jvmDescriptor);
8
+ if (descriptorCompare !== 0) {
9
+ return descriptorCompare;
10
+ }
11
+ return left.ownerFqn.localeCompare(right.ownerFqn);
12
+ });
13
+ }
14
+ export function sortDiffMemberChanges(changes) {
15
+ return [...changes].sort((left, right) => {
16
+ const keyCompare = left.key.localeCompare(right.key);
17
+ if (keyCompare !== 0) {
18
+ return keyCompare;
19
+ }
20
+ const fromOwnerCompare = (left.from?.ownerFqn ?? "").localeCompare(right.from?.ownerFqn ?? "");
21
+ if (fromOwnerCompare !== 0) {
22
+ return fromOwnerCompare;
23
+ }
24
+ return (left.to?.ownerFqn ?? "").localeCompare(right.to?.ownerFqn ?? "");
25
+ });
26
+ }
27
+ export function changedMemberFields(fromMember, toMember, includeDescriptor) {
28
+ const changed = [];
29
+ if (fromMember.accessFlags !== toMember.accessFlags) {
30
+ changed.push("accessFlags");
31
+ }
32
+ if (fromMember.isSynthetic !== toMember.isSynthetic) {
33
+ changed.push("isSynthetic");
34
+ }
35
+ if (fromMember.javaSignature !== toMember.javaSignature) {
36
+ changed.push("javaSignature");
37
+ }
38
+ if (includeDescriptor && fromMember.jvmDescriptor !== toMember.jvmDescriptor) {
39
+ changed.push("jvmDescriptor");
40
+ }
41
+ return changed;
42
+ }
43
+ export function diffMembersByKey(fromMembersInput, toMembersInput, buildKey, includeDescriptorInModified) {
44
+ const fromMembers = sortDiffMembers(fromMembersInput);
45
+ const toMembers = sortDiffMembers(toMembersInput);
46
+ const fromByKey = new Map();
47
+ const toByKey = new Map();
48
+ for (const member of fromMembers) {
49
+ const key = buildKey(member);
50
+ if (!fromByKey.has(key)) {
51
+ fromByKey.set(key, member);
52
+ }
53
+ }
54
+ for (const member of toMembers) {
55
+ const key = buildKey(member);
56
+ if (!toByKey.has(key)) {
57
+ toByKey.set(key, member);
58
+ }
59
+ }
60
+ const added = [];
61
+ const removed = [];
62
+ const modified = [];
63
+ for (const [key, toMember] of toByKey.entries()) {
64
+ const fromMember = fromByKey.get(key);
65
+ if (!fromMember) {
66
+ added.push(toMember);
67
+ continue;
68
+ }
69
+ const changed = changedMemberFields(fromMember, toMember, includeDescriptorInModified);
70
+ if (changed.length > 0) {
71
+ modified.push({
72
+ key,
73
+ from: fromMember,
74
+ to: toMember,
75
+ changed
76
+ });
77
+ }
78
+ }
79
+ for (const [key, fromMember] of fromByKey.entries()) {
80
+ if (!toByKey.has(key)) {
81
+ removed.push(fromMember);
82
+ }
83
+ }
84
+ return {
85
+ added: sortDiffMembers(added),
86
+ removed: sortDiffMembers(removed),
87
+ modified: sortDiffMemberChanges(modified)
88
+ };
89
+ }
90
+ export function emptyDiffDelta() {
91
+ return {
92
+ added: [],
93
+ removed: [],
94
+ modified: []
95
+ };
96
+ }
97
+ export function compactDiffDelta(delta) {
98
+ return {
99
+ added: delta.added,
100
+ removed: delta.removed,
101
+ modified: delta.modified.map((change) => ({
102
+ key: change.key,
103
+ changed: [...change.changed]
104
+ }))
105
+ };
106
+ }
107
+ //# sourceMappingURL=diff-utils.js.map
@@ -0,0 +1,2 @@
1
+ import type { DiffClassSignaturesInput, DiffClassSignaturesOutput, SourceService } from "../../source-service.js";
2
+ export declare function diffClassSignatures(svc: SourceService, input: DiffClassSignaturesInput): Promise<DiffClassSignaturesOutput>;