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 +98 -3
- package/build/index.js +96 -2
- package/package.json +1 -1
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
|
|
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.
|
|
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",
|