hoi4-agent-tools 2.1.1 → 2.3.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 +24 -0
- package/README.md +32 -18
- package/dist/bin/stdio.js +2 -1
- package/dist/hoi4_agent_tools/core/clausewitz-evaluation.js +90 -0
- package/dist/hoi4_agent_tools/core/engine.js +14 -0
- package/dist/hoi4_agent_tools/core/index.js +349 -2
- package/dist/hoi4_agent_tools/core/source/ast-path.js +41 -0
- package/dist/hoi4_agent_tools/core/source/index.js +1 -0
- package/dist/hoi4_agent_tools/event/limits.js +1 -1
- package/dist/hoi4_agent_tools/focus/render.js +37 -13
- package/dist/hoi4_agent_tools/mcp/server/create.js +5 -1
- package/dist/hoi4_agent_tools/mcp/server/result.js +4 -4
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +39 -2
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +413 -0
- package/dist/hoi4_agent_tools/mcp/tools/technology.js +368 -0
- package/dist/hoi4_agent_tools/probability/adapters.js +344 -0
- package/dist/hoi4_agent_tools/probability/compare.js +178 -0
- package/dist/hoi4_agent_tools/probability/evaluation.js +649 -0
- package/dist/hoi4_agent_tools/probability/index.js +13 -0
- package/dist/hoi4_agent_tools/probability/model.js +5 -0
- package/dist/hoi4_agent_tools/probability/rational.js +145 -0
- package/dist/hoi4_agent_tools/probability/render.js +307 -0
- package/dist/hoi4_agent_tools/probability/sequence.js +858 -0
- package/dist/hoi4_agent_tools/probability/service.js +1168 -0
- package/dist/hoi4_agent_tools/probability/simulation.js +545 -0
- package/dist/hoi4_agent_tools/probability/source-analysis.js +458 -0
- package/dist/hoi4_agent_tools/probability/sweep.js +265 -0
- package/dist/hoi4_agent_tools/probability/trigger-evaluator.js +280 -0
- package/dist/hoi4_agent_tools/probability/value-evaluator.js +357 -0
- package/dist/hoi4_agent_tools/schemas/probability.js +930 -0
- package/dist/hoi4_agent_tools/schemas/technology.js +77 -0
- package/dist/hoi4_agent_tools/technology/compare.js +240 -0
- package/dist/hoi4_agent_tools/technology/graph.js +1196 -0
- package/dist/hoi4_agent_tools/technology/index.js +7 -0
- package/dist/hoi4_agent_tools/technology/model.js +2 -0
- package/dist/hoi4_agent_tools/technology/queries.js +300 -0
- package/dist/hoi4_agent_tools/technology/render.js +641 -0
- package/dist/hoi4_agent_tools/technology/service.js +686 -0
- package/dist/hoi4_agent_tools/technology/source-analysis.js +967 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +2 -0
- package/docs/development.md +7 -1
- package/docs/probability.md +80 -0
- package/docs/research/probability-adapter-evidence.md +64 -0
- package/docs/setup.md +1 -1
- package/docs/technology.md +90 -0
- package/examples/clients/codex-config.toml +3 -0
- package/examples/clients/generic.json +8 -0
- package/examples/probability/adaptive-event-pool.json +92 -0
- package/examples/probability/adaptive-event-sequence-input.json +57 -0
- package/examples/probability/event-mtth-scenarios.json +57 -0
- package/examples/probability/event-mtth-simulate-input.json +37 -0
- package/examples/probability/focus-route-scenarios.json +61 -0
- package/examples/probability/focus-route-sweep-input.json +36 -0
- package/package.json +26 -8
- package/schemas/configuration.schema.json +747 -0
- package/schemas/continuous-focus-palette.schema.json +325 -0
- package/schemas/custom-weighted-pool.schema.json +298 -0
- package/schemas/event-feature-manifest.schema.json +63 -0
- package/schemas/focus-plan.schema.json +927 -0
- package/schemas/focus-planning-sidecar.schema.json +407 -0
- package/schemas/gui-animation-source.schema.json +176 -0
- package/schemas/gui-helper.schema.json +301 -0
- package/schemas/gui-scenario.schema.json +298 -0
- package/schemas/map-operation.schema.json +2284 -0
- package/schemas/operation-result.schema.json +327 -0
- package/schemas/probability-analysis-result.schema.json +2211 -0
- package/schemas/probability-compare-input.schema.json +664 -0
- package/schemas/probability-evaluate-input.schema.json +665 -0
- package/schemas/probability-inspect-input.schema.json +97 -0
- package/schemas/probability-render-input.schema.json +141 -0
- package/schemas/probability-scenario-set.schema.json +430 -0
- package/schemas/probability-sequence-input.schema.json +886 -0
- package/schemas/probability-simulate-input.schema.json +691 -0
- package/schemas/probability-sweep-input.schema.json +702 -0
- package/server.json +3 -3
|
@@ -0,0 +1,1196 @@
|
|
|
1
|
+
import { canonicalJson, compareCodeUnits, deterministicId, hashCanonical, } from '../core/canonical.js';
|
|
2
|
+
import { sortDiagnostics } from '../core/diagnostics.js';
|
|
3
|
+
import { ServiceError } from '../core/result.js';
|
|
4
|
+
import { analyzeTechnologySource, technologyAnalysisDiagnostics, technologySourceFiles, technologySourceFragmentCacheKey, } from './source-analysis.js';
|
|
5
|
+
import { TECHNOLOGY_GRAPH_SCHEMA_VERSION, TECHNOLOGY_PARSER_VERSION, } from './model.js';
|
|
6
|
+
const MAX_TECHNOLOGIES = 100_000;
|
|
7
|
+
const MAX_EDGES = 500_000;
|
|
8
|
+
const MAX_EXTERNAL_REFERENCES = 1_000_000;
|
|
9
|
+
const MAX_ISSUES = 100_000;
|
|
10
|
+
const MAX_HELPER_DEPTH = 64;
|
|
11
|
+
const MAX_HELPER_PROJECTIONS = 500_000;
|
|
12
|
+
const confidenceRank = {
|
|
13
|
+
confirmed: 0,
|
|
14
|
+
high: 1,
|
|
15
|
+
medium: 2,
|
|
16
|
+
low: 3,
|
|
17
|
+
unresolved: 4,
|
|
18
|
+
};
|
|
19
|
+
function worseConfidence(left, right) {
|
|
20
|
+
return confidenceRank[left] >= confidenceRank[right] ? left : right;
|
|
21
|
+
}
|
|
22
|
+
function sourceKey(source) {
|
|
23
|
+
return `${source.path}:${source.location.start.offset}`;
|
|
24
|
+
}
|
|
25
|
+
function activeDefinitions(values) {
|
|
26
|
+
const groups = new Map();
|
|
27
|
+
for (const value of values) {
|
|
28
|
+
const group = groups.get(value.id) ?? [];
|
|
29
|
+
group.push(value);
|
|
30
|
+
groups.set(value.id, group);
|
|
31
|
+
}
|
|
32
|
+
const active = [];
|
|
33
|
+
const duplicates = [];
|
|
34
|
+
for (const group of groups.values()) {
|
|
35
|
+
group.sort((left, right) => right.source.loadOrder - left.source.loadOrder ||
|
|
36
|
+
compareCodeUnits(sourceKey(left.source), sourceKey(right.source)));
|
|
37
|
+
const maximum = group[0].source.loadOrder;
|
|
38
|
+
const sameLevel = group.filter(({ source }) => source.loadOrder === maximum);
|
|
39
|
+
active.push(sameLevel[0]);
|
|
40
|
+
if (sameLevel.length > 1)
|
|
41
|
+
duplicates.push(sameLevel);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
active: active.sort((left, right) => compareCodeUnits(left.id, right.id)),
|
|
45
|
+
duplicates: duplicates.sort((left, right) => compareCodeUnits(left[0].id, right[0].id)),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function activeCategories(values) {
|
|
49
|
+
return activeDefinitions(values).active;
|
|
50
|
+
}
|
|
51
|
+
function normalizedAssetPath(value) {
|
|
52
|
+
return value
|
|
53
|
+
.replaceAll('\\', '/')
|
|
54
|
+
.replace(/\/{2,}/gu, '/')
|
|
55
|
+
.replace(/^\.\//u, '')
|
|
56
|
+
.toLowerCase();
|
|
57
|
+
}
|
|
58
|
+
function assetMap(files) {
|
|
59
|
+
const result = new Map();
|
|
60
|
+
for (const file of [...files].sort((left, right) => left.loadOrder - right.loadOrder || compareCodeUnits(left.displayPath, right.displayPath))) {
|
|
61
|
+
if (file.shadowedBy === undefined)
|
|
62
|
+
result.set(normalizedAssetPath(file.relativePath), file);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
function iconStatus(spriteName, index, assets) {
|
|
67
|
+
const sprite = index.find('sprite', spriteName);
|
|
68
|
+
if (sprite === undefined) {
|
|
69
|
+
return {
|
|
70
|
+
sprite: spriteName,
|
|
71
|
+
status: index.hasSkippedSourceForKind('sprite') ? 'partial' : 'missing_sprite',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const texture = typeof sprite.metadata.texture === 'string' ? sprite.metadata.texture : undefined;
|
|
75
|
+
if (texture === undefined)
|
|
76
|
+
return { sprite: spriteName, spritePath: sprite.path, status: 'missing_texture' };
|
|
77
|
+
return {
|
|
78
|
+
sprite: spriteName,
|
|
79
|
+
spritePath: sprite.path,
|
|
80
|
+
texturePath: texture,
|
|
81
|
+
status: assets.has(normalizedAssetPath(texture)) ? 'resolved' : 'missing_texture',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function localisationValue(index, language, key) {
|
|
85
|
+
const symbol = index.find('localisation', `${language}:${key}`);
|
|
86
|
+
return typeof symbol?.metadata.value === 'string' ? symbol.metadata.value : undefined;
|
|
87
|
+
}
|
|
88
|
+
function localiseTechnologies(values, index, assets) {
|
|
89
|
+
const partialLocalisation = index.hasSkippedSourceForKind('localisation');
|
|
90
|
+
return values.map(({ iconSprite, effectSignatureFields, ...technology }) => {
|
|
91
|
+
const language = 'l_english';
|
|
92
|
+
const name = localisationValue(index, language, technology.id);
|
|
93
|
+
const descriptionKey = `${technology.id}_desc`;
|
|
94
|
+
const description = localisationValue(index, language, descriptionKey);
|
|
95
|
+
return {
|
|
96
|
+
...technology,
|
|
97
|
+
icon: iconStatus(iconSprite, index, assets),
|
|
98
|
+
localisation: {
|
|
99
|
+
language,
|
|
100
|
+
nameKey: technology.id,
|
|
101
|
+
descriptionKey,
|
|
102
|
+
...(name === undefined ? {} : { name }),
|
|
103
|
+
...(description === undefined ? {} : { description }),
|
|
104
|
+
status: name !== undefined && description !== undefined
|
|
105
|
+
? 'resolved'
|
|
106
|
+
: partialLocalisation
|
|
107
|
+
? 'partial'
|
|
108
|
+
: 'missing',
|
|
109
|
+
},
|
|
110
|
+
effectSignature: hashCanonical(effectSignatureFields),
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function localiseFolders(values, index) {
|
|
115
|
+
const partial = index.hasSkippedSourceForKind('localisation');
|
|
116
|
+
return values.map((folder) => {
|
|
117
|
+
const name = localisationValue(index, 'l_english', folder.id);
|
|
118
|
+
const description = localisationValue(index, 'l_english', `${folder.id}_desc`);
|
|
119
|
+
return {
|
|
120
|
+
...folder,
|
|
121
|
+
localisation: {
|
|
122
|
+
language: 'l_english',
|
|
123
|
+
...(name === undefined ? {} : { name }),
|
|
124
|
+
...(description === undefined ? {} : { description }),
|
|
125
|
+
status: name !== undefined && description !== undefined
|
|
126
|
+
? 'resolved'
|
|
127
|
+
: partial
|
|
128
|
+
? 'partial'
|
|
129
|
+
: 'missing',
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function localiseDoctrines(values, index, assets) {
|
|
135
|
+
return values.map(({ iconSprite, ...definition }) => ({
|
|
136
|
+
...definition,
|
|
137
|
+
...(iconSprite === undefined ? {} : { icon: iconStatus(iconSprite, index, assets) }),
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
function selectOwnedRecords(values, owner, location, active) {
|
|
141
|
+
return values.filter((value) => active.get(owner(value))?.source.path === location(value).path);
|
|
142
|
+
}
|
|
143
|
+
function activeGridboxes(values) {
|
|
144
|
+
const groups = new Map();
|
|
145
|
+
for (const gridbox of values) {
|
|
146
|
+
const key = `${gridbox.folderId ?? '<global>'}\0${gridbox.name}`;
|
|
147
|
+
const group = groups.get(key) ?? [];
|
|
148
|
+
group.push(gridbox);
|
|
149
|
+
groups.set(key, group);
|
|
150
|
+
}
|
|
151
|
+
return [...groups.values()]
|
|
152
|
+
.map((group) => group.sort((left, right) => right.loadOrder - left.loadOrder ||
|
|
153
|
+
compareCodeUnits(left.sourcePath, right.sourcePath) ||
|
|
154
|
+
left.location.start.offset - right.location.start.offset)[0])
|
|
155
|
+
.sort((left, right) => compareCodeUnits(left.folderId ?? '', right.folderId ?? '') ||
|
|
156
|
+
compareCodeUnits(left.name, right.name));
|
|
157
|
+
}
|
|
158
|
+
function enrichPlacements(placements, edges, gridboxes, signal) {
|
|
159
|
+
const placementsByTechnology = new Map();
|
|
160
|
+
for (const placement of placements) {
|
|
161
|
+
const group = placementsByTechnology.get(placement.technologyId) ?? [];
|
|
162
|
+
group.push(placement);
|
|
163
|
+
placementsByTechnology.set(placement.technologyId, group);
|
|
164
|
+
}
|
|
165
|
+
const parents = new Map();
|
|
166
|
+
for (const edge of edges.filter(({ kind, ignoreForLayout }) => kind === 'prerequisite' && ignoreForLayout !== true)) {
|
|
167
|
+
const group = parents.get(edge.to) ?? [];
|
|
168
|
+
group.push(edge.from);
|
|
169
|
+
parents.set(edge.to, group);
|
|
170
|
+
}
|
|
171
|
+
const rootsFor = (technologyId, folderId) => {
|
|
172
|
+
const roots = new Set();
|
|
173
|
+
const pending = [technologyId];
|
|
174
|
+
const visited = new Set();
|
|
175
|
+
while (pending.length > 0) {
|
|
176
|
+
signal?.throwIfAborted();
|
|
177
|
+
const current = pending.pop();
|
|
178
|
+
if (visited.has(current))
|
|
179
|
+
continue;
|
|
180
|
+
visited.add(current);
|
|
181
|
+
const currentParents = parents.get(current) ?? [];
|
|
182
|
+
if (currentParents.length === 0) {
|
|
183
|
+
if (current === technologyId ||
|
|
184
|
+
(placementsByTechnology.get(current) ?? []).some(({ folderId: candidate }) => candidate === folderId))
|
|
185
|
+
roots.add(current);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
pending.push(...currentParents);
|
|
189
|
+
}
|
|
190
|
+
return [...roots].sort(compareCodeUnits);
|
|
191
|
+
};
|
|
192
|
+
return placements.map((placement) => {
|
|
193
|
+
const roots = rootsFor(placement.technologyId, placement.folderId);
|
|
194
|
+
const branchRootId = roots.length === 1 ? roots[0] : undefined;
|
|
195
|
+
const expectedGridbox = branchRootId === undefined ? undefined : `${branchRootId}_tree`;
|
|
196
|
+
const candidates = expectedGridbox === undefined
|
|
197
|
+
? []
|
|
198
|
+
: gridboxes.filter(({ name, folderId }) => name === expectedGridbox &&
|
|
199
|
+
(folderId === undefined || folderId === placement.folderId));
|
|
200
|
+
const gridbox = candidates[0];
|
|
201
|
+
let pixelX;
|
|
202
|
+
let pixelY;
|
|
203
|
+
if (gridbox !== undefined &&
|
|
204
|
+
placement.x !== undefined &&
|
|
205
|
+
placement.y !== undefined &&
|
|
206
|
+
gridbox.position.x !== undefined &&
|
|
207
|
+
gridbox.position.y !== undefined &&
|
|
208
|
+
gridbox.slotSize.width !== undefined &&
|
|
209
|
+
gridbox.slotSize.height !== undefined) {
|
|
210
|
+
const format = gridbox.format?.toUpperCase() ?? 'UP';
|
|
211
|
+
if (format === 'LEFT') {
|
|
212
|
+
pixelX = gridbox.position.x + placement.y * gridbox.slotSize.width;
|
|
213
|
+
pixelY = gridbox.position.y + placement.x * gridbox.slotSize.height;
|
|
214
|
+
}
|
|
215
|
+
else if (format === 'UP') {
|
|
216
|
+
pixelX = gridbox.position.x + placement.x * gridbox.slotSize.width;
|
|
217
|
+
pixelY = gridbox.position.y + placement.y * gridbox.slotSize.height;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
...placement,
|
|
222
|
+
...(branchRootId === undefined ? {} : { branchRootId }),
|
|
223
|
+
...(gridbox === undefined ? {} : { gridboxId: gridbox.id }),
|
|
224
|
+
...(pixelX === undefined ? {} : { pixelX }),
|
|
225
|
+
...(pixelY === undefined ? {} : { pixelY }),
|
|
226
|
+
geometryStatus: pixelX !== undefined && pixelY !== undefined
|
|
227
|
+
? 'source_pixel'
|
|
228
|
+
: placement.x !== undefined && placement.y !== undefined
|
|
229
|
+
? 'source_coordinate'
|
|
230
|
+
: 'unresolved',
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
function addIssue(issues, issue) {
|
|
235
|
+
if (issues.length >= MAX_ISSUES)
|
|
236
|
+
return;
|
|
237
|
+
issues.push({ ...issue, blockers: issue.blockers ?? [], details: issue.details ?? {} });
|
|
238
|
+
}
|
|
239
|
+
function issueKey(issue) {
|
|
240
|
+
return hashCanonical({
|
|
241
|
+
code: issue.code,
|
|
242
|
+
classification: issue.classification,
|
|
243
|
+
location: issue.location,
|
|
244
|
+
details: issue.details,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
function stableIssues(issues) {
|
|
248
|
+
const deduplicated = new Map();
|
|
249
|
+
for (const issue of issues)
|
|
250
|
+
if (!deduplicated.has(issueKey(issue)))
|
|
251
|
+
deduplicated.set(issueKey(issue), issue);
|
|
252
|
+
return [...deduplicated.values()].sort((left, right) => compareCodeUnits(left.code, right.code) ||
|
|
253
|
+
compareCodeUnits(left.location?.path ?? '', right.location?.path ?? '') ||
|
|
254
|
+
(left.location?.start.offset ?? 0) - (right.location?.start.offset ?? 0));
|
|
255
|
+
}
|
|
256
|
+
function stronglyConnectedComponents(nodeIds, edges, signal) {
|
|
257
|
+
const adjacency = new Map();
|
|
258
|
+
const reverse = new Map();
|
|
259
|
+
for (const id of nodeIds) {
|
|
260
|
+
adjacency.set(id, []);
|
|
261
|
+
reverse.set(id, []);
|
|
262
|
+
}
|
|
263
|
+
for (const edge of edges.filter(({ kind }) => kind === 'prerequisite')) {
|
|
264
|
+
if (!adjacency.has(edge.from) || !adjacency.has(edge.to))
|
|
265
|
+
continue;
|
|
266
|
+
adjacency.get(edge.from).push(edge.to);
|
|
267
|
+
reverse.get(edge.to).push(edge.from);
|
|
268
|
+
}
|
|
269
|
+
for (const list of [...adjacency.values(), ...reverse.values()])
|
|
270
|
+
list.sort(compareCodeUnits);
|
|
271
|
+
const visited = new Set();
|
|
272
|
+
const order = [];
|
|
273
|
+
for (const start of [...nodeIds].sort(compareCodeUnits)) {
|
|
274
|
+
if (visited.has(start))
|
|
275
|
+
continue;
|
|
276
|
+
const stack = [{ id: start, next: 0 }];
|
|
277
|
+
visited.add(start);
|
|
278
|
+
while (stack.length > 0) {
|
|
279
|
+
signal?.throwIfAborted();
|
|
280
|
+
const frame = stack.at(-1);
|
|
281
|
+
const targets = adjacency.get(frame.id) ?? [];
|
|
282
|
+
const target = targets[frame.next];
|
|
283
|
+
if (target === undefined) {
|
|
284
|
+
order.push(frame.id);
|
|
285
|
+
stack.pop();
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
frame.next += 1;
|
|
289
|
+
if (!visited.has(target)) {
|
|
290
|
+
visited.add(target);
|
|
291
|
+
stack.push({ id: target, next: 0 });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const assigned = new Set();
|
|
296
|
+
const components = [];
|
|
297
|
+
for (const start of order.reverse()) {
|
|
298
|
+
if (assigned.has(start))
|
|
299
|
+
continue;
|
|
300
|
+
const component = [];
|
|
301
|
+
const stack = [start];
|
|
302
|
+
assigned.add(start);
|
|
303
|
+
while (stack.length > 0) {
|
|
304
|
+
signal?.throwIfAborted();
|
|
305
|
+
const current = stack.pop();
|
|
306
|
+
component.push(current);
|
|
307
|
+
for (const target of reverse.get(current) ?? []) {
|
|
308
|
+
if (assigned.has(target))
|
|
309
|
+
continue;
|
|
310
|
+
assigned.add(target);
|
|
311
|
+
stack.push(target);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
components.push(component.sort(compareCodeUnits));
|
|
315
|
+
}
|
|
316
|
+
return components.sort((left, right) => compareCodeUnits(left[0] ?? '', right[0] ?? ''));
|
|
317
|
+
}
|
|
318
|
+
function expandHelperReferences(references, calls, unresolved, signal) {
|
|
319
|
+
const direct = references.filter(({ sourceKind }) => sourceKind === 'scripted_effect');
|
|
320
|
+
const publicReferences = references.filter(({ sourceKind }) => sourceKind !== 'scripted_effect');
|
|
321
|
+
const refsByHelper = new Map();
|
|
322
|
+
for (const reference of direct) {
|
|
323
|
+
const group = refsByHelper.get(reference.sourceId) ?? [];
|
|
324
|
+
group.push(reference);
|
|
325
|
+
refsByHelper.set(reference.sourceId, group);
|
|
326
|
+
}
|
|
327
|
+
const callsBySource = new Map();
|
|
328
|
+
for (const call of calls) {
|
|
329
|
+
const key = `${call.sourceKind}:${call.sourceId}`;
|
|
330
|
+
const group = callsBySource.get(key) ?? [];
|
|
331
|
+
group.push(call);
|
|
332
|
+
callsBySource.set(key, group);
|
|
333
|
+
}
|
|
334
|
+
const projected = [...publicReferences, ...direct];
|
|
335
|
+
let projectionCount = 0;
|
|
336
|
+
for (const rootCall of calls.filter(({ sourceKind }) => sourceKind !== 'scripted_effect')) {
|
|
337
|
+
const stack = [
|
|
338
|
+
{
|
|
339
|
+
helperId: rootCall.helperId,
|
|
340
|
+
helperStack: [rootCall.helperId],
|
|
341
|
+
confidence: rootCall.confidence,
|
|
342
|
+
},
|
|
343
|
+
];
|
|
344
|
+
const visited = new Set();
|
|
345
|
+
while (stack.length > 0) {
|
|
346
|
+
signal?.throwIfAborted();
|
|
347
|
+
const current = stack.pop();
|
|
348
|
+
const visitKey = current.helperStack.join('\0');
|
|
349
|
+
if (visited.has(visitKey))
|
|
350
|
+
continue;
|
|
351
|
+
visited.add(visitKey);
|
|
352
|
+
if (current.helperStack.length > MAX_HELPER_DEPTH) {
|
|
353
|
+
unresolved.push({
|
|
354
|
+
id: deterministicId('tech-unresolved', { rootCall, helperStack: current.helperStack }),
|
|
355
|
+
kind: 'unsupported_construct',
|
|
356
|
+
expression: current.helperStack.join(' -> '),
|
|
357
|
+
ownerId: rootCall.sourceId,
|
|
358
|
+
location: rootCall.location,
|
|
359
|
+
confidence: 'unresolved',
|
|
360
|
+
blockers: [
|
|
361
|
+
{
|
|
362
|
+
code: 'TECH_HELPER_DEPTH_BLOCKED',
|
|
363
|
+
message: `Scripted-effect expansion exceeds depth ${MAX_HELPER_DEPTH}`,
|
|
364
|
+
location: rootCall.location,
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
});
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
for (const reference of refsByHelper.get(current.helperId) ?? []) {
|
|
371
|
+
projectionCount += 1;
|
|
372
|
+
if (projectionCount > MAX_HELPER_PROJECTIONS)
|
|
373
|
+
break;
|
|
374
|
+
projected.push({
|
|
375
|
+
...reference,
|
|
376
|
+
id: deterministicId('tech-external-projection', {
|
|
377
|
+
root: rootCall.id,
|
|
378
|
+
reference: reference.id,
|
|
379
|
+
helperStack: current.helperStack,
|
|
380
|
+
}),
|
|
381
|
+
sourceKind: rootCall.sourceKind,
|
|
382
|
+
sourceId: rootCall.sourceId,
|
|
383
|
+
helperStack: [...current.helperStack],
|
|
384
|
+
confidence: worseConfidence(current.confidence, reference.confidence),
|
|
385
|
+
metadata: {
|
|
386
|
+
...reference.metadata,
|
|
387
|
+
projectedFromScriptedEffect: reference.sourceId,
|
|
388
|
+
callLocation: canonicalJson(rootCall.location),
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
if (projectionCount > MAX_HELPER_PROJECTIONS)
|
|
393
|
+
break;
|
|
394
|
+
for (const call of callsBySource.get(`scripted_effect:${current.helperId}`) ?? []) {
|
|
395
|
+
if (current.helperStack.includes(call.helperId))
|
|
396
|
+
continue;
|
|
397
|
+
stack.push({
|
|
398
|
+
helperId: call.helperId,
|
|
399
|
+
helperStack: [...current.helperStack, call.helperId],
|
|
400
|
+
confidence: worseConfidence(current.confidence, call.confidence),
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (projectionCount > MAX_HELPER_PROJECTIONS)
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
if (projectionCount > MAX_HELPER_PROJECTIONS) {
|
|
408
|
+
unresolved.push({
|
|
409
|
+
id: 'tech-unresolved-helper-projection-limit',
|
|
410
|
+
kind: 'unsupported_construct',
|
|
411
|
+
expression: 'scripted_effect expansion',
|
|
412
|
+
confidence: 'unresolved',
|
|
413
|
+
blockers: [
|
|
414
|
+
{
|
|
415
|
+
code: 'TECH_HELPER_PROJECTION_LIMIT',
|
|
416
|
+
message: `Scripted-effect reference expansion exceeds ${MAX_HELPER_PROJECTIONS} projections`,
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
const deduplicated = new Map();
|
|
422
|
+
for (const reference of projected)
|
|
423
|
+
if (!deduplicated.has(reference.id))
|
|
424
|
+
deduplicated.set(reference.id, reference);
|
|
425
|
+
return [...deduplicated.values()].sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
426
|
+
}
|
|
427
|
+
function diagnose(technologies, folders, placements, gridboxes, edges, categories, doctrines, unlocks, externalReferences, duplicates, unresolved, signal) {
|
|
428
|
+
const issues = [];
|
|
429
|
+
const technologyById = new Map(technologies.map((technology) => [technology.id, technology]));
|
|
430
|
+
const folderById = new Map(folders.map((folder) => [folder.id, folder]));
|
|
431
|
+
const categoryById = new Map(categories.map((category) => [category.id, category]));
|
|
432
|
+
const placementsByTech = new Map();
|
|
433
|
+
for (const placement of placements) {
|
|
434
|
+
const group = placementsByTech.get(placement.technologyId) ?? [];
|
|
435
|
+
group.push(placement);
|
|
436
|
+
placementsByTech.set(placement.technologyId, group);
|
|
437
|
+
}
|
|
438
|
+
const incoming = new Map();
|
|
439
|
+
const outgoing = new Map();
|
|
440
|
+
for (const edge of edges.filter(({ kind }) => kind === 'prerequisite')) {
|
|
441
|
+
const to = incoming.get(edge.to) ?? [];
|
|
442
|
+
to.push(edge);
|
|
443
|
+
incoming.set(edge.to, to);
|
|
444
|
+
const from = outgoing.get(edge.from) ?? [];
|
|
445
|
+
from.push(edge);
|
|
446
|
+
outgoing.set(edge.from, from);
|
|
447
|
+
}
|
|
448
|
+
for (const group of duplicates) {
|
|
449
|
+
const first = group[0];
|
|
450
|
+
addIssue(issues, {
|
|
451
|
+
code: 'TECH_DUPLICATE_ID',
|
|
452
|
+
classification: 'confirmed_error',
|
|
453
|
+
severity: 'error',
|
|
454
|
+
message: `Technology ${first.id} has multiple active definitions at the same load order`,
|
|
455
|
+
confidence: 'confirmed',
|
|
456
|
+
location: first.source.location,
|
|
457
|
+
related: group.slice(1).map(({ source }) => source.location),
|
|
458
|
+
details: { technologyId: first.id },
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
for (const edge of edges) {
|
|
462
|
+
signal?.throwIfAborted();
|
|
463
|
+
const owner = technologyById.get(edge.from);
|
|
464
|
+
if (edge.from === edge.to) {
|
|
465
|
+
addIssue(issues, {
|
|
466
|
+
code: 'TECH_SELF_LINK',
|
|
467
|
+
classification: 'confirmed_error',
|
|
468
|
+
severity: 'error',
|
|
469
|
+
message: `Technology ${edge.from} links to itself through ${edge.kind}`,
|
|
470
|
+
confidence: 'confirmed',
|
|
471
|
+
location: edge.location,
|
|
472
|
+
details: { technologyId: edge.from, edgeKind: edge.kind },
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (!technologyById.has(edge.to)) {
|
|
476
|
+
addIssue(issues, {
|
|
477
|
+
code: edge.kind === 'exclusive' ? 'TECH_EXCLUSIVE_TARGET_MISSING' : 'TECH_TARGET_MISSING',
|
|
478
|
+
classification: 'confirmed_error',
|
|
479
|
+
severity: 'error',
|
|
480
|
+
message: `Technology ${edge.from} references missing ${edge.kind} target ${edge.to}`,
|
|
481
|
+
confidence: 'confirmed',
|
|
482
|
+
location: edge.location,
|
|
483
|
+
details: { technologyId: edge.from, targetId: edge.to, edgeKind: edge.kind },
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
if (owner === undefined)
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
for (const component of stronglyConnectedComponents(technologies.map(({ id }) => id), edges, signal)) {
|
|
490
|
+
if (component.length < 2)
|
|
491
|
+
continue;
|
|
492
|
+
const first = technologyById.get(component[0]);
|
|
493
|
+
addIssue(issues, {
|
|
494
|
+
code: 'TECH_PREREQUISITE_CYCLE',
|
|
495
|
+
classification: 'confirmed_error',
|
|
496
|
+
severity: 'error',
|
|
497
|
+
message: `Technology prerequisite cycle: ${component.join(' -> ')}`,
|
|
498
|
+
confidence: 'confirmed',
|
|
499
|
+
location: first.source.location,
|
|
500
|
+
details: { technologyIds: component },
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
const coordinateGroups = new Map();
|
|
504
|
+
for (const placement of placements) {
|
|
505
|
+
if (!folderById.has(placement.folderId)) {
|
|
506
|
+
addIssue(issues, {
|
|
507
|
+
code: 'TECH_FOLDER_REFERENCE_MISSING',
|
|
508
|
+
classification: 'confirmed_error',
|
|
509
|
+
severity: 'error',
|
|
510
|
+
message: `Technology ${placement.technologyId} references missing folder ${placement.folderId}`,
|
|
511
|
+
confidence: 'confirmed',
|
|
512
|
+
location: placement.location,
|
|
513
|
+
details: { technologyId: placement.technologyId, folderId: placement.folderId },
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
if (placement.x === undefined || placement.y === undefined) {
|
|
517
|
+
addIssue(issues, {
|
|
518
|
+
code: 'TECH_PLACEMENT_COORDINATE_UNRESOLVED',
|
|
519
|
+
classification: 'unresolved_analysis',
|
|
520
|
+
severity: 'warning',
|
|
521
|
+
message: `Technology ${placement.technologyId} has an unresolved source coordinate in ${placement.folderId}`,
|
|
522
|
+
confidence: 'unresolved',
|
|
523
|
+
location: placement.location,
|
|
524
|
+
blockers: [
|
|
525
|
+
{
|
|
526
|
+
code: 'TECH_COORDINATE_EXPRESSION_UNSUPPORTED',
|
|
527
|
+
message: 'The coordinate is not a numeric literal or resolvable file-local constant',
|
|
528
|
+
location: placement.location,
|
|
529
|
+
},
|
|
530
|
+
],
|
|
531
|
+
details: {
|
|
532
|
+
technologyId: placement.technologyId,
|
|
533
|
+
folderId: placement.folderId,
|
|
534
|
+
xExpression: placement.xExpression ?? null,
|
|
535
|
+
yExpression: placement.yExpression ?? null,
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
const key = placement.pixelX !== undefined && placement.pixelY !== undefined
|
|
541
|
+
? `${placement.folderId}\0pixel\0${placement.pixelX}\0${placement.pixelY}`
|
|
542
|
+
: `${placement.folderId}\0${placement.branchRootId ?? '<ambiguous>'}\0${placement.x}\0${placement.y}`;
|
|
543
|
+
const group = coordinateGroups.get(key) ?? [];
|
|
544
|
+
group.push(placement);
|
|
545
|
+
coordinateGroups.set(key, group);
|
|
546
|
+
}
|
|
547
|
+
for (const group of coordinateGroups.values()) {
|
|
548
|
+
const technologyIds = [...new Set(group.map(({ technologyId }) => technologyId))];
|
|
549
|
+
if (technologyIds.length < 2)
|
|
550
|
+
continue;
|
|
551
|
+
addIssue(issues, {
|
|
552
|
+
code: 'TECH_FOLDER_COORDINATE_OVERLAP',
|
|
553
|
+
classification: 'confirmed_error',
|
|
554
|
+
severity: 'error',
|
|
555
|
+
message: `Technologies ${technologyIds.join(', ')} overlap at one source coordinate in ${group[0].folderId}`,
|
|
556
|
+
confidence: 'confirmed',
|
|
557
|
+
location: group[0].location,
|
|
558
|
+
related: group.slice(1).map(({ location }) => location),
|
|
559
|
+
details: {
|
|
560
|
+
folderId: group[0].folderId,
|
|
561
|
+
x: group[0].x,
|
|
562
|
+
y: group[0].y,
|
|
563
|
+
pixelX: group[0].pixelX ?? null,
|
|
564
|
+
pixelY: group[0].pixelY ?? null,
|
|
565
|
+
technologyIds,
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
for (const [technologyId, techPlacements] of placementsByTech) {
|
|
570
|
+
const byFolder = new Map();
|
|
571
|
+
for (const placement of techPlacements) {
|
|
572
|
+
const group = byFolder.get(placement.folderId) ?? [];
|
|
573
|
+
group.push(placement);
|
|
574
|
+
byFolder.set(placement.folderId, group);
|
|
575
|
+
}
|
|
576
|
+
for (const group of byFolder.values()) {
|
|
577
|
+
const coordinates = new Set(group.map(({ x, y, xExpression, yExpression }) => `${x ?? xExpression}:${y ?? yExpression}`));
|
|
578
|
+
if (coordinates.size < 2)
|
|
579
|
+
continue;
|
|
580
|
+
addIssue(issues, {
|
|
581
|
+
code: 'TECH_PLACEMENT_CONFLICT',
|
|
582
|
+
classification: 'confirmed_error',
|
|
583
|
+
severity: 'error',
|
|
584
|
+
message: `Technology ${technologyId} has conflicting placements in folder ${group[0].folderId}`,
|
|
585
|
+
confidence: 'confirmed',
|
|
586
|
+
location: group[0].location,
|
|
587
|
+
related: group.slice(1).map(({ location }) => location),
|
|
588
|
+
details: {
|
|
589
|
+
technologyId,
|
|
590
|
+
folderId: group[0].folderId,
|
|
591
|
+
coordinates: [...coordinates].sort(compareCodeUnits),
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
const gridboxById = new Map(gridboxes.map((gridbox) => [gridbox.id, gridbox]));
|
|
597
|
+
for (const placement of placements) {
|
|
598
|
+
if (placement.geometryStatus === 'source_pixel')
|
|
599
|
+
continue;
|
|
600
|
+
if (placement.branchRootId === undefined) {
|
|
601
|
+
addIssue(issues, {
|
|
602
|
+
code: 'TECH_GRIDBOX_BRANCH_AMBIGUOUS',
|
|
603
|
+
classification: 'probable_defect',
|
|
604
|
+
severity: 'warning',
|
|
605
|
+
message: `Technology ${placement.technologyId} cannot be assigned to one source branch gridbox in ${placement.folderId}`,
|
|
606
|
+
confidence: 'high',
|
|
607
|
+
location: placement.location,
|
|
608
|
+
details: { technologyId: placement.technologyId, folderId: placement.folderId },
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
else if (placement.gridboxId === undefined) {
|
|
612
|
+
addIssue(issues, {
|
|
613
|
+
code: 'TECH_GRIDBOX_MISSING',
|
|
614
|
+
classification: 'probable_defect',
|
|
615
|
+
severity: 'warning',
|
|
616
|
+
message: `Technology branch ${placement.branchRootId} has no matching ${placement.branchRootId}_tree gridbox in ${placement.folderId}`,
|
|
617
|
+
confidence: 'high',
|
|
618
|
+
location: placement.location,
|
|
619
|
+
details: {
|
|
620
|
+
technologyId: placement.technologyId,
|
|
621
|
+
folderId: placement.folderId,
|
|
622
|
+
branchRootId: placement.branchRootId,
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
const gridbox = gridboxById.get(placement.gridboxId);
|
|
628
|
+
addIssue(issues, {
|
|
629
|
+
code: 'TECH_GRIDBOX_GEOMETRY_UNRESOLVED',
|
|
630
|
+
classification: 'unresolved_analysis',
|
|
631
|
+
severity: 'warning',
|
|
632
|
+
message: `Gridbox geometry for technology ${placement.technologyId} cannot be reduced to source pixel coordinates`,
|
|
633
|
+
confidence: 'unresolved',
|
|
634
|
+
location: gridbox?.location ?? placement.location,
|
|
635
|
+
details: {
|
|
636
|
+
technologyId: placement.technologyId,
|
|
637
|
+
folderId: placement.folderId,
|
|
638
|
+
gridbox: gridbox?.name ?? null,
|
|
639
|
+
format: gridbox?.format ?? null,
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const roots = technologies
|
|
645
|
+
.filter(({ id, hidden }) => hidden !== true && (incoming.get(id)?.length ?? 0) === 0)
|
|
646
|
+
.map(({ id }) => id);
|
|
647
|
+
const reachable = new Set();
|
|
648
|
+
const pending = [...roots];
|
|
649
|
+
while (pending.length > 0) {
|
|
650
|
+
signal?.throwIfAborted();
|
|
651
|
+
const current = pending.pop();
|
|
652
|
+
if (reachable.has(current))
|
|
653
|
+
continue;
|
|
654
|
+
reachable.add(current);
|
|
655
|
+
for (const edge of outgoing.get(current) ?? [])
|
|
656
|
+
if (technologyById.has(edge.to))
|
|
657
|
+
pending.push(edge.to);
|
|
658
|
+
}
|
|
659
|
+
const grantIds = new Set(externalReferences.flatMap(({ technologyId, kind }) => technologyId !== undefined && (kind === 'grant' || kind === 'starting_technology')
|
|
660
|
+
? [technologyId]
|
|
661
|
+
: []));
|
|
662
|
+
const unlocksByTechnology = new Map();
|
|
663
|
+
for (const unlock of unlocks) {
|
|
664
|
+
const group = unlocksByTechnology.get(unlock.technologyId) ?? [];
|
|
665
|
+
group.push(unlock);
|
|
666
|
+
unlocksByTechnology.set(unlock.technologyId, group);
|
|
667
|
+
}
|
|
668
|
+
for (const technology of technologies) {
|
|
669
|
+
signal?.throwIfAborted();
|
|
670
|
+
const techPlacements = placementsByTech.get(technology.id) ?? [];
|
|
671
|
+
if (technology.hidden !== true && techPlacements.length > 0 && !reachable.has(technology.id)) {
|
|
672
|
+
addIssue(issues, {
|
|
673
|
+
code: 'TECH_VISIBLE_WITHOUT_ROOT_PATH',
|
|
674
|
+
classification: 'probable_defect',
|
|
675
|
+
severity: 'warning',
|
|
676
|
+
message: `Visible technology ${technology.id} has no valid prerequisite path from a visible root`,
|
|
677
|
+
confidence: 'high',
|
|
678
|
+
location: technology.source.location,
|
|
679
|
+
details: { technologyId: technology.id },
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
if ((technology.hidden === true || techPlacements.length === 0) &&
|
|
683
|
+
!grantIds.has(technology.id)) {
|
|
684
|
+
addIssue(issues, {
|
|
685
|
+
code: 'TECH_HIDDEN_OR_UNPLACED_WITHOUT_GRANT',
|
|
686
|
+
classification: 'design_warning',
|
|
687
|
+
severity: 'warning',
|
|
688
|
+
message: `Technology ${technology.id} is hidden or unplaced and has no discovered static grant path`,
|
|
689
|
+
confidence: technology.hidden === 'unknown' ? 'medium' : 'high',
|
|
690
|
+
location: technology.source.location,
|
|
691
|
+
details: {
|
|
692
|
+
technologyId: technology.id,
|
|
693
|
+
hidden: technology.hidden,
|
|
694
|
+
placementCount: techPlacements.length,
|
|
695
|
+
},
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
const directParents = (incoming.get(technology.id) ?? [])
|
|
699
|
+
.map(({ from }) => technologyById.get(from))
|
|
700
|
+
.filter((value) => value !== undefined && value.hidden !== true);
|
|
701
|
+
const year = Number(technology.startYear);
|
|
702
|
+
const parentYears = directParents
|
|
703
|
+
.map(({ startYear }) => Number(startYear))
|
|
704
|
+
.filter(Number.isFinite);
|
|
705
|
+
if (Number.isFinite(year) &&
|
|
706
|
+
parentYears.length > 0 &&
|
|
707
|
+
parentYears.every((parentYear) => year < parentYear)) {
|
|
708
|
+
addIssue(issues, {
|
|
709
|
+
code: 'TECH_CHILD_EARLIER_THAN_PARENTS',
|
|
710
|
+
classification: 'design_warning',
|
|
711
|
+
severity: 'warning',
|
|
712
|
+
message: `Technology ${technology.id} is dated earlier than every visible direct prerequisite`,
|
|
713
|
+
confidence: 'high',
|
|
714
|
+
location: technology.source.location,
|
|
715
|
+
details: { technologyId: technology.id, startYear: year, parentYears },
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
const coefficientEdges = (incoming.get(technology.id) ?? []).filter(({ coefficient }) => coefficient !== undefined);
|
|
719
|
+
for (const edge of coefficientEdges) {
|
|
720
|
+
const coefficient = Number(edge.coefficient);
|
|
721
|
+
if (!Number.isFinite(coefficient) || (coefficient >= 0.1 && coefficient <= 5))
|
|
722
|
+
continue;
|
|
723
|
+
addIssue(issues, {
|
|
724
|
+
code: 'TECH_EDGE_COEFFICIENT_OUTLIER',
|
|
725
|
+
classification: 'design_warning',
|
|
726
|
+
severity: 'warning',
|
|
727
|
+
message: `Prerequisite edge ${edge.from} -> ${edge.to} has unusual research coefficient ${edge.coefficient}`,
|
|
728
|
+
confidence: 'high',
|
|
729
|
+
location: edge.location,
|
|
730
|
+
details: { from: edge.from, to: edge.to, coefficient: edge.coefficient },
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
const degree = (incoming.get(technology.id)?.length ?? 0) + (outgoing.get(technology.id)?.length ?? 0);
|
|
734
|
+
const technologyUnlocks = unlocksByTechnology.get(technology.id) ?? [];
|
|
735
|
+
if (technology.effectKeys.length === 0 && technologyUnlocks.length === 0 && degree === 0) {
|
|
736
|
+
addIssue(issues, {
|
|
737
|
+
code: 'TECH_NO_VISIBLE_ROLE',
|
|
738
|
+
classification: 'design_warning',
|
|
739
|
+
severity: 'warning',
|
|
740
|
+
message: `Technology ${technology.id} has no visible effect, unlock, or routing role`,
|
|
741
|
+
confidence: 'high',
|
|
742
|
+
location: technology.source.location,
|
|
743
|
+
details: { technologyId: technology.id },
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
else if (technology.effectKeys.length === 0 &&
|
|
747
|
+
technologyUnlocks.length === 0 &&
|
|
748
|
+
(incoming.get(technology.id)?.length ?? 0) > 0 &&
|
|
749
|
+
(outgoing.get(technology.id)?.length ?? 0) === 0) {
|
|
750
|
+
addIssue(issues, {
|
|
751
|
+
code: 'TECH_TERMINAL_WITHOUT_VISIBLE_PAYOFF',
|
|
752
|
+
classification: 'design_warning',
|
|
753
|
+
severity: 'warning',
|
|
754
|
+
message: `Terminal technology ${technology.id} has no visible effect or unlock`,
|
|
755
|
+
confidence: 'high',
|
|
756
|
+
location: technology.source.location,
|
|
757
|
+
details: { technologyId: technology.id },
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
if (technology.localisation.status === 'missing') {
|
|
761
|
+
addIssue(issues, {
|
|
762
|
+
code: 'TECH_LOCALISATION_MISSING',
|
|
763
|
+
classification: 'confirmed_error',
|
|
764
|
+
severity: 'error',
|
|
765
|
+
message: `Technology ${technology.id} is missing English name or description localisation`,
|
|
766
|
+
confidence: 'confirmed',
|
|
767
|
+
location: technology.source.location,
|
|
768
|
+
details: {
|
|
769
|
+
technologyId: technology.id,
|
|
770
|
+
nameKey: technology.localisation.nameKey,
|
|
771
|
+
descriptionKey: technology.localisation.descriptionKey,
|
|
772
|
+
},
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
if (technology.icon.status === 'missing_sprite' ||
|
|
776
|
+
technology.icon.status === 'missing_texture') {
|
|
777
|
+
addIssue(issues, {
|
|
778
|
+
code: technology.icon.status === 'missing_sprite'
|
|
779
|
+
? 'TECH_ICON_SPRITE_MISSING'
|
|
780
|
+
: 'TECH_ICON_TEXTURE_MISSING',
|
|
781
|
+
classification: 'confirmed_error',
|
|
782
|
+
severity: 'error',
|
|
783
|
+
message: technology.icon.status === 'missing_sprite'
|
|
784
|
+
? `Technology ${technology.id} references missing sprite ${technology.icon.sprite}`
|
|
785
|
+
: `Technology ${technology.id} cannot resolve texture for sprite ${technology.icon.sprite}`,
|
|
786
|
+
confidence: 'confirmed',
|
|
787
|
+
location: technology.source.location,
|
|
788
|
+
details: {
|
|
789
|
+
technologyId: technology.id,
|
|
790
|
+
sprite: technology.icon.sprite,
|
|
791
|
+
texturePath: technology.icon.texturePath ?? null,
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
for (const category of technology.categories) {
|
|
796
|
+
if (categoryById.has(category))
|
|
797
|
+
continue;
|
|
798
|
+
addIssue(issues, {
|
|
799
|
+
code: 'TECH_CATEGORY_REFERENCE_MISSING',
|
|
800
|
+
classification: 'confirmed_error',
|
|
801
|
+
severity: 'error',
|
|
802
|
+
message: `Technology ${technology.id} references missing category ${category}`,
|
|
803
|
+
confidence: 'confirmed',
|
|
804
|
+
location: technology.source.location,
|
|
805
|
+
details: { technologyId: technology.id, categoryId: category },
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
for (const tag of technology.tags) {
|
|
809
|
+
if (categoryById.get(tag)?.kind === 'tag')
|
|
810
|
+
continue;
|
|
811
|
+
addIssue(issues, {
|
|
812
|
+
code: 'TECH_TAG_REFERENCE_MISSING',
|
|
813
|
+
classification: 'confirmed_error',
|
|
814
|
+
severity: 'error',
|
|
815
|
+
message: `Technology ${technology.id} references missing tag ${tag}`,
|
|
816
|
+
confidence: 'confirmed',
|
|
817
|
+
location: technology.source.location,
|
|
818
|
+
details: { technologyId: technology.id, tagId: tag },
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
const branchPeers = [
|
|
822
|
+
...new Set([
|
|
823
|
+
...(incoming.get(technology.id) ?? []).flatMap(({ from }) => (outgoing.get(from) ?? []).map(({ to }) => to)),
|
|
824
|
+
...(outgoing.get(technology.id) ?? []).flatMap(({ to }) => (incoming.get(to) ?? []).map(({ from }) => from)),
|
|
825
|
+
]),
|
|
826
|
+
]
|
|
827
|
+
.filter((id) => id !== technology.id)
|
|
828
|
+
.map((id) => technologyById.get(id))
|
|
829
|
+
.filter((value) => value !== undefined);
|
|
830
|
+
if (technology.hidden !== true &&
|
|
831
|
+
degree > 0 &&
|
|
832
|
+
branchPeers.some(({ ai }) => ai.present && ai.zero !== true) &&
|
|
833
|
+
(!technology.ai.present || technology.ai.zero === true)) {
|
|
834
|
+
addIssue(issues, {
|
|
835
|
+
code: 'TECH_BRANCH_AI_COVERAGE_GAP',
|
|
836
|
+
classification: 'design_warning',
|
|
837
|
+
severity: 'warning',
|
|
838
|
+
message: `Technology ${technology.id} has missing or zero AI willingness while adjacent branch peers have active AI metadata`,
|
|
839
|
+
confidence: 'medium',
|
|
840
|
+
location: technology.source.location,
|
|
841
|
+
details: {
|
|
842
|
+
technologyId: technology.id,
|
|
843
|
+
aiPresent: technology.ai.present,
|
|
844
|
+
aiZero: technology.ai.zero,
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
for (const unlock of unlocks) {
|
|
850
|
+
if (unlock.resolved !== false)
|
|
851
|
+
continue;
|
|
852
|
+
addIssue(issues, {
|
|
853
|
+
code: 'TECH_UNLOCK_TARGET_MISSING',
|
|
854
|
+
classification: 'confirmed_error',
|
|
855
|
+
severity: 'error',
|
|
856
|
+
message: `Technology ${unlock.technologyId} unlocks missing ${unlock.kind} ${unlock.targetId}`,
|
|
857
|
+
confidence: 'confirmed',
|
|
858
|
+
location: unlock.location,
|
|
859
|
+
details: {
|
|
860
|
+
technologyId: unlock.technologyId,
|
|
861
|
+
targetKind: unlock.kind,
|
|
862
|
+
targetId: unlock.targetId,
|
|
863
|
+
},
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
const technologiesByCategory = new Map();
|
|
867
|
+
for (const technology of technologies)
|
|
868
|
+
for (const category of technology.categories) {
|
|
869
|
+
const group = technologiesByCategory.get(category) ?? [];
|
|
870
|
+
group.push(technology.id);
|
|
871
|
+
technologiesByCategory.set(category, group);
|
|
872
|
+
}
|
|
873
|
+
for (const reference of externalReferences) {
|
|
874
|
+
if (reference.technologyId !== undefined &&
|
|
875
|
+
!technologyById.has(reference.technologyId) &&
|
|
876
|
+
!reference.dynamic) {
|
|
877
|
+
addIssue(issues, {
|
|
878
|
+
code: reference.kind === 'research_bonus'
|
|
879
|
+
? 'TECH_BONUS_TARGET_MISSING'
|
|
880
|
+
: 'TECH_EXTERNAL_REFERENCE_MISSING',
|
|
881
|
+
classification: 'confirmed_error',
|
|
882
|
+
severity: 'error',
|
|
883
|
+
message: `${reference.sourceKind} ${reference.sourceId} references missing technology ${reference.technologyId}`,
|
|
884
|
+
confidence: reference.confidence,
|
|
885
|
+
location: reference.location,
|
|
886
|
+
details: {
|
|
887
|
+
sourceKind: reference.sourceKind,
|
|
888
|
+
sourceId: reference.sourceId,
|
|
889
|
+
technologyId: reference.technologyId,
|
|
890
|
+
referenceKind: reference.kind,
|
|
891
|
+
},
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
if (reference.categoryId !== undefined &&
|
|
895
|
+
!categoryById.has(reference.categoryId) &&
|
|
896
|
+
!reference.dynamic) {
|
|
897
|
+
addIssue(issues, {
|
|
898
|
+
code: 'TECH_EXTERNAL_CATEGORY_MISSING',
|
|
899
|
+
classification: 'confirmed_error',
|
|
900
|
+
severity: 'error',
|
|
901
|
+
message: `${reference.sourceKind} ${reference.sourceId} references missing technology category ${reference.categoryId}`,
|
|
902
|
+
confidence: reference.confidence,
|
|
903
|
+
location: reference.location,
|
|
904
|
+
details: {
|
|
905
|
+
sourceKind: reference.sourceKind,
|
|
906
|
+
sourceId: reference.sourceId,
|
|
907
|
+
categoryId: reference.categoryId,
|
|
908
|
+
referenceKind: reference.kind,
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
else if (reference.kind === 'research_bonus' &&
|
|
913
|
+
reference.categoryId !== undefined &&
|
|
914
|
+
(technologiesByCategory.get(reference.categoryId)?.length ?? 0) === 0) {
|
|
915
|
+
addIssue(issues, {
|
|
916
|
+
code: 'TECH_BONUS_CATEGORY_EMPTY',
|
|
917
|
+
classification: 'probable_defect',
|
|
918
|
+
severity: 'warning',
|
|
919
|
+
message: `Research bonus category ${reference.categoryId} matches no indexed technology`,
|
|
920
|
+
confidence: 'high',
|
|
921
|
+
location: reference.location,
|
|
922
|
+
details: {
|
|
923
|
+
sourceKind: reference.sourceKind,
|
|
924
|
+
sourceId: reference.sourceId,
|
|
925
|
+
categoryId: reference.categoryId,
|
|
926
|
+
},
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
for (const doctrine of doctrines) {
|
|
931
|
+
if (doctrine.folderId !== undefined) {
|
|
932
|
+
const folderExists = doctrines.some(({ kind, id }) => kind === 'folder' && id === doctrine.folderId);
|
|
933
|
+
if (!folderExists)
|
|
934
|
+
addIssue(issues, {
|
|
935
|
+
code: 'TECH_DOCTRINE_FOLDER_MISSING',
|
|
936
|
+
classification: 'confirmed_error',
|
|
937
|
+
severity: 'error',
|
|
938
|
+
message: `Doctrine ${doctrine.id} references missing doctrine folder ${doctrine.folderId}`,
|
|
939
|
+
confidence: 'confirmed',
|
|
940
|
+
location: doctrine.source.location,
|
|
941
|
+
details: { doctrineId: doctrine.id, folderId: doctrine.folderId },
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
for (const trackId of doctrine.trackIds) {
|
|
945
|
+
if (doctrines.some(({ kind, id }) => kind === 'track' && id === trackId))
|
|
946
|
+
continue;
|
|
947
|
+
addIssue(issues, {
|
|
948
|
+
code: 'TECH_DOCTRINE_TRACK_MISSING',
|
|
949
|
+
classification: 'confirmed_error',
|
|
950
|
+
severity: 'error',
|
|
951
|
+
message: `Doctrine ${doctrine.id} references missing doctrine track ${trackId}`,
|
|
952
|
+
confidence: 'confirmed',
|
|
953
|
+
location: doctrine.source.location,
|
|
954
|
+
details: { doctrineId: doctrine.id, trackId },
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
for (const exclusiveId of doctrine.exclusiveIds) {
|
|
958
|
+
if (doctrines.some(({ id }) => id === exclusiveId))
|
|
959
|
+
continue;
|
|
960
|
+
addIssue(issues, {
|
|
961
|
+
code: 'TECH_DOCTRINE_EXCLUSIVE_TARGET_MISSING',
|
|
962
|
+
classification: 'confirmed_error',
|
|
963
|
+
severity: 'error',
|
|
964
|
+
message: `Doctrine ${doctrine.id} references missing exclusive choice ${exclusiveId}`,
|
|
965
|
+
confidence: 'confirmed',
|
|
966
|
+
location: doctrine.source.location,
|
|
967
|
+
details: { doctrineId: doctrine.id, targetId: exclusiveId },
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
const signatureGroups = new Map();
|
|
972
|
+
for (const technology of technologies) {
|
|
973
|
+
if (technology.effectKeys.length === 0 &&
|
|
974
|
+
(unlocksByTechnology.get(technology.id)?.length ?? 0) === 0)
|
|
975
|
+
continue;
|
|
976
|
+
const key = hashCanonical({
|
|
977
|
+
signature: technology.effectSignature,
|
|
978
|
+
year: technology.startYear ?? null,
|
|
979
|
+
folders: technology.folders,
|
|
980
|
+
categories: technology.categories,
|
|
981
|
+
});
|
|
982
|
+
const group = signatureGroups.get(key) ?? [];
|
|
983
|
+
group.push(technology);
|
|
984
|
+
signatureGroups.set(key, group);
|
|
985
|
+
}
|
|
986
|
+
for (const group of signatureGroups.values()) {
|
|
987
|
+
if (group.length < 2)
|
|
988
|
+
continue;
|
|
989
|
+
addIssue(issues, {
|
|
990
|
+
code: 'TECH_SUSPICIOUS_IDENTICAL_SIGNATURE',
|
|
991
|
+
classification: 'design_warning',
|
|
992
|
+
severity: 'warning',
|
|
993
|
+
message: `Technologies ${group.map(({ id }) => id).join(', ')} have identical effects, unlocks, year, folders, and categories`,
|
|
994
|
+
confidence: 'medium',
|
|
995
|
+
location: group[0].source.location,
|
|
996
|
+
related: group.slice(1).map(({ source }) => source.location),
|
|
997
|
+
details: { technologyIds: group.map(({ id }) => id) },
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
for (const item of unresolved) {
|
|
1001
|
+
addIssue(issues, {
|
|
1002
|
+
code: 'TECH_ANALYSIS_UNRESOLVED',
|
|
1003
|
+
classification: 'unresolved_analysis',
|
|
1004
|
+
severity: 'warning',
|
|
1005
|
+
message: `Static technology analysis could not resolve ${item.expression}`,
|
|
1006
|
+
confidence: item.confidence,
|
|
1007
|
+
...(item.location === undefined ? {} : { location: item.location }),
|
|
1008
|
+
blockers: item.blockers,
|
|
1009
|
+
details: { unresolvedId: item.id, kind: item.kind, ownerId: item.ownerId ?? null },
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
return stableIssues(issues);
|
|
1013
|
+
}
|
|
1014
|
+
function assertLimits(graph) {
|
|
1015
|
+
if (graph.technologies.length > MAX_TECHNOLOGIES)
|
|
1016
|
+
throw new ServiceError('TECH_GRAPH_NODE_LIMIT', `Technology graph exceeds ${MAX_TECHNOLOGIES} technologies`);
|
|
1017
|
+
if (graph.edges.length > MAX_EDGES)
|
|
1018
|
+
throw new ServiceError('TECH_GRAPH_EDGE_LIMIT', `Technology graph exceeds ${MAX_EDGES} edges`);
|
|
1019
|
+
if (graph.externalReferences.length > MAX_EXTERNAL_REFERENCES)
|
|
1020
|
+
throw new ServiceError('TECH_GRAPH_REFERENCE_LIMIT', `Technology graph exceeds ${MAX_EXTERNAL_REFERENCES} external references`);
|
|
1021
|
+
}
|
|
1022
|
+
function sourceHashes(files) {
|
|
1023
|
+
return Object.fromEntries(files
|
|
1024
|
+
.filter(({ shadowedBy }) => shadowedBy === undefined)
|
|
1025
|
+
.map(({ displayPath, sha256 }) => [displayPath, sha256])
|
|
1026
|
+
.sort(([left], [right]) => compareCodeUnits(left, right)));
|
|
1027
|
+
}
|
|
1028
|
+
function helperCatalogFingerprint(snapshot) {
|
|
1029
|
+
return hashCanonical(snapshot.index
|
|
1030
|
+
.findAll('scripted_effect')
|
|
1031
|
+
.filter(({ overridden, sourceShadowed }) => !overridden && !sourceShadowed)
|
|
1032
|
+
.map(({ id, path }) => ({ id, path }))
|
|
1033
|
+
.sort((left, right) => compareCodeUnits(left.id, right.id) || compareCodeUnits(left.path, right.path)));
|
|
1034
|
+
}
|
|
1035
|
+
function fragmentsFor(snapshot, options) {
|
|
1036
|
+
const helperIds = new Set(snapshot.index
|
|
1037
|
+
.findAll('scripted_effect')
|
|
1038
|
+
.filter(({ overridden, sourceShadowed }) => !overridden && !sourceShadowed)
|
|
1039
|
+
.map(({ id }) => id));
|
|
1040
|
+
const context = {
|
|
1041
|
+
helperIds,
|
|
1042
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
1043
|
+
};
|
|
1044
|
+
const fingerprint = helperCatalogFingerprint(snapshot);
|
|
1045
|
+
const fragments = [];
|
|
1046
|
+
for (const file of technologySourceFiles(snapshot.files)) {
|
|
1047
|
+
options.signal?.throwIfAborted();
|
|
1048
|
+
const key = technologySourceFragmentCacheKey(file, fingerprint);
|
|
1049
|
+
let fragment = options.cache?.get(key);
|
|
1050
|
+
if (fragment === undefined) {
|
|
1051
|
+
fragment = analyzeTechnologySource(file, context);
|
|
1052
|
+
options.cache?.set(key, fragment, file.bytes.length);
|
|
1053
|
+
}
|
|
1054
|
+
fragments.push(fragment);
|
|
1055
|
+
}
|
|
1056
|
+
return fragments;
|
|
1057
|
+
}
|
|
1058
|
+
function resolveUnlocks(unlocks, targets, complete) {
|
|
1059
|
+
const keys = new Set(targets.map(({ kind, targetId }) => `${kind}:${targetId}`));
|
|
1060
|
+
return unlocks.map((unlock) => ({
|
|
1061
|
+
...unlock,
|
|
1062
|
+
resolved: keys.has(`${unlock.kind}:${unlock.targetId}`) ? true : complete ? false : 'unknown',
|
|
1063
|
+
}));
|
|
1064
|
+
}
|
|
1065
|
+
export function buildTechnologyGraph(snapshot, options) {
|
|
1066
|
+
options.signal?.throwIfAborted();
|
|
1067
|
+
const fragments = fragmentsFor(snapshot, options);
|
|
1068
|
+
const assets = assetMap(options.assetFiles ?? []);
|
|
1069
|
+
const selectedTechnologies = activeDefinitions(fragments.flatMap(({ technologies }) => technologies));
|
|
1070
|
+
const technologies = localiseTechnologies(selectedTechnologies.active, snapshot.index, assets);
|
|
1071
|
+
const technologyById = new Map(technologies.map((technology) => [technology.id, technology]));
|
|
1072
|
+
const selectedFolders = activeDefinitions(fragments.flatMap(({ folders }) => folders));
|
|
1073
|
+
const folders = localiseFolders(selectedFolders.active, snapshot.index);
|
|
1074
|
+
const gridboxes = activeGridboxes(fragments.flatMap(({ gridboxes }) => gridboxes));
|
|
1075
|
+
const categories = activeCategories(fragments.flatMap(({ categories }) => categories));
|
|
1076
|
+
const selectedDoctrines = activeDefinitions(fragments.flatMap(({ doctrineDefinitions }) => doctrineDefinitions));
|
|
1077
|
+
const doctrineDefinitions = localiseDoctrines(selectedDoctrines.active, snapshot.index, assets);
|
|
1078
|
+
const selectedTargets = activeDefinitions(fragments
|
|
1079
|
+
.flatMap(({ unlockTargets }) => unlockTargets)
|
|
1080
|
+
.filter((target) => target.source !== undefined));
|
|
1081
|
+
const unlockTargets = selectedTargets.active;
|
|
1082
|
+
const basePlacements = selectOwnedRecords(fragments.flatMap(({ placements }) => placements), ({ technologyId }) => technologyId, ({ location }) => location, technologyById).sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
1083
|
+
const edges = selectOwnedRecords(fragments.flatMap(({ edges }) => edges), ({ from }) => from, ({ location }) => location, technologyById).sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
1084
|
+
const placements = enrichPlacements(basePlacements, edges, gridboxes, options.signal);
|
|
1085
|
+
const unlocks = resolveUnlocks(selectOwnedRecords(fragments.flatMap(({ unlocks }) => unlocks), ({ technologyId }) => technologyId, ({ location }) => location, technologyById).sort((left, right) => compareCodeUnits(left.id, right.id)), unlockTargets, snapshot.complete);
|
|
1086
|
+
const unresolved = fragments.flatMap(({ unresolved }) => unresolved);
|
|
1087
|
+
const helperCalls = fragments.flatMap(({ helperCalls }) => helperCalls);
|
|
1088
|
+
const externalReferences = expandHelperReferences(fragments.flatMap(({ externalReferences }) => externalReferences), helperCalls, unresolved, options.signal);
|
|
1089
|
+
const issues = diagnose(technologies, folders, placements, gridboxes, edges, categories, doctrineDefinitions, unlocks, externalReferences, selectedTechnologies.duplicates.map((group) => group.map(({ iconSprite, effectSignatureFields, ...technology }) => ({
|
|
1090
|
+
...technology,
|
|
1091
|
+
icon: iconStatus(iconSprite, snapshot.index, assets),
|
|
1092
|
+
localisation: {
|
|
1093
|
+
language: 'l_english',
|
|
1094
|
+
nameKey: technology.id,
|
|
1095
|
+
descriptionKey: `${technology.id}_desc`,
|
|
1096
|
+
status: 'missing',
|
|
1097
|
+
},
|
|
1098
|
+
effectSignature: hashCanonical(effectSignatureFields),
|
|
1099
|
+
}))), unresolved, options.signal);
|
|
1100
|
+
const allFiles = [...snapshot.files, ...(options.assetFiles ?? [])];
|
|
1101
|
+
const hashes = sourceHashes(allFiles);
|
|
1102
|
+
const revision = hashCanonical({
|
|
1103
|
+
scanRevision: snapshot.revision,
|
|
1104
|
+
sourceHashes: hashes,
|
|
1105
|
+
technologyIds: technologies.map(({ id }) => id),
|
|
1106
|
+
});
|
|
1107
|
+
const diagnostics = sortDiagnostics([
|
|
1108
|
+
...snapshot.diagnostics,
|
|
1109
|
+
...technologyAnalysisDiagnostics(fragments),
|
|
1110
|
+
]);
|
|
1111
|
+
const graph = {
|
|
1112
|
+
schemaVersion: TECHNOLOGY_GRAPH_SCHEMA_VERSION,
|
|
1113
|
+
parserVersion: TECHNOLOGY_PARSER_VERSION,
|
|
1114
|
+
workspaceId: snapshot.workspaceId,
|
|
1115
|
+
workspaceIdentity: options.workspaceIdentity,
|
|
1116
|
+
revision,
|
|
1117
|
+
complete: snapshot.complete && unresolved.every(({ kind }) => kind !== 'partial_source'),
|
|
1118
|
+
analysisBoundary: {
|
|
1119
|
+
staticAnalysis: true,
|
|
1120
|
+
language: 'l_english',
|
|
1121
|
+
loadOrder: 'game -> dependencies -> active mod; same-path files are shadowed and duplicate active IDs are retained as diagnostics',
|
|
1122
|
+
generatedLayoutsLabelled: true,
|
|
1123
|
+
unsupportedConstructs: [
|
|
1124
|
+
'runtime-evaluated visibility and allow triggers',
|
|
1125
|
+
'meta-generated or variable technology identifiers',
|
|
1126
|
+
'exact AI research choice and runtime research time',
|
|
1127
|
+
'dynamic scripted localisation without supplied runtime values',
|
|
1128
|
+
],
|
|
1129
|
+
assumptions: [
|
|
1130
|
+
'classic technology paths point from the defining technology to leads_to_tech',
|
|
1131
|
+
'GFX_<technology>_medium is the default technology icon sprite',
|
|
1132
|
+
'file-local @ constants are resolved only when they reduce to numeric literals',
|
|
1133
|
+
],
|
|
1134
|
+
},
|
|
1135
|
+
sourceHashes: hashes,
|
|
1136
|
+
filesScanned: Object.keys(hashes).sort(compareCodeUnits),
|
|
1137
|
+
skippedSourceCount: snapshot.skippedSourceCount,
|
|
1138
|
+
skippedSources: snapshot.skippedSources,
|
|
1139
|
+
technologies,
|
|
1140
|
+
folders,
|
|
1141
|
+
placements,
|
|
1142
|
+
gridboxes,
|
|
1143
|
+
edges,
|
|
1144
|
+
categories,
|
|
1145
|
+
doctrineDefinitions,
|
|
1146
|
+
unlockTargets,
|
|
1147
|
+
unlocks,
|
|
1148
|
+
externalReferences,
|
|
1149
|
+
helperCalls: helperCalls.sort((left, right) => compareCodeUnits(left.id, right.id)),
|
|
1150
|
+
issues,
|
|
1151
|
+
diagnostics,
|
|
1152
|
+
unresolved: unresolved.sort((left, right) => compareCodeUnits(left.id, right.id)),
|
|
1153
|
+
statistics: {
|
|
1154
|
+
technologyCount: technologies.filter(({ kind }) => kind === 'technology').length,
|
|
1155
|
+
legacyDoctrineCount: technologies.filter(({ kind }) => kind === 'legacy_doctrine').length,
|
|
1156
|
+
folderCount: folders.length,
|
|
1157
|
+
placementCount: placements.length,
|
|
1158
|
+
gridboxCount: gridboxes.length,
|
|
1159
|
+
prerequisiteCount: edges.filter(({ kind }) => kind === 'prerequisite').length,
|
|
1160
|
+
exclusiveCount: edges.filter(({ kind }) => kind === 'exclusive').length,
|
|
1161
|
+
categoryCount: categories.length,
|
|
1162
|
+
doctrineDefinitionCount: doctrineDefinitions.length,
|
|
1163
|
+
unlockCount: unlocks.length,
|
|
1164
|
+
externalReferenceCount: externalReferences.length,
|
|
1165
|
+
issueCount: issues.length,
|
|
1166
|
+
unresolvedCount: unresolved.length,
|
|
1167
|
+
},
|
|
1168
|
+
};
|
|
1169
|
+
assertLimits(graph);
|
|
1170
|
+
options.signal?.throwIfAborted();
|
|
1171
|
+
return graph;
|
|
1172
|
+
}
|
|
1173
|
+
export function technologyAssetPatterns(graph) {
|
|
1174
|
+
return [
|
|
1175
|
+
...new Set([
|
|
1176
|
+
...graph.technologies.flatMap(({ icon }) => icon.texturePath ?? []),
|
|
1177
|
+
...graph.doctrineDefinitions.flatMap(({ icon }) => icon?.texturePath ?? []),
|
|
1178
|
+
]
|
|
1179
|
+
.map((value) => value
|
|
1180
|
+
.replaceAll('\\', '/')
|
|
1181
|
+
.replace(/\/{2,}/gu, '/')
|
|
1182
|
+
.replace(/^\.\//u, ''))
|
|
1183
|
+
.filter((value) => value.length > 0 && !value.includes('..') && !/[[\]{}*?!]/u.test(value))),
|
|
1184
|
+
].sort(compareCodeUnits);
|
|
1185
|
+
}
|
|
1186
|
+
export function technologyGraphSourceFiles(snapshot, assetFiles = []) {
|
|
1187
|
+
return [...technologySourceFiles(snapshot.files), ...assetFiles].sort((left, right) => compareCodeUnits(left.displayPath, right.displayPath));
|
|
1188
|
+
}
|
|
1189
|
+
export function technologyGraphRootKinds(graph) {
|
|
1190
|
+
const kinds = new Set();
|
|
1191
|
+
for (const technology of graph.technologies)
|
|
1192
|
+
kinds.add(technology.source.rootKind);
|
|
1193
|
+
for (const folder of graph.folders)
|
|
1194
|
+
kinds.add(folder.source.rootKind);
|
|
1195
|
+
return [...kinds].sort(compareCodeUnits);
|
|
1196
|
+
}
|