hoi4-agent-tools 3.6.0 → 3.8.0
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/CHANGELOG.md +23 -0
- package/README.md +124 -28
- package/dist/hoi4_agent_tools/core/configuration.js +2 -0
- package/dist/hoi4_agent_tools/core/engine.js +7 -7
- package/dist/hoi4_agent_tools/core/file-glob.js +164 -0
- package/dist/hoi4_agent_tools/core/index-segments.js +4 -0
- package/dist/hoi4_agent_tools/core/job-operation-registration.js +31 -0
- package/dist/hoi4_agent_tools/core/job-store.js +19 -40
- package/dist/hoi4_agent_tools/core/job-worker-host.js +84 -19
- package/dist/hoi4_agent_tools/core/job-worker.js +8 -16
- package/dist/hoi4_agent_tools/core/operation-tasks.js +2 -7
- package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +37 -6
- package/dist/hoi4_agent_tools/core/scanner.js +136 -47
- package/dist/hoi4_agent_tools/core/shared-request-capacity.js +27 -13
- package/dist/hoi4_agent_tools/core/source/cache.js +4 -0
- package/dist/hoi4_agent_tools/core/windows-sharing.js +42 -0
- package/dist/hoi4_agent_tools/core/workspace.js +114 -8
- package/dist/hoi4_agent_tools/event/artifact-validation.js +1 -1
- package/dist/hoi4_agent_tools/event/service.js +1 -1
- package/dist/hoi4_agent_tools/focus/service.js +2 -2
- package/dist/hoi4_agent_tools/gui/studio.js +16 -3
- package/dist/hoi4_agent_tools/map/model.js +81 -2
- package/dist/hoi4_agent_tools/map/service.js +3 -2
- package/dist/hoi4_agent_tools/map/validation.js +60 -4
- package/dist/hoi4_agent_tools/mcp/tools/reference.js +10 -1
- package/dist/hoi4_agent_tools/package-check/service.js +1 -1
- package/dist/hoi4_agent_tools/reference/lines.js +12 -0
- package/dist/hoi4_agent_tools/reference/operations.js +45 -2
- package/dist/hoi4_agent_tools/reference/ranking.js +309 -0
- package/dist/hoi4_agent_tools/reference/script-validation.js +267 -0
- package/dist/hoi4_agent_tools/reference/service.js +285 -177
- package/dist/hoi4_agent_tools/reference/source-locate.js +156 -0
- package/dist/hoi4_agent_tools/reference/source-lookup.js +125 -18
- package/dist/hoi4_agent_tools/reference/source-navigation.js +130 -0
- package/dist/hoi4_agent_tools/reference/suggestions.js +38 -0
- package/dist/hoi4_agent_tools/schemas/reference.js +135 -2
- package/dist/hoi4_agent_tools/schemas/script-validation.js +69 -0
- package/dist/hoi4_agent_tools/schemas/transaction.js +1 -1
- package/dist/hoi4_agent_tools/technology/service.js +1 -1
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +1 -0
- package/docs/development.md +6 -0
- package/docs/examples.md +3 -13
- package/docs/gui.md +1 -15
- package/docs/map.md +10 -0
- package/docs/reference.md +47 -4
- package/docs/research/reference-benchmark.md +80 -0
- package/docs/setup.md +15 -0
- package/docs/tools.md +37 -36
- package/npm-shrinkwrap.json +7101 -0
- package/package.json +8 -7
- package/schemas/configuration.schema.json +95 -88
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/helper-expansion-request.schema.json +1 -1
- package/schemas/helper-expansion-summary.schema.json +1 -1
- package/schemas/impact-inspect-input.schema.json +1 -1
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/mechanic-test-input.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +1 -1
- package/schemas/package-manifest.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/schemas/reference-context-input.schema.json +1 -1
- package/schemas/reference-read-input.schema.json +9 -1
- package/schemas/reference-search-input.schema.json +1 -1
- package/schemas/scenario-suite.schema.json +1 -1
- package/schemas/scenario-test-input.schema.json +1 -1
- package/schemas/script-validate-input.schema.json +92 -0
- package/schemas/shared-scenario.schema.json +1 -1
- package/schemas/source-lookup-input.schema.json +144 -15
- package/server.json +2 -2
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { hashCanonical } from '../core/canonical.js';
|
|
3
|
+
import { ServiceError } from '../core/result.js';
|
|
4
|
+
import { locationFor, parseClausewitz, } from '../core/source/index.js';
|
|
5
|
+
import { editDistance } from './suggestions.js';
|
|
6
|
+
import { publicReferenceSection } from './service.js';
|
|
7
|
+
const MAX_COMMANDS = 256;
|
|
8
|
+
const booleanCommands = new Set(['AND', 'OR', 'NOT', 'XOR', 'NAND', 'NOR']);
|
|
9
|
+
const containers = new Set([
|
|
10
|
+
'and',
|
|
11
|
+
'or',
|
|
12
|
+
'not',
|
|
13
|
+
'xor',
|
|
14
|
+
'nand',
|
|
15
|
+
'nor',
|
|
16
|
+
'hidden_trigger',
|
|
17
|
+
'hidden_effect',
|
|
18
|
+
]);
|
|
19
|
+
const conditionals = new Set(['if', 'else_if', 'else']);
|
|
20
|
+
const iteratorParameters = new Set([
|
|
21
|
+
'tooltip',
|
|
22
|
+
'random_select_amount',
|
|
23
|
+
'display_individual_scopes',
|
|
24
|
+
'count',
|
|
25
|
+
]);
|
|
26
|
+
/** Check only rules explicitly available from one selected documentation authority. */
|
|
27
|
+
export async function validateScript(references, workspace, input, signal) {
|
|
28
|
+
if (Buffer.byteLength(input.source, 'utf8') > 64_000)
|
|
29
|
+
throw new ServiceError('SCRIPT_SOURCE_TOO_LARGE', 'Snippet exceeds 64000 UTF-8 bytes');
|
|
30
|
+
const inventory = await references.inventory(workspace, signal, [input.documentation]);
|
|
31
|
+
const catalogs = { effect: new Map(), trigger: new Map() };
|
|
32
|
+
const sources = new Map();
|
|
33
|
+
for (const section of inventory.sections) {
|
|
34
|
+
const filename = path.basename(section.path).toLowerCase();
|
|
35
|
+
const match = /^(effects?|triggers?)_(?:documentation|docs)\.(?:md|log)$/u.exec(filename);
|
|
36
|
+
if (match === null)
|
|
37
|
+
continue;
|
|
38
|
+
sources.set(section.path, section.revision);
|
|
39
|
+
if (!/^[a-z_][a-z0-9_]{0,255}$/u.test(section.heading))
|
|
40
|
+
continue;
|
|
41
|
+
const scopeLine = /^\s*(?:\*\s*)?Supported Scopes:\s*(.+?)\s*$/imu.exec(section.lines.join('\n'));
|
|
42
|
+
if (scopeLine === null || !/^[a-z_,\s]+$/iu.test(scopeLine[1]))
|
|
43
|
+
continue;
|
|
44
|
+
const scopes = [
|
|
45
|
+
...new Set(scopeLine[1]
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.split(/[,\s]+/u)
|
|
48
|
+
.filter(Boolean)),
|
|
49
|
+
].sort();
|
|
50
|
+
const kind = match[1].startsWith('effect') ? 'effect' : 'trigger';
|
|
51
|
+
const previous = catalogs[kind].get(section.heading);
|
|
52
|
+
const reference = publicReferenceSection(section);
|
|
53
|
+
if (previous !== undefined) {
|
|
54
|
+
previous.conflict ||= previous.scopes.join(',') !== scopes.join(',');
|
|
55
|
+
}
|
|
56
|
+
else
|
|
57
|
+
catalogs[kind].set(section.heading, { reference, scopes, conflict: false });
|
|
58
|
+
}
|
|
59
|
+
const document = parseClausewitz(Buffer.from(input.source, 'utf8'), 'snippet');
|
|
60
|
+
const findings = [];
|
|
61
|
+
let visited = 0;
|
|
62
|
+
let checkedCommands = 0;
|
|
63
|
+
let argumentBlocksUnchecked = 0;
|
|
64
|
+
let suggestionBudget = 4000;
|
|
65
|
+
const bindings = new Map();
|
|
66
|
+
for (const binding of input.bindings) {
|
|
67
|
+
if (bindings.has(binding.name))
|
|
68
|
+
throw new ServiceError('SCRIPT_BINDING_DUPLICATE', 'Scope bindings must have unique names');
|
|
69
|
+
if (binding.name === 'ROOT' || binding.name === 'THIS' || binding.name === 'PREV')
|
|
70
|
+
throw new ServiceError('SCRIPT_BINDING_RESERVED', 'ROOT, THIS and PREV follow the supplied scope and traversal stack');
|
|
71
|
+
bindings.set(binding.name, binding.scope);
|
|
72
|
+
}
|
|
73
|
+
const report = (entry, kind, scope, details) => {
|
|
74
|
+
const position = locationFor('snippet', document.lineIndex, entry.start, entry.end).start;
|
|
75
|
+
findings.push({
|
|
76
|
+
...details,
|
|
77
|
+
command: entry.key.value.slice(0, 256),
|
|
78
|
+
kind,
|
|
79
|
+
scope,
|
|
80
|
+
line: position.line,
|
|
81
|
+
column: position.column,
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
for (const diagnostic of document.diagnostics)
|
|
85
|
+
findings.push({
|
|
86
|
+
code: diagnostic.code,
|
|
87
|
+
status: 'error',
|
|
88
|
+
command: '',
|
|
89
|
+
kind: input.kind,
|
|
90
|
+
scope: input.scope,
|
|
91
|
+
line: diagnostic.location?.start.line ?? 1,
|
|
92
|
+
column: diagnostic.location?.start.column ?? 1,
|
|
93
|
+
message: diagnostic.message.slice(0, 500),
|
|
94
|
+
suggestions: [],
|
|
95
|
+
});
|
|
96
|
+
const walk = (block, kind, scope, previous) => {
|
|
97
|
+
for (const entry of block.entries) {
|
|
98
|
+
signal?.throwIfAborted();
|
|
99
|
+
if (++visited > MAX_COMMANDS) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (entry.type !== 'assignment') {
|
|
103
|
+
const position = locationFor('snippet', document.lineIndex, entry.start, entry.end).start;
|
|
104
|
+
findings.push({
|
|
105
|
+
code: 'SCRIPT_EXPECTED_COMMAND',
|
|
106
|
+
status: 'error',
|
|
107
|
+
command: '',
|
|
108
|
+
kind,
|
|
109
|
+
scope,
|
|
110
|
+
line: position.line,
|
|
111
|
+
column: position.column,
|
|
112
|
+
message: 'Expected a command assignment in this effect or trigger body',
|
|
113
|
+
suggestions: [],
|
|
114
|
+
});
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const raw = entry.key.value;
|
|
118
|
+
if (raw.startsWith('@'))
|
|
119
|
+
continue;
|
|
120
|
+
const name = booleanCommands.has(raw) ? raw.toLowerCase() : raw;
|
|
121
|
+
const explicitScope = raw === 'ROOT'
|
|
122
|
+
? input.scope
|
|
123
|
+
: raw === 'THIS'
|
|
124
|
+
? scope
|
|
125
|
+
: raw === 'PREV'
|
|
126
|
+
? previous
|
|
127
|
+
: bindings.get(raw);
|
|
128
|
+
const fixedScope = /^[A-Z][A-Z0-9]{2}$/u.test(raw) &&
|
|
129
|
+
!['ROOT', 'THIS', 'PREV', 'FROM', 'AND', 'NOT', 'XOR', 'NOR'].includes(raw)
|
|
130
|
+
? 'country'
|
|
131
|
+
: /^\d+$/u.test(raw)
|
|
132
|
+
? 'state'
|
|
133
|
+
: undefined;
|
|
134
|
+
if (entry.value.type === 'block' &&
|
|
135
|
+
(explicitScope !== undefined || fixedScope !== undefined)) {
|
|
136
|
+
walk(entry.value, kind, explicitScope ?? fixedScope, scope);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const signature = catalogs[kind].get(name);
|
|
140
|
+
if (signature === undefined) {
|
|
141
|
+
const other = catalogs[kind === 'effect' ? 'trigger' : 'effect'].get(name);
|
|
142
|
+
const suggestions = name.length > 128
|
|
143
|
+
? []
|
|
144
|
+
: [...catalogs[kind].keys()]
|
|
145
|
+
.filter((candidate) => Math.abs(candidate.length - name.length) <= 3)
|
|
146
|
+
.filter(() => suggestionBudget-- > 0)
|
|
147
|
+
.map((candidate) => ({ candidate, distance: editDistance(name, candidate) }))
|
|
148
|
+
.filter(({ distance }) => distance <= 3)
|
|
149
|
+
.sort((a, b) => a.distance - b.distance || a.candidate.localeCompare(b.candidate, 'en'))
|
|
150
|
+
.slice(0, 3)
|
|
151
|
+
.map(({ candidate }) => candidate);
|
|
152
|
+
report(entry, kind, scope, {
|
|
153
|
+
code: other === undefined ? 'SCRIPT_COMMAND_UNRESOLVED' : 'SCRIPT_COMMAND_WRONG_KIND',
|
|
154
|
+
status: other === undefined ? 'unresolved' : 'error',
|
|
155
|
+
message: other === undefined
|
|
156
|
+
? 'Not documented as a native command in the selected source; inspect scripted helpers or declare the scope binding'
|
|
157
|
+
: `Documented as a ${kind === 'effect' ? 'trigger' : 'effect'}, not a ${kind}`,
|
|
158
|
+
suggestions,
|
|
159
|
+
...(other === undefined ? {} : { reference: other.reference }),
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
checkedCommands++;
|
|
164
|
+
const wrongScope = !signature.scopes.includes('any') && !signature.scopes.includes(scope);
|
|
165
|
+
report(entry, kind, scope, {
|
|
166
|
+
code: signature.conflict
|
|
167
|
+
? 'SCRIPT_DOCUMENTATION_CONFLICT'
|
|
168
|
+
: wrongScope
|
|
169
|
+
? 'SCRIPT_COMMAND_WRONG_SCOPE'
|
|
170
|
+
: 'SCRIPT_COMMAND_SUPPORTED',
|
|
171
|
+
status: signature.conflict ? 'unresolved' : wrongScope ? 'error' : 'supported',
|
|
172
|
+
suggestions: [],
|
|
173
|
+
reference: signature.reference,
|
|
174
|
+
message: signature.conflict
|
|
175
|
+
? 'Conflicting supported scopes in the selected documentation'
|
|
176
|
+
: wrongScope
|
|
177
|
+
? `Supported scopes: ${signature.scopes.join(', ')}`
|
|
178
|
+
: 'Command kind and declared scope match the selected documentation',
|
|
179
|
+
});
|
|
180
|
+
const iterator = /^(?:every|random|any|all)_(?:(?:owned|controlled|neighbor)_)?(state|country)$/u.exec(name);
|
|
181
|
+
if (entry.value.type !== 'block') {
|
|
182
|
+
if (containers.has(name) || conditionals.has(name) || iterator !== null)
|
|
183
|
+
report(entry, kind, scope, {
|
|
184
|
+
code: 'SCRIPT_EXPECTED_BLOCK',
|
|
185
|
+
status: 'error',
|
|
186
|
+
message: 'This control command requires a block',
|
|
187
|
+
suggestions: [],
|
|
188
|
+
reference: signature.reference,
|
|
189
|
+
});
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (containers.has(name)) {
|
|
193
|
+
walk(entry.value, kind, scope, previous);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (conditionals.has(name) || iterator !== null) {
|
|
197
|
+
const childScope = iterator?.[1] ?? scope;
|
|
198
|
+
for (const child of entry.value.entries) {
|
|
199
|
+
if (child.type === 'assignment' && child.key.value === 'limit') {
|
|
200
|
+
if (child.value.type === 'block')
|
|
201
|
+
walk(child.value, 'trigger', childScope, iterator === null ? previous : scope);
|
|
202
|
+
else
|
|
203
|
+
report(child, 'trigger', childScope, {
|
|
204
|
+
code: 'SCRIPT_EXPECTED_BLOCK',
|
|
205
|
+
status: 'error',
|
|
206
|
+
message: 'limit requires a trigger block',
|
|
207
|
+
suggestions: [],
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
else if (iterator !== null &&
|
|
211
|
+
child.type === 'assignment' &&
|
|
212
|
+
iteratorParameters.has(child.key.value)) {
|
|
213
|
+
argumentBlocksUnchecked++;
|
|
214
|
+
}
|
|
215
|
+
else
|
|
216
|
+
walk({ type: 'block', entries: [child], start: child.start, end: child.end }, kind, childScope, iterator === null ? previous : scope);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
argumentBlocksUnchecked++;
|
|
221
|
+
report(entry, kind, scope, {
|
|
222
|
+
code: 'SCRIPT_ARGUMENT_BLOCK_UNCHECKED',
|
|
223
|
+
status: 'unresolved',
|
|
224
|
+
message: 'Native argument block is not validated from prose documentation; read the cited syntax before use',
|
|
225
|
+
suggestions: [],
|
|
226
|
+
reference: signature.reference,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
if (document.diagnostics.every(({ severity }) => severity !== 'error' && severity !== 'blocker'))
|
|
232
|
+
walk(document.root, input.kind, input.scope);
|
|
233
|
+
const unresolvedCount = findings.filter(({ status }) => status === 'unresolved').length;
|
|
234
|
+
const invalid = findings.some(({ status }) => status === 'error');
|
|
235
|
+
const truncated = visited > MAX_COMMANDS;
|
|
236
|
+
const incomplete = unresolvedCount > 0 || truncated || inventory.skipped > 0 || catalogs[input.kind].size === 0;
|
|
237
|
+
const order = { error: 0, unresolved: 1, supported: 2 };
|
|
238
|
+
findings.sort((a, b) => order[a.status] - order[b.status] ||
|
|
239
|
+
a.line - b.line ||
|
|
240
|
+
a.column - b.column ||
|
|
241
|
+
a.code.localeCompare(b.code, 'en'));
|
|
242
|
+
const result = {
|
|
243
|
+
valid: invalid ? false : incomplete ? null : true,
|
|
244
|
+
checksPerformed: ['syntax', 'command_kind', 'declared_scope'],
|
|
245
|
+
parametersChecked: false,
|
|
246
|
+
documentation: {
|
|
247
|
+
source: input.documentation,
|
|
248
|
+
revision: hashCanonical([...sources].sort()),
|
|
249
|
+
effectCount: catalogs.effect.size,
|
|
250
|
+
triggerCount: catalogs.trigger.size,
|
|
251
|
+
skippedSources: inventory.skipped,
|
|
252
|
+
},
|
|
253
|
+
findings: findings.slice(0, input.limit),
|
|
254
|
+
totalFindings: findings.length,
|
|
255
|
+
omittedFindings: Math.max(0, findings.length - input.limit),
|
|
256
|
+
checkedCommands,
|
|
257
|
+
unresolvedCount,
|
|
258
|
+
truncated,
|
|
259
|
+
argumentBlocksUnchecked,
|
|
260
|
+
};
|
|
261
|
+
while (Buffer.byteLength(JSON.stringify(result), 'utf8') > 16_000 && result.findings.length > 1)
|
|
262
|
+
result.findings.pop();
|
|
263
|
+
result.omittedFindings = result.totalFindings - result.findings.length;
|
|
264
|
+
if (Buffer.byteLength(JSON.stringify(result), 'utf8') > 16_000)
|
|
265
|
+
throw new ServiceError('REFERENCE_RESPONSE_TOO_LARGE', 'Command documentation metadata exceeds the bounded reply');
|
|
266
|
+
return result;
|
|
267
|
+
}
|