@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
@@ -2,6 +2,17 @@ import { VersionService } from "./version-service.js";
2
2
  import { RegistryService } from "./registry-service.js";
3
3
  import type { Config } from "./types.js";
4
4
  export type CompareVersionsCategory = "classes" | "registry" | "all";
5
+ /**
6
+ * Namespace the compared class names live in.
7
+ *
8
+ * A vanilla client jar for an obfuscated release lists `dlp.class`, not
9
+ * `net/minecraft/world/item/Item.class`, so a diff taken straight off the jar
10
+ * entries answers in obfuscated names. Every caller-facing name — the `added`
11
+ * and `removed` lists AND `packageFilter` — is expressed in mojang names when
12
+ * the official mappings for both versions can be loaded, and the namespace is
13
+ * always reported so a zero result is never ambiguous.
14
+ */
15
+ export type ClassDiffNamespace = "mojang" | "obfuscated";
5
16
  export type CompareVersionsInput = {
6
17
  fromVersion: string;
7
18
  toVersion: string;
@@ -18,6 +29,15 @@ export type CompareVersionsOutput = {
18
29
  addedCount: number;
19
30
  removedCount: number;
20
31
  unchanged: number;
32
+ /** Namespace the class names above (and any packageFilter) are expressed in. */
33
+ namespace: ClassDiffNamespace;
34
+ /** Present only when the caller passed packageFilter; reports what it matched. */
35
+ packageFilter?: {
36
+ value: string;
37
+ namespace: ClassDiffNamespace;
38
+ matchedFrom: number;
39
+ matchedTo: number;
40
+ };
21
41
  };
22
42
  registry?: {
23
43
  added: Record<string, string[]>;
@@ -36,6 +56,19 @@ export declare class VersionDiffService {
36
56
  private readonly config;
37
57
  private readonly versionService;
38
58
  private readonly registryService;
59
+ private readonly mojangClassMaps;
39
60
  constructor(config: Config, versionService: VersionService, registryService: RegistryService);
61
+ /**
62
+ * obfuscated -> mojang class map for one version, or undefined when the
63
+ * official mappings cannot be obtained (offline, or a release that publishes
64
+ * none). Never throws: a missing map degrades the diff to obfuscated names
65
+ * with a warning rather than failing the comparison.
66
+ */
67
+ private loadMojangClassMap;
68
+ /**
69
+ * Lifts one version's jar class list into mojang names when possible.
70
+ * Returns the namespace the returned set is actually expressed in.
71
+ */
72
+ private toMojangNames;
40
73
  compareVersions(input: CompareVersionsInput): Promise<CompareVersionsOutput>;
41
74
  }
@@ -1,5 +1,7 @@
1
+ import { readFile } from "node:fs/promises";
1
2
  import { createError, ERROR_CODES } from "./errors.js";
2
3
  import { log } from "./logger.js";
4
+ import { resolveMojangTinyFile } from "./mojang-tiny-mapping-service.js";
3
5
  import { listJarEntries } from "./source-jar-reader.js";
4
6
  const DEFAULT_MAX_CLASS_RESULTS = 500;
5
7
  const MAX_CLASS_RESULTS_LIMIT = 5000;
@@ -25,6 +27,49 @@ function filterByPackage(classes, prefix) {
25
27
  function filterSetByPackage(classes, prefix) {
26
28
  return new Set(filterByPackage([...classes], prefix));
27
29
  }
30
+ /**
31
+ * True when the jar's class names are obfuscated.
32
+ *
33
+ * Obfuscation collapses every Minecraft class into the DEFAULT package with a
34
+ * short generated name (`dlp`, `ije`), so a jar where a large share of the
35
+ * classes carry no package at all is obfuscated. Unobfuscated releases, jars
36
+ * remapped before listing, and ordinary library/mod jars all keep real package
37
+ * structure and need no mapping load.
38
+ */
39
+ function looksObfuscated(classes) {
40
+ if (classes.size === 0) {
41
+ return false;
42
+ }
43
+ let defaultPackage = 0;
44
+ for (const fqn of classes) {
45
+ if (!fqn.includes(".")) {
46
+ defaultPackage += 1;
47
+ }
48
+ }
49
+ return defaultPackage > 0 && defaultPackage * 4 >= classes.size;
50
+ }
51
+ /** Parses the `c<TAB>obf<TAB>mojang` rows of a merged mojang tiny v2 file. */
52
+ function parseTinyClassRows(content) {
53
+ const classMap = new Map();
54
+ for (const line of content.split("\n")) {
55
+ if (line.charCodeAt(0) !== 99 /* 'c' */ || line.charCodeAt(1) !== 9 /* TAB */) {
56
+ continue;
57
+ }
58
+ const parts = line.split("\t");
59
+ if (parts.length < 3) {
60
+ continue;
61
+ }
62
+ const obfuscated = parts[1];
63
+ const mojang = parts[2];
64
+ if (!obfuscated || !mojang) {
65
+ continue;
66
+ }
67
+ classMap.set(obfuscated.replaceAll("/", "."), mojang.replaceAll("/", "."));
68
+ }
69
+ return classMap;
70
+ }
71
+ /** Bounded per-process memo so a repeated comparison re-reads nothing. */
72
+ const MOJANG_CLASS_MAP_CACHE_LIMIT = 4;
28
73
  function diffSets(from, to) {
29
74
  const added = [];
30
75
  const removed = [];
@@ -128,11 +173,76 @@ export class VersionDiffService {
128
173
  config;
129
174
  versionService;
130
175
  registryService;
176
+ mojangClassMaps = new Map();
131
177
  constructor(config, versionService, registryService) {
132
178
  this.config = config;
133
179
  this.versionService = versionService;
134
180
  this.registryService = registryService;
135
181
  }
182
+ /**
183
+ * obfuscated -> mojang class map for one version, or undefined when the
184
+ * official mappings cannot be obtained (offline, or a release that publishes
185
+ * none). Never throws: a missing map degrades the diff to obfuscated names
186
+ * with a warning rather than failing the comparison.
187
+ */
188
+ async loadMojangClassMap(version) {
189
+ const cached = this.mojangClassMaps.get(version);
190
+ if (cached) {
191
+ return cached;
192
+ }
193
+ try {
194
+ const { path } = await resolveMojangTinyFile(version, this.config, {
195
+ versionService: this.versionService
196
+ });
197
+ const classMap = parseTinyClassRows(await readFile(path, "utf8"));
198
+ if (classMap.size === 0) {
199
+ return undefined;
200
+ }
201
+ if (this.mojangClassMaps.size >= MOJANG_CLASS_MAP_CACHE_LIMIT) {
202
+ const oldest = this.mojangClassMaps.keys().next().value;
203
+ if (oldest !== undefined) {
204
+ this.mojangClassMaps.delete(oldest);
205
+ }
206
+ }
207
+ this.mojangClassMaps.set(version, classMap);
208
+ return classMap;
209
+ }
210
+ catch (error) {
211
+ log("warn", "version-diff.mojang_class_map_unavailable", {
212
+ version,
213
+ error: error instanceof Error ? error.message : String(error)
214
+ });
215
+ return undefined;
216
+ }
217
+ }
218
+ /**
219
+ * Lifts one version's jar class list into mojang names when possible.
220
+ * Returns the namespace the returned set is actually expressed in.
221
+ */
222
+ async toMojangNames(version, classes) {
223
+ if (!looksObfuscated(classes)) {
224
+ return { classes, namespace: "mojang", unmapped: 0 };
225
+ }
226
+ const classMap = await this.loadMojangClassMap(version);
227
+ if (!classMap) {
228
+ return { classes, namespace: "obfuscated", unmapped: classes.size };
229
+ }
230
+ const mapped = new Set();
231
+ let unmapped = 0;
232
+ for (const fqn of classes) {
233
+ const mojang = classMap.get(fqn);
234
+ if (mojang) {
235
+ mapped.add(mojang);
236
+ }
237
+ else {
238
+ // Library classes shipped inside the jar (and anything the mappings do
239
+ // not cover) keep the name the jar carries; they are already readable.
240
+ mapped.add(fqn);
241
+ unmapped += 1;
242
+ }
243
+ }
244
+ return { classes: mapped, namespace: "mojang", unmapped };
245
+ }
136
246
  async compareVersions(input) {
137
247
  const fromVersion = input.fromVersion.trim();
138
248
  const toVersion = input.toVersion.trim();
@@ -168,14 +278,38 @@ export class VersionDiffService {
168
278
  listJarEntries(fromJar.jarPath),
169
279
  listJarEntries(toJar.jarPath)
170
280
  ]);
171
- const fromClasses = extractClassEntries(fromEntries);
172
- const toClasses = extractClassEntries(toEntries);
281
+ // The class names must be lifted OUT of the jar's own namespace
282
+ // before anything is compared or filtered: a mojang packageFilter
283
+ // against obfuscated jar entries matched nothing and reported a
284
+ // silent all-zero diff that read as "no changes".
285
+ const [fromNames, toNames] = await Promise.all([
286
+ this.toMojangNames(fromVersion, extractClassEntries(fromEntries)),
287
+ this.toMojangNames(toVersion, extractClassEntries(toEntries))
288
+ ]);
289
+ const namespace = fromNames.namespace === "mojang" && toNames.namespace === "mojang"
290
+ ? "mojang"
291
+ : "obfuscated";
292
+ if (namespace === "obfuscated") {
293
+ warnings.push(`Official Mojang mappings could not be loaded for ${fromNames.namespace === "obfuscated" ? fromVersion : toVersion}, so class names (and packageFilter) are compared in the OBFUSCATED namespace. Deobfuscated prefixes such as "net.minecraft.world.item" cannot match here.`);
294
+ }
295
+ const fromClasses = fromNames.classes;
296
+ const toClasses = toNames.classes;
173
297
  const filteredFromClasses = input.packageFilter
174
298
  ? filterSetByPackage(fromClasses, input.packageFilter)
175
299
  : fromClasses;
176
300
  const filteredToClasses = input.packageFilter
177
301
  ? filterSetByPackage(toClasses, input.packageFilter)
178
302
  : toClasses;
303
+ if (input.packageFilter && filteredFromClasses.size === 0 && filteredToClasses.size === 0) {
304
+ // An empty diff and a non-matching filter look identical on the
305
+ // wire, so the filter has to say which one happened.
306
+ warnings.push(`packageFilter "${input.packageFilter}" matched no class in either ${fromVersion} or ${toVersion}; ` +
307
+ `the reported zeros mean "filter matched nothing", not "nothing changed". ` +
308
+ `Class names are compared in the ${namespace} namespace — ` +
309
+ (namespace === "obfuscated"
310
+ ? "an obfuscated jar has no package structure to filter on, so omit packageFilter."
311
+ : "check the package prefix spelling, e.g. \"net.minecraft.world.item\"."));
312
+ }
179
313
  const { added, removed, unchanged } = diffSets(filteredFromClasses, filteredToClasses);
180
314
  const truncatedAdded = added.slice(0, maxClassResults);
181
315
  const truncatedRemoved = removed.slice(0, maxClassResults);
@@ -190,7 +324,18 @@ export class VersionDiffService {
190
324
  removed: truncatedRemoved,
191
325
  addedCount: added.length,
192
326
  removedCount: removed.length,
193
- unchanged
327
+ unchanged,
328
+ namespace,
329
+ ...(input.packageFilter
330
+ ? {
331
+ packageFilter: {
332
+ value: input.packageFilter,
333
+ namespace,
334
+ matchedFrom: filteredFromClasses.size,
335
+ matchedTo: filteredToClasses.size
336
+ }
337
+ }
338
+ : {})
194
339
  };
195
340
  })());
196
341
  }
@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
4
  import { buildSuggestedCall } from "./build-suggested-call.js";
5
- import { createError, ERROR_CODES } from "./errors.js";
5
+ import { createError, ERROR_CODES, isAppError } from "./errors.js";
6
6
  import { computeFileSha1 } from "./hash.js";
7
7
  import { defaultDownloadPath, downloadToCache } from "./repo-downloader.js";
8
8
  const DEFAULT_VERSION_MANIFEST_URL = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
@@ -352,26 +352,48 @@ export class VersionService {
352
352
  return details;
353
353
  }
354
354
  async fetchJson(url) {
355
- const response = await this.fetchFn(url);
356
- if (!response.ok) {
355
+ const timeout = new AbortController();
356
+ const timer = setTimeout(() => timeout.abort(), this.config.fetchTimeoutMs);
357
+ try {
358
+ const response = await this.fetchFn(url, { signal: timeout.signal });
359
+ if (!response.ok) {
360
+ throw createError({
361
+ code: ERROR_CODES.REPO_FETCH_FAILED,
362
+ message: `Request failed for "${url}" with status ${response.status}.`,
363
+ details: {
364
+ url,
365
+ statusCode: response.status
366
+ }
367
+ });
368
+ }
369
+ try {
370
+ return await response.json();
371
+ }
372
+ catch {
373
+ throw createError({
374
+ code: ERROR_CODES.REPO_FETCH_FAILED,
375
+ message: `Response from "${url}" is not valid JSON.`,
376
+ details: { url }
377
+ });
378
+ }
379
+ }
380
+ catch (error) {
381
+ // A typed status/JSON error is more precise than the timeout mapping even
382
+ // when the abort timer fired concurrently — never rewrite it.
383
+ if (isAppError(error) || !timeout.signal.aborted) {
384
+ throw error;
385
+ }
357
386
  throw createError({
358
387
  code: ERROR_CODES.REPO_FETCH_FAILED,
359
- message: `Request failed for "${url}" with status ${response.status}.`,
388
+ message: `Request timed out for "${url}" after ${this.config.fetchTimeoutMs}ms.`,
360
389
  details: {
361
390
  url,
362
- statusCode: response.status
391
+ timeoutMs: this.config.fetchTimeoutMs
363
392
  }
364
393
  });
365
394
  }
366
- try {
367
- return await response.json();
368
- }
369
- catch {
370
- throw createError({
371
- code: ERROR_CODES.REPO_FETCH_FAILED,
372
- message: `Response from "${url}" is not valid JSON.`,
373
- details: { url }
374
- });
395
+ finally {
396
+ clearTimeout(timer);
375
397
  }
376
398
  }
377
399
  cacheIndexPath() {
@@ -31,7 +31,24 @@ const WARNING_RULES = [
31
31
  affectedFields: ["mapping", "version"]
32
32
  },
33
33
  {
34
- test: /falling back to vanilla|resolution failed; falling back|sources jar\.?\s*Falling back|does not include net\.minecraft/i,
34
+ // compare-versions: the filter is fine but matched nothing, which used to
35
+ // be indistinguishable from "nothing changed".
36
+ test: /packageFilter ".*" matched no class/i,
37
+ code: "filter_matched_nothing",
38
+ category: "validation",
39
+ severity: "warning",
40
+ affectedFields: ["packageFilter"]
41
+ },
42
+ {
43
+ // compare-versions could not lift the jars out of the obfuscated namespace.
44
+ test: /compared in the OBFUSCATED namespace/i,
45
+ code: "namespace_fallback",
46
+ category: "mapping",
47
+ severity: "warning",
48
+ affectedFields: ["packageFilter"]
49
+ },
50
+ {
51
+ test: /falling back to vanilla|resolution failed; falling back|sources jar\.?\s*Falling back|(?:does not include|excludes) net\.minecraft/i,
35
52
  code: "partial_coverage",
36
53
  category: "coverage",
37
54
  severity: "warning",
package/docs/README-ja.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@adhisang/minecraft-modding-mcp)](https://www.npmjs.com/package/@adhisang/minecraft-modding-mcp)
4
4
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](../LICENSE)
5
- [![Node.js >=22](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)](https://nodejs.org/)
5
+ [![Node.js >=22.13.0](https://img.shields.io/badge/node-%3E%3D22.13.0-brightgreen.svg)](https://nodejs.org/)
6
6
  [![CI](https://github.com/adhi-jp/minecraft-modding-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/adhi-jp/minecraft-modding-mcp/actions/workflows/ci.yml)
7
7
 
8
8
  **[English](../README.md)** | 日本語
@@ -33,7 +33,7 @@ stdio で動作し、Claude Desktop、Claude Code、VS Code、Codex CLI、Gemini
33
33
 
34
34
  要件:
35
35
 
36
- - Node.js 22+
36
+ - Node.js 22.13.0+
37
37
  - Java は `remap-mod-jar` と、Vineflower または tiny-remapper を必要とするデコンパイル / リマップ処理でのみ必要です
38
38
 
39
39
  ローカルでサーバーを起動:
@@ -64,6 +64,8 @@ codex mcp add minecraft-modding -- npx -y @adhisang/minecraft-modding-mcp
64
64
 
65
65
  stdio トランスポートは、改行区切り形式と `Content-Length` フレーミングの両方を自動判別します。そのため、Codex と標準的な MCP クライアントで同じサーバー起動コマンドを利用できます。
66
66
 
67
+ サーバーは MCP プロトコルリビジョン `2026-07-28`(`server/discover` とリクエスト単位 `_meta` によるステートレス動作)と、従来の `initialize` ベースのレガシープロトコル(`2025-11-25`〜`2024-10-07`)の両方を同一バイナリでサポートします。1 プロセスはクライアントの最初のシグナルで選択された era を提供します。ワイヤレベルの詳細は [docs/tool-reference.md → MCP Protocol Support](tool-reference.md#mcp-protocol-support) を参照してください。
68
+
67
69
  #### Claude Desktop
68
70
 
69
71
  `claude_desktop_config.json` に次を追加します:
@@ -378,7 +380,7 @@ Mixin ソース、Access Widener ファイル、Forge / NeoForge の Access Tran
378
380
 
379
381
  リポジトリ要件:
380
382
 
381
- - Node.js 22+
383
+ - Node.js 22.13.0+
382
384
  - `pnpm`
383
385
  - ローカルでリマップやデコンパイルを実行する場合は Java
384
386
 
@@ -405,12 +407,16 @@ pnpm test
405
407
 
406
408
  必要に応じて実行:
407
409
 
410
+ - `pnpm test:file <path...>`: 通常テストをファイル指定で絞り込む場合
411
+ - `pnpm test:grep <name-pattern>`: 通常テストを再帰的に集め、テスト名で絞り込む場合
408
412
  - `pnpm test:manual:stdio-smoke`: MCP トランスポート、登録、手動ワークフローの変更時
409
413
  - `pnpm test:manual:package-smoke`: パッケージインストールや配布形態の検証時
410
414
  - `pnpm test:perf`: 検索、インデックス、性能に影響する変更時
411
415
  - `pnpm test:coverage` または `pnpm test:coverage:lcov`: カバレッジ確認時(`lines=80`, `branches=70`, `functions=80`)
412
416
  - `pnpm validate`: ローカルの完全検証スイートを実行する場合
413
417
 
418
+ 通常の `.test.ts` ファイルは `tests/` 配下のドメイン別ディレクトリ(例: `source-service/`, `entry-tools/`, `mapping/`, `mixin/`, `integration/mcp-tools/`, `contracts/`, `utils/`)に置きます。ヘルパー専用モジュールは `tests/helpers` に残し、手動 smoke、性能、リソース、smoke 専用ファイルは既存の専用ディレクトリに残します。
419
+
414
420
  ## ライセンス
415
421
 
416
422
  [MIT](../LICENSE)