gdharness 0.12.7 → 0.12.10

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
@@ -7401,7 +7401,7 @@ class UpdateCheck {
7401
7401
  }
7402
7402
  }
7403
7403
  refresh(now = Date.now()) {
7404
- if (!this.enabled || this.checking || now < this.retryAt || now - this.checkedAt < CACHE_MS) {
7404
+ if (!this.enabled || this.checking || now < this.retryAt || !this.stale(now)) {
7405
7405
  return;
7406
7406
  }
7407
7407
  this.checking = true;
@@ -7421,15 +7421,21 @@ class UpdateCheck {
7421
7421
  this.checking = false;
7422
7422
  });
7423
7423
  }
7424
+ stale(now) {
7425
+ return now - this.checkedAt >= CACHE_MS;
7426
+ }
7424
7427
  scheduleRetry(now) {
7425
7428
  this.retryAt = now + this.backoffMs;
7426
7429
  this.backoffMs = Math.min(this.backoffMs * 2, MAX_RETRY_MS);
7427
7430
  }
7428
- notice() {
7431
+ notice(now = Date.now()) {
7429
7432
  const latest = this.latest;
7430
7433
  if (!this.enabled || latest === null || !isNewer(latest, this.current)) {
7431
7434
  return null;
7432
7435
  }
7436
+ if (this.checking && this.stale(now)) {
7437
+ return null;
7438
+ }
7433
7439
  return {
7434
7440
  current: this.current,
7435
7441
  latest,
@@ -7524,9 +7530,7 @@ function staleClassNames(projectPath) {
7524
7530
  }
7525
7531
  function unseenByEditor(projectPath, editorHolds) {
7526
7532
  const held = new Set(editorHolds);
7527
- const declared = declaredClasses(projectPath);
7528
- const source = cachedClasses(projectPath) ?? declared;
7529
- return [...source].filter(([name]) => !held.has(name) && declared.has(name)).map(([className, path]) => ({ className, path: declared.get(className) ?? path }));
7533
+ return [...declaredClasses(projectPath)].filter(([name]) => !held.has(name)).map(([className, path]) => ({ className, path }));
7530
7534
  }
7531
7535
  var init_class_cache = () => {};
7532
7536
 
@@ -15759,7 +15763,7 @@ var init_tool_definitions = __esm(() => {
15759
15763
  },
15760
15764
  {
15761
15765
  name: "editor_rescan",
15762
- 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 is a change to the declaring script, or editor_launch restart; project_import refresh_classes rewrites the cache and does not touch what the editor is holding. Needs the editor connected.",
15766
+ 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 is a change to the declaring script, or editor_launch restart; project_import refresh_classes rewrites the cache and does not touch what the editor is holding. The scan also writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class it cannot resolve is one the scan drops out of that file: unseenByEditor names it before a fresh engine, a CI run or a clone inherits it. Needs the editor connected.",
15763
15767
  parameters: {
15764
15768
  projectPath: PROJECT_PATH,
15765
15769
  timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
@@ -15842,7 +15846,7 @@ var init_tool_definitions = __esm(() => {
15842
15846
  limit: {
15843
15847
  type: "number",
15844
15848
  ops: ["find", "text"],
15845
- description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with truncated saying whether there were more."
15849
+ description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with omitted saying how many lines that left behind. A screen whose dialog sits under a long list is a screen read with omitted greater than zero, so raise this or point root at the dialog rather than reading the answer as what is on screen."
15846
15850
  },
15847
15851
  includeHidden: {
15848
15852
  type: "boolean",
@@ -39848,7 +39852,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
39848
39852
 
39849
39853
  | Ask | Tool |
39850
39854
  | --- | --- |
39851
- | What a screen says | \`runtime_inspect text\`, which reads what is drawn and leaves out what is hidden |
39855
+ | What a screen says | \`runtime_inspect text\`, which reads what is drawn and leaves out what is hidden. Read \`omitted\`: a long list above a dialog pushes it past the limit, and what comes back then is the top of the screen rather than the screen |
39852
39856
  | Find a control by the word on it | \`runtime_inspect find\` with \`says\`, rather than listing a screen and reading each one |
39853
39857
  | Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
39854
39858
  | What a property reads right now | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
@@ -39893,6 +39897,10 @@ them. \`editor_rescan\` asks it to scan and names any class it still cannot reso
39893
39897
  declaration and the cache can both be right while the editor stays blind to it. Change the
39894
39898
  declaring script, or restart the editor.
39895
39899
 
39900
+ The scan writes \`.godot/global_script_class_cache.cfg\` from the list the editor is holding, so
39901
+ a class it cannot resolve is dropped out of that file too, and the next fresh engine, CI run or
39902
+ clone starts from the narrower one. That is the same class \`unseenByEditor\` names.
39903
+
39896
39904
  A diagnostic about a member rather than a type, \`Static function "x()" not found in base "Y"\`
39897
39905
  about something the engine compiles, is a different thing and is no longer stale here: each ask
39898
39906
  gives the document back when its answer arrives, so the next one is read off disk. If one turns
@@ -258,7 +258,8 @@ func _matches_apart_from_name(node: Node, wanted: Dictionary[String, String]) ->
258
258
  ##
259
259
  ## [param limit] is the first few lines rather than all of them, which is how the top of a screen
260
260
  ## is read without the hall under it: the bar along the top of a guild is eleven lines and the
261
- ## panel it sits on is two hundred.
261
+ ## panel it sits on is two hundred. `omitted` says how many lines that left behind, so a screen
262
+ ## whose interesting half is below the cut says so in a number rather than in a flag.
262
263
  func read_text(params: Dictionary) -> Dictionary:
263
264
  var root_path: String = str(params.get("root", "/root"))
264
265
  var include_hidden: bool = bool(params.get("include_hidden", false))
@@ -269,19 +270,20 @@ func read_text(params: Dictionary) -> Dictionary:
269
270
  return reached
270
271
  var root: Node = reached["node"]
271
272
 
272
- # One line further than asked for, so that whether anything was left behind is read off the
273
- # walk rather than guessed at from the count: a panel of exactly as many lines as the caller
274
- # asked for is one they have read all of, and saying otherwise sends them back for nothing.
275
- var read: PackedStringArray = PackedStringArray()
276
- _read_into(root, include_hidden, limit + 1, read)
277
- var more: bool = read.size() > limit
278
- var lines: PackedStringArray = read.slice(0, limit) if more else read
273
+ # The rest of the subtree is walked and counted rather than stopped at. A flag on its own gets
274
+ # read as a footnote: a screen answered with its first few hundred lines and the dialog the
275
+ # player is being asked to answer below them reads exactly like a dialog that is not there,
276
+ # and a bare `truncated: true` was looked straight past twice before the limit was suspected.
277
+ # A number is what sends somebody back.
278
+ var lines: PackedStringArray = PackedStringArray()
279
+ var left_out: int = _read_into(root, include_hidden, limit, lines)
279
280
  return {
280
281
  "type": "text",
281
282
  "root": root_path,
282
283
  "lines": lines,
283
284
  "count": lines.size(),
284
- "truncated": more,
285
+ "truncated": left_out > 0,
286
+ "omitted": left_out,
285
287
  }
286
288
 
287
289
 
@@ -293,16 +295,21 @@ func read_text(params: Dictionary) -> Dictionary:
293
295
  ## screen full of forms answered with every label on it and none of the values in it. The hidden
294
296
  ## check covers a [Window] for the same walk: a dropdown's popup is a child that is not drawn
295
297
  ## until it is opened, and reading a closed menu would put every item on the screen.
296
- func _read_into(node: Node, include_hidden: bool, most: int, into: PackedStringArray) -> void:
297
- if into.size() >= most:
298
- return
298
+ ## Fills [param into] up to [param most] lines and answers with how many further lines the rest of
299
+ ## the subtree says, which is what the caller is told rather than left to infer.
300
+ func _read_into(node: Node, include_hidden: bool, most: int, into: PackedStringArray) -> int:
299
301
  if not include_hidden and not _drawn(node):
300
- return
302
+ return 0
303
+ var left_out: int = 0
301
304
  var said: String = said_by(node)
302
305
  if not said.is_empty():
303
- into.append(said)
306
+ if into.size() < most:
307
+ into.append(said)
308
+ else:
309
+ left_out += 1
304
310
  for child: Node in node.get_children(true):
305
- _read_into(child, include_hidden, most, into)
311
+ left_out += _read_into(child, include_hidden, most, into)
312
+ return left_out
306
313
 
307
314
 
308
315
  ## Whether [param node] is on the screen at all, for the three kinds of thing that can be hidden.
@@ -4,11 +4,23 @@ extends RefCounted
4
4
  # walk descends into every visible directory and matches whole names, while the extension walk
5
5
  # skips anything beginning with a dot, including .import sidecars that would otherwise answer
6
6
  # for the resource they belong to.
7
+ #
8
+ # Both stop at a `.gdignore`, because the engine does: nothing under one is imported, so what is
9
+ # in there is not a resource, not a dependency and not a global class. Walking in anyway hands
10
+ # every caller files the engine will never answer for.
11
+
12
+
13
+ # A directory the engine steps over, marker file and all.
14
+ func is_stepped_over(path: String) -> bool:
15
+ return FileAccess.file_exists(path + ".gdignore")
7
16
 
8
17
 
9
18
  # Files under `path` whose name ends with `extension`, which is passed with its dot.
10
19
  func find_files(path: String, extension: String) -> Array[String]:
11
20
  var files: Array[String] = []
21
+ if is_stepped_over(path):
22
+ return files
23
+
12
24
  var dir: DirAccess = DirAccess.open(path)
13
25
 
14
26
  if dir:
@@ -29,6 +41,9 @@ func find_files(path: String, extension: String) -> Array[String]:
29
41
  # Files under `path` whose extension is in `extensions`, which are passed without their dot.
30
42
  func find_files_with_extensions(path: String, extensions: Array) -> Array[String]:
31
43
  var files: Array[String] = []
44
+ if is_stepped_over(path):
45
+ return files
46
+
32
47
  var dir: DirAccess = DirAccess.open(path)
33
48
 
34
49
  if dir:
package/build/index.js CHANGED
@@ -9972,7 +9972,7 @@ class UpdateCheck {
9972
9972
  }
9973
9973
  }
9974
9974
  refresh(now = Date.now()) {
9975
- if (!this.enabled || this.checking || now < this.retryAt || now - this.checkedAt < CACHE_MS) {
9975
+ if (!this.enabled || this.checking || now < this.retryAt || !this.stale(now)) {
9976
9976
  return;
9977
9977
  }
9978
9978
  this.checking = true;
@@ -9992,15 +9992,21 @@ class UpdateCheck {
9992
9992
  this.checking = false;
9993
9993
  });
9994
9994
  }
9995
+ stale(now) {
9996
+ return now - this.checkedAt >= CACHE_MS;
9997
+ }
9995
9998
  scheduleRetry(now) {
9996
9999
  this.retryAt = now + this.backoffMs;
9997
10000
  this.backoffMs = Math.min(this.backoffMs * 2, MAX_RETRY_MS);
9998
10001
  }
9999
- notice() {
10002
+ notice(now = Date.now()) {
10000
10003
  const latest = this.latest;
10001
10004
  if (!this.enabled || latest === null || !isNewer(latest, this.current)) {
10002
10005
  return null;
10003
10006
  }
10007
+ if (this.checking && this.stale(now)) {
10008
+ return null;
10009
+ }
10004
10010
  return {
10005
10011
  current: this.current,
10006
10012
  latest,
@@ -24812,9 +24818,7 @@ function staleClassNames(projectPath) {
24812
24818
  }
24813
24819
  function unseenByEditor(projectPath, editorHolds) {
24814
24820
  const held = new Set(editorHolds);
24815
- const declared = declaredClasses(projectPath);
24816
- const source = cachedClasses(projectPath) ?? declared;
24817
- return [...source].filter(([name]) => !held.has(name) && declared.has(name)).map(([className, path]) => ({ className, path: declared.get(className) ?? path }));
24821
+ return [...declaredClasses(projectPath)].filter(([name]) => !held.has(name)).map(([className, path]) => ({ className, path }));
24818
24822
  }
24819
24823
 
24820
24824
  // src/dap_client.ts
@@ -28634,7 +28638,7 @@ var TOOL_SPECS = [
28634
28638
  },
28635
28639
  {
28636
28640
  name: "editor_rescan",
28637
- 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 is a change to the declaring script, or editor_launch restart; project_import refresh_classes rewrites the cache and does not touch what the editor is holding. Needs the editor connected.",
28641
+ 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 is a change to the declaring script, or editor_launch restart; project_import refresh_classes rewrites the cache and does not touch what the editor is holding. The scan also writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class it cannot resolve is one the scan drops out of that file: unseenByEditor names it before a fresh engine, a CI run or a clone inherits it. Needs the editor connected.",
28638
28642
  parameters: {
28639
28643
  projectPath: PROJECT_PATH,
28640
28644
  timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
@@ -28717,7 +28721,7 @@ var TOOL_SPECS = [
28717
28721
  limit: {
28718
28722
  type: "number",
28719
28723
  ops: ["find", "text"],
28720
- description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with truncated saying whether there were more."
28724
+ description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with omitted saying how many lines that left behind. A screen whose dialog sits under a long list is a screen read with omitted greater than zero, so raise this or point root at the dialog rather than reading the answer as what is on screen."
28721
28725
  },
28722
28726
  includeHidden: {
28723
28727
  type: "boolean",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.12.7",
3
+ "version": "0.12.10",
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",