gdharness 0.13.13 → 0.13.15

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.
package/build/cli.js CHANGED
@@ -7557,6 +7557,59 @@ function cachedClasses(projectPath) {
7557
7557
  }
7558
7558
  return listed;
7559
7559
  }
7560
+ function missingMemberIn(message) {
7561
+ const found = /The (method|property) "([^"(]+)(?:\(\))?" is not present on the inferred type "([^"]+)"/.exec(message);
7562
+ if (found === null) {
7563
+ return null;
7564
+ }
7565
+ return {
7566
+ kind: found[1] === "property" ? "property" : "method",
7567
+ member: found[2] ?? "",
7568
+ type: found[3] ?? ""
7569
+ };
7570
+ }
7571
+ function declaresMember(source, missing) {
7572
+ const name = missing.member.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7573
+ const declaration = missing.kind === "method" ? new RegExp(String.raw`^\s*(?:static\s+)?func\s+${name}\s*\(`, "m") : new RegExp(String.raw`^\s*(?:static\s+)?(?:@export\s+)?(?:var|const)\s+${name}\b`, "m");
7574
+ return declaration.test(source);
7575
+ }
7576
+ function unknownTypeIn(message) {
7577
+ const found = /(?:Could not find (?:base class|type)|Cannot find class|Identifier) "?([A-Za-z_][A-Za-z0-9_]*)"?(?: not declared| in the current scope|\b)/.exec(message);
7578
+ return found?.[1] ?? null;
7579
+ }
7580
+ function unloadedTypes(messages, declared, cached) {
7581
+ const seen = new Set;
7582
+ const found = [];
7583
+ for (const message of messages) {
7584
+ const type = unknownTypeIn(message);
7585
+ const declaredIn = type === null ? undefined : declared.get(type);
7586
+ if (type === null || declaredIn === undefined || seen.has(type)) {
7587
+ continue;
7588
+ }
7589
+ seen.add(type);
7590
+ found.push({ type, declaredIn, inTheClassCache: cached?.has(type) === true });
7591
+ }
7592
+ return found;
7593
+ }
7594
+ function contradictedDiagnostics(messages, classes, sourceOf) {
7595
+ const read = new Map;
7596
+ const found = [];
7597
+ for (const message of messages) {
7598
+ const missing = missingMemberIn(message);
7599
+ const declaredIn = missing === null ? undefined : classes.get(missing.type);
7600
+ if (missing === null || declaredIn === undefined) {
7601
+ continue;
7602
+ }
7603
+ if (!read.has(declaredIn)) {
7604
+ read.set(declaredIn, sourceOf(declaredIn));
7605
+ }
7606
+ const source = read.get(declaredIn) ?? null;
7607
+ if (source !== null && declaresMember(source, missing)) {
7608
+ found.push({ ...missing, declaredIn });
7609
+ }
7610
+ }
7611
+ return found;
7612
+ }
7560
7613
  function cacheWrittenAt(projectPath) {
7561
7614
  const cache = join6(projectPath, ".godot", "global_script_class_cache.cfg");
7562
7615
  try {
@@ -15749,7 +15802,7 @@ var init_tool_definitions = __esm(() => {
15749
15802
  },
15750
15803
  {
15751
15804
  name: "script_diagnostics",
15752
- description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running.",
15805
+ description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says which remedy applies, because they differ: a stale analysed type needs editor_launch restart, while a class missing from the cache needs project_import refresh_classes.",
15753
15806
  parameters: {
15754
15807
  projectPath: PROJECT_PATH,
15755
15808
  scriptPath: SCRIPT_PATH
@@ -38015,14 +38068,55 @@ class GodotServer {
38015
38068
  const severity = asParams(entry)["severity"];
38016
38069
  return severity === 1 || severity === "error" || severity === "ERROR";
38017
38070
  }).length;
38071
+ const disagrees = this.whereTheFileDisagrees(diagnostics, args);
38018
38072
  return this.jsonTextResponse(markIfStale({
38019
38073
  scriptPath: readString(args, "scriptPath"),
38020
38074
  clean: errors === 0,
38021
38075
  errors,
38022
38076
  warnings: diagnostics.length - errors,
38023
- diagnostics
38077
+ diagnostics,
38078
+ ...disagrees
38024
38079
  }, this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
38025
38080
  }
38081
+ whereTheFileDisagrees(diagnostics, args) {
38082
+ const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
38083
+ const worthChecking = messages.some((message) => missingMemberIn(message) !== null || unknownTypeIn(message) !== null);
38084
+ const project = worthChecking ? this.project(args) : null;
38085
+ if (project === null || !project.ok) {
38086
+ return {};
38087
+ }
38088
+ const projectPath = project.value.path;
38089
+ const cached = cachedClasses(projectPath);
38090
+ const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
38091
+ const contained = resolveWithinProject(projectPath, resourcePath);
38092
+ if (!contained.ok || !existsSync14(contained.absolutePath)) {
38093
+ return null;
38094
+ }
38095
+ try {
38096
+ return readFileSync15(contained.absolutePath, "utf8");
38097
+ } catch {
38098
+ return null;
38099
+ }
38100
+ });
38101
+ const unloaded = messages.some((message) => unknownTypeIn(message) !== null) ? unloadedTypes(messages, declaredClasses(projectPath), cached) : [];
38102
+ const notes = [];
38103
+ if (contradicted.length > 0) {
38104
+ notes.push(`The editor is reporting against an older copy of ${contradicted.length === 1 ? "a type" : "some types"} named under contradictedByTheFile. Each member listed is declared in the file the class cache points at, so those diagnostics are wrong however the code is written. editor_launch restart clears it; editor_rescan and project_import refresh_classes do not, because neither reaches the analysed types the language server built at startup.`);
38105
+ }
38106
+ const uncached = unloaded.filter((type) => !type.inTheClassCache);
38107
+ if (uncached.length > 0) {
38108
+ notes.push(`${uncached.map((type) => type.type).join(", ")} ${uncached.length === 1 ? "is declared" : "are declared"} in this project and missing from .godot/global_script_class_cache.cfg, so a game launched now would not resolve ${uncached.length === 1 ? "it" : "them"} either. project_import refresh_classes rewrites the cache from the declarations on disk.`);
38109
+ }
38110
+ const stale = unloaded.filter((type) => type.inTheClassCache);
38111
+ if (stale.length > 0) {
38112
+ notes.push(`${stale.map((type) => type.type).join(", ")} ${stale.length === 1 ? "is" : "are"} in the class cache and still unresolved here, which is the editor's loaded list being behind rather than anything on disk. A game launched now reads the cache and resolves ${stale.length === 1 ? "it" : "them"}, so the run and the diagnostics disagree. editor_launch restart clears it.`);
38113
+ }
38114
+ return {
38115
+ ...contradicted.length === 0 ? {} : { contradictedByTheFile: contradicted },
38116
+ ...unloaded.length === 0 ? {} : { typesTheEditorHasNotLoaded: unloaded },
38117
+ ...notes.length === 0 ? {} : { staleAnalysis: notes.join(" ") }
38118
+ };
38119
+ }
38026
38120
  async handleLSP(toolName, args) {
38027
38121
  const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
38028
38122
  if (this.lspClient !== null && this.lspClient.port !== port) {
@@ -40459,7 +40553,8 @@ function headlessSection() {
40459
40553
  "Needing nothing is not the same as changing nothing, and a gate wants both. Most of these",
40460
40554
  "write to the project because that is what they are for: `script_edit create` writes a file,",
40461
40555
  "`project_settings set` rewrites `project.godot`, `project_import reimport` and `refresh_classes`",
40462
- "write under `.godot`. The reading ops are the ones to gate on, and which is which is in each",
40556
+ "write under `.godot`, and `project_import refresh_uids` writes a `.uid` beside each script that",
40557
+ "had none. The reading ops are the ones to gate on, and which is which is in each",
40463
40558
  "op's own summary below rather than assumed from this list.",
40464
40559
  "",
40465
40560
  "Everything else needs the editor (`editor_*`, `debug_*`, `scene_*`, `resource_*`) or a running",
package/build/index.js CHANGED
@@ -24817,6 +24817,59 @@ function cachedClasses(projectPath) {
24817
24817
  }
24818
24818
  return listed;
24819
24819
  }
24820
+ function missingMemberIn(message) {
24821
+ const found = /The (method|property) "([^"(]+)(?:\(\))?" is not present on the inferred type "([^"]+)"/.exec(message);
24822
+ if (found === null) {
24823
+ return null;
24824
+ }
24825
+ return {
24826
+ kind: found[1] === "property" ? "property" : "method",
24827
+ member: found[2] ?? "",
24828
+ type: found[3] ?? ""
24829
+ };
24830
+ }
24831
+ function declaresMember(source, missing) {
24832
+ const name = missing.member.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
24833
+ const declaration = missing.kind === "method" ? new RegExp(String.raw`^\s*(?:static\s+)?func\s+${name}\s*\(`, "m") : new RegExp(String.raw`^\s*(?:static\s+)?(?:@export\s+)?(?:var|const)\s+${name}\b`, "m");
24834
+ return declaration.test(source);
24835
+ }
24836
+ function unknownTypeIn(message) {
24837
+ const found = /(?:Could not find (?:base class|type)|Cannot find class|Identifier) "?([A-Za-z_][A-Za-z0-9_]*)"?(?: not declared| in the current scope|\b)/.exec(message);
24838
+ return found?.[1] ?? null;
24839
+ }
24840
+ function unloadedTypes(messages, declared, cached) {
24841
+ const seen = new Set;
24842
+ const found = [];
24843
+ for (const message of messages) {
24844
+ const type = unknownTypeIn(message);
24845
+ const declaredIn = type === null ? undefined : declared.get(type);
24846
+ if (type === null || declaredIn === undefined || seen.has(type)) {
24847
+ continue;
24848
+ }
24849
+ seen.add(type);
24850
+ found.push({ type, declaredIn, inTheClassCache: cached?.has(type) === true });
24851
+ }
24852
+ return found;
24853
+ }
24854
+ function contradictedDiagnostics(messages, classes, sourceOf) {
24855
+ const read = new Map;
24856
+ const found = [];
24857
+ for (const message of messages) {
24858
+ const missing = missingMemberIn(message);
24859
+ const declaredIn = missing === null ? undefined : classes.get(missing.type);
24860
+ if (missing === null || declaredIn === undefined) {
24861
+ continue;
24862
+ }
24863
+ if (!read.has(declaredIn)) {
24864
+ read.set(declaredIn, sourceOf(declaredIn));
24865
+ }
24866
+ const source = read.get(declaredIn) ?? null;
24867
+ if (source !== null && declaresMember(source, missing)) {
24868
+ found.push({ ...missing, declaredIn });
24869
+ }
24870
+ }
24871
+ return found;
24872
+ }
24820
24873
  function cacheWrittenAt(projectPath) {
24821
24874
  const cache = join3(projectPath, ".godot", "global_script_class_cache.cfg");
24822
24875
  try {
@@ -28866,7 +28919,7 @@ var TOOL_SPECS = [
28866
28919
  },
28867
28920
  {
28868
28921
  name: "script_diagnostics",
28869
- description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running.",
28922
+ description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says which remedy applies, because they differ: a stale analysed type needs editor_launch restart, while a class missing from the cache needs project_import refresh_classes.",
28870
28923
  parameters: {
28871
28924
  projectPath: PROJECT_PATH,
28872
28925
  scriptPath: SCRIPT_PATH
@@ -30767,14 +30820,55 @@ class GodotServer {
30767
30820
  const severity = asParams(entry)["severity"];
30768
30821
  return severity === 1 || severity === "error" || severity === "ERROR";
30769
30822
  }).length;
30823
+ const disagrees = this.whereTheFileDisagrees(diagnostics, args);
30770
30824
  return this.jsonTextResponse(markIfStale({
30771
30825
  scriptPath: readString(args, "scriptPath"),
30772
30826
  clean: errors === 0,
30773
30827
  errors,
30774
30828
  warnings: diagnostics.length - errors,
30775
- diagnostics
30829
+ diagnostics,
30830
+ ...disagrees
30776
30831
  }, this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
30777
30832
  }
30833
+ whereTheFileDisagrees(diagnostics, args) {
30834
+ const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
30835
+ const worthChecking = messages.some((message) => missingMemberIn(message) !== null || unknownTypeIn(message) !== null);
30836
+ const project = worthChecking ? this.project(args) : null;
30837
+ if (project === null || !project.ok) {
30838
+ return {};
30839
+ }
30840
+ const projectPath = project.value.path;
30841
+ const cached = cachedClasses(projectPath);
30842
+ const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
30843
+ const contained = resolveWithinProject(projectPath, resourcePath);
30844
+ if (!contained.ok || !existsSync9(contained.absolutePath)) {
30845
+ return null;
30846
+ }
30847
+ try {
30848
+ return readFileSync11(contained.absolutePath, "utf8");
30849
+ } catch {
30850
+ return null;
30851
+ }
30852
+ });
30853
+ const unloaded = messages.some((message) => unknownTypeIn(message) !== null) ? unloadedTypes(messages, declaredClasses(projectPath), cached) : [];
30854
+ const notes = [];
30855
+ if (contradicted.length > 0) {
30856
+ notes.push(`The editor is reporting against an older copy of ${contradicted.length === 1 ? "a type" : "some types"} named under contradictedByTheFile. Each member listed is declared in the file the class cache points at, so those diagnostics are wrong however the code is written. editor_launch restart clears it; editor_rescan and project_import refresh_classes do not, because neither reaches the analysed types the language server built at startup.`);
30857
+ }
30858
+ const uncached = unloaded.filter((type) => !type.inTheClassCache);
30859
+ if (uncached.length > 0) {
30860
+ notes.push(`${uncached.map((type) => type.type).join(", ")} ${uncached.length === 1 ? "is declared" : "are declared"} in this project and missing from .godot/global_script_class_cache.cfg, so a game launched now would not resolve ${uncached.length === 1 ? "it" : "them"} either. project_import refresh_classes rewrites the cache from the declarations on disk.`);
30861
+ }
30862
+ const stale = unloaded.filter((type) => type.inTheClassCache);
30863
+ if (stale.length > 0) {
30864
+ notes.push(`${stale.map((type) => type.type).join(", ")} ${stale.length === 1 ? "is" : "are"} in the class cache and still unresolved here, which is the editor's loaded list being behind rather than anything on disk. A game launched now reads the cache and resolves ${stale.length === 1 ? "it" : "them"}, so the run and the diagnostics disagree. editor_launch restart clears it.`);
30865
+ }
30866
+ return {
30867
+ ...contradicted.length === 0 ? {} : { contradictedByTheFile: contradicted },
30868
+ ...unloaded.length === 0 ? {} : { typesTheEditorHasNotLoaded: unloaded },
30869
+ ...notes.length === 0 ? {} : { staleAnalysis: notes.join(" ") }
30870
+ };
30871
+ }
30778
30872
  async handleLSP(toolName, args) {
30779
30873
  const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
30780
30874
  if (this.lspClient !== null && this.lspClient.port !== port) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.13.13",
3
+ "version": "0.13.15",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",