@adhisang/minecraft-modding-mcp 3.0.0 → 3.1.1

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.
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
- import { createError, ERROR_CODES } from "../errors.js";
2
+ import { createError, ERROR_CODES, isAppError } from "../errors.js";
3
3
  import { buildIncludeSchema, detailSchema, positiveIntSchema } from "./entry-tool-schema.js";
4
- import { buildEntryToolResult, buildEntryToolMeta, createNextAction, createTruncationMeta } from "./response-contract.js";
4
+ import { buildEntryToolResult, buildEntryToolMeta, createNextAction, createSummarySubject, createTruncationMeta } from "./response-contract.js";
5
5
  import { capArray, nextActionsOrUndefined, resolveDetail, resolveInclude } from "./request-normalizers.js";
6
6
  const INCLUDE_GROUPS = ["warnings", "provenance", "candidates", "members", "source", "files", "samples", "artifact", "timings"];
7
7
  const TASKS = ["auto", "versions", "artifact", "class-overview", "class-source", "class-members", "search", "file", "list-files"];
@@ -41,7 +41,7 @@ const workspaceFocusSchema = z.discriminatedUnion("kind", [
41
41
  symbolKind: z.enum(["class", "interface", "enum", "record", "method", "field"]).optional(),
42
42
  packagePrefix: nonEmptyString.optional(),
43
43
  fileGlob: nonEmptyString.optional(),
44
- queryMode: z.enum(["auto", "token", "literal"]).optional()
44
+ queryMode: z.enum(["auto", "token", "literal"]).default("auto")
45
45
  })
46
46
  ]);
47
47
  const subjectSchema = z.discriminatedUnion("kind", [
@@ -87,7 +87,7 @@ const subjectSchema = z.discriminatedUnion("kind", [
87
87
  symbolKind: z.enum(["class", "interface", "enum", "record", "method", "field"]).optional(),
88
88
  packagePrefix: nonEmptyString.optional(),
89
89
  fileGlob: nonEmptyString.optional(),
90
- queryMode: z.enum(["auto", "token", "literal"]).optional()
90
+ queryMode: z.enum(["auto", "token", "literal"]).default("auto")
91
91
  }),
92
92
  z.object({
93
93
  kind: z.literal("workspace"),
@@ -102,7 +102,7 @@ const subjectSchema = z.discriminatedUnion("kind", [
102
102
  export const inspectMinecraftShape = {
103
103
  task: z.enum(TASKS).optional(),
104
104
  subject: subjectSchema.optional(),
105
- includeSnapshots: z.boolean().optional(),
105
+ includeSnapshots: z.boolean().default(false),
106
106
  detail: detailSchema.optional(),
107
107
  include: buildIncludeSchema(INCLUDE_GROUPS),
108
108
  limit: positiveIntSchema.optional(),
@@ -116,7 +116,7 @@ export const inspectMinecraftSchema = z.object(inspectMinecraftShape).superRefin
116
116
  message: "subject is required unless task=versions."
117
117
  });
118
118
  }
119
- if (value.includeSnapshots !== undefined && value.task && value.task !== "versions") {
119
+ if (value.includeSnapshots && value.task && value.task !== "versions") {
120
120
  ctx.addIssue({
121
121
  code: z.ZodIssueCode.custom,
122
122
  path: ["includeSnapshots"],
@@ -124,6 +124,32 @@ export const inspectMinecraftSchema = z.object(inspectMinecraftShape).superRefin
124
124
  });
125
125
  }
126
126
  });
127
+ function hasPartialVanillaCoverage(artifact) {
128
+ return artifact?.qualityFlags.includes("partial-source-no-net-minecraft") === true
129
+ || artifact?.artifactContents.sourceCoverage === "partial";
130
+ }
131
+ function looksLikeClassQuery(query) {
132
+ const trimmed = query.trim();
133
+ if (!/^[A-Za-z_$][A-Za-z0-9_$.]*$/.test(trimmed)) {
134
+ return false;
135
+ }
136
+ const simpleName = trimmed.split(".").at(-1) ?? trimmed;
137
+ return /^[A-Z_$]/.test(simpleName) || /^class_\d+(?:\$class_\d+)*$/.test(simpleName);
138
+ }
139
+ function classNameToFilePath(className) {
140
+ const topLevelClassName = className.split("$")[0] ?? className;
141
+ return `${topLevelClassName.replace(/\./g, "/")}.java`;
142
+ }
143
+ function isVanillaNamespacePath(filePath) {
144
+ return filePath.startsWith("net/minecraft/") || filePath.startsWith("com/mojang/");
145
+ }
146
+ function hitTargetsVanillaNamespace(hit) {
147
+ if (isVanillaNamespacePath(hit.filePath)) {
148
+ return true;
149
+ }
150
+ const qualifiedName = hit.symbol?.qualifiedName;
151
+ return qualifiedName?.startsWith("net.minecraft.") === true || qualifiedName?.startsWith("com.mojang.") === true;
152
+ }
127
153
  export class InspectMinecraftService {
128
154
  deps;
129
155
  constructor(deps) {
@@ -200,11 +226,11 @@ export class InspectMinecraftService {
200
226
  strictVersion: subject.strictVersion
201
227
  };
202
228
  }
203
- async resolveClassArtifactReference(subject, classSubject) {
229
+ async resolveClassArtifactReference(subject, classSubject, task) {
204
230
  if (subject.kind === "workspace") {
205
231
  return this.resolveWorkspaceArtifactReference(subject, classSubject.artifact);
206
232
  }
207
- return this.resolveArtifactReference(classSubject);
233
+ return this.resolveArtifactReference(classSubject, task);
208
234
  }
209
235
  async resolveWorkspaceArtifactReference(subject, artifactRef) {
210
236
  if (!artifactRef) {
@@ -260,15 +286,150 @@ export class InspectMinecraftService {
260
286
  return "search";
261
287
  }
262
288
  }
263
- async resolveArtifactReference(subject) {
289
+ summarizeRequestedSubject(subject) {
290
+ if (subject.kind === "search") {
291
+ if (subject.queryMode !== "auto") {
292
+ return subject;
293
+ }
294
+ const { queryMode: _queryMode, ...requestedSubject } = subject;
295
+ return requestedSubject;
296
+ }
297
+ if (subject.kind === "workspace" && subject.focus?.kind === "search") {
298
+ if (subject.focus.queryMode !== "auto") {
299
+ return subject;
300
+ }
301
+ const { queryMode: _queryMode, ...requestedFocus } = subject.focus;
302
+ return {
303
+ ...subject,
304
+ focus: requestedFocus
305
+ };
306
+ }
307
+ return subject;
308
+ }
309
+ async exampleVersionForSubject(subject) {
310
+ if ("projectPath" in subject && typeof subject.projectPath === "string") {
311
+ const detectedVersion = await this.deps.detectProjectMinecraftVersion(subject.projectPath);
312
+ if (detectedVersion) {
313
+ return detectedVersion;
314
+ }
315
+ }
316
+ return "<version>";
317
+ }
318
+ async buildArtifactContextSuggestedCall(task, subject) {
319
+ return {
320
+ tool: "inspect-minecraft",
321
+ params: {
322
+ task,
323
+ subject: {
324
+ ...subject,
325
+ artifact: {
326
+ type: "resolve-target",
327
+ target: {
328
+ kind: "version",
329
+ value: await this.exampleVersionForSubject(subject)
330
+ }
331
+ }
332
+ }
333
+ }
334
+ };
335
+ }
336
+ taskForSubject(subject) {
337
+ return this.resolveTask(undefined, subject);
338
+ }
339
+ invalidTaskSubjectError(task, subject) {
340
+ if (task === "class-source" && subject.kind === "version") {
341
+ throw createError({
342
+ code: ERROR_CODES.INVALID_INPUT,
343
+ message: "class-source requires a class subject; version subjects resolve artifacts, not class names.",
344
+ details: {
345
+ nextAction: "Retry class-source with subject.kind=class and attach artifact context, or use task=artifact to inspect the version first.",
346
+ suggestedCall: {
347
+ tool: "inspect-minecraft",
348
+ params: {
349
+ task: "class-source",
350
+ subject: {
351
+ kind: "class",
352
+ className: "net.minecraft.world.item.Item",
353
+ artifact: {
354
+ type: "resolve-target",
355
+ target: {
356
+ kind: "version",
357
+ value: subject.version
358
+ }
359
+ }
360
+ }
361
+ }
362
+ }
363
+ }
364
+ });
365
+ }
366
+ const suggestedTask = this.taskForSubject(subject);
367
+ throw createError({
368
+ code: ERROR_CODES.INVALID_INPUT,
369
+ message: `${task} is not compatible with subject.kind="${subject.kind}".`,
370
+ details: {
371
+ nextAction: suggestedTask === "artifact"
372
+ ? `Retry with task=artifact for this ${subject.kind} subject, or reshape the subject so it supplies the input that ${task} needs.`
373
+ : `Retry with task=${suggestedTask} for this subject, or reshape the subject so it supplies the input that ${task} needs.`,
374
+ suggestedCall: {
375
+ tool: "inspect-minecraft",
376
+ params: {
377
+ task: suggestedTask,
378
+ subject
379
+ }
380
+ }
381
+ }
382
+ });
383
+ }
384
+ async resolveBinaryBackedClass(className, input) {
385
+ if (!this.deps.checkSymbolExists || !input.version) {
386
+ return undefined;
387
+ }
388
+ let lookup;
389
+ try {
390
+ lookup = await this.deps.checkSymbolExists({
391
+ version: input.version,
392
+ kind: "class",
393
+ name: className,
394
+ sourceMapping: input.mapping ?? "obfuscated",
395
+ nameMode: className.includes(".") ? "fqcn" : "auto",
396
+ maxCandidates: 10
397
+ });
398
+ }
399
+ catch (caughtError) {
400
+ if (isAppError(caughtError)) {
401
+ return undefined;
402
+ }
403
+ throw caughtError;
404
+ }
405
+ const resolvedClassName = lookup.resolvedSymbol?.name;
406
+ if (!resolvedClassName) {
407
+ return undefined;
408
+ }
409
+ return {
410
+ className: resolvedClassName,
411
+ warnings: lookup.warnings
412
+ };
413
+ }
414
+ async resolveArtifactReference(subject, task) {
264
415
  if (subject.kind === "artifact") {
265
416
  return this.resolveArtifactRef(subject.artifact, subject);
266
417
  }
267
418
  if (subject.kind === "class" || subject.kind === "file" || subject.kind === "search") {
268
419
  if (!subject.artifact) {
420
+ const suggestedTask = task
421
+ ?? (subject.kind === "class"
422
+ ? "class-overview"
423
+ : subject.kind === "search"
424
+ ? "search"
425
+ : "file");
269
426
  throw createError({
270
427
  code: ERROR_CODES.INVALID_INPUT,
271
- message: `${subject.kind} subject requires artifact context.`
428
+ message: `${subject.kind} subject requires artifact context.`,
429
+ details: {
430
+ nextAction: "Add subject.artifact or use subject.kind=workspace so inspect-minecraft can resolve the artifact first.",
431
+ suggestedCall: await this.buildArtifactContextSuggestedCall(suggestedTask, subject)
432
+ }
272
433
  });
273
434
  }
274
435
  return this.resolveArtifactRef(subject.artifact, subject);
@@ -341,6 +502,12 @@ export class InspectMinecraftService {
341
502
  const summary = {
342
503
  status: "ok",
343
504
  headline: `Found ${versions.totalAvailable} Minecraft versions.`,
505
+ subject: createSummarySubject({
506
+ task: "versions",
507
+ kind: "versions",
508
+ includeSnapshots: input.includeSnapshots === false ? undefined : input.includeSnapshots,
509
+ limit: input.limit
510
+ }),
344
511
  counts: {
345
512
  releases: versions.releases.length,
346
513
  snapshots: versions.snapshots?.length ?? 0
@@ -379,6 +546,10 @@ export class InspectMinecraftService {
379
546
  const summary = {
380
547
  status: "blocked",
381
548
  headline: "Could not resolve an artifact without a Minecraft version.",
549
+ subject: createSummarySubject({
550
+ task: "artifact",
551
+ requested: subject
552
+ }),
382
553
  nextActions: nextActionsOrUndefined([
383
554
  createNextAction("inspect-minecraft", {
384
555
  task: "artifact",
@@ -408,6 +579,12 @@ export class InspectMinecraftService {
408
579
  const summary = {
409
580
  status: "ok",
410
581
  headline: `Resolved artifact ${resolved.artifactId}.`,
582
+ subject: createSummarySubject({
583
+ task: "artifact",
584
+ requested: subject,
585
+ artifactId: resolved.artifactId,
586
+ version: resolved.version
587
+ }),
411
588
  counts: {
412
589
  warnings: resolved.warnings.length
413
590
  }
@@ -443,18 +620,20 @@ export class InspectMinecraftService {
443
620
  }
444
621
  async handleClassOverview(subject, detail, include) {
445
622
  if (subject.kind !== "class" && !(subject.kind === "workspace" && subject.focus?.kind === "class")) {
446
- throw createError({
447
- code: ERROR_CODES.INVALID_INPUT,
448
- message: "class-overview requires a class or workspace focus subject."
449
- });
623
+ this.invalidTaskSubjectError("class-overview", subject);
450
624
  }
451
625
  const classSubject = this.buildClassSubject(subject);
452
626
  const className = classSubject.className;
453
- const artifact = await this.resolveClassArtifactReference(subject, classSubject);
627
+ const artifact = await this.resolveClassArtifactReference(subject, classSubject, "class-overview");
454
628
  if (!artifact.artifactId) {
455
629
  const summary = {
456
630
  status: "blocked",
457
- headline: `Could not resolve artifact context for ${className}.`
631
+ headline: `Could not resolve artifact context for ${className}.`,
632
+ subject: createSummarySubject({
633
+ task: "class-overview",
634
+ requested: subject,
635
+ className
636
+ })
458
637
  };
459
638
  return {
460
639
  ...buildEntryToolResult({
@@ -478,9 +657,78 @@ export class InspectMinecraftService {
478
657
  limit: 10
479
658
  });
480
659
  if (matches.total === 0) {
660
+ const partialSourceFallback = subject.kind === "workspace" && hasPartialVanillaCoverage(artifact.artifact)
661
+ ? await this.resolveBinaryBackedClass(className, {
662
+ version: artifact.version,
663
+ mapping: classSubject.mapping
664
+ })
665
+ : undefined;
666
+ if (partialSourceFallback) {
667
+ const metadata = await this.deps.getClassSource({
668
+ className: partialSourceFallback.className,
669
+ artifactId: artifact.artifactId,
670
+ mapping: classSubject.mapping,
671
+ scope: classSubject.scope,
672
+ projectPath: classSubject.projectPath,
673
+ preferProjectVersion: classSubject.preferProjectVersion,
674
+ strictVersion: classSubject.strictVersion,
675
+ mode: "metadata"
676
+ });
677
+ const summary = {
678
+ status: "ok",
679
+ headline: `Resolved class overview for ${partialSourceFallback.className}.`,
680
+ subject: createSummarySubject({
681
+ task: "class-overview",
682
+ requested: subject,
683
+ className: partialSourceFallback.className,
684
+ artifactId: metadata.artifactId
685
+ }),
686
+ counts: {
687
+ totalLines: metadata.totalLines
688
+ },
689
+ notes: [
690
+ "Source coverage was partial, so inspect-minecraft confirmed the vanilla class through binary-backed symbol lookup."
691
+ ]
692
+ };
693
+ return {
694
+ ...buildEntryToolResult({
695
+ task: "class-overview",
696
+ summary,
697
+ detail,
698
+ include,
699
+ blocks: {
700
+ subject: {
701
+ requested: subject,
702
+ resolved: {
703
+ artifactId: metadata.artifactId,
704
+ className: partialSourceFallback.className
705
+ }
706
+ },
707
+ class: {
708
+ className: partialSourceFallback.className,
709
+ totalLines: metadata.totalLines,
710
+ returnedNamespace: metadata.returnedNamespace
711
+ }
712
+ },
713
+ alwaysBlocks: ["subject"]
714
+ }),
715
+ warnings: [
716
+ ...artifact.warnings,
717
+ ...matches.warnings,
718
+ ...partialSourceFallback.warnings,
719
+ ...metadata.warnings
720
+ ]
721
+ };
722
+ }
481
723
  const summary = {
482
724
  status: "not_found",
483
- headline: `No class match was found for ${className}.`
725
+ headline: `No class match was found for ${className}.`,
726
+ subject: createSummarySubject({
727
+ task: "class-overview",
728
+ requested: subject,
729
+ className,
730
+ artifactId: artifact.artifactId
731
+ })
484
732
  };
485
733
  return {
486
734
  ...buildEntryToolResult({
@@ -517,6 +765,12 @@ export class InspectMinecraftService {
517
765
  const summary = {
518
766
  status: "ambiguous",
519
767
  headline: `Found ${matches.total} class matches for ${className}.`,
768
+ subject: createSummarySubject({
769
+ task: "class-overview",
770
+ requested: subject,
771
+ className,
772
+ artifactId: artifact.artifactId
773
+ }),
520
774
  counts: {
521
775
  matches: matches.total
522
776
  },
@@ -551,6 +805,12 @@ export class InspectMinecraftService {
551
805
  const summary = {
552
806
  status: "ok",
553
807
  headline: `Resolved class overview for ${match.qualifiedName}.`,
808
+ subject: createSummarySubject({
809
+ task: "class-overview",
810
+ requested: subject,
811
+ className: match.qualifiedName,
812
+ artifactId: artifact.artifactId
813
+ }),
554
814
  counts: {
555
815
  totalLines: metadata.totalLines
556
816
  },
@@ -604,14 +864,11 @@ export class InspectMinecraftService {
604
864
  }
605
865
  async handleClassSource(subject, detail, include) {
606
866
  if (subject.kind !== "class" && !(subject.kind === "workspace" && subject.focus?.kind === "class")) {
607
- throw createError({
608
- code: ERROR_CODES.INVALID_INPUT,
609
- message: "class-source requires a class or workspace focus subject."
610
- });
867
+ this.invalidTaskSubjectError("class-source", subject);
611
868
  }
612
869
  const classSubject = this.buildClassSubject(subject);
613
870
  const className = classSubject.className;
614
- const artifactContext = await this.resolveClassArtifactReference(subject, classSubject);
871
+ const artifactContext = await this.resolveClassArtifactReference(subject, classSubject, "class-source");
615
872
  const source = await this.deps.getClassSource({
616
873
  className,
617
874
  artifactId: artifactContext.artifactId || undefined,
@@ -625,6 +882,12 @@ export class InspectMinecraftService {
625
882
  const summary = {
626
883
  status: "ok",
627
884
  headline: `Resolved source for ${source.className}.`,
885
+ subject: createSummarySubject({
886
+ task: "class-source",
887
+ requested: subject,
888
+ className: source.className,
889
+ artifactId: source.artifactId
890
+ }),
628
891
  counts: {
629
892
  totalLines: source.totalLines
630
893
  }
@@ -658,13 +921,10 @@ export class InspectMinecraftService {
658
921
  }
659
922
  async handleClassMembers(subject, detail, include, limit) {
660
923
  if (subject.kind !== "class" && !(subject.kind === "workspace" && subject.focus?.kind === "class")) {
661
- throw createError({
662
- code: ERROR_CODES.INVALID_INPUT,
663
- message: "class-members requires a class or workspace focus subject."
664
- });
924
+ this.invalidTaskSubjectError("class-members", subject);
665
925
  }
666
926
  const classSubject = this.buildClassSubject(subject);
667
- const artifact = await this.resolveClassArtifactReference(subject, classSubject);
927
+ const artifact = await this.resolveClassArtifactReference(subject, classSubject, "class-members");
668
928
  const members = await this.deps.getClassMembers({
669
929
  className: classSubject.className,
670
930
  artifactId: artifact.artifactId || undefined,
@@ -678,6 +938,12 @@ export class InspectMinecraftService {
678
938
  const summary = {
679
939
  status: members.truncated ? "partial" : "ok",
680
940
  headline: `Collected ${members.counts.total} members for ${members.className}.`,
941
+ subject: createSummarySubject({
942
+ task: "class-members",
943
+ requested: subject,
944
+ className: members.className,
945
+ artifactId: members.artifactId
946
+ }),
681
947
  counts: members.counts
682
948
  };
683
949
  return {
@@ -727,21 +993,20 @@ export class InspectMinecraftService {
727
993
  }
728
994
  async handleSearch(subject, detail, include, limit, cursor) {
729
995
  if (subject.kind !== "search" && !(subject.kind === "workspace" && subject.focus?.kind === "search")) {
730
- throw createError({
731
- code: ERROR_CODES.INVALID_INPUT,
732
- message: "search requires a search or workspace focus subject."
733
- });
996
+ this.invalidTaskSubjectError("search", subject);
734
997
  }
735
998
  const searchSubject = subject.kind === "search" ? subject : this.requireWorkspaceSearchFocus(subject);
999
+ const requestedSubject = this.summarizeRequestedSubject(subject);
1000
+ const queryMode = searchSubject.queryMode ?? "auto";
736
1001
  const artifact = subject.kind === "search"
737
- ? await this.resolveArtifactReference(subject)
1002
+ ? await this.resolveArtifactReference(subject, "search")
738
1003
  : await this.resolveWorkspaceArtifactReference(subject, searchSubject.artifact);
739
1004
  const search = await this.deps.searchClassSource({
740
1005
  artifactId: artifact.artifactId,
741
1006
  query: searchSubject.query,
742
1007
  intent: searchSubject.intent,
743
1008
  match: searchSubject.match,
744
- queryMode: searchSubject.queryMode,
1009
+ queryMode,
745
1010
  limit,
746
1011
  cursor,
747
1012
  scope: searchSubject.packagePrefix || searchSubject.fileGlob || searchSubject.symbolKind
@@ -752,15 +1017,106 @@ export class InspectMinecraftService {
752
1017
  }
753
1018
  : undefined
754
1019
  });
755
- const sampledHits = capArray(search.hits, 5);
1020
+ const needsBinaryBackedClassHit = subject.kind === "workspace" &&
1021
+ hasPartialVanillaCoverage(artifact.artifact) &&
1022
+ looksLikeClassQuery(searchSubject.query) &&
1023
+ !search.hits.some((hit) => hitTargetsVanillaNamespace(hit));
1024
+ const binaryBackedClassHit = needsBinaryBackedClassHit
1025
+ ? await this.resolveBinaryBackedClass(searchSubject.query, {
1026
+ version: artifact.version,
1027
+ mapping: subject.mapping
1028
+ })
1029
+ : undefined;
1030
+ const binaryBackedHitRecord = binaryBackedClassHit == null
1031
+ ? undefined
1032
+ : {
1033
+ filePath: classNameToFilePath(binaryBackedClassHit.className),
1034
+ score: 100,
1035
+ matchedIn: "symbol",
1036
+ reasonCodes: ["binary-class-lookup"],
1037
+ symbol: {
1038
+ symbolKind: "class",
1039
+ symbolName: binaryBackedClassHit.className.split(".").at(-1) ?? binaryBackedClassHit.className,
1040
+ qualifiedName: binaryBackedClassHit.className,
1041
+ line: 1
1042
+ }
1043
+ };
1044
+ const effectiveHits = binaryBackedHitRecord == null
1045
+ ? search.hits
1046
+ : [
1047
+ binaryBackedHitRecord,
1048
+ ...search.hits.filter((hit) => hit.filePath !== binaryBackedHitRecord.filePath)
1049
+ ];
1050
+ const sampledHits = capArray(effectiveHits, 5);
1051
+ const isAutoSeparatorMiss = effectiveHits.length === 0 &&
1052
+ queryMode === "auto" &&
1053
+ /[._$]/.test(searchSubject.query);
1054
+ const literalRetrySubject = subject.kind === "search"
1055
+ ? {
1056
+ ...subject,
1057
+ queryMode: "literal"
1058
+ }
1059
+ : {
1060
+ ...subject,
1061
+ focus: {
1062
+ ...searchSubject,
1063
+ kind: "search",
1064
+ queryMode: "literal"
1065
+ }
1066
+ };
756
1067
  const summary = {
757
- status: search.hits.length > 0 ? "ok" : "not_found",
758
- headline: search.hits.length > 0
759
- ? `Found ${search.hits.length} source hits for ${searchSubject.query}.`
1068
+ status: effectiveHits.length > 0 ? "ok" : "not_found",
1069
+ headline: effectiveHits.length > 0
1070
+ ? `Found ${effectiveHits.length} source hits for ${searchSubject.query}.`
760
1071
  : `No source hits were found for ${searchSubject.query}.`,
1072
+ subject: createSummarySubject({
1073
+ task: "search",
1074
+ requested: requestedSubject,
1075
+ query: searchSubject.query,
1076
+ artifactId: artifact.artifactId
1077
+ }),
761
1078
  counts: {
762
- hits: search.hits.length
763
- }
1079
+ hits: effectiveHits.length
1080
+ },
1081
+ nextActions: nextActionsOrUndefined([
1082
+ ...(effectiveHits.length > 0
1083
+ ? [
1084
+ createNextAction("inspect-minecraft", {
1085
+ task: "file",
1086
+ subject: {
1087
+ kind: "file",
1088
+ filePath: effectiveHits[0].filePath,
1089
+ artifact: {
1090
+ type: "resolved-id",
1091
+ artifactId: artifact.artifactId
1092
+ }
1093
+ },
1094
+ include: ["source"]
1095
+ })
1096
+ ]
1097
+ : []),
1098
+ ...(isAutoSeparatorMiss
1099
+ ? [
1100
+ createNextAction("inspect-minecraft", {
1101
+ task: "search",
1102
+ subject: literalRetrySubject
1103
+ })
1104
+ ]
1105
+ : [])
1106
+ ]),
1107
+ ...(isAutoSeparatorMiss
1108
+ ? {
1109
+ notes: [
1110
+ "Separator query returned no indexed hits. Retry with queryMode=\"literal\" for an explicit full substring scan."
1111
+ ]
1112
+ }
1113
+ : binaryBackedClassHit
1114
+ ? {
1115
+ notes: [
1116
+ "Source coverage was partial, so inspect-minecraft returned a binary-backed class match for the vanilla symbol."
1117
+ ]
1118
+ }
1119
+ : {})
764
1120
  };
765
1121
  return {
766
1122
  ...buildEntryToolResult({
@@ -770,32 +1126,29 @@ export class InspectMinecraftService {
770
1126
  include,
771
1127
  blocks: {
772
1128
  subject: {
773
- requested: subject,
1129
+ requested: requestedSubject,
774
1130
  resolved: {
775
1131
  artifactId: artifact.artifactId
776
1132
  }
777
1133
  },
778
1134
  search: {
779
1135
  query: searchSubject.query,
780
- hits: detail === "summary" ? sampledHits.items : search.hits,
1136
+ hits: detail === "summary" ? sampledHits.items : effectiveHits,
781
1137
  nextCursor: search.nextCursor
782
1138
  }
783
1139
  },
784
1140
  alwaysBlocks: ["subject"]
785
1141
  }),
786
- warnings: [...artifact.warnings]
1142
+ warnings: [...artifact.warnings, ...(binaryBackedClassHit?.warnings ?? [])]
787
1143
  };
788
1144
  }
789
1145
  async handleFile(subject, detail, include) {
790
1146
  if (subject.kind !== "file" && !(subject.kind === "workspace" && subject.focus?.kind === "file")) {
791
- throw createError({
792
- code: ERROR_CODES.INVALID_INPUT,
793
- message: "file task requires a file or workspace focus subject."
794
- });
1147
+ this.invalidTaskSubjectError("file", subject);
795
1148
  }
796
1149
  const fileSubject = subject.kind === "file" ? subject : this.requireWorkspaceFileFocus(subject);
797
1150
  const artifact = subject.kind === "file"
798
- ? await this.resolveArtifactReference(subject)
1151
+ ? await this.resolveArtifactReference(subject, "file")
799
1152
  : await this.resolveWorkspaceArtifactReference(subject, fileSubject.artifact);
800
1153
  const file = await this.deps.getArtifactFile({
801
1154
  artifactId: artifact.artifactId,
@@ -804,6 +1157,12 @@ export class InspectMinecraftService {
804
1157
  const summary = {
805
1158
  status: "ok",
806
1159
  headline: `Read ${file.filePath}.`,
1160
+ subject: createSummarySubject({
1161
+ task: "file",
1162
+ requested: subject,
1163
+ filePath: file.filePath,
1164
+ artifactId: artifact.artifactId
1165
+ }),
807
1166
  counts: {
808
1167
  bytes: file.contentBytes
809
1168
  }
@@ -835,19 +1194,69 @@ export class InspectMinecraftService {
835
1194
  };
836
1195
  }
837
1196
  async handleListFiles(subject, detail, include, limit, cursor) {
838
- const artifact = await this.resolveArtifactReference(subject);
1197
+ const artifact = await this.resolveArtifactReference(subject, "list-files");
839
1198
  const files = await this.deps.listArtifactFiles({
840
1199
  artifactId: artifact.artifactId,
841
1200
  limit,
842
1201
  cursor
843
1202
  });
844
1203
  const sampled = capArray(files.items, 10);
1204
+ const partialCoverage = subject.kind === "workspace" && hasPartialVanillaCoverage(artifact.artifact);
1205
+ const nextActions = [
1206
+ ...(files.items.length > 0
1207
+ ? [
1208
+ createNextAction("inspect-minecraft", {
1209
+ task: "file",
1210
+ subject: {
1211
+ kind: "file",
1212
+ filePath: files.items[0],
1213
+ artifact: {
1214
+ type: "resolved-id",
1215
+ artifactId: artifact.artifactId
1216
+ }
1217
+ }
1218
+ })
1219
+ ]
1220
+ : []),
1221
+ ...(partialCoverage
1222
+ ? [
1223
+ createNextAction("inspect-minecraft", {
1224
+ task: "class-source",
1225
+ subject: {
1226
+ kind: "workspace",
1227
+ projectPath: subject.projectPath,
1228
+ mapping: subject.mapping,
1229
+ scope: subject.scope,
1230
+ preferProjectVersion: subject.preferProjectVersion,
1231
+ strictVersion: subject.strictVersion,
1232
+ focus: {
1233
+ kind: "class",
1234
+ className: "net.minecraft.world.item.Item"
1235
+ }
1236
+ }
1237
+ })
1238
+ ]
1239
+ : [])
1240
+ ];
845
1241
  const summary = {
846
- status: "ok",
1242
+ status: partialCoverage ? "partial" : "ok",
847
1243
  headline: `Listed ${files.items.length} files for ${artifact.artifactId}.`,
1244
+ subject: createSummarySubject({
1245
+ task: "list-files",
1246
+ requested: subject,
1247
+ artifactId: artifact.artifactId
1248
+ }),
848
1249
  counts: {
849
1250
  files: files.items.length
850
- }
1251
+ },
1252
+ nextActions: nextActionsOrUndefined(nextActions),
1253
+ ...(partialCoverage
1254
+ ? {
1255
+ notes: [
1256
+ "This listing is partial because the resolved source artifact does not contain net.minecraft entries."
1257
+ ]
1258
+ }
1259
+ : {})
851
1260
  };
852
1261
  return {
853
1262
  ...buildEntryToolResult({
@@ -864,7 +1273,15 @@ export class InspectMinecraftService {
864
1273
  },
865
1274
  files: {
866
1275
  items: detail === "summary" ? sampled.items : files.items,
867
- nextCursor: files.nextCursor
1276
+ nextCursor: files.nextCursor,
1277
+ ...(partialCoverage
1278
+ ? {
1279
+ coverage: {
1280
+ sourceCoverage: "partial",
1281
+ missingNamespaces: ["net.minecraft"]
1282
+ }
1283
+ }
1284
+ : {})
868
1285
  }
869
1286
  },
870
1287
  alwaysBlocks: ["subject"]