hoi4-agent-tools 3.3.0 → 3.4.1
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 +22 -0
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +20 -8
- package/dist/hoi4_agent_tools/core/helper-expansion.js +3 -0
- 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/event/comparison-selection.js +78 -0
- package/dist/hoi4_agent_tools/event/helper-expansion.js +48 -0
- package/dist/hoi4_agent_tools/event/operations.js +32 -10
- package/dist/hoi4_agent_tools/event/service.js +21 -2
- package/dist/hoi4_agent_tools/focus/compact.js +241 -27
- package/dist/hoi4_agent_tools/focus/layout.js +119 -8
- package/dist/hoi4_agent_tools/focus/render.js +62 -0
- package/dist/hoi4_agent_tools/focus/scenario.js +99 -0
- package/dist/hoi4_agent_tools/focus/tool-operations.js +30 -4
- package/dist/hoi4_agent_tools/gui/assets.js +54 -4
- package/dist/hoi4_agent_tools/gui/layout.js +935 -104
- package/dist/hoi4_agent_tools/gui/renderer.js +47 -17
- package/dist/hoi4_agent_tools/gui/shader.js +515 -0
- package/dist/hoi4_agent_tools/gui/source-graph.js +166 -6
- package/dist/hoi4_agent_tools/gui/studio.js +245 -19
- package/dist/hoi4_agent_tools/gui/tool-operations.js +9 -0
- package/dist/hoi4_agent_tools/gui/validators.js +41 -12
- package/dist/hoi4_agent_tools/map/operations.js +70 -1
- package/dist/hoi4_agent_tools/map/render.js +70 -1
- package/dist/hoi4_agent_tools/map/service.js +30 -6
- package/dist/hoi4_agent_tools/map/tool-operations.js +67 -1
- package/dist/hoi4_agent_tools/mcp/tools/event.js +10 -1
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +14 -4
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +5 -1
- package/dist/hoi4_agent_tools/mcp/tools/map.js +12 -2
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +5 -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/evaluation.js +3 -0
- package/dist/hoi4_agent_tools/probability/operations.js +8 -0
- package/dist/hoi4_agent_tools/probability/service.js +33 -12
- package/dist/hoi4_agent_tools/probability/source-analysis.js +1 -0
- package/dist/hoi4_agent_tools/schemas/event.js +8 -0
- package/dist/hoi4_agent_tools/schemas/focus-requests.js +40 -1
- package/dist/hoi4_agent_tools/schemas/gui-requests.js +8 -0
- package/dist/hoi4_agent_tools/schemas/map-requests.js +22 -1
- package/dist/hoi4_agent_tools/schemas/technology.js +7 -0
- package/dist/hoi4_agent_tools/technology/graph.js +25 -10
- package/dist/hoi4_agent_tools/technology/operations.js +29 -1
- package/dist/hoi4_agent_tools/technology/render.js +287 -31
- package/dist/hoi4_agent_tools/technology/service.js +56 -8
- package/dist/hoi4_agent_tools/technology/source-analysis.js +166 -3
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/events.md +4 -2
- package/docs/focus.md +10 -0
- package/docs/gui.md +50 -3
- package/docs/jobs.md +1 -0
- package/docs/map.md +8 -0
- package/docs/mechanics.md +2 -1
- package/docs/probability.md +3 -1
- package/docs/technology.md +11 -2
- package/package.json +1 -1
- package/schemas/configuration.schema.json +1 -1
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/helper-expansion-request.schema.json +1 -1
- package/schemas/helper-expansion-summary.schema.json +1 -1
- package/schemas/impact-inspect-input.schema.json +1 -1
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/mechanic-test-input.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +1 -1
- package/schemas/package-manifest.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/schemas/scenario-suite.schema.json +1 -1
- package/schemas/scenario-test-input.schema.json +1 -1
- package/schemas/shared-scenario.schema.json +1 -1
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 3.4.1 - 2026-09-25
|
|
6
|
+
|
|
7
|
+
- Validate technology renders against the requested view's source placement, node coverage, and sprite resolution without inheriting unrelated graph-wide diagnostics, while reporting deferred whole-workspace helper projection as a separate analysis boundary; resolve subtechnology art from the source GUI slot before using a technology-id fallback and include every referenced slot texture in the asset scan.
|
|
8
|
+
- Accept focused event inspection and render evidence when every event source was scanned and no blocking diagnostics remain, while retaining deferred workspace-wide helper and lifecycle projections as an explicit boundary.
|
|
9
|
+
- Keep repeated window-local GUI control names additive in the shared index, exclude engine-owned compound-control roles from scripted click-effect requirements, and preserve compact operation summaries when linked evidence exceeds the MCP wire budget.
|
|
10
|
+
- Parse each active scripted-effect source document once when building mechanic helper inventories so bounded real-workspace scenarios do not multiply parser state by the number of helpers in a file.
|
|
11
|
+
- Interpret declared temporary-variable assignment, arithmetic, rounding, clamping, and scope-valued inputs with chain-wide unscoped semantics in bounded mechanic scenarios.
|
|
12
|
+
- Evaluate `has_variable` against declared scenario state so guarded defaults and subsequent temporary-variable arithmetic remain deterministic.
|
|
13
|
+
- Follow a live replacement worker through its terminal publication instead of returning a nonterminal job record during concurrent checkpoint recovery.
|
|
14
|
+
|
|
15
|
+
## 3.4.0 - 2026-09-20
|
|
16
|
+
|
|
17
|
+
- Add branch-local focus cleanup with pinned anchors and symmetry groups, connector measurements, source-linked crops, and scenario-backed structural route diagnostics.
|
|
18
|
+
- Report GUI visibility and enabled branches, measured label and background boxes, and matched source-baseline renders alongside scenario comparisons.
|
|
19
|
+
- Expose complete source-backed probability catalogs, actionable missing scenario inputs, and eligible, excluded, and unresolved inventories.
|
|
20
|
+
- Add selected event-chain comparisons and revision-bound bounded helper expansion while retaining explicit lifecycle coverage limits.
|
|
21
|
+
- Compose technology folders from source pixel placements, native item skins, icon and subtechnology positions, panel and background artwork, year labels, and supplied research scenarios; report unresolved placement and asset coverage honestly.
|
|
22
|
+
- Resolve source `GFX_*_medium` artwork and design-team overlays at native item size for chemical and biological warfare folders.
|
|
23
|
+
- Compose installed GUI backgrounds, focus inlays, container scrolling, native button and progress shaders, and scenario-driven rows from source assets and control states.
|
|
24
|
+
- Bound focus connector and overlap diagnostics on anchored trees with thousands of nodes.
|
|
25
|
+
- Add focused map lookups, reusable render tiles, affected-area output, and typed external script-reference updates for map renumbering.
|
|
26
|
+
|
|
5
27
|
## 3.3.0 - 2026-09-20
|
|
6
28
|
|
|
7
29
|
- Add `hoi4.mechanic_test` with copied versioned scenario state, bounded source effects, explicit time advancement, traces, and typed gameplay assertions.
|
|
@@ -96,7 +96,7 @@ function numberValue(expression, scenario, context, definitions, path, seen = ne
|
|
|
96
96
|
}
|
|
97
97
|
return undefined;
|
|
98
98
|
}
|
|
99
|
-
function unresolved(assignment, candidate, reason) {
|
|
99
|
+
function unresolved(assignment, candidate, reason, scenarioInput) {
|
|
100
100
|
return {
|
|
101
101
|
state: 'unresolved',
|
|
102
102
|
unresolved: [
|
|
@@ -104,6 +104,7 @@ function unresolved(assignment, candidate, reason) {
|
|
|
104
104
|
code: 'TRIGGER_UNRESOLVED',
|
|
105
105
|
message: reason,
|
|
106
106
|
path: assignment.key.value,
|
|
107
|
+
...(scenarioInput === undefined ? {} : { details: { scenarioInput } }),
|
|
107
108
|
candidateId: candidate.id,
|
|
108
109
|
...(candidate.provenance[0] === undefined ? {} : { provenance: candidate.provenance[0] }),
|
|
109
110
|
},
|
|
@@ -136,7 +137,11 @@ function evaluateCheckVariable(block, scenario, candidate, assignment, scopeCont
|
|
|
136
137
|
const leftNumber = numberValue(variable, scenario, scopeContext, definitions, path);
|
|
137
138
|
const rightNumber = numberValue(target, scenario, scopeContext, definitions, path);
|
|
138
139
|
if (leftNumber === undefined || rightNumber === undefined)
|
|
139
|
-
return unresolved(assignment, candidate, `Scenario does not resolve numeric operands ${variable} and ${target}
|
|
140
|
+
return unresolved(assignment, candidate, `Scenario does not resolve numeric operands ${variable} and ${target}`, leftNumber === undefined && !/^(?:@|constant:)/u.test(variable)
|
|
141
|
+
? `variable.${variable}`
|
|
142
|
+
: rightNumber === undefined && !/^(?:@|constant:)/u.test(target)
|
|
143
|
+
? `variable.${target}`
|
|
144
|
+
: undefined);
|
|
140
145
|
const compareName = firstScalar(block, 'compare')?.value ??
|
|
141
146
|
(short?.operator.text === '>'
|
|
142
147
|
? 'greater_than'
|
|
@@ -202,7 +207,7 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
|
|
|
202
207
|
const array = scopeStateValue(scenario, `array.${arrayId}`, scopeContext) ??
|
|
203
208
|
rootStateValue(scenario, `array.${arrayId}`);
|
|
204
209
|
if (!Array.isArray(array))
|
|
205
|
-
return unresolved(assignment, candidate, `Scenario does not declare array ${arrayId}`);
|
|
210
|
+
return unresolved(assignment, candidate, `Scenario does not declare array ${arrayId}`, `array.${arrayId}`);
|
|
206
211
|
const compared = ['THIS', 'ROOT', 'PREV'].includes(declaredValue.toUpperCase()) ||
|
|
207
212
|
resolveScopeContext(scenario, declaredValue, scopeContext) !== undefined
|
|
208
213
|
? scopeIdentity(resolveScopeContext(scenario, declaredValue, scopeContext) ?? scopeContext)
|
|
@@ -244,7 +249,7 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
|
|
|
244
249
|
const declaredScope = bool(rootStateValue(scenario, `scope.${key}`));
|
|
245
250
|
if (declaredScope !== undefined)
|
|
246
251
|
return { state: declaredScope ? 'true' : 'false', unresolved: [] };
|
|
247
|
-
return unresolved(assignment, candidate, `Scoped or compound trigger ${key} is not declared by the scenario`);
|
|
252
|
+
return unresolved(assignment, candidate, `Scoped or compound trigger ${key} is not declared by the scenario`, `scope.${key}`);
|
|
248
253
|
}
|
|
249
254
|
const right = assignment.value.value;
|
|
250
255
|
if (key === 'always') {
|
|
@@ -263,7 +268,7 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
|
|
|
263
268
|
? scenario.flags
|
|
264
269
|
: scopeContext.binding.flags;
|
|
265
270
|
if (scenario.closedFlags === false && flags?.includes(right) !== true)
|
|
266
|
-
return unresolved(assignment, candidate, `Scenario does not declare flag ${right}`);
|
|
271
|
+
return unresolved(assignment, candidate, `Scenario does not declare flag ${right}`, `flag.${right}`);
|
|
267
272
|
return { state: flags?.includes(right) === true ? 'true' : 'false', unresolved: [] };
|
|
268
273
|
}
|
|
269
274
|
if (key === 'has_event_target') {
|
|
@@ -273,12 +278,19 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
|
|
|
273
278
|
unresolved: [],
|
|
274
279
|
};
|
|
275
280
|
}
|
|
281
|
+
if (key === 'has_variable') {
|
|
282
|
+
const state = scopeContext.expression === 'ROOT' ? scenario.state : scopeContext.binding.state;
|
|
283
|
+
return {
|
|
284
|
+
state: Object.hasOwn(state, right) || Object.hasOwn(state, `variable.${right}`) ? 'true' : 'false',
|
|
285
|
+
unresolved: [],
|
|
286
|
+
};
|
|
287
|
+
}
|
|
276
288
|
if (key === 'tag' || key === 'original_tag') {
|
|
277
289
|
const actor = scopeContext.expression === 'ROOT'
|
|
278
290
|
? scenario.actor
|
|
279
291
|
: (scopeContext.binding.actor ?? scopeContext.binding.id);
|
|
280
292
|
if (actor === undefined)
|
|
281
|
-
return unresolved(assignment, candidate, `Scenario does not declare actor for ${key}
|
|
293
|
+
return unresolved(assignment, candidate, `Scenario does not declare actor for ${key}`, 'actor');
|
|
282
294
|
return {
|
|
283
295
|
state: compare(actor, right, assignment.operator.text) ? 'true' : 'false',
|
|
284
296
|
unresolved: [],
|
|
@@ -286,7 +298,7 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
|
|
|
286
298
|
}
|
|
287
299
|
if (key === 'date') {
|
|
288
300
|
if (scenario.date === undefined)
|
|
289
|
-
return unresolved(assignment, candidate, 'Scenario does not declare date');
|
|
301
|
+
return unresolved(assignment, candidate, 'Scenario does not declare date', 'date');
|
|
290
302
|
const dateNumber = (value) => {
|
|
291
303
|
const parts = /^(\d+)\.(\d{1,2})\.(\d{1,2})$/u.exec(value);
|
|
292
304
|
if (parts === null)
|
|
@@ -336,7 +348,7 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
|
|
|
336
348
|
const declared = scopeStateValue(scenario, key, scopeContext) ??
|
|
337
349
|
numberValue(key, scenario, scopeContext, definitions, candidate.provenance[0]?.path);
|
|
338
350
|
if (declared === undefined)
|
|
339
|
-
return unresolved(assignment, candidate, `Scenario does not declare trigger ${key}
|
|
351
|
+
return unresolved(assignment, candidate, `Scenario does not declare trigger ${key}`, key);
|
|
340
352
|
const expectedBoolean = bool(right);
|
|
341
353
|
const actualBoolean = bool(declared);
|
|
342
354
|
if (expectedBoolean !== undefined && actualBoolean !== undefined)
|
|
@@ -104,6 +104,9 @@ export async function inspectHelperExpansion(engine, workspaceId, inventory, req
|
|
|
104
104
|
kind,
|
|
105
105
|
rootId: input.roots[root],
|
|
106
106
|
path: path.map((index) => inventory.edges[index].id),
|
|
107
|
+
...(inventory.recordDetails === undefined
|
|
108
|
+
? {}
|
|
109
|
+
: inventory.recordDetails(path.map((index) => inventory.edges[index]))),
|
|
107
110
|
}));
|
|
108
111
|
const continuationName = `${inventory.domain}-helper-continuation.json`;
|
|
109
112
|
const report = {
|
|
@@ -1369,6 +1369,7 @@ export class SymbolIndex {
|
|
|
1369
1369
|
this.#active.set(key, active);
|
|
1370
1370
|
const additiveCategory = active.kind === 'decision_category' ||
|
|
1371
1371
|
active.kind === 'technology_category' ||
|
|
1372
|
+
active.kind === 'gui_element' ||
|
|
1372
1373
|
active.kind === 'texture';
|
|
1373
1374
|
for (const candidate of group) {
|
|
1374
1375
|
candidate.overridden = additiveCategory ? candidate.sourceShadowed : candidate !== active;
|
|
@@ -73,7 +73,12 @@ export class JobWorkerHost {
|
|
|
73
73
|
for (;;) {
|
|
74
74
|
try {
|
|
75
75
|
await this.scheduler.run(this.owner, 1024, signal, () => this.capacity.run(signal, (lease) => this.launch(workspaceId, id, lease, principal)));
|
|
76
|
-
|
|
76
|
+
const current = await this.jobs.get(workspaceId, id, principal);
|
|
77
|
+
if (['completed', 'cancelled', 'failed'].includes(current.status))
|
|
78
|
+
return current;
|
|
79
|
+
if (current.owner !== undefined && jobOwnerLiveness(current.owner) === 'alive')
|
|
80
|
+
return await this.followOwner(workspaceId, id, principal);
|
|
81
|
+
throw new ServiceError('JOB_WORKER_EXIT', 'The worker stopped without publishing a terminal job outcome');
|
|
77
82
|
}
|
|
78
83
|
catch (error) {
|
|
79
84
|
const current = await this.jobs.get(workspaceId, id, principal);
|
|
@@ -108,6 +113,19 @@ export class JobWorkerHost {
|
|
|
108
113
|
}
|
|
109
114
|
}
|
|
110
115
|
}
|
|
116
|
+
async followOwner(workspaceId, id, principal) {
|
|
117
|
+
for (;;) {
|
|
118
|
+
const record = await this.jobs.get(workspaceId, id, principal);
|
|
119
|
+
if (['completed', 'failed', 'cancelled'].includes(record.status))
|
|
120
|
+
return record;
|
|
121
|
+
const liveness = record.owner === undefined ? 'dead' : jobOwnerLiveness(record.owner);
|
|
122
|
+
if (liveness !== 'alive')
|
|
123
|
+
throw new ServiceError(liveness === 'unknown' ? 'JOB_OWNER_UNRESOLVED' : 'JOB_WORKER_EXIT', liveness === 'unknown'
|
|
124
|
+
? 'The replacement execution owner cannot be verified on this host'
|
|
125
|
+
: 'The replacement worker stopped without publishing a terminal job outcome');
|
|
126
|
+
await delay(100);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
111
129
|
async launch(workspaceId, id, lease, principal) {
|
|
112
130
|
// Resolve the same configured workspace and grants again immediately before dispatch.
|
|
113
131
|
await this.jobs.get(workspaceId, id, principal);
|
|
@@ -114,6 +114,9 @@ function boundedResult(result, actualBytes) {
|
|
|
114
114
|
: 'Tool output exceeded the wire budget; nonessential inline details were omitted',
|
|
115
115
|
details: { actualBytes, maxBytes: MAX_TOOL_RESULT_BYTES },
|
|
116
116
|
};
|
|
117
|
+
const compactData = result.data !== null && typeof result.data === 'object' && !Array.isArray(result.data)
|
|
118
|
+
? result.data
|
|
119
|
+
: {};
|
|
117
120
|
return {
|
|
118
121
|
status: result.status,
|
|
119
122
|
code: result.code.slice(0, 256),
|
|
@@ -141,7 +144,7 @@ function boundedResult(result, actualBytes) {
|
|
|
141
144
|
},
|
|
142
145
|
},
|
|
143
146
|
],
|
|
144
|
-
data: {},
|
|
147
|
+
data: jsonBytesWithin(compactData, 2_048) ? compactData : {},
|
|
145
148
|
};
|
|
146
149
|
}
|
|
147
150
|
function jsonStringBytes(value) {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { compareCodeUnits } from '../core/canonical.js';
|
|
2
|
+
import { traceEventGraph } from './algorithms.js';
|
|
3
|
+
import { selectEventNodes } from './queries.js';
|
|
4
|
+
/** Project a whole-graph comparison onto the downstream chain in each revision. */
|
|
5
|
+
export function selectEventComparison(before, after, comparison, selector, maxChainNodes, signal) {
|
|
6
|
+
const trace = (graph) => {
|
|
7
|
+
const starts = selectEventNodes(graph, selector, signal).map(({ id }) => id);
|
|
8
|
+
return traceEventGraph(graph, starts, {
|
|
9
|
+
maxDepth: 64,
|
|
10
|
+
maxNodes: maxChainNodes,
|
|
11
|
+
maxEdges: Math.max(1, Math.min(20_000, graph.edges.length)),
|
|
12
|
+
direction: 'downstream',
|
|
13
|
+
expandHelpers: true,
|
|
14
|
+
}, signal);
|
|
15
|
+
};
|
|
16
|
+
const beforeTrace = trace(before);
|
|
17
|
+
const afterTrace = trace(after);
|
|
18
|
+
const beforeNodeIds = beforeTrace.nodes.map(({ id }) => id).sort(compareCodeUnits);
|
|
19
|
+
const afterNodeIds = afterTrace.nodes.map(({ id }) => id).sort(compareCodeUnits);
|
|
20
|
+
const selectedNodes = new Set([...beforeNodeIds, ...afterNodeIds]);
|
|
21
|
+
const edges = new Map([...before.edges, ...after.edges].map((edge) => [edge.id, edge]));
|
|
22
|
+
const accesses = new Map([...before.stateAccesses, ...after.stateAccesses].map((access) => [access.id, access]));
|
|
23
|
+
const links = new Map([...before.stateLinks, ...after.stateLinks].map((link) => [link.id, link]));
|
|
24
|
+
const unresolved = new Map([...before.unresolved, ...after.unresolved].map((item) => [item.id, item]));
|
|
25
|
+
const association = (change) => {
|
|
26
|
+
const { kind, subjectId } = change;
|
|
27
|
+
if (kind.startsWith('node_') || kind === 'caller_removed' || kind.endsWith('_disconnected'))
|
|
28
|
+
return selectedNodes.has(subjectId);
|
|
29
|
+
if (kind.startsWith('edge_')) {
|
|
30
|
+
const edge = edges.get(subjectId);
|
|
31
|
+
return edge === undefined
|
|
32
|
+
? undefined
|
|
33
|
+
: selectedNodes.has(edge.from) || selectedNodes.has(edge.to);
|
|
34
|
+
}
|
|
35
|
+
if (kind.startsWith('state_access_')) {
|
|
36
|
+
const access = accesses.get(subjectId);
|
|
37
|
+
return access === undefined ? undefined : selectedNodes.has(access.ownerId);
|
|
38
|
+
}
|
|
39
|
+
if (kind.startsWith('state_link_')) {
|
|
40
|
+
const link = links.get(subjectId);
|
|
41
|
+
if (link === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
const producer = accesses.get(link.producerId);
|
|
44
|
+
const consumer = accesses.get(link.consumerId);
|
|
45
|
+
return producer === undefined && consumer === undefined
|
|
46
|
+
? undefined
|
|
47
|
+
: (producer !== undefined && selectedNodes.has(producer.ownerId)) ||
|
|
48
|
+
(consumer !== undefined && selectedNodes.has(consumer.ownerId));
|
|
49
|
+
}
|
|
50
|
+
if (kind.startsWith('unresolved_')) {
|
|
51
|
+
const item = unresolved.get(subjectId);
|
|
52
|
+
return item?.ownerId === undefined ? undefined : selectedNodes.has(item.ownerId);
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
};
|
|
56
|
+
const selectedChangeIds = [];
|
|
57
|
+
const unattributedChangeIds = [];
|
|
58
|
+
for (const [index, change] of comparison.changes.entries()) {
|
|
59
|
+
if ((index & 255) === 0)
|
|
60
|
+
signal?.throwIfAborted();
|
|
61
|
+
const relevant = association(change);
|
|
62
|
+
if (relevant === true)
|
|
63
|
+
selectedChangeIds.push(change.id);
|
|
64
|
+
else if (relevant === undefined)
|
|
65
|
+
unattributedChangeIds.push(change.id);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
selector,
|
|
69
|
+
maxChainNodes,
|
|
70
|
+
beforeNodeIds,
|
|
71
|
+
afterNodeIds,
|
|
72
|
+
selectedChangeIds,
|
|
73
|
+
selectedChangeCount: selectedChangeIds.length,
|
|
74
|
+
omittedChangeCount: comparison.changes.length - selectedChangeIds.length,
|
|
75
|
+
unattributedChangeIds,
|
|
76
|
+
truncated: beforeTrace.truncated || afterTrace.truncated,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -2,6 +2,14 @@ import { compareCodeUnits } from '../core/canonical.js';
|
|
|
2
2
|
export function eventHelperInventory(graph) {
|
|
3
3
|
const helpers = new Set(graph.nodes.filter(({ kind }) => kind === 'helper').map(({ id }) => id));
|
|
4
4
|
const structural = graph.edges.filter(({ derived }) => !derived);
|
|
5
|
+
const structuralById = new Map(structural.map((edge) => [edge.id, edge]));
|
|
6
|
+
const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
7
|
+
const optionParents = new Map(graph.edges
|
|
8
|
+
.filter(({ reason }) => reason === 'option_branch')
|
|
9
|
+
.map(({ from, to }) => [to, from]));
|
|
10
|
+
const accessById = new Map(graph.stateAccesses
|
|
11
|
+
.filter(({ ownerId }) => helpers.has(ownerId))
|
|
12
|
+
.map((access) => [`state-access:${access.id}`, access]));
|
|
5
13
|
return {
|
|
6
14
|
domain: 'event',
|
|
7
15
|
sourceRevision: graph.revision,
|
|
@@ -29,5 +37,45 @@ export function eventHelperInventory(graph) {
|
|
|
29
37
|
.map(({ id }) => id)
|
|
30
38
|
.sort(compareCodeUnits),
|
|
31
39
|
branches: [...helpers].sort(compareCodeUnits),
|
|
40
|
+
recordDetails: (path) => {
|
|
41
|
+
let current = 'unknown';
|
|
42
|
+
const root = structuralById.get(path[0]?.id ?? '');
|
|
43
|
+
if (root !== undefined) {
|
|
44
|
+
const caller = nodesById.get(optionParents.get(root.from) ?? root.from);
|
|
45
|
+
const candidate = root.scope?.source ?? caller?.metadata.expectedScope ?? caller?.metadata.scope;
|
|
46
|
+
if (candidate === 'country' ||
|
|
47
|
+
candidate === 'state' ||
|
|
48
|
+
candidate === 'province' ||
|
|
49
|
+
candidate === 'unit_leader' ||
|
|
50
|
+
candidate === 'operative' ||
|
|
51
|
+
candidate === 'character' ||
|
|
52
|
+
candidate === 'global')
|
|
53
|
+
current = candidate;
|
|
54
|
+
}
|
|
55
|
+
const transitions = [];
|
|
56
|
+
for (const { id } of path) {
|
|
57
|
+
const edge = structuralById.get(id);
|
|
58
|
+
if (edge?.scope !== undefined) {
|
|
59
|
+
const { source, destination, confidence } = edge.scope;
|
|
60
|
+
current =
|
|
61
|
+
current !== 'unknown' && source !== 'unknown' && current !== source
|
|
62
|
+
? 'unknown'
|
|
63
|
+
: destination;
|
|
64
|
+
transitions.push({ edgeId: id, source, destination: current, confidence });
|
|
65
|
+
}
|
|
66
|
+
const access = accessById.get(id);
|
|
67
|
+
if (access !== undefined) {
|
|
68
|
+
return {
|
|
69
|
+
scopeEvidence: {
|
|
70
|
+
stateAccessId: access.id,
|
|
71
|
+
declaredScope: access.scope,
|
|
72
|
+
resolvedScope: access.scope === 'unknown' ? current : access.scope,
|
|
73
|
+
transitions,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { scopeEvidence: { resolvedScope: current, transitions } };
|
|
79
|
+
},
|
|
32
80
|
};
|
|
33
81
|
}
|
|
@@ -37,21 +37,27 @@ export function eventGraphCounts(graph, artifacts) {
|
|
|
37
37
|
}
|
|
38
38
|
export function eventGraphValidation(graph) {
|
|
39
39
|
const counts = eventGraphCounts(graph, 0);
|
|
40
|
+
const analysisBoundaryPassed = graph.complete || (graph.analysisMode === 'focused' && graph.skippedSourceCount === 0);
|
|
41
|
+
const passed = analysisBoundaryPassed && counts.blockingDiagnostics === 0;
|
|
40
42
|
return {
|
|
41
|
-
passed
|
|
43
|
+
passed,
|
|
42
44
|
checks: [
|
|
43
45
|
{
|
|
44
46
|
id: 'event-analysis',
|
|
45
|
-
passed
|
|
46
|
-
message:
|
|
47
|
-
?
|
|
47
|
+
passed,
|
|
48
|
+
message: counts.blockingDiagnostics > 0
|
|
49
|
+
? `${counts.blockingDiagnostics} blocking event-chain diagnostics; full evidence is linked`
|
|
48
50
|
: graph.complete
|
|
49
|
-
?
|
|
50
|
-
: graph.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
? 'The event-chain analysis is complete and has no blocking diagnostics'
|
|
52
|
+
: graph.analysisMode === 'focused' && graph.skippedSourceCount === 0
|
|
53
|
+
? 'Whole-workspace helper projections and lifecycle passes were deferred; the requested event analysis retains direct source evidence'
|
|
54
|
+
: graph.analysisMode === 'focused'
|
|
55
|
+
? `${graph.skippedSourceCount} event-analysis source(s) were skipped; linked evidence records the partial boundary`
|
|
56
|
+
: graph.issues.some(({ code }) => code === 'EVENT_HELPER_DEPTH_LIMIT' ||
|
|
57
|
+
code === 'EVENT_HELPER_PROJECTION_LIMIT' ||
|
|
58
|
+
code === 'EVENT_HELPER_STATE_PROJECTION_LIMIT')
|
|
59
|
+
? 'Helper expansion reached a depth or materialization boundary; helper_expansion mode provides bounded source-linked continuation'
|
|
60
|
+
: `${graph.skippedSourceCount} event-analysis source(s) were skipped; full evidence is linked`,
|
|
55
61
|
},
|
|
56
62
|
],
|
|
57
63
|
};
|
|
@@ -130,6 +136,15 @@ export async function compareEvents(viewer, input) {
|
|
|
130
136
|
...(compared.render === undefined ? {} : { renderHashes: compared.render.hashes }),
|
|
131
137
|
counts: {
|
|
132
138
|
changes: comparison.changes.length,
|
|
139
|
+
...(compared.selection === undefined
|
|
140
|
+
? {}
|
|
141
|
+
: {
|
|
142
|
+
selectedChanges: compared.selection.selectedChangeCount,
|
|
143
|
+
omittedChanges: compared.selection.omittedChangeCount,
|
|
144
|
+
unattributedChanges: compared.selection.unattributedChangeIds.length,
|
|
145
|
+
selectedBeforeNodes: compared.selection.beforeNodeIds.length,
|
|
146
|
+
selectedAfterNodes: compared.selection.afterNodeIds.length,
|
|
147
|
+
}),
|
|
133
148
|
addedNodes: comparison.addedNodeIds.length,
|
|
134
149
|
removedNodes: comparison.removedNodeIds.length,
|
|
135
150
|
changedNodes: comparison.changedNodeIds.length,
|
|
@@ -155,6 +170,13 @@ export async function compareEvents(viewer, input) {
|
|
|
155
170
|
},
|
|
156
171
|
boundary: {
|
|
157
172
|
proposedSources: input.proposedSources?.length ?? 0,
|
|
173
|
+
...(compared.selection === undefined
|
|
174
|
+
? {}
|
|
175
|
+
: {
|
|
176
|
+
selector: compared.selection.selector,
|
|
177
|
+
maxChainNodes: compared.selection.maxChainNodes,
|
|
178
|
+
selectionTruncated: compared.selection.truncated,
|
|
179
|
+
}),
|
|
158
180
|
render: input.render ?? true,
|
|
159
181
|
maxRenderNodes: input.maxRenderNodes ?? 120,
|
|
160
182
|
refresh: compared.refresh,
|
|
@@ -13,6 +13,7 @@ import { buildEventGraph, buildEventGraphAsync } from './graph.js';
|
|
|
13
13
|
import { EVENT_FRAGMENT_CACHE_MAX_ENTRIES, EVENT_FRAGMENT_CACHE_MAX_SOURCE_BYTES, EVENT_PROPOSED_SOURCE_MAX_BYTES, EVENT_PROPOSED_SOURCE_MAX_FILES, } from './limits.js';
|
|
14
14
|
import { analyzeEventImpact, discoverEventRoots, explainEventPath, inspectEventStateFlow, lintEventGraph, traceSelectedEvents, } from './queries.js';
|
|
15
15
|
import { compareEventGraphs, eventGraphHash } from './compare.js';
|
|
16
|
+
import { selectEventComparison } from './comparison-selection.js';
|
|
16
17
|
import { eventFlowEdges } from './algorithms.js';
|
|
17
18
|
import { validateEventGraphArtifact } from './artifact-validation.js';
|
|
18
19
|
import { renderEventGraph, } from './render.js';
|
|
@@ -1331,16 +1332,29 @@ export class EventChainViewer {
|
|
|
1331
1332
|
after = current;
|
|
1332
1333
|
}
|
|
1333
1334
|
const comparison = compareEventGraphs(before, after, input.signal);
|
|
1335
|
+
const selection = input.selector === undefined
|
|
1336
|
+
? undefined
|
|
1337
|
+
: selectEventComparison(before, after, comparison, input.selector, input.maxChainNodes ?? 1_000, input.signal);
|
|
1338
|
+
if (selection?.beforeNodeIds.length === 0 && selection.afterNodeIds.length === 0)
|
|
1339
|
+
throw new ServiceError('EVENT_COMPARISON_SELECTOR_NOT_FOUND', 'The selector matched no event-chain nodes in either revision');
|
|
1334
1340
|
graphHashCache.set(before, comparison.beforeGraphHash);
|
|
1335
1341
|
graphHashCache.set(after, comparison.afterGraphHash);
|
|
1336
1342
|
let render;
|
|
1337
1343
|
if (input.render !== false) {
|
|
1338
1344
|
const projection = comparisonRenderProjection(before, after, comparison, input.signal);
|
|
1345
|
+
const selectedChainIds = selection === undefined
|
|
1346
|
+
? undefined
|
|
1347
|
+
: new Set([...selection.beforeNodeIds, ...selection.afterNodeIds]);
|
|
1348
|
+
const changedNodeIds = selectedChainIds === undefined
|
|
1349
|
+
? projection.selectedNodeIds
|
|
1350
|
+
: projection.selectedNodeIds.filter((id) => selectedChainIds.has(id));
|
|
1339
1351
|
const selector = {
|
|
1340
1352
|
kind: 'manifest',
|
|
1341
1353
|
manifest: {
|
|
1342
1354
|
id: 'comparison',
|
|
1343
|
-
nodeIds:
|
|
1355
|
+
nodeIds: changedNodeIds.length > 0
|
|
1356
|
+
? changedNodeIds
|
|
1357
|
+
: (selection?.afterNodeIds.slice(0, 1) ?? selection?.beforeNodeIds.slice(0, 1) ?? []),
|
|
1344
1358
|
},
|
|
1345
1359
|
};
|
|
1346
1360
|
render = await renderEventGraph(projection.graph, {
|
|
@@ -1354,7 +1368,7 @@ export class EventChainViewer {
|
|
|
1354
1368
|
});
|
|
1355
1369
|
}
|
|
1356
1370
|
const workspace = this.engine.resolver.get(input.workspaceId, input.principal);
|
|
1357
|
-
const prefix = safeSlug(`event-compare-${before.revision.slice(0, 8)}-${after.revision.slice(0, 8)}`);
|
|
1371
|
+
const prefix = safeSlug(`event-compare-${before.revision.slice(0, 8)}-${after.revision.slice(0, 8)}${selection === undefined ? '' : `-${hashCanonical({ selector: selection.selector, maxChainNodes: selection.maxChainNodes }).slice(0, 8)}`}`);
|
|
1358
1372
|
const resourceNames = {
|
|
1359
1373
|
json: `${prefix}.json`,
|
|
1360
1374
|
...(render === undefined
|
|
@@ -1380,6 +1394,9 @@ export class EventChainViewer {
|
|
|
1380
1394
|
filters: {
|
|
1381
1395
|
...(input.before === undefined ? {} : { before: input.before }),
|
|
1382
1396
|
...(input.after === undefined ? {} : { after: input.after }),
|
|
1397
|
+
...(input.selector === undefined
|
|
1398
|
+
? {}
|
|
1399
|
+
: { selector: input.selector, maxChainNodes: input.maxChainNodes ?? 1_000 }),
|
|
1383
1400
|
proposedSources: input.proposedSources?.map(({ relativePath, source, expectedSourceHash }) => ({
|
|
1384
1401
|
relativePath,
|
|
1385
1402
|
operation: source === null ? 'delete' : 'overlay',
|
|
@@ -1397,6 +1414,7 @@ export class EventChainViewer {
|
|
|
1397
1414
|
evidence: comparisonEntityEvidence(before, after, comparison, input.signal),
|
|
1398
1415
|
resources: resourceNames,
|
|
1399
1416
|
comparison,
|
|
1417
|
+
...(selection === undefined ? {} : { selection }),
|
|
1400
1418
|
})}\n`;
|
|
1401
1419
|
const provenance = eventProvenance(after, 'event-comparison', {
|
|
1402
1420
|
beforeRevision: before.revision,
|
|
@@ -1447,6 +1465,7 @@ export class EventChainViewer {
|
|
|
1447
1465
|
after,
|
|
1448
1466
|
refresh,
|
|
1449
1467
|
comparison,
|
|
1468
|
+
...(selection === undefined ? {} : { selection }),
|
|
1450
1469
|
comparisonJson,
|
|
1451
1470
|
...(render === undefined ? {} : { render }),
|
|
1452
1471
|
artifacts,
|