@adhisang/minecraft-modding-mcp 6.2.0 → 7.0.0-rc.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 (98) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +21 -5
  3. package/dist/cache-policy.d.ts +71 -0
  4. package/dist/cache-policy.js +83 -0
  5. package/dist/cache-registry.js +6 -6
  6. package/dist/cli.js +74 -3
  7. package/dist/compat-stdio-transport.d.ts +1 -1
  8. package/dist/compat-stdio-transport.js +13 -1
  9. package/dist/config.d.ts +3 -0
  10. package/dist/config.js +8 -2
  11. package/dist/decompiler/vineflower.d.ts +1 -0
  12. package/dist/decompiler/vineflower.js +8 -5
  13. package/dist/entry-tools/analyze-mod-service.d.ts +70 -136
  14. package/dist/entry-tools/analyze-symbol-service.d.ts +112 -150
  15. package/dist/entry-tools/compare-minecraft-service.d.ts +59 -145
  16. package/dist/entry-tools/entry-tool-schema.d.ts +38 -4
  17. package/dist/entry-tools/entry-tool-schema.js +4 -1
  18. package/dist/entry-tools/inspect-minecraft/internal.d.ts +235 -799
  19. package/dist/entry-tools/inspect-minecraft/internal.js +65 -28
  20. package/dist/entry-tools/inspect-minecraft-service.d.ts +372 -1736
  21. package/dist/entry-tools/inspect-minecraft-service.js +1 -1
  22. package/dist/entry-tools/manage-cache-service.d.ts +81 -91
  23. package/dist/entry-tools/validate-project/cases/project-summary.d.ts +7 -7
  24. package/dist/entry-tools/validate-project-service.d.ts +164 -592
  25. package/dist/entry-tools/verify-mixin-target-service.d.ts +3 -19
  26. package/dist/era-classifier.d.ts +161 -0
  27. package/dist/era-classifier.js +292 -0
  28. package/dist/error-mapping.js +9 -2
  29. package/dist/index.d.ts +42 -4
  30. package/dist/index.js +637 -475
  31. package/dist/java-process.d.ts +2 -0
  32. package/dist/java-process.js +22 -2
  33. package/dist/json-rpc-framing.d.ts +77 -1
  34. package/dist/json-rpc-framing.js +249 -13
  35. package/dist/mapping/loaders/tiny-loom-selection.d.ts +88 -0
  36. package/dist/mapping/loaders/tiny-loom-selection.js +223 -0
  37. package/dist/mapping/loaders/tiny-loom.js +45 -33
  38. package/dist/mapping/loaders/tiny-maven.js +6 -11
  39. package/dist/mapping/parsers/tiny.d.ts +57 -0
  40. package/dist/mapping/parsers/tiny.js +99 -22
  41. package/dist/mapping-service.d.ts +19 -0
  42. package/dist/mapping-service.js +93 -9
  43. package/dist/mcp-helpers.d.ts +19 -2
  44. package/dist/mcp-helpers.js +48 -6
  45. package/dist/minecraft-explorer-service.d.ts +1 -1
  46. package/dist/mixin/types.d.ts +8 -0
  47. package/dist/mod-analyzer.js +7 -7
  48. package/dist/mod-decompile-service.js +1 -0
  49. package/dist/nbt/java-nbt-codec.js +12 -2
  50. package/dist/nbt/json-patch.js +14 -3
  51. package/dist/nbt/pipeline.js +40 -3
  52. package/dist/nbt/typed-json.js +26 -1
  53. package/dist/registration-adapter.d.ts +32 -0
  54. package/dist/registration-adapter.js +52 -0
  55. package/dist/request-context.d.ts +7 -0
  56. package/dist/request-context.js +9 -0
  57. package/dist/resources.d.ts +1 -1
  58. package/dist/resources.js +25 -19
  59. package/dist/server-identity.d.ts +27 -0
  60. package/dist/server-identity.js +26 -0
  61. package/dist/source/access-validate.js +53 -0
  62. package/dist/source/artifact-resolver.d.ts +69 -1
  63. package/dist/source/artifact-resolver.js +215 -14
  64. package/dist/source/class-source.d.ts +22 -0
  65. package/dist/source/class-source.js +162 -29
  66. package/dist/source/did-you-mean.d.ts +12 -1
  67. package/dist/source/did-you-mean.js +6 -2
  68. package/dist/source/file-access.js +150 -46
  69. package/dist/source/indexer.js +1 -0
  70. package/dist/source/nested-jars.d.ts +19 -0
  71. package/dist/source/nested-jars.js +90 -21
  72. package/dist/source/shared-utils.d.ts +21 -0
  73. package/dist/source/shared-utils.js +23 -0
  74. package/dist/source-service.d.ts +12 -0
  75. package/dist/source-service.js +3 -0
  76. package/dist/stdio-supervisor.d.ts +357 -2
  77. package/dist/stdio-supervisor.js +1031 -80
  78. package/dist/storage/db.d.ts +2 -1
  79. package/dist/storage/db.js +15 -8
  80. package/dist/synthetic-decorator.d.ts +24 -0
  81. package/dist/synthetic-decorator.js +48 -0
  82. package/dist/tool-contract-manifest.js +1 -1
  83. package/dist/tool-guidance.d.ts +17 -1
  84. package/dist/tool-guidance.js +417 -13
  85. package/dist/tool-schema-registry.d.ts +2 -0
  86. package/dist/tool-schema-registry.js +4 -0
  87. package/dist/tool-schemas.d.ts +2214 -3915
  88. package/dist/tool-schemas.js +34 -7
  89. package/dist/types.d.ts +35 -0
  90. package/dist/v1-parity-schemas.d.ts +7 -0
  91. package/dist/v1-parity-schemas.js +5584 -0
  92. package/dist/version-diff-service.d.ts +33 -0
  93. package/dist/version-diff-service.js +148 -3
  94. package/dist/version-service.js +36 -14
  95. package/dist/warning-details.js +18 -1
  96. package/docs/README-ja.md +9 -3
  97. package/docs/tool-reference.md +267 -22
  98. package/package.json +12 -9
@@ -0,0 +1,223 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { open, stat } from "node:fs/promises";
4
+ import { dirname } from "node:path";
5
+ import v8 from "node:v8";
6
+ import { pairKey } from "../parsers/normalize.js";
7
+ import { parseTinyHeader } from "../parsers/tiny.js";
8
+ /** Upper bound on the bytes read while sniffing a file's header line. */
9
+ const HEADER_PROBE_BYTES = 64 * 1024;
10
+ /**
11
+ * Bytes of JS heap one index slot costs, measured on Minecraft 1.21.10 loom
12
+ * mappings (2,080 MB peak / 6,755,994 slots ~= 323 B; the Mojang proguard index
13
+ * lands at 330 B). Rounded up so the derived budget errs toward stopping early.
14
+ */
15
+ export const HEAP_BYTES_PER_INDEX_ENTRY = 340;
16
+ /** Share of the heap still free at load time that a single tiny load may claim. */
17
+ const HEAP_BUDGET_FRACTION = 0.7;
18
+ const MIN_INDEX_ENTRY_BUDGET = 1_000_000;
19
+ const MAX_INDEX_ENTRY_BUDGET = 64_000_000;
20
+ /**
21
+ * How many index slots one Loom tiny load may accumulate before it stops and
22
+ * warns instead of exhausting the heap.
23
+ *
24
+ * The default is derived from the live V8 heap limit, so raising
25
+ * `--max-old-space-size` raises the budget automatically. `MCP_LOOM_TINY_MAX_INDEX_ENTRIES`
26
+ * overrides it outright.
27
+ */
28
+ export function resolveTinyIndexEntryBudget(value = process.env.MCP_LOOM_TINY_MAX_INDEX_ENTRIES, heapStats = v8.getHeapStatistics()) {
29
+ const override = Number.parseInt(value ?? "", 10);
30
+ if (Number.isFinite(override) && override > 0) {
31
+ return override;
32
+ }
33
+ const free = Math.max(0, heapStats.heap_size_limit - heapStats.used_heap_size);
34
+ const derived = Math.floor((free * HEAP_BUDGET_FRACTION) / HEAP_BYTES_PER_INDEX_ENTRY);
35
+ return Math.min(MAX_INDEX_ENTRY_BUDGET, Math.max(MIN_INDEX_ENTRY_BUDGET, derived));
36
+ }
37
+ /**
38
+ * Read just the first line of a tiny file and derive its namespace layout.
39
+ * Returns `undefined` for anything {@link parseTinyHeader} rejects, so the body
40
+ * of an unusable file is never read.
41
+ */
42
+ export async function readTinyFileCandidate(path) {
43
+ let handle;
44
+ try {
45
+ const info = await stat(path);
46
+ if (!info.isFile() || info.size === 0) {
47
+ return undefined;
48
+ }
49
+ handle = await open(path, "r");
50
+ const buffer = Buffer.allocUnsafe(Math.min(HEADER_PROBE_BYTES, info.size));
51
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
52
+ const chunk = buffer.subarray(0, bytesRead).toString("utf8");
53
+ const newlineIndex = chunk.search(/\r?\n/);
54
+ // A file whose first line does not fit the probe cannot be a tiny header.
55
+ const headerLine = newlineIndex >= 0 ? chunk.slice(0, newlineIndex) : bytesRead < HEADER_PROBE_BYTES ? chunk : "";
56
+ const header = parseTinyHeader(headerLine);
57
+ if (!header) {
58
+ return undefined;
59
+ }
60
+ return {
61
+ path,
62
+ bytes: info.size,
63
+ descriptorNamespace: header.descriptorNamespace,
64
+ namespaces: header.namespaces
65
+ };
66
+ }
67
+ catch {
68
+ return undefined;
69
+ }
70
+ finally {
71
+ await handle?.close().catch(() => undefined);
72
+ }
73
+ }
74
+ async function hashFile(path) {
75
+ try {
76
+ const hash = createHash("sha256");
77
+ for await (const chunk of createReadStream(path)) {
78
+ hash.update(chunk);
79
+ }
80
+ return hash.digest("hex");
81
+ }
82
+ catch {
83
+ return undefined;
84
+ }
85
+ }
86
+ /** Every ordered namespace pair a candidate's rows would populate. */
87
+ export function candidatePairKeys(candidate) {
88
+ const keys = [];
89
+ for (const from of candidate.namespaces) {
90
+ for (const to of candidate.namespaces) {
91
+ if (from !== to) {
92
+ keys.push(pairKey(from, to));
93
+ }
94
+ }
95
+ }
96
+ return keys;
97
+ }
98
+ /**
99
+ * Richest first: more namespaces beats fewer; then a file whose descriptors are
100
+ * written in obfuscated coordinates beats one that is not, because that is the
101
+ * form callers query with and the form every other tiny source this server reads
102
+ * (Fabric intermediary and yarn artifacts, the Maven loader) produces; then larger
103
+ * beats smaller; then path order so the result never depends on filesystem
104
+ * enumeration order.
105
+ *
106
+ * The descriptor-coordinate rule also decides which rendering becomes the one the
107
+ * descriptor-conflict filter measures the rest against, so it must come before size:
108
+ * Loom's `mappings-base.tiny` is written with `intermediary` first AND is the larger
109
+ * file, and letting it win would leave the whole index carrying intermediary-coordinate
110
+ * descriptors.
111
+ */
112
+ export function compareCandidates(left, right) {
113
+ if (left.namespaces.length !== right.namespaces.length) {
114
+ return right.namespaces.length - left.namespaces.length;
115
+ }
116
+ const leftCanonical = left.descriptorNamespace === "obfuscated" ? 1 : 0;
117
+ const rightCanonical = right.descriptorNamespace === "obfuscated" ? 1 : 0;
118
+ if (leftCanonical !== rightCanonical) {
119
+ return rightCanonical - leftCanonical;
120
+ }
121
+ if (left.bytes !== right.bytes) {
122
+ return right.bytes - left.bytes;
123
+ }
124
+ return left.path.localeCompare(right.path);
125
+ }
126
+ /**
127
+ * Decide which of the discovered tiny files to merge.
128
+ *
129
+ * Loom keeps several renderings of one layered mapping set side by side in one
130
+ * output directory: the final `mappings.tiny`, a `-mojang` rendering with an extra
131
+ * column, `-migrated` renderings per loader build, and a `mappings-base.tiny`
132
+ * written with the namespace columns rotated. Two filters apply:
133
+ *
134
+ * 1. Byte-identical files are collapsed. Loom copies the same rendering into every
135
+ * layered variant directory, so re-reading them is pure repeated work.
136
+ * 2. A file is dropped as an alternative rendering when ALL of the following hold:
137
+ * it declares its descriptors in a different namespace than the rendering
138
+ * already chosen, it sits in a directory that already yielded a selected file,
139
+ * and every namespace pair it carries is already covered. Tiny v2 writes each
140
+ * descriptor once, in the file's first namespace; merging an `intermediary`-first
141
+ * rendering into an `official`-first index registers every method a second time
142
+ * under an incompatible descriptor, which turns unique lookups into ambiguous
143
+ * ones. All three conditions are required so that a genuinely different mapping
144
+ * file — a standalone yarn tiny, a cache in another Gradle root — is never
145
+ * mistaken for a rendering of data already loaded.
146
+ */
147
+ export async function selectTinyFiles(paths) {
148
+ const candidates = [];
149
+ const unsupported = [];
150
+ for (const path of paths) {
151
+ const candidate = await readTinyFileCandidate(path);
152
+ if (candidate) {
153
+ candidates.push(candidate);
154
+ }
155
+ else {
156
+ unsupported.push(path);
157
+ }
158
+ }
159
+ candidates.sort(compareCandidates);
160
+ // Only equal-sized files can be byte-identical, so hash nothing unless a size
161
+ // actually collides.
162
+ const sizeCounts = new Map();
163
+ for (const candidate of candidates) {
164
+ sizeCounts.set(candidate.bytes, (sizeCounts.get(candidate.bytes) ?? 0) + 1);
165
+ }
166
+ const hashes = new Map();
167
+ const directoriesByHash = new Map();
168
+ for (const candidate of candidates) {
169
+ if ((sizeCounts.get(candidate.bytes) ?? 0) < 2) {
170
+ continue;
171
+ }
172
+ const digest = await hashFile(candidate.path);
173
+ if (!digest) {
174
+ continue;
175
+ }
176
+ const hash = `${candidate.bytes}:${digest}`;
177
+ hashes.set(candidate.path, hash);
178
+ const directories = directoriesByHash.get(hash) ?? new Set();
179
+ directories.add(dirname(candidate.path));
180
+ directoriesByHash.set(hash, directories);
181
+ }
182
+ const selected = [];
183
+ const descriptorConflicts = [];
184
+ const duplicateOf = new Map();
185
+ const seenHashes = new Map();
186
+ const coveredPairs = new Set();
187
+ // Directories a selected rendering was read from, including the directories its
188
+ // byte-identical copies live in: any of them may hold sibling renderings.
189
+ const renderedDirectories = new Set();
190
+ let descriptorNamespace;
191
+ for (const candidate of candidates) {
192
+ const hash = hashes.get(candidate.path);
193
+ if (hash) {
194
+ const previous = seenHashes.get(hash);
195
+ if (previous) {
196
+ duplicateOf.set(candidate.path, previous);
197
+ continue;
198
+ }
199
+ seenHashes.set(hash, candidate.path);
200
+ }
201
+ const pairs = candidatePairKeys(candidate);
202
+ if (descriptorNamespace != null &&
203
+ candidate.descriptorNamespace !== descriptorNamespace &&
204
+ renderedDirectories.has(dirname(candidate.path)) &&
205
+ pairs.every((key) => coveredPairs.has(key))) {
206
+ descriptorConflicts.push(candidate);
207
+ continue;
208
+ }
209
+ descriptorNamespace ??= candidate.descriptorNamespace;
210
+ for (const key of pairs) {
211
+ coveredPairs.add(key);
212
+ }
213
+ renderedDirectories.add(dirname(candidate.path));
214
+ if (hash) {
215
+ for (const directory of directoriesByHash.get(hash) ?? []) {
216
+ renderedDirectories.add(directory);
217
+ }
218
+ }
219
+ selected.push(candidate);
220
+ }
221
+ return { selected, duplicateOf, descriptorConflicts, unsupported };
222
+ }
223
+ //# sourceMappingURL=tiny-loom-selection.js.map
@@ -4,15 +4,14 @@ import { join } from "node:path";
4
4
  import fastGlob from "fast-glob";
5
5
  import { buildVersionSourceSearchRoots } from "../../gradle-paths.js";
6
6
  import { effectiveLoomSearchProjectPath } from "../lookup.js";
7
- import { mergeDirectionIndexes } from "../parsers/symbol-records.js";
8
- import { parseTinyMappings } from "../parsers/tiny.js";
7
+ import { parseTinyMappingsInto } from "../parsers/tiny.js";
8
+ import { resolveTinyIndexEntryBudget, selectTinyFiles } from "./tiny-loom-selection.js";
9
9
  const GLOB_SPECIAL_CHARS = /[\\!*+?()[\]{}@|]/g;
10
10
  export async function loadTinyPairsFromLoom(version, projectPath, gradleUserHome) {
11
11
  const searchRoots = buildVersionSourceSearchRoots({
12
12
  projectPath: effectiveLoomSearchProjectPath(projectPath),
13
13
  gradleUserHome
14
14
  });
15
- const merged = new Map();
16
15
  const discoveredPaths = new Set();
17
16
  for (const root of searchRoots) {
18
17
  let discovered = [];
@@ -33,44 +32,57 @@ export async function loadTinyPairsFromLoom(version, projectPath, gradleUserHome
33
32
  catch {
34
33
  continue;
35
34
  }
36
- const byVersion = discovered
37
- .filter((path) => path.replaceAll("\\", "/").includes(`/${version}/`))
38
- .sort((left, right) => left.localeCompare(right));
39
- if (byVersion.length === 0) {
40
- continue;
41
- }
42
- for (const path of byVersion) {
43
- discoveredPaths.add(path);
44
- try {
45
- const content = await readFile(path, "utf8");
46
- const parsed = parseTinyMappings(content);
47
- for (const [key, index] of parsed.entries()) {
48
- const existing = merged.get(key);
49
- if (!existing) {
50
- merged.set(key, index);
51
- }
52
- else {
53
- mergeDirectionIndexes(existing, index);
54
- }
55
- }
56
- }
57
- catch {
58
- // best effort: skip unreadable or invalid files
35
+ for (const path of discovered) {
36
+ if (path.replaceAll("\\", "/").includes(`/${version}/`)) {
37
+ discoveredPaths.add(path);
59
38
  }
60
39
  }
61
40
  }
62
41
  const orderedPaths = [...discoveredPaths].sort((left, right) => left.localeCompare(right));
63
- if (orderedPaths.length > 0) {
42
+ if (orderedPaths.length === 0) {
64
43
  return {
65
- pairs: merged,
66
- warnings: [],
67
- mappingArtifact: orderedPaths[0]
44
+ pairs: new Map(),
45
+ warnings: [`No Loom tiny mapping files matched version "${version}".`],
46
+ mappingArtifact: "loom-cache:none"
68
47
  };
69
48
  }
49
+ // Read headers only, then drop byte-identical copies and descriptor-namespace
50
+ // conflicts before any file body is loaded. See tiny-loom-selection.ts.
51
+ const selection = await selectTinyFiles(orderedPaths);
52
+ const warnings = [];
53
+ const merged = new Map();
54
+ const maxIndexEntries = resolveTinyIndexEntryBudget();
55
+ const mergedPaths = [];
56
+ let truncatedAt;
57
+ for (const candidate of selection.selected) {
58
+ if (truncatedAt) {
59
+ break;
60
+ }
61
+ try {
62
+ const content = await readFile(candidate.path, "utf8");
63
+ const result = parseTinyMappingsInto(merged, content, { maxIndexEntries });
64
+ if (result.parsed) {
65
+ mergedPaths.push(candidate.path);
66
+ }
67
+ if (result.truncated) {
68
+ truncatedAt = candidate.path;
69
+ }
70
+ }
71
+ catch {
72
+ // best effort: skip unreadable or invalid files
73
+ }
74
+ }
75
+ if (truncatedAt) {
76
+ const skipped = selection.selected.length - mergedPaths.length;
77
+ warnings.push(`Loom tiny mappings for "${version}" hit the ${maxIndexEntries}-entry index budget while reading "${truncatedAt}"; ` +
78
+ `${skipped} of ${selection.selected.length} selected file(s) were left unread and some symbols may be missing. ` +
79
+ `Raise MCP_LOOM_TINY_MAX_INDEX_ENTRIES or start the server with a larger --max-old-space-size.`);
80
+ }
70
81
  return {
71
- pairs: new Map(),
72
- warnings: [`No Loom tiny mapping files matched version "${version}".`],
73
- mappingArtifact: "loom-cache:none"
82
+ pairs: merged,
83
+ warnings,
84
+ // The richest merged file, not merely the alphabetically first discovered one.
85
+ mappingArtifact: mergedPaths[0] ?? orderedPaths[0]
74
86
  };
75
87
  }
76
88
  //# sourceMappingURL=tiny-loom.js.map
@@ -1,7 +1,7 @@
1
1
  import { defaultDownloadPath, downloadToCache } from "../../repo-downloader.js";
2
2
  import { collectMatchedJarEntriesAsUtf8 } from "../../source-jar-reader.js";
3
3
  import { mergeDirectionIndexes } from "../parsers/symbol-records.js";
4
- import { parseTinyMappings } from "../parsers/tiny.js";
4
+ import { parseTinyMappingsInto } from "../parsers/tiny.js";
5
5
  async function fetchYarnCoordinates(fetchFn, repoBase, version) {
6
6
  const metadataUrl = `${repoBase}/net/fabricmc/yarn/maven-metadata.xml`;
7
7
  try {
@@ -29,19 +29,14 @@ async function fetchYarnCoordinates(fetchFn, repoBase, version) {
29
29
  }
30
30
  async function parseTinyFromJar(jarPath) {
31
31
  const tinyEntries = (await collectMatchedJarEntriesAsUtf8(jarPath, (entry) => entry.toLowerCase().endsWith(".tiny") || entry.toLowerCase().endsWith(".tinyv2"), { continueOnError: true })).sort((left, right) => left.filePath.localeCompare(right.filePath));
32
+ // Parsed straight into the shared accumulator: a parse-then-merge loop would
33
+ // hold each entry's full index alongside the accumulated one. `ensurePairIndex`
34
+ // + `addLookupEntries` union into what is already there, matching what
35
+ // `mergeDirectionIndexes` did.
32
36
  const merged = new Map();
33
37
  for (const entry of tinyEntries) {
34
38
  try {
35
- const parsed = parseTinyMappings(entry.content);
36
- for (const [key, index] of parsed.entries()) {
37
- const existing = merged.get(key);
38
- if (!existing) {
39
- merged.set(key, index);
40
- }
41
- else {
42
- mergeDirectionIndexes(existing, index);
43
- }
44
- }
39
+ parseTinyMappingsInto(merged, entry.content);
45
40
  }
46
41
  catch {
47
42
  // skip malformed tiny entries
@@ -5,5 +5,62 @@
5
5
  import type { SourceMapping } from "../../types.js";
6
6
  import type { DirectionIndex, MappingSymbolRecord, PairKey } from "../internal-types.js";
7
7
  export declare function normalizeTinyNamespace(namespace: string): SourceMapping | undefined;
8
+ /**
9
+ * Namespace layout of a tiny v2 file, derivable from the header line alone.
10
+ *
11
+ * `descriptorNamespace` identifies the FIRST namespace column, normalized when this
12
+ * server recognizes it (so `official` and `obfuscated` compare equal) and otherwise
13
+ * the raw lower-cased token. Tiny v2 stores every field/method descriptor in that
14
+ * namespace's coordinates and never repeats it per namespace, so two files whose
15
+ * first column differs describe the same members with mutually incompatible
16
+ * descriptors. Callers that merge several files into one index must compare this
17
+ * value — see `src/mapping/loaders/tiny-loom-selection.ts`.
18
+ */
19
+ export type TinyHeader = {
20
+ descriptorNamespace: SourceMapping | string;
21
+ /** Recognized namespaces paired with their data-column offset, in file order. */
22
+ columns: Array<{
23
+ mapping: SourceMapping;
24
+ columnIndex: number;
25
+ }>;
26
+ /** Distinct recognized namespaces, in file order. */
27
+ namespaces: SourceMapping[];
28
+ };
29
+ /**
30
+ * Parse only the header line of a tiny v2 file. Returns `undefined` when the line
31
+ * is not a tiny v2 header or declares fewer than two namespaces this server knows,
32
+ * which is exactly when {@link parseTinyMappingsInto} would contribute nothing.
33
+ * Lets callers classify a file without reading its body.
34
+ */
35
+ export declare function parseTinyHeader(headerLine: string): TinyHeader | undefined;
8
36
  export declare function addPairRecords(target: Map<PairKey, DirectionIndex>, records: Map<SourceMapping, MappingSymbolRecord>): void;
37
+ /** Total number of index slots held by a pair map; the unit the parse budget counts. */
38
+ export declare function countIndexEntries(pairs: Map<PairKey, DirectionIndex>): number;
39
+ export type TinyParseOptions = {
40
+ /**
41
+ * Stop parsing once the target holds this many index slots (see
42
+ * {@link countIndexEntries}). Guards a single unbounded accumulator against
43
+ * caches large enough to exhaust the V8 heap. Omit for no bound.
44
+ */
45
+ maxIndexEntries?: number;
46
+ };
47
+ export type TinyParseResult = {
48
+ /** False when the header was not usable, so nothing was added. */
49
+ parsed: boolean;
50
+ /** True when `maxIndexEntries` stopped the parse before the last line. */
51
+ truncated: boolean;
52
+ /** Index slots held by the target after this parse. */
53
+ indexEntries: number;
54
+ };
55
+ /**
56
+ * Parse `text` directly into `target`.
57
+ *
58
+ * Writing into the caller's accumulator rather than into a private map and
59
+ * merging afterwards is what keeps multi-file loads inside the heap: a
60
+ * parse-then-merge loop holds the whole per-file index AND the accumulated index
61
+ * at the same time, roughly doubling peak usage on the largest file. Merging is
62
+ * not lost — `ensurePairIndex` + `addLookupEntries` union into whatever is
63
+ * already present, which is precisely what `mergeDirectionIndexes` did.
64
+ */
65
+ export declare function parseTinyMappingsInto(target: Map<PairKey, DirectionIndex>, text: string, options?: TinyParseOptions): TinyParseResult;
9
66
  export declare function parseTinyMappings(text: string): Map<PairKey, DirectionIndex>;
@@ -20,6 +20,40 @@ export function normalizeTinyNamespace(namespace) {
20
20
  }
21
21
  return undefined;
22
22
  }
23
+ /**
24
+ * Parse only the header line of a tiny v2 file. Returns `undefined` when the line
25
+ * is not a tiny v2 header or declares fewer than two namespaces this server knows,
26
+ * which is exactly when {@link parseTinyMappingsInto} would contribute nothing.
27
+ * Lets callers classify a file without reading its body.
28
+ */
29
+ export function parseTinyHeader(headerLine) {
30
+ const header = headerLine.split("\t");
31
+ if (header.length < 5 || header[0] !== "tiny" || header[1] !== "2") {
32
+ return undefined;
33
+ }
34
+ const columns = header
35
+ .slice(3)
36
+ .map((namespace, index) => ({
37
+ mapping: normalizeTinyNamespace(namespace),
38
+ columnIndex: index + 1
39
+ }))
40
+ .filter((entry) => entry.mapping != null);
41
+ if (columns.length < 2) {
42
+ return undefined;
43
+ }
44
+ const namespaces = [];
45
+ for (const column of columns) {
46
+ if (!namespaces.includes(column.mapping)) {
47
+ namespaces.push(column.mapping);
48
+ }
49
+ }
50
+ const rawDescriptorNamespace = header[3].trim().toLowerCase();
51
+ return {
52
+ descriptorNamespace: normalizeTinyNamespace(rawDescriptorNamespace) ?? rawDescriptorNamespace,
53
+ columns,
54
+ namespaces
55
+ };
56
+ }
23
57
  export function addPairRecords(target, records) {
24
58
  for (const [sourceMapping, sourceRecord] of records.entries()) {
25
59
  for (const [targetMapping, targetRecord] of records.entries()) {
@@ -30,30 +64,68 @@ export function addPairRecords(target, records) {
30
64
  }
31
65
  }
32
66
  }
33
- export function parseTinyMappings(text) {
34
- const lines = text.split(/\r?\n/).filter((line) => line.trim().length > 0);
35
- if (lines.length === 0) {
36
- return new Map();
67
+ /** Total number of index slots held by a pair map; the unit the parse budget counts. */
68
+ export function countIndexEntries(pairs) {
69
+ let total = 0;
70
+ for (const index of pairs.values()) {
71
+ total += index.exact.size + index.normalized.size + index.simple.size + index.records.size;
37
72
  }
38
- const header = lines[0].split("\t");
39
- if (header.length < 5 || header[0] !== "tiny" || header[1] !== "2") {
40
- return new Map();
73
+ return total;
74
+ }
75
+ /** How often the running index size is re-measured while parsing. */
76
+ const BUDGET_CHECK_INTERVAL = 512;
77
+ /**
78
+ * Parse `text` directly into `target`.
79
+ *
80
+ * Writing into the caller's accumulator rather than into a private map and
81
+ * merging afterwards is what keeps multi-file loads inside the heap: a
82
+ * parse-then-merge loop holds the whole per-file index AND the accumulated index
83
+ * at the same time, roughly doubling peak usage on the largest file. Merging is
84
+ * not lost — `ensurePairIndex` + `addLookupEntries` union into whatever is
85
+ * already present, which is precisely what `mergeDirectionIndexes` did.
86
+ */
87
+ export function parseTinyMappingsInto(target, text, options) {
88
+ const budget = options?.maxIndexEntries;
89
+ const lines = text.split(/\r?\n/);
90
+ let headerIndex = -1;
91
+ for (let index = 0; index < lines.length; index += 1) {
92
+ if (lines[index].trim().length > 0) {
93
+ headerIndex = index;
94
+ break;
95
+ }
41
96
  }
42
- const namespaceColumns = header.slice(3).map((namespace, index) => ({
43
- mapping: normalizeTinyNamespace(namespace),
44
- columnIndex: index + 1
45
- }));
46
- const recognized = namespaceColumns.filter((entry) => entry.mapping != null);
47
- if (recognized.length < 2) {
48
- return new Map();
97
+ if (headerIndex < 0) {
98
+ return { parsed: false, truncated: false, indexEntries: countIndexEntries(target) };
99
+ }
100
+ const header = parseTinyHeader(lines[headerIndex]);
101
+ if (!header) {
102
+ return { parsed: false, truncated: false, indexEntries: countIndexEntries(target) };
103
+ }
104
+ let entries = countIndexEntries(target);
105
+ if (budget != null && entries >= budget) {
106
+ return { parsed: true, truncated: true, indexEntries: entries };
49
107
  }
50
- const result = new Map();
51
108
  const currentClassNames = new Map();
52
- for (const line of lines.slice(1)) {
109
+ let sinceBudgetCheck = 0;
110
+ for (let lineIndex = headerIndex + 1; lineIndex < lines.length; lineIndex += 1) {
111
+ const line = lines[lineIndex];
112
+ if (line.trim().length === 0) {
113
+ continue;
114
+ }
115
+ if (budget != null) {
116
+ sinceBudgetCheck += 1;
117
+ if (sinceBudgetCheck >= BUDGET_CHECK_INTERVAL) {
118
+ sinceBudgetCheck = 0;
119
+ entries = countIndexEntries(target);
120
+ if (entries >= budget) {
121
+ return { parsed: true, truncated: true, indexEntries: entries };
122
+ }
123
+ }
124
+ }
53
125
  const columns = line.split("\t");
54
126
  if (columns[0] === "c") {
55
127
  const classRecords = new Map();
56
- for (const namespace of recognized) {
128
+ for (const namespace of header.columns) {
57
129
  const value = columns[namespace.columnIndex]?.trim() ?? "";
58
130
  if (!value) {
59
131
  continue;
@@ -61,12 +133,12 @@ export function parseTinyMappings(text) {
61
133
  currentClassNames.set(namespace.mapping, value);
62
134
  classRecords.set(namespace.mapping, createClassSymbolRecord(value));
63
135
  }
64
- addPairRecords(result, classRecords);
136
+ addPairRecords(target, classRecords);
65
137
  continue;
66
138
  }
67
139
  if (columns[0] === "" && columns[1] === "f") {
68
140
  const fieldRecords = new Map();
69
- for (const namespace of recognized) {
141
+ for (const namespace of header.columns) {
70
142
  const owner = currentClassNames.get(namespace.mapping);
71
143
  const value = columns[namespace.columnIndex + 2]?.trim() ?? "";
72
144
  if (!owner || !value) {
@@ -74,13 +146,13 @@ export function parseTinyMappings(text) {
74
146
  }
75
147
  fieldRecords.set(namespace.mapping, createFieldSymbolRecord(owner, value));
76
148
  }
77
- addPairRecords(result, fieldRecords);
149
+ addPairRecords(target, fieldRecords);
78
150
  continue;
79
151
  }
80
152
  if (columns[0] === "" && columns[1] === "m") {
81
153
  const descriptor = columns[2]?.trim() || undefined;
82
154
  const methodRecords = new Map();
83
- for (const namespace of recognized) {
155
+ for (const namespace of header.columns) {
84
156
  const owner = currentClassNames.get(namespace.mapping);
85
157
  const value = columns[namespace.columnIndex + 2]?.trim() ?? "";
86
158
  if (!owner || !value) {
@@ -88,9 +160,14 @@ export function parseTinyMappings(text) {
88
160
  }
89
161
  methodRecords.set(namespace.mapping, createMethodSymbolRecord(owner, value, descriptor));
90
162
  }
91
- addPairRecords(result, methodRecords);
163
+ addPairRecords(target, methodRecords);
92
164
  }
93
165
  }
166
+ return { parsed: true, truncated: false, indexEntries: countIndexEntries(target) };
167
+ }
168
+ export function parseTinyMappings(text) {
169
+ const result = new Map();
170
+ parseTinyMappingsInto(result, text);
94
171
  return result;
95
172
  }
96
173
  //# sourceMappingURL=tiny.js.map
@@ -37,6 +37,25 @@ export declare class MappingService {
37
37
  checkSymbolExists(input: SymbolExistenceInput): Promise<SymbolExistenceOutput>;
38
38
  private mapRecordBetweenMappings;
39
39
  private mapCandidatesAlongPath;
40
+ /**
41
+ * The owner names that a query owner is allowed to appear under once candidates have
42
+ * been projected along `path`.
43
+ *
44
+ * The query owner arrives in the SOURCE namespace while `mapCandidatesAlongPath`
45
+ * hands back records whose `owner` is the declaring class in the TARGET namespace
46
+ * (both parsers build member records with `createMethodSymbolRecord(<owner in that
47
+ * namespace>, ...)`), so the two are only comparable after the owner itself has been
48
+ * carried along the same path. Projecting the owner as a class record through
49
+ * `mapCandidatesAlongPath` is that carriage, and it is the same mechanism
50
+ * `projectMethodDescriptorToTarget` already uses for class references inside a
51
+ * descriptor.
52
+ *
53
+ * `simple-name` class matches are excluded on purpose: they mean "some other class
54
+ * that happens to share this simple name", which is exactly the foreign-owner noise
55
+ * the strict filter exists to remove. The source-namespace name is kept as an
56
+ * accepted alias so an owner that the mapping does not rename still matches itself.
57
+ */
58
+ private acceptedOwnersAlongPath;
40
59
  private projectMethodDescriptorToTarget;
41
60
  private provenanceForPath;
42
61
  /**