gdharness 0.13.20 → 0.13.22

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
@@ -7591,6 +7591,15 @@ function unloadedTypes(messages, declared, cached) {
7591
7591
  }
7592
7592
  return found;
7593
7593
  }
7594
+ function classesNamedIn(source, known) {
7595
+ const named = new Set;
7596
+ for (const [word] of source.matchAll(/\b[A-Z][A-Za-z0-9_]*\b/g)) {
7597
+ if (known.has(word)) {
7598
+ named.add(word);
7599
+ }
7600
+ }
7601
+ return [...named].sort();
7602
+ }
7594
7603
  function contradictedDiagnostics(messages, classes, sourceOf) {
7595
7604
  const read = new Map;
7596
7605
  const found = [];
@@ -15955,7 +15964,11 @@ var init_tool_definitions = __esm(() => {
15955
15964
  description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure measured downstream is a change to the declaring script, saved, and then a rescan: the change on its own left every affected file reporting the same errors, and the rescan after it cleared them. Whether a rescan alone would have done it there is not known, because the rescans in that report may have answered before the scan ran, which is the timing this tool no longer has. editor_launch restart also does it, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve goes out of that file with it. That is not a reason to refuse the scan, because on a class the editor has merely not walked yet the scan is the cure; what the answer does instead is name the loss under cacheLost and rebuild the cache from the files, naming what came back under cacheRestored, so no fresh engine, CI run or clone inherits the short file. An editor that lost classes is still holding the short list, so the note says to restart it before scanning again. Needs the editor connected.",
15956
15965
  parameters: {
15957
15966
  projectPath: PROJECT_PATH,
15958
- timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
15967
+ timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." },
15968
+ reloadScript: {
15969
+ type: "string",
15970
+ description: "A script to recompile in the editor after the scan, for the fault a scan cannot reach: a script the editor has loaded keeps the copy it built, and that copy is refreshed when one of its dependencies changes rather than when it changes itself, so a method added to a loaded class reads as missing at every caller. This recompiles into the same object, so the holders that kept the stale copy alive see the new one. The methods it has afterwards come back under reloadedMethods, because a reload that compiled nothing and answered OK is the failure worth catching."
15971
+ }
15959
15972
  },
15960
15973
  requires: ["projectPath"]
15961
15974
  },
@@ -38105,7 +38118,19 @@ class GodotServer {
38105
38118
  const unloaded = messages.some((message) => unknownTypeIn(message) !== null) ? unloadedTypes(messages, declaredClasses(projectPath), cached) : [];
38106
38119
  const notes = [];
38107
38120
  if (contradicted.length > 0) {
38108
- 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. Try editor_rescan first: it costs about half a second and cleared this in one of five measured attempts. If it does not, changing anything the named type itself depends on and rescanning again cleared it on the one occasion that has been tried. editor_launch restart is the only remedy that has always worked, and is the one to reach for when the count above is not good enough odds. project_import refresh_classes does not, and answers added: [] while this is happening.`);
38121
+ const dependsOn = new Set;
38122
+ for (const one of contradicted) {
38123
+ const source = cached === null ? null : this.sourceOfClass(projectPath, one.declaredIn);
38124
+ if (source === null || cached === null) {
38125
+ continue;
38126
+ }
38127
+ for (const name of classesNamedIn(source, cached)) {
38128
+ if (name !== one.type) {
38129
+ dependsOn.add(name);
38130
+ }
38131
+ }
38132
+ }
38133
+ 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. Try editor_rescan first: it costs about half a second and cleared this in one of five measured attempts. If it does not, the lever is that a held type is refreshed when something it depends on changes rather than when it changes itself${dependsOn.size === 0 ? ", though the named types depend on no other global class, which leaves only the restart" : `, so change ${[...dependsOn].sort().join(", ")} and rescan again`}. editor_launch restart is the only remedy that has always worked, and is the one to reach for when the count above is not good enough odds. project_import refresh_classes does not, and answers added: [] while this is happening.`);
38109
38134
  }
38110
38135
  const uncached = unloaded.filter((type) => !type.inTheClassCache);
38111
38136
  if (uncached.length > 0) {
@@ -38121,6 +38146,17 @@ class GodotServer {
38121
38146
  ...notes.length === 0 ? {} : { staleAnalysis: notes.join(" ") }
38122
38147
  };
38123
38148
  }
38149
+ sourceOfClass(projectPath, resourcePath) {
38150
+ const contained = resolveWithinProject(projectPath, resourcePath);
38151
+ if (!contained.ok || !existsSync14(contained.absolutePath)) {
38152
+ return null;
38153
+ }
38154
+ try {
38155
+ return readFileSync15(contained.absolutePath, "utf8");
38156
+ } catch {
38157
+ return null;
38158
+ }
38159
+ }
38124
38160
  async handleLSP(toolName, args) {
38125
38161
  const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
38126
38162
  if (this.lspClient !== null && this.lspClient.port !== port) {
@@ -38958,6 +38994,17 @@ class GodotServer {
38958
38994
  const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
38959
38995
  const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
38960
38996
  const restored = lost.length > 0 ? await this.rebuildCacheAfterLoss(projectPath, lost) : [];
38997
+ const reloading = readNonEmptyString(args, "reloadScript");
38998
+ let reloaded = {};
38999
+ if (reloading !== undefined) {
39000
+ try {
39001
+ const answer = asParams(await this.godotBridge.invokeTool("reload_script", { ...args, scriptPath: reloading }));
39002
+ const methods = readArray(answer, "methods");
39003
+ reloaded = methods ? { methods: methods.map(String) } : { problem: `the editor would not say what ${reloading} has after reloading it` };
39004
+ } catch (error) {
39005
+ reloaded = { problem: `${reloading} could not be reloaded: ${errorMessage(error)}` };
39006
+ }
39007
+ }
38961
39008
  const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
38962
39009
  const unseen = checked.unseen;
38963
39010
  const stillGone = lost.filter((name) => !restored.includes(name));
@@ -38965,6 +39012,8 @@ class GodotServer {
38965
39012
  ok: !busy && unseen.length === 0 && stillGone.length === 0 && checked.unchecked === undefined,
38966
39013
  stillWorking: busy,
38967
39014
  waitedMs: Date.now() - started,
39015
+ reloadedMethods: reloaded.methods,
39016
+ reloadProblem: reloaded.problem,
38968
39017
  unseenByEditor: unseen.length > 0 ? unseen : undefined,
38969
39018
  cacheLost: lost.length > 0 ? lost : undefined,
38970
39019
  cacheRestored: restored.length > 0 ? restored : undefined,
@@ -80,6 +80,7 @@ func _init_tools() -> void:
80
80
  "list_connections": [_scene_tools, "list_connections"],
81
81
  "rescan_filesystem": [_scene_tools, "rescan_filesystem"],
82
82
  "global_classes": [_class_tools, "global_classes"],
83
+ "reload_script": [_class_tools, "reload_script"],
83
84
  # Settings, for a caller that asked the editor rather than the file on disk
84
85
  "get_project_setting": [_settings_tools, "get_project_setting"],
85
86
  # Resource tools
@@ -29,6 +29,59 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
29
29
  _editor_plugin = plugin
30
30
 
31
31
 
32
+ ## Recompiles a script the editor is already holding, in place.
33
+ ##
34
+ ## The fault this exists for: a script the editor has loaded keeps the copy it compiled, and that
35
+ ## copy is refreshed when one of its own dependencies changes rather than when it changes itself.
36
+ ## So a method added to a loaded class is reported missing at every caller until something
37
+ ## recompiles it, and a filesystem scan does not, because the scan updates what the editor knows
38
+ ## about files rather than what it has already built from them.
39
+ ##
40
+ ## `reload` is the only call that reaches the built copy. It recompiles from source into the same
41
+ ## object, so everything holding a reference sees the new version rather than a second one, which
42
+ ## is the whole point: the holders are why the stale copy survived a scan.
43
+ ##
44
+ ## `keep_state` is true so instances that already exist keep their properties. A caller asking for
45
+ ## this is mid-edit with a scene open, and dropping the state of every node using the script would
46
+ ## be a larger surprise than the wrong diagnostic it is here to clear.
47
+ ##
48
+ ## The list of methods afterwards is the evidence rather than an `ok` beside it. A reload that
49
+ ## returned OK and compiled nothing is exactly the failure worth catching, and the members it now
50
+ ## has are the only thing that can tell those apart.
51
+ func reload_script(args: Dictionary) -> Dictionary:
52
+ var path: String = str(args.get("scriptPath", ""))
53
+ if path.is_empty():
54
+ return {"ok": false, "error": "scriptPath is required"}
55
+ if not ResourceLoader.exists(path):
56
+ return {"ok": false, "error": "No script at " + path}
57
+
58
+ var loaded: Resource = load(path)
59
+ var script: GDScript = loaded as GDScript
60
+ if script == null:
61
+ return {"ok": false, "error": path + " is not a GDScript"}
62
+
63
+ # The source is read off disk and put back before reloading, which is the whole of the fix and
64
+ # is not obvious. `reload` recompiles from the object's own `source_code`, not from the file, so
65
+ # a script the editor loaded before the change recompiles the text it was holding and comes back
66
+ # with exactly the members it already had. Measured: reloading without this answers OK and the
67
+ # method added since is still absent from the method list.
68
+ var reader: FileAccess = FileAccess.open(path, FileAccess.READ)
69
+ if reader == null:
70
+ return {"ok": false, "error": "Could not read " + path + ": " + str(FileAccess.get_open_error())}
71
+ script.source_code = reader.get_as_text()
72
+ reader.close()
73
+
74
+ var failed: Error = script.reload(true)
75
+ if failed != OK:
76
+ return {"ok": false, "error": "Reloading " + path + " answered error " + str(failed)}
77
+
78
+ var methods: Array[String] = []
79
+ for entry: Dictionary in script.get_script_method_list():
80
+ methods.append(str(entry.get("name", "")))
81
+ methods.sort()
82
+ return {"ok": true, "script": path, "methods": methods}
83
+
84
+
32
85
  func global_classes(_args: Dictionary) -> Dictionary:
33
86
  if not _editor_plugin:
34
87
  return {"ok": false, "error": "Editor plugin unavailable"}
package/build/index.js CHANGED
@@ -24851,6 +24851,15 @@ function unloadedTypes(messages, declared, cached) {
24851
24851
  }
24852
24852
  return found;
24853
24853
  }
24854
+ function classesNamedIn(source, known) {
24855
+ const named = new Set;
24856
+ for (const [word] of source.matchAll(/\b[A-Z][A-Za-z0-9_]*\b/g)) {
24857
+ if (known.has(word)) {
24858
+ named.add(word);
24859
+ }
24860
+ }
24861
+ return [...named].sort();
24862
+ }
24854
24863
  function contradictedDiagnostics(messages, classes, sourceOf) {
24855
24864
  const read = new Map;
24856
24865
  const found = [];
@@ -29072,7 +29081,11 @@ var TOOL_SPECS = [
29072
29081
  description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure measured downstream is a change to the declaring script, saved, and then a rescan: the change on its own left every affected file reporting the same errors, and the rescan after it cleared them. Whether a rescan alone would have done it there is not known, because the rescans in that report may have answered before the scan ran, which is the timing this tool no longer has. editor_launch restart also does it, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve goes out of that file with it. That is not a reason to refuse the scan, because on a class the editor has merely not walked yet the scan is the cure; what the answer does instead is name the loss under cacheLost and rebuild the cache from the files, naming what came back under cacheRestored, so no fresh engine, CI run or clone inherits the short file. An editor that lost classes is still holding the short list, so the note says to restart it before scanning again. Needs the editor connected.",
29073
29082
  parameters: {
29074
29083
  projectPath: PROJECT_PATH,
29075
- timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
29084
+ timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." },
29085
+ reloadScript: {
29086
+ type: "string",
29087
+ description: "A script to recompile in the editor after the scan, for the fault a scan cannot reach: a script the editor has loaded keeps the copy it built, and that copy is refreshed when one of its dependencies changes rather than when it changes itself, so a method added to a loaded class reads as missing at every caller. This recompiles into the same object, so the holders that kept the stale copy alive see the new one. The methods it has afterwards come back under reloadedMethods, because a reload that compiled nothing and answered OK is the failure worth catching."
29088
+ }
29076
29089
  },
29077
29090
  requires: ["projectPath"]
29078
29091
  },
@@ -30857,7 +30870,19 @@ class GodotServer {
30857
30870
  const unloaded = messages.some((message) => unknownTypeIn(message) !== null) ? unloadedTypes(messages, declaredClasses(projectPath), cached) : [];
30858
30871
  const notes = [];
30859
30872
  if (contradicted.length > 0) {
30860
- 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. Try editor_rescan first: it costs about half a second and cleared this in one of five measured attempts. If it does not, changing anything the named type itself depends on and rescanning again cleared it on the one occasion that has been tried. editor_launch restart is the only remedy that has always worked, and is the one to reach for when the count above is not good enough odds. project_import refresh_classes does not, and answers added: [] while this is happening.`);
30873
+ const dependsOn = new Set;
30874
+ for (const one of contradicted) {
30875
+ const source = cached === null ? null : this.sourceOfClass(projectPath, one.declaredIn);
30876
+ if (source === null || cached === null) {
30877
+ continue;
30878
+ }
30879
+ for (const name of classesNamedIn(source, cached)) {
30880
+ if (name !== one.type) {
30881
+ dependsOn.add(name);
30882
+ }
30883
+ }
30884
+ }
30885
+ 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. Try editor_rescan first: it costs about half a second and cleared this in one of five measured attempts. If it does not, the lever is that a held type is refreshed when something it depends on changes rather than when it changes itself${dependsOn.size === 0 ? ", though the named types depend on no other global class, which leaves only the restart" : `, so change ${[...dependsOn].sort().join(", ")} and rescan again`}. editor_launch restart is the only remedy that has always worked, and is the one to reach for when the count above is not good enough odds. project_import refresh_classes does not, and answers added: [] while this is happening.`);
30861
30886
  }
30862
30887
  const uncached = unloaded.filter((type) => !type.inTheClassCache);
30863
30888
  if (uncached.length > 0) {
@@ -30873,6 +30898,17 @@ class GodotServer {
30873
30898
  ...notes.length === 0 ? {} : { staleAnalysis: notes.join(" ") }
30874
30899
  };
30875
30900
  }
30901
+ sourceOfClass(projectPath, resourcePath) {
30902
+ const contained = resolveWithinProject(projectPath, resourcePath);
30903
+ if (!contained.ok || !existsSync9(contained.absolutePath)) {
30904
+ return null;
30905
+ }
30906
+ try {
30907
+ return readFileSync11(contained.absolutePath, "utf8");
30908
+ } catch {
30909
+ return null;
30910
+ }
30911
+ }
30876
30912
  async handleLSP(toolName, args) {
30877
30913
  const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
30878
30914
  if (this.lspClient !== null && this.lspClient.port !== port) {
@@ -31710,6 +31746,17 @@ class GodotServer {
31710
31746
  const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
31711
31747
  const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
31712
31748
  const restored = lost.length > 0 ? await this.rebuildCacheAfterLoss(projectPath, lost) : [];
31749
+ const reloading = readNonEmptyString(args, "reloadScript");
31750
+ let reloaded = {};
31751
+ if (reloading !== undefined) {
31752
+ try {
31753
+ const answer = asParams(await this.godotBridge.invokeTool("reload_script", { ...args, scriptPath: reloading }));
31754
+ const methods = readArray(answer, "methods");
31755
+ reloaded = methods ? { methods: methods.map(String) } : { problem: `the editor would not say what ${reloading} has after reloading it` };
31756
+ } catch (error) {
31757
+ reloaded = { problem: `${reloading} could not be reloaded: ${errorMessage(error)}` };
31758
+ }
31759
+ }
31713
31760
  const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
31714
31761
  const unseen = checked.unseen;
31715
31762
  const stillGone = lost.filter((name) => !restored.includes(name));
@@ -31717,6 +31764,8 @@ class GodotServer {
31717
31764
  ok: !busy && unseen.length === 0 && stillGone.length === 0 && checked.unchecked === undefined,
31718
31765
  stillWorking: busy,
31719
31766
  waitedMs: Date.now() - started,
31767
+ reloadedMethods: reloaded.methods,
31768
+ reloadProblem: reloaded.problem,
31720
31769
  unseenByEditor: unseen.length > 0 ? unseen : undefined,
31721
31770
  cacheLost: lost.length > 0 ? lost : undefined,
31722
31771
  cacheRestored: restored.length > 0 ? restored : undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.13.20",
3
+ "version": "0.13.22",
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",