hoi4-agent-tools 3.4.0 → 3.5.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 +27 -0
- package/README.md +26 -18
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +48 -2
- package/dist/hoi4_agent_tools/core/condition-schema.js +18 -0
- package/dist/hoi4_agent_tools/core/configuration.js +10 -0
- package/dist/hoi4_agent_tools/core/domain-scan-patterns.js +9 -1
- package/dist/hoi4_agent_tools/core/impact-semantic.js +11 -4
- package/dist/hoi4_agent_tools/core/index.js +1 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +19 -1
- package/dist/hoi4_agent_tools/core/operation-result.js +4 -1
- package/dist/hoi4_agent_tools/core/scanner.js +1 -1
- package/dist/hoi4_agent_tools/core/scenario-model.js +6 -0
- package/dist/hoi4_agent_tools/core/scenario-state.js +3 -0
- package/dist/hoi4_agent_tools/core/workspace.js +13 -1
- package/dist/hoi4_agent_tools/event/graph.js +8 -1
- package/dist/hoi4_agent_tools/event/operations.js +16 -10
- package/dist/hoi4_agent_tools/event/service.js +1 -0
- package/dist/hoi4_agent_tools/event/source-analysis.js +1 -1
- package/dist/hoi4_agent_tools/focus/render.js +10 -2
- package/dist/hoi4_agent_tools/gui/layout.js +58 -11
- package/dist/hoi4_agent_tools/gui/scenario.js +3 -1
- package/dist/hoi4_agent_tools/gui/source-graph.js +4 -1
- package/dist/hoi4_agent_tools/gui/studio.js +32 -10
- package/dist/hoi4_agent_tools/gui/validators.js +3 -0
- package/dist/hoi4_agent_tools/map/model.js +121 -12
- package/dist/hoi4_agent_tools/map/operations.js +80 -7
- package/dist/hoi4_agent_tools/map/service.js +1 -0
- package/dist/hoi4_agent_tools/map/validation.js +38 -1
- package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +18 -1
- package/dist/hoi4_agent_tools/mcp/server/create.js +2 -0
- package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
- package/dist/hoi4_agent_tools/mcp/tools/reference.js +60 -0
- package/dist/hoi4_agent_tools/mechanic/interpreter.js +91 -30
- package/dist/hoi4_agent_tools/mechanic/source.js +19 -4
- package/dist/hoi4_agent_tools/probability/service.js +2 -2
- package/dist/hoi4_agent_tools/probability/source-analysis.js +16 -0
- package/dist/hoi4_agent_tools/probability/value-evaluator.js +2 -1
- package/dist/hoi4_agent_tools/reference/lines.js +52 -0
- package/dist/hoi4_agent_tools/reference/operations.js +41 -0
- package/dist/hoi4_agent_tools/reference/service.js +427 -0
- package/dist/hoi4_agent_tools/reference/source-lookup.js +90 -0
- package/dist/hoi4_agent_tools/schemas/analysis.js +2 -0
- package/dist/hoi4_agent_tools/schemas/gui-requests.js +1 -1
- package/dist/hoi4_agent_tools/schemas/probability-requests.js +1 -1
- package/dist/hoi4_agent_tools/schemas/probability.js +26 -2
- package/dist/hoi4_agent_tools/schemas/reference.js +160 -0
- package/dist/hoi4_agent_tools/technology/operations.js +29 -1
- package/dist/hoi4_agent_tools/technology/render.js +11 -5
- package/dist/hoi4_agent_tools/technology/service.js +1 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +3 -0
- package/docs/events.md +1 -1
- package/docs/gui.md +7 -0
- package/docs/jobs.md +1 -0
- package/docs/map.md +8 -0
- package/docs/mechanics.md +2 -1
- package/docs/probability.md +7 -0
- package/docs/reference.md +32 -0
- package/docs/research/reference-benchmark.md +28 -0
- package/docs/setup.md +6 -0
- package/docs/technology.md +5 -1
- package/examples/clients/codex-config.toml +2 -0
- package/package.json +4 -2
- package/schemas/configuration.schema.json +72 -58
- 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 +43 -26
- 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 +102 -75
- 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 +49 -32
- 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 +160 -131
- package/schemas/probability-evaluate-input.schema.json +161 -132
- package/schemas/probability-inspect-input.schema.json +20 -11
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +110 -90
- package/schemas/probability-sequence-input.schema.json +154 -134
- package/schemas/probability-simulate-input.schema.json +161 -132
- package/schemas/probability-sweep-input.schema.json +163 -134
- package/schemas/reference-context-input.schema.json +42 -0
- package/schemas/reference-read-input.schema.json +41 -0
- package/schemas/reference-search-input.schema.json +45 -0
- package/schemas/scenario-suite.schema.json +92 -75
- package/schemas/scenario-test-input.schema.json +103 -86
- package/schemas/shared-scenario.schema.json +39 -22
- package/schemas/source-lookup-input.schema.json +61 -0
- package/server.json +2 -2
- package/docs/images/readme/chaos-meter-ingame.png +0 -0
- package/docs/images/readme/chaos-meter-mcp.png +0 -0
- package/docs/images/readme/communist-insurgency-ingame.png +0 -0
- package/docs/images/readme/communist-insurgency-mcp.png +0 -0
- package/docs/images/readme/event-chain-mcp.png +0 -0
- package/docs/images/readme/focus-tree-mcp.png +0 -0
- package/docs/images/readme/map-mcp.png +0 -0
- package/docs/images/readme/probability-mcp.png +0 -0
- package/docs/images/readme/settings-advanced-mcp.png +0 -0
- package/docs/images/readme/settings-events-mcp.png +0 -0
- package/docs/images/readme/technology-tree-mcp.png +0 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { open, realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import { compareCodeUnits, sha256Bytes } from '../core/canonical.js';
|
|
5
|
+
import { ServiceError } from '../core/result.js';
|
|
6
|
+
import { isWithin } from '../core/workspace.js';
|
|
7
|
+
import { readBoundedLines } from './lines.js';
|
|
8
|
+
import { readBoundedFile } from '../core/scanner.js';
|
|
9
|
+
const MAX_FILES = 512;
|
|
10
|
+
const MAX_FILE_BYTES = 2_000_000;
|
|
11
|
+
const MAX_TOTAL_BYTES = 32_000_000;
|
|
12
|
+
const MAX_SECTIONS = 20_000;
|
|
13
|
+
const searchStopWords = new Set([
|
|
14
|
+
'a',
|
|
15
|
+
'an',
|
|
16
|
+
'and',
|
|
17
|
+
'at',
|
|
18
|
+
'be',
|
|
19
|
+
'can',
|
|
20
|
+
'come',
|
|
21
|
+
'do',
|
|
22
|
+
'does',
|
|
23
|
+
'for',
|
|
24
|
+
'from',
|
|
25
|
+
'how',
|
|
26
|
+
'in',
|
|
27
|
+
'is',
|
|
28
|
+
'of',
|
|
29
|
+
'the',
|
|
30
|
+
'to',
|
|
31
|
+
'where',
|
|
32
|
+
'with',
|
|
33
|
+
'work',
|
|
34
|
+
]);
|
|
35
|
+
function normalizeTerm(term) {
|
|
36
|
+
if (term === 'dated')
|
|
37
|
+
return 'date';
|
|
38
|
+
if (term.endsWith('ies') && term.length > 5)
|
|
39
|
+
return `${term.slice(0, -3)}y`;
|
|
40
|
+
if (term.endsWith('ses') && term.length > 5)
|
|
41
|
+
return term.slice(0, -2);
|
|
42
|
+
if (term.endsWith('s') && term.length > 4)
|
|
43
|
+
return term.slice(0, -1);
|
|
44
|
+
return term;
|
|
45
|
+
}
|
|
46
|
+
function queryTerms(query) {
|
|
47
|
+
return [
|
|
48
|
+
...new Set((query.toLowerCase().match(/[a-z0-9_.:-]+/gu) ?? [])
|
|
49
|
+
.map(normalizeTerm)
|
|
50
|
+
.filter((term) => /[a-z0-9]/u.test(term) && !searchStopWords.has(term))),
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
function containsTerm(text, term) {
|
|
54
|
+
if (term.length > 2)
|
|
55
|
+
return text.includes(term);
|
|
56
|
+
const escaped = term.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
57
|
+
return new RegExp(`(^|[^a-z0-9_])${escaped}($|[^a-z0-9_])`, 'u').test(text);
|
|
58
|
+
}
|
|
59
|
+
function snippet(lines, terms = []) {
|
|
60
|
+
let matching = -1;
|
|
61
|
+
let best = -1;
|
|
62
|
+
for (const [index, line] of lines.entries()) {
|
|
63
|
+
const count = terms.filter((term) => containsTerm(line.toLowerCase(), term)).length;
|
|
64
|
+
if (count > best) {
|
|
65
|
+
matching = index;
|
|
66
|
+
best = count;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const selected = lines
|
|
70
|
+
.slice(Math.max(0, matching), Math.max(0, matching) + 3)
|
|
71
|
+
.map((line) => line.trim())
|
|
72
|
+
.filter(Boolean)
|
|
73
|
+
.join(' ');
|
|
74
|
+
return selected.slice(0, 300);
|
|
75
|
+
}
|
|
76
|
+
function sectionize(root, filePath, content, revision) {
|
|
77
|
+
const lines = content.replace(/^\uFEFF/u, '').split(/\r?\n/u);
|
|
78
|
+
const relative = path.relative(root.root, filePath).replaceAll('\\', '/');
|
|
79
|
+
const title = path
|
|
80
|
+
.basename(filePath)
|
|
81
|
+
.replace(/\.(md|log)$/u, '')
|
|
82
|
+
.replaceAll('_', ' ')
|
|
83
|
+
.replace(/ - Hearts of Iron 4 Wiki$/u, '');
|
|
84
|
+
const headings = [];
|
|
85
|
+
let inFence = false;
|
|
86
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
87
|
+
const line = lines[index];
|
|
88
|
+
if (/^\s*(```|~~~)/u.test(line)) {
|
|
89
|
+
inFence = !inFence;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (inFence)
|
|
93
|
+
continue;
|
|
94
|
+
const match = /^(#{1,6})\s+(.+?)\s*#*\s*$/u.exec(line);
|
|
95
|
+
if (match)
|
|
96
|
+
headings.push({ heading: match[2].replace(/<a\b[^>]*><\/a>/gu, '').trim(), start: index });
|
|
97
|
+
else if (root.kind === 'script_doc' &&
|
|
98
|
+
filePath.endsWith('.log') &&
|
|
99
|
+
/^[A-Za-z_][A-Za-z0-9_.:-]*$/u.test(line.trim()) &&
|
|
100
|
+
!/^\s/u.test(line) &&
|
|
101
|
+
/^\s+\S/u.test(lines[index + 1] ?? '')) {
|
|
102
|
+
headings.push({ heading: line.trim(), start: index });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (headings.length === 0 || headings[0].start > 0)
|
|
106
|
+
headings.unshift({ heading: title, start: 0 });
|
|
107
|
+
return headings.map((current, index) => {
|
|
108
|
+
const end = headings[index + 1]?.start ?? lines.length;
|
|
109
|
+
const sectionLines = lines.slice(current.start, end);
|
|
110
|
+
const id = sha256Bytes(Buffer.from(`${root.kind}\0${relative}\0${current.start + 1}\0${current.heading}`, 'utf8'));
|
|
111
|
+
return {
|
|
112
|
+
id,
|
|
113
|
+
revision,
|
|
114
|
+
source: root.kind,
|
|
115
|
+
title,
|
|
116
|
+
heading: current.heading.slice(0, 512),
|
|
117
|
+
path: filePath,
|
|
118
|
+
startLine: current.start + 1,
|
|
119
|
+
endLine: end,
|
|
120
|
+
excerpt: snippet(sectionLines),
|
|
121
|
+
authority: root.authority,
|
|
122
|
+
lines: sectionLines,
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function sourceRoots(workspace) {
|
|
127
|
+
const gameRoot = workspace.gameRoot ?? (workspace.registration.kind === 'game' ? workspace.modRoot : undefined);
|
|
128
|
+
return [
|
|
129
|
+
...(gameRoot === undefined
|
|
130
|
+
? []
|
|
131
|
+
: [
|
|
132
|
+
{
|
|
133
|
+
kind: 'game_doc',
|
|
134
|
+
root: gameRoot,
|
|
135
|
+
patterns: ['documentation/**/*.md', 'common/**/*documentation.md'],
|
|
136
|
+
authority: 'Installed game documentation',
|
|
137
|
+
},
|
|
138
|
+
]),
|
|
139
|
+
{
|
|
140
|
+
kind: 'wiki',
|
|
141
|
+
root: workspace.wikiRoot ?? path.join(workspace.modRoot, 'paradox_wiki'),
|
|
142
|
+
patterns: ['**/*.md'],
|
|
143
|
+
authority: 'Offline Paradox Wiki snapshot',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
kind: 'script_doc',
|
|
147
|
+
root: workspace.scriptDocsRoot ?? path.join(workspace.modRoot, 'script_docs'),
|
|
148
|
+
patterns: ['**/*.md', '**/*.log'],
|
|
149
|
+
authority: 'User-provided game script documentation',
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
const surfaceNames = {
|
|
154
|
+
general: ['Data structures', 'Triggers', 'Effects', 'Modifiers', 'Scopes'],
|
|
155
|
+
event: [
|
|
156
|
+
'Event modding',
|
|
157
|
+
'Data structures',
|
|
158
|
+
'Effects',
|
|
159
|
+
'Triggers',
|
|
160
|
+
'Scopes',
|
|
161
|
+
'On actions',
|
|
162
|
+
'Localisation',
|
|
163
|
+
],
|
|
164
|
+
decision: ['Decision modding', 'Effects', 'Triggers', 'Scopes', 'Localisation'],
|
|
165
|
+
idea: ['Idea modding', 'Modifiers', 'Localisation'],
|
|
166
|
+
focus: ['National focus modding', 'AI focuses', 'Effects', 'Triggers', 'Localisation'],
|
|
167
|
+
technology: ['Technology modding', 'Equipment modding', 'Modifiers', 'Localisation'],
|
|
168
|
+
gui: [
|
|
169
|
+
'Interface modding',
|
|
170
|
+
'Scripted GUI modding',
|
|
171
|
+
'Graphical asset modding',
|
|
172
|
+
'Localisation',
|
|
173
|
+
'Scopes',
|
|
174
|
+
],
|
|
175
|
+
map: [
|
|
176
|
+
'Map modding',
|
|
177
|
+
'State modding',
|
|
178
|
+
'Strategic region modding',
|
|
179
|
+
'Bookmark modding',
|
|
180
|
+
'Localisation',
|
|
181
|
+
],
|
|
182
|
+
localisation: ['Localisation', 'Data structures'],
|
|
183
|
+
ai: ['AI modding', 'AI focuses', 'Triggers', 'Scopes'],
|
|
184
|
+
};
|
|
185
|
+
const gameDocNames = {
|
|
186
|
+
general: ['documentation/script_concept_documentation'],
|
|
187
|
+
event: [
|
|
188
|
+
'documentation/effects_documentation',
|
|
189
|
+
'documentation/triggers_documentation',
|
|
190
|
+
'common/on_actions/_documentation',
|
|
191
|
+
],
|
|
192
|
+
decision: ['common/decisions/_documentation', 'documentation/effects_documentation'],
|
|
193
|
+
idea: ['documentation/modifiers_documentation'],
|
|
194
|
+
focus: [
|
|
195
|
+
'documentation/effects_documentation',
|
|
196
|
+
'documentation/triggers_documentation',
|
|
197
|
+
'common/focus_inlay_windows/documentation',
|
|
198
|
+
],
|
|
199
|
+
technology: ['common/units/equipment/_documentation', 'documentation/modifiers_documentation'],
|
|
200
|
+
gui: ['common/scripted_guis/_documentation'],
|
|
201
|
+
map: ['common/map_modes/documentation', 'common/strategic_locations/documentation'],
|
|
202
|
+
localisation: [
|
|
203
|
+
'documentation/loc_formatter_documentation',
|
|
204
|
+
'documentation/loc_objects_documentation',
|
|
205
|
+
],
|
|
206
|
+
ai: ['common/ai_strategy/_documentation', 'documentation/triggers_documentation'],
|
|
207
|
+
};
|
|
208
|
+
/** A workspace-private, read-only reference index over explicitly known local roots. */
|
|
209
|
+
export class ReferenceService {
|
|
210
|
+
#cache = new Map();
|
|
211
|
+
#cacheBytes = 0;
|
|
212
|
+
retain(key, value) {
|
|
213
|
+
this.#cacheBytes -= this.#cache.get(key)?.bytes ?? 0;
|
|
214
|
+
this.#cache.delete(key);
|
|
215
|
+
this.#cache.set(key, value);
|
|
216
|
+
this.#cacheBytes += value.bytes;
|
|
217
|
+
while (this.#cacheBytes > MAX_TOTAL_BYTES || this.#cache.size > 1_024) {
|
|
218
|
+
const oldest = this.#cache.entries().next().value;
|
|
219
|
+
if (oldest === undefined)
|
|
220
|
+
break;
|
|
221
|
+
this.#cache.delete(oldest[0]);
|
|
222
|
+
this.#cacheBytes -= oldest[1].bytes;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async inventory(workspace, signal) {
|
|
226
|
+
const sections = [];
|
|
227
|
+
const coverage = {
|
|
228
|
+
game_doc: { files: 0, sections: 0, unavailable: true },
|
|
229
|
+
wiki: { files: 0, sections: 0, unavailable: true },
|
|
230
|
+
script_doc: { files: 0, sections: 0, unavailable: true },
|
|
231
|
+
};
|
|
232
|
+
let skipped = 0;
|
|
233
|
+
let bytes = 0;
|
|
234
|
+
for (const root of sourceRoots(workspace)) {
|
|
235
|
+
signal?.throwIfAborted();
|
|
236
|
+
let canonicalRoot;
|
|
237
|
+
try {
|
|
238
|
+
canonicalRoot = await realpath(root.root);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
coverage[root.kind].unavailable = true;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const configuredAuthorityRoot = root.kind === 'game_doc'
|
|
245
|
+
? root.root
|
|
246
|
+
: root.kind === 'wiki'
|
|
247
|
+
? (workspace.wikiRoot ?? workspace.modRoot)
|
|
248
|
+
: (workspace.scriptDocsRoot ?? workspace.modRoot);
|
|
249
|
+
if (!isWithin(configuredAuthorityRoot, canonicalRoot)) {
|
|
250
|
+
throw new ServiceError('REFERENCE_ROOT_ESCAPE', 'Reference root resolves outside its configured workspace');
|
|
251
|
+
}
|
|
252
|
+
coverage[root.kind].unavailable = false;
|
|
253
|
+
const names = await fg(root.patterns, {
|
|
254
|
+
cwd: canonicalRoot,
|
|
255
|
+
onlyFiles: true,
|
|
256
|
+
followSymbolicLinks: false,
|
|
257
|
+
dot: false,
|
|
258
|
+
});
|
|
259
|
+
names.sort(compareCodeUnits);
|
|
260
|
+
for (const name of names) {
|
|
261
|
+
signal?.throwIfAborted();
|
|
262
|
+
if (coverage[root.kind].files >= MAX_FILES || sections.length >= MAX_SECTIONS) {
|
|
263
|
+
skipped++;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const candidate = path.join(canonicalRoot, name);
|
|
267
|
+
const canonical = await realpath(candidate);
|
|
268
|
+
if (!isWithin(canonicalRoot, canonical)) {
|
|
269
|
+
skipped++;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const metadata = await stat(canonical);
|
|
273
|
+
if (metadata.size > MAX_FILE_BYTES || bytes + metadata.size > MAX_TOTAL_BYTES) {
|
|
274
|
+
skipped++;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
bytes += metadata.size;
|
|
278
|
+
coverage[root.kind].files++;
|
|
279
|
+
const key = `${root.kind}:${canonicalRoot}:${canonical}`;
|
|
280
|
+
const handle = await open(canonical, 'r');
|
|
281
|
+
let content;
|
|
282
|
+
try {
|
|
283
|
+
const opened = await handle.stat();
|
|
284
|
+
const resolvedAfterOpen = await realpath(canonical);
|
|
285
|
+
const current = await stat(resolvedAfterOpen);
|
|
286
|
+
if (!opened.isFile() ||
|
|
287
|
+
!isWithin(canonicalRoot, resolvedAfterOpen) ||
|
|
288
|
+
opened.dev !== current.dev ||
|
|
289
|
+
opened.ino !== current.ino) {
|
|
290
|
+
throw new ServiceError('REFERENCE_SOURCE_CHANGED', 'Reference path changed while opening it; retry the request');
|
|
291
|
+
}
|
|
292
|
+
content = await readBoundedFile(handle, Math.min(MAX_FILE_BYTES, MAX_TOTAL_BYTES - bytes + metadata.size), signal);
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
await handle.close();
|
|
296
|
+
}
|
|
297
|
+
bytes += content.length - metadata.size;
|
|
298
|
+
const revision = sha256Bytes(content);
|
|
299
|
+
let cached = this.#cache.get(key);
|
|
300
|
+
if (cached?.revision !== revision) {
|
|
301
|
+
cached = {
|
|
302
|
+
revision,
|
|
303
|
+
bytes: content.length,
|
|
304
|
+
sections: sectionize(root, canonical, content.toString('utf8'), revision),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
this.retain(key, cached);
|
|
308
|
+
const admitted = cached.sections.slice(0, MAX_SECTIONS - sections.length);
|
|
309
|
+
if (admitted.length < cached.sections.length)
|
|
310
|
+
skipped++;
|
|
311
|
+
sections.push(...admitted);
|
|
312
|
+
coverage[root.kind].sections += admitted.length;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return { sections, coverage, skipped };
|
|
316
|
+
}
|
|
317
|
+
async search(workspace, input, signal) {
|
|
318
|
+
const inventory = await this.inventory(workspace, signal);
|
|
319
|
+
const matches = this.rank(inventory.sections, input.query, input.sources);
|
|
320
|
+
return {
|
|
321
|
+
results: matches.slice(0, input.limit).map(({ section, score, excerpt }) => ({
|
|
322
|
+
...this.publicSection(section),
|
|
323
|
+
score,
|
|
324
|
+
excerpt,
|
|
325
|
+
})),
|
|
326
|
+
total: matches.length,
|
|
327
|
+
coverage: inventory.coverage,
|
|
328
|
+
skipped: inventory.skipped,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
rank(sections, question, sources) {
|
|
332
|
+
const query = question.toLowerCase();
|
|
333
|
+
const terms = queryTerms(query);
|
|
334
|
+
return sections
|
|
335
|
+
.flatMap((section) => {
|
|
336
|
+
if (sources !== undefined && !sources.includes(section.source))
|
|
337
|
+
return [];
|
|
338
|
+
const title = section.title.toLowerCase();
|
|
339
|
+
const heading = section.heading.toLowerCase();
|
|
340
|
+
const body = section.lines.join('\n').toLowerCase();
|
|
341
|
+
let matched = 0;
|
|
342
|
+
let score = 0;
|
|
343
|
+
for (const term of terms) {
|
|
344
|
+
const inTitle = containsTerm(title, term);
|
|
345
|
+
const inHeading = containsTerm(heading, term);
|
|
346
|
+
const inBody = containsTerm(body, term);
|
|
347
|
+
if (!inTitle && !inHeading && !inBody)
|
|
348
|
+
continue;
|
|
349
|
+
matched++;
|
|
350
|
+
score += (inTitle ? 24 : 0) + (inHeading ? 18 : 0) + (inBody ? 3 : 0);
|
|
351
|
+
}
|
|
352
|
+
if (matched < Math.max(1, Math.ceil(terms.length * 0.6)))
|
|
353
|
+
return [];
|
|
354
|
+
if (heading === query)
|
|
355
|
+
score += 100;
|
|
356
|
+
else if (heading.includes(query))
|
|
357
|
+
score += 70;
|
|
358
|
+
if (body.includes(query))
|
|
359
|
+
score += 30;
|
|
360
|
+
return [{ section, score, excerpt: snippet(section.lines, terms) }];
|
|
361
|
+
})
|
|
362
|
+
.sort((left, right) => right.score - left.score ||
|
|
363
|
+
compareCodeUnits(left.section.path, right.section.path) ||
|
|
364
|
+
left.section.startLine - right.section.startLine);
|
|
365
|
+
}
|
|
366
|
+
async read(workspace, input, signal) {
|
|
367
|
+
const inventory = await this.inventory(workspace, signal);
|
|
368
|
+
const section = inventory.sections.find(({ id }) => id === input.id);
|
|
369
|
+
if (section === undefined)
|
|
370
|
+
throw new ServiceError('REFERENCE_SECTION_UNKNOWN', 'Reference section is not in this workspace');
|
|
371
|
+
if (section.revision !== input.revision)
|
|
372
|
+
throw new ServiceError('REFERENCE_REVISION_STALE', 'Reference source changed; search again', {
|
|
373
|
+
currentRevision: section.revision,
|
|
374
|
+
});
|
|
375
|
+
const startLine = input.startLine ?? section.startLine;
|
|
376
|
+
const selected = readBoundedLines(section.lines, section.startLine, startLine, input.startColumn ?? 1, input.maxLines, 8_000);
|
|
377
|
+
return {
|
|
378
|
+
...this.publicSection(section),
|
|
379
|
+
sectionStartLine: section.startLine,
|
|
380
|
+
sectionEndLine: section.endLine,
|
|
381
|
+
startLine,
|
|
382
|
+
startColumn: input.startColumn ?? 1,
|
|
383
|
+
...selected,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
async context(workspace, input, signal) {
|
|
387
|
+
const inventory = await this.inventory(workspace, signal);
|
|
388
|
+
const wanted = surfaceNames[input.surface];
|
|
389
|
+
const selected = [];
|
|
390
|
+
const missing = [];
|
|
391
|
+
for (const name of [...new Set(wanted)]) {
|
|
392
|
+
const section = inventory.sections.find((entry) => entry.source === 'wiki' && entry.title.toLowerCase() === name.toLowerCase());
|
|
393
|
+
if (section === undefined)
|
|
394
|
+
missing.push(name);
|
|
395
|
+
else
|
|
396
|
+
selected.push(section);
|
|
397
|
+
}
|
|
398
|
+
for (const name of gameDocNames[input.surface]) {
|
|
399
|
+
const section = inventory.sections.find((entry) => entry.source === 'game_doc' &&
|
|
400
|
+
entry.path.replaceAll('\\', '/').toLowerCase().endsWith(`${name}.md`));
|
|
401
|
+
if (section !== undefined)
|
|
402
|
+
selected.push(section);
|
|
403
|
+
else
|
|
404
|
+
missing.push(name);
|
|
405
|
+
}
|
|
406
|
+
const ranked = input.question === undefined ? [] : this.rank(inventory.sections, input.question).slice(0, 5);
|
|
407
|
+
const prioritized = [
|
|
408
|
+
...ranked.map(({ section }) => section),
|
|
409
|
+
...selected.filter((section) => !ranked.some((entry) => entry.section.id === section.id)),
|
|
410
|
+
];
|
|
411
|
+
return {
|
|
412
|
+
surface: input.surface,
|
|
413
|
+
sections: prioritized.slice(0, input.limit).map((section) => ({
|
|
414
|
+
...this.publicSection(section),
|
|
415
|
+
excerpt: ranked.find((entry) => entry.section.id === section.id)?.excerpt ?? section.excerpt,
|
|
416
|
+
})),
|
|
417
|
+
omitted: Math.max(0, prioritized.length - input.limit),
|
|
418
|
+
missing,
|
|
419
|
+
coverage: inventory.coverage,
|
|
420
|
+
skipped: inventory.skipped,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
publicSection(section) {
|
|
424
|
+
const { lines: _lines, ...publicSection } = section;
|
|
425
|
+
return publicSection;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ServiceError } from '../core/result.js';
|
|
2
|
+
import { decodeSource } from '../core/source/encoding.js';
|
|
3
|
+
import { readBoundedLines } from './lines.js';
|
|
4
|
+
import { scanImpactSemanticReferences } from '../core/impact-semantic.js';
|
|
5
|
+
/** Exact source definitions and usage over the existing shared Clausewitz index. */
|
|
6
|
+
export async function sourceLookup(engine, workspaceId, input, principal, signal) {
|
|
7
|
+
const snapshot = await engine.scan(workspaceId, {}, principal, signal);
|
|
8
|
+
if (input.expectedRevision !== undefined && input.expectedRevision !== snapshot.revision) {
|
|
9
|
+
throw new ServiceError('SOURCE_REVISION_STALE', 'Indexed source changed; look up the symbol again', { currentRevision: snapshot.revision });
|
|
10
|
+
}
|
|
11
|
+
const definitions = snapshot.index.symbols
|
|
12
|
+
.filter((entry) => entry.id === input.symbol && (input.kind === undefined || entry.kind === input.kind))
|
|
13
|
+
.sort((left, right) => Number(left.overridden) - Number(right.overridden) || right.loadOrder - left.loadOrder)
|
|
14
|
+
.slice(0, input.maxDefinitions)
|
|
15
|
+
.map((entry, index) => {
|
|
16
|
+
const source = snapshot.index.files.get(entry.path);
|
|
17
|
+
const start = entry.location?.start.line;
|
|
18
|
+
const end = entry.location?.end.line;
|
|
19
|
+
const fromLine = index === 0 ? (input.fromLine ?? start) : start;
|
|
20
|
+
if (fromLine !== undefined &&
|
|
21
|
+
start !== undefined &&
|
|
22
|
+
end !== undefined &&
|
|
23
|
+
(fromLine < start || fromLine > end)) {
|
|
24
|
+
throw new ServiceError('SOURCE_LINE_OUTSIDE_DEFINITION', 'Requested line is outside a matched definition');
|
|
25
|
+
}
|
|
26
|
+
const lines = source === undefined || fromLine === undefined
|
|
27
|
+
? []
|
|
28
|
+
: decodeSource(source.bytes)
|
|
29
|
+
.text.split(/\r?\n/u)
|
|
30
|
+
.slice((start ?? fromLine) - 1, end);
|
|
31
|
+
const selected = fromLine === undefined || lines.length === 0
|
|
32
|
+
? null
|
|
33
|
+
: readBoundedLines(lines, start ?? fromLine, fromLine, index === 0 ? (input.fromColumn ?? 1) : 1, input.maxLines, 2_000);
|
|
34
|
+
return {
|
|
35
|
+
kind: entry.kind,
|
|
36
|
+
id: entry.id,
|
|
37
|
+
path: entry.path,
|
|
38
|
+
rootKind: entry.rootKind,
|
|
39
|
+
loadOrder: entry.loadOrder,
|
|
40
|
+
overridden: entry.overridden,
|
|
41
|
+
sourceShadowed: entry.sourceShadowed,
|
|
42
|
+
startLine: start ?? null,
|
|
43
|
+
endLine: end ?? null,
|
|
44
|
+
fromLine: fromLine ?? null,
|
|
45
|
+
toLine: selected?.endLine ?? null,
|
|
46
|
+
nextLine: selected?.nextLine ?? null,
|
|
47
|
+
nextColumn: selected?.nextColumn ?? null,
|
|
48
|
+
text: selected?.text ?? '',
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
const semantic = input.includeReferences
|
|
52
|
+
? scanImpactSemanticReferences(snapshot, 200_000, { includeOnActions: true })
|
|
53
|
+
: undefined;
|
|
54
|
+
const referenceIdentities = new Set();
|
|
55
|
+
const matchedReferences = input.includeReferences
|
|
56
|
+
? [...snapshot.index.references, ...(semantic?.references ?? [])]
|
|
57
|
+
.filter((entry) => entry.to === input.symbol && (input.kind === undefined || entry.toKind === input.kind))
|
|
58
|
+
.filter((entry) => {
|
|
59
|
+
const key = `${entry.path}:${entry.location?.start.offset ?? 0}:${entry.toKind}:${entry.to}:${entry.kind}`;
|
|
60
|
+
if (referenceIdentities.has(key))
|
|
61
|
+
return false;
|
|
62
|
+
referenceIdentities.add(key);
|
|
63
|
+
return true;
|
|
64
|
+
})
|
|
65
|
+
: [];
|
|
66
|
+
const references = matchedReferences.slice(0, input.maxReferences).map((entry) => ({
|
|
67
|
+
kind: entry.kind,
|
|
68
|
+
from: entry.from,
|
|
69
|
+
toKind: entry.toKind,
|
|
70
|
+
path: entry.path,
|
|
71
|
+
line: entry.location?.start.line ?? null,
|
|
72
|
+
}));
|
|
73
|
+
return {
|
|
74
|
+
revision: snapshot.revision,
|
|
75
|
+
complete: snapshot.complete,
|
|
76
|
+
skippedSourceCount: snapshot.skippedSourceCount,
|
|
77
|
+
definitionCount: snapshot.index.symbols.filter((entry) => entry.id === input.symbol && (input.kind === undefined || entry.kind === input.kind)).length,
|
|
78
|
+
definitions,
|
|
79
|
+
references,
|
|
80
|
+
referencesIncluded: input.includeReferences,
|
|
81
|
+
referenceCount: matchedReferences.length,
|
|
82
|
+
referencesTruncated: matchedReferences.length > references.length,
|
|
83
|
+
referencesComplete: input.includeReferences &&
|
|
84
|
+
snapshot.complete &&
|
|
85
|
+
semantic?.complete === true &&
|
|
86
|
+
semantic.unresolved.length === 0 &&
|
|
87
|
+
matchedReferences.length <= references.length,
|
|
88
|
+
unresolvedReferenceCount: semantic?.unresolved.length ?? 0,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod/v4';
|
|
2
2
|
import { workspaceIdSchema, workspaceRelativePathSchema } from './common.js';
|
|
3
|
+
import { ConditionControlMapSchema } from '../core/condition-schema.js';
|
|
3
4
|
const symbolKindSchema = z.enum([
|
|
4
5
|
'focus_tree',
|
|
5
6
|
'focus',
|
|
@@ -111,6 +112,7 @@ export const declaredConditionScenarioSchema = z
|
|
|
111
112
|
id: z.string().min(1).max(512),
|
|
112
113
|
actor: z.string().max(256).optional(),
|
|
113
114
|
date: z.string().max(64).optional(),
|
|
115
|
+
controls: ConditionControlMapSchema.optional(),
|
|
114
116
|
state: scenarioStateSchema,
|
|
115
117
|
flags: z.array(z.string().min(1).max(512)).max(10_000).optional(),
|
|
116
118
|
eventTargets: z.record(z.string().max(512), z.string().max(512)).optional(),
|
|
@@ -8,7 +8,7 @@ import { backgroundRequestKeySchema, workspaceIdSchema, workspaceRelativePathSch
|
|
|
8
8
|
function compact(schema, description) {
|
|
9
9
|
return z.unknown().describe(description).pipe(schema);
|
|
10
10
|
}
|
|
11
|
-
const compactGuiScenarioSchema = compact(GuiPreviewScenarioSchema, 'GUI preview scenario.');
|
|
11
|
+
const compactGuiScenarioSchema = compact(GuiPreviewScenarioSchema, 'GUI preview scenario; optional date and controls {stateId: controllerTag}.');
|
|
12
12
|
const compactGeneratedScenarioOptionsSchema = compact(GuiGeneratedScenarioOptionsSchema, 'GUI generated-scenario options.');
|
|
13
13
|
const compactGuiHelperSchema = compact(GuiHelperDocumentSchema, 'GUI helper document.');
|
|
14
14
|
export const guiPreviewStateSchema = z.enum([
|
|
@@ -4,7 +4,7 @@ import { workspaceIdSchema } from './common.js';
|
|
|
4
4
|
function compact(schema, description) {
|
|
5
5
|
return z.unknown().describe(description).pipe(schema);
|
|
6
6
|
}
|
|
7
|
-
const nestedSource = compact(probabilitySourceSchema, 'Source selector
|
|
7
|
+
const nestedSource = compact(probabilitySourceSchema, 'Source selector; snapshotPath binds frozen bytes to path with expectedSourceHash.');
|
|
8
8
|
const nestedScenarios = compact(probabilityScenarioSetSchema, 'Explicit world-state scenarios.');
|
|
9
9
|
const nestedManifest = compact(customWeightedPoolManifestSchema, 'Declared custom weighted pool and state transitions.');
|
|
10
10
|
const nestedRenderFilter = compact(probabilityRenderFilterSchema, 'Optional scenario, candidate, metric, diagnostic, source, and page filter.');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod/v4';
|
|
2
|
-
import { diagnosticSchema, sourceLocationSchema, workspaceIdSchema } from './common.js';
|
|
2
|
+
import { diagnosticSchema, sourceLocationSchema, workspaceIdSchema, workspaceRelativePathSchema, } from './common.js';
|
|
3
3
|
import { artifactLinkSchema } from './transaction.js';
|
|
4
|
+
import { ConditionControlMapSchema } from '../core/condition-schema.js';
|
|
4
5
|
export const probabilityAdapterIdSchema = z.enum([
|
|
5
6
|
'event_mean_time_to_happen',
|
|
6
7
|
'event_option_ai_chance',
|
|
@@ -145,6 +146,7 @@ export const probabilityScenarioSchema = z
|
|
|
145
146
|
prevalence: z.number().min(0).max(1).optional(),
|
|
146
147
|
actor: z.string().max(256).optional(),
|
|
147
148
|
date: z.string().max(64).optional(),
|
|
149
|
+
controls: ConditionControlMapSchema.optional(),
|
|
148
150
|
state: z.record(z.string().min(1).max(1024), scenarioValueSchema),
|
|
149
151
|
flags: z.array(z.string().min(1).max(512)).max(100_000).optional(),
|
|
150
152
|
eventTargets: z.record(z.string().max(512), z.string().max(512)).optional(),
|
|
@@ -257,10 +259,12 @@ export const probabilityDiagnosticThresholdsSchema = z
|
|
|
257
259
|
})
|
|
258
260
|
.strict();
|
|
259
261
|
export const probabilitySamplingMethodSchema = z.enum(['latin_hypercube', 'pseudo_random']);
|
|
262
|
+
const probabilitySourcePathSchema = workspaceRelativePathSchema.refine((value) => !/[:*?[\]{}!]/u.test(value), 'Source path must be an exact workspace-relative file path');
|
|
260
263
|
export const probabilitySourceSchema = z
|
|
261
264
|
.object({
|
|
262
265
|
identifier: z.string().min(1).max(1024).optional(),
|
|
263
|
-
path:
|
|
266
|
+
path: probabilitySourcePathSchema.optional(),
|
|
267
|
+
snapshotPath: probabilitySourcePathSchema.optional(),
|
|
264
268
|
line: z.number().int().min(1).optional(),
|
|
265
269
|
inlineClausewitz: z.string().max(16_777_216).optional(),
|
|
266
270
|
virtualPatch: z.string().max(16_777_216).optional(),
|
|
@@ -270,6 +274,26 @@ export const probabilitySourceSchema = z
|
|
|
270
274
|
.optional(),
|
|
271
275
|
})
|
|
272
276
|
.strict()
|
|
277
|
+
.superRefine((value, context) => {
|
|
278
|
+
if (value.snapshotPath === undefined)
|
|
279
|
+
return;
|
|
280
|
+
if (value.path === undefined || value.expectedSourceHash === undefined)
|
|
281
|
+
context.addIssue({
|
|
282
|
+
code: 'custom',
|
|
283
|
+
message: 'snapshotPath requires the logical source path and expectedSourceHash of the frozen bytes',
|
|
284
|
+
});
|
|
285
|
+
if (value.inlineClausewitz !== undefined || value.virtualPatch !== undefined)
|
|
286
|
+
context.addIssue({
|
|
287
|
+
code: 'custom',
|
|
288
|
+
message: 'snapshotPath cannot be combined with inline source or a virtual patch',
|
|
289
|
+
});
|
|
290
|
+
if (value.path !== undefined && !workspaceRelativePathSchema.safeParse(value.path).success)
|
|
291
|
+
context.addIssue({
|
|
292
|
+
code: 'custom',
|
|
293
|
+
path: ['path'],
|
|
294
|
+
message: 'Snapshot logical source path must be workspace-relative',
|
|
295
|
+
});
|
|
296
|
+
})
|
|
273
297
|
.refine((value) => value.identifier !== undefined ||
|
|
274
298
|
value.path !== undefined ||
|
|
275
299
|
value.inlineClausewitz !== undefined ||
|