gdharness 0.13.31 → 0.13.33

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
@@ -15593,7 +15593,7 @@ var init_tool_definitions = __esm(() => {
15593
15593
  direction: {
15594
15594
  type: "string",
15595
15595
  enum: ["forward", "reverse"],
15596
- description: "forward: what this resource loads, with cycles reported. reverse: every file that refers to it and how, a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name. Default forward."
15596
+ description: "forward: what this resource loads, with cycles reported. reverse: every file that refers to it and how, a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name. Each reference carries a kind, and two of them are mentions rather than uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs, and comment is one in ordinary prose, which it would only make stale. summary.in_code is the count with both of those left out, which is the number to read when the question is whether anything still uses this. Default forward."
15597
15597
  },
15598
15598
  depth: { type: "number", description: "forward: how many levels to follow. Default unlimited." },
15599
15599
  includeBuiltin: {
@@ -38870,6 +38870,10 @@ class GodotServer {
38870
38870
  nothingOfOursIsRunning() {
38871
38871
  const status = this.godotBridge.getStatus();
38872
38872
  const noEditor = !status.connected ? " No editor is connected, so a game the editor is playing cannot be seen from here at all:" + " editor_status says whether one is on its way, and a run of yours that is still up is" + " reachable again once it is." : "";
38873
+ const live = this.aRuntimeOfOursIsAnswering();
38874
+ if (live !== null) {
38875
+ return `A game is running for ${live.project.path} and has announced its runtime on port ${live.port}, pid ${live.pid},` + " but this server did not start it and cannot reach it through the editor, so editor_run and" + " editor_output have nothing to answer about. The runtime_* tools reach it now." + " Do not start another: editor_run start replaces the game that is playing rather than" + " adding one, so it would end this one." + (this.godotBridge.getStatus().connected ? "" : " editor_status says whether an editor is on its way, and editor_run can end it once one is.");
38876
+ }
38873
38877
  const record = readRunRecord();
38874
38878
  if (record === null) {
38875
38879
  return `No game is running.${noEditor} Start one with editor_run.`;
@@ -38879,6 +38883,14 @@ class GodotServer {
38879
38883
  const ours = mine === null ? "this server was started without GDHARNESS_PROJECT and has no editor connected, so it cannot say whose that run is" : `this server serves ${mine}`;
38880
38884
  return `No game of this server's is running.${noEditor} A run started from ${whose} is recorded on this machine, and ${ours}, so it is not this server's to answer for or to end. Start one with editor_run.`;
38881
38885
  }
38886
+ aRuntimeOfOursIsAnswering() {
38887
+ const status = this.godotBridge.getStatus();
38888
+ const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
38889
+ if (mine === null) {
38890
+ return null;
38891
+ }
38892
+ return discoverRuntimes().find((one) => one.project.path !== "" && isSameDirectory(mine, one.project.path)) ?? null;
38893
+ }
38882
38894
  couldBeOurs(project) {
38883
38895
  const status = this.godotBridge.getStatus();
38884
38896
  const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
@@ -128,6 +128,7 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
128
128
  var usages: Array[Dictionary] = []
129
129
  var by_kind: Dictionary = {}
130
130
  var total: int = 0
131
+ var in_code: int = 0
131
132
 
132
133
  for file_path: String in all_files:
133
134
  if file_path == resource_path:
@@ -141,16 +142,32 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
141
142
  var references: Array[Dictionary] = []
142
143
  for i: int in range(lines.size()):
143
144
  var line: String = lines[i]
145
+ var by_its_path: bool = by_path.search(line) != null
146
+ var by_its_name: bool = not by_its_path and by_class != null and by_class.search(line) != null
147
+ if not by_its_path and not by_its_name:
148
+ continue
149
+ var trimmed: String = line.strip_edges()
144
150
  var kind: String = ""
145
- if by_path.search(line) != null:
151
+ # A mention in a comment is not a use, and both were answered as one. The question a
152
+ # reverse walk is asked is whether anything still uses a class, and a project that
153
+ # documents itself names its collaborators in `##` links: four references to one class,
154
+ # three of them prose, all counted as code. The more carefully a project is documented
155
+ # the worse that ratio gets. `##` and `#` are kept apart because renaming the class
156
+ # breaks Godot's generated documentation and leaves the prose merely stale.
157
+ if trimmed.begins_with("##"):
158
+ kind = "doc"
159
+ elif trimmed.begins_with("#"):
160
+ kind = "comment"
161
+ elif by_its_path:
146
162
  kind = _path_reference_kind(line)
147
- elif by_class != null and by_class.search(line) != null:
148
- var bare: String = Patterns.without_annotations(line)
149
- kind = "extends" if bare.begins_with("extends ") else "class_name"
150
- if kind.is_empty():
151
- continue
152
- references.append({"line": i + 1, "kind": kind, "text": line.strip_edges()})
163
+ else:
164
+ kind = (
165
+ "extends" if Patterns.without_annotations(line).begins_with("extends ") else "class_name"
166
+ )
167
+ references.append({"line": i + 1, "kind": kind, "text": trimmed})
153
168
  by_kind[kind] = Read.as_int(by_kind.get(kind, 0)) + 1
169
+ if kind != "doc" and kind != "comment":
170
+ in_code += 1
154
171
 
155
172
  if not references.is_empty():
156
173
  usages.append({"file": file_path, "references": references})
@@ -168,6 +185,9 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
168
185
  "files_searched": all_files.size(),
169
186
  "files_with_usages": usages.size(),
170
187
  "total": total,
188
+ # The arithmetic done here rather than left to the caller, because the caller who most
189
+ # needs it is the one asking whether a class is dead, and that caller reads one number.
190
+ "in_code": in_code,
171
191
  "by_kind": by_kind,
172
192
  },
173
193
  }
@@ -80,6 +80,10 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
80
80
  # Only a declaration, because an unbalanced line anywhere else is a file mid-edit and
81
81
  # joining from one swallows the rest of it: a body holding `print(` with nothing closing it
82
82
  # took every declaration below it, which is the state an agent is most likely to ask about.
83
+ # `header` rather than `stripped`: an annotation in front pushes the keyword off the start
84
+ # of the line, so asking the raw line whether it can wrap says no to every annotated
85
+ # declaration that does. Held by the combined case, which nothing else in the fixture
86
+ # reaches: the single-feature cases each carry one of these and never a second.
83
87
  if _can_wrap(header):
84
88
  while _bracket_depth(stripped) > 0 and i + 1 < lines.size():
85
89
  i += 1
@@ -479,6 +483,12 @@ func _up_to(text: String, needle: String) -> String:
479
483
 
480
484
 
481
485
  # The line with any trailing comment taken off, quote-aware so a `#` inside a string stays.
486
+ #
487
+ # The only one of these scanners that does not count brackets, on purpose: a `#` starts a comment
488
+ # wherever it is, and there is no depth at which it does not. Giving this one the depth its
489
+ # neighbours have puts the comment into the value of `var table := { # keyed by name`, because the
490
+ # brace opens before the `#` and closes on a later line. Held by a case, since the argument for the
491
+ # difference is not visible from the code and reads like an oversight beside its neighbours.
482
492
  func _without_comment(line: String) -> String:
483
493
  var quote: String = ""
484
494
  for i: int in range(line.length()):
@@ -510,9 +520,9 @@ func _closing_paren(text: String, from: int) -> int:
510
520
  if ch == '"' or ch == "'":
511
521
  quote = ch
512
522
  continue
513
- if ch == "(":
523
+ if ch == "(" or ch == "[" or ch == "{":
514
524
  depth += 1
515
- elif ch == ")":
525
+ elif ch == ")" or ch == "]" or ch == "}":
516
526
  depth -= 1
517
527
  if depth == 0:
518
528
  return i
@@ -16,6 +16,11 @@ extends RefCounted
16
16
  # Kept, because the readers below call these per line of every script in a project and a fresh
17
17
  # compile each time is the whole cost of the scan. The patterns are literals in this repository, so
18
18
  # the set is bounded by the source rather than by anything a caller passes.
19
+ #
20
+ # Held by `test/support/gd/patterns_cache.gd`, with `is_same` rather than `==`: two RegEx built from
21
+ # one pattern match the same text and compare equal on everything else, so identity is the only
22
+ # question that separates a cache from a recompile. Without that case, taking the cache away would
23
+ # have broken no answer anywhere and failed nothing, which is the argued-and-unheld shape.
19
24
  static var _compiled: Dictionary = {}
20
25
 
21
26
 
package/build/index.js CHANGED
@@ -28675,7 +28675,7 @@ var TOOL_SPECS = [
28675
28675
  direction: {
28676
28676
  type: "string",
28677
28677
  enum: ["forward", "reverse"],
28678
- description: "forward: what this resource loads, with cycles reported. reverse: every file that refers to it and how, a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name. Default forward."
28678
+ description: "forward: what this resource loads, with cycles reported. reverse: every file that refers to it and how, a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name. Each reference carries a kind, and two of them are mentions rather than uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs, and comment is one in ordinary prose, which it would only make stale. summary.in_code is the count with both of those left out, which is the number to read when the question is whether anything still uses this. Default forward."
28679
28679
  },
28680
28680
  depth: { type: "number", description: "forward: how many levels to follow. Default unlimited." },
28681
28681
  includeBuiltin: {
@@ -31610,6 +31610,10 @@ class GodotServer {
31610
31610
  nothingOfOursIsRunning() {
31611
31611
  const status = this.godotBridge.getStatus();
31612
31612
  const noEditor = !status.connected ? " No editor is connected, so a game the editor is playing cannot be seen from here at all:" + " editor_status says whether one is on its way, and a run of yours that is still up is" + " reachable again once it is." : "";
31613
+ const live = this.aRuntimeOfOursIsAnswering();
31614
+ if (live !== null) {
31615
+ return `A game is running for ${live.project.path} and has announced its runtime on port ${live.port}, pid ${live.pid},` + " but this server did not start it and cannot reach it through the editor, so editor_run and" + " editor_output have nothing to answer about. The runtime_* tools reach it now." + " Do not start another: editor_run start replaces the game that is playing rather than" + " adding one, so it would end this one." + (this.godotBridge.getStatus().connected ? "" : " editor_status says whether an editor is on its way, and editor_run can end it once one is.");
31616
+ }
31613
31617
  const record = readRunRecord();
31614
31618
  if (record === null) {
31615
31619
  return `No game is running.${noEditor} Start one with editor_run.`;
@@ -31619,6 +31623,14 @@ class GodotServer {
31619
31623
  const ours = mine === null ? "this server was started without GDHARNESS_PROJECT and has no editor connected, so it cannot say whose that run is" : `this server serves ${mine}`;
31620
31624
  return `No game of this server's is running.${noEditor} A run started from ${whose} is recorded on this machine, and ${ours}, so it is not this server's to answer for or to end. Start one with editor_run.`;
31621
31625
  }
31626
+ aRuntimeOfOursIsAnswering() {
31627
+ const status = this.godotBridge.getStatus();
31628
+ const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
31629
+ if (mine === null) {
31630
+ return null;
31631
+ }
31632
+ return discoverRuntimes().find((one) => one.project.path !== "" && isSameDirectory(mine, one.project.path)) ?? null;
31633
+ }
31622
31634
  couldBeOurs(project) {
31623
31635
  const status = this.godotBridge.getStatus();
31624
31636
  const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.13.31",
3
+ "version": "0.13.33",
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",