hoi4-agent-tools 3.3.0 → 3.4.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/hoi4_agent_tools/core/condition-evaluator.js +13 -8
  3. package/dist/hoi4_agent_tools/core/helper-expansion.js +3 -0
  4. package/dist/hoi4_agent_tools/event/comparison-selection.js +78 -0
  5. package/dist/hoi4_agent_tools/event/helper-expansion.js +48 -0
  6. package/dist/hoi4_agent_tools/event/operations.js +16 -0
  7. package/dist/hoi4_agent_tools/event/service.js +21 -2
  8. package/dist/hoi4_agent_tools/focus/compact.js +241 -27
  9. package/dist/hoi4_agent_tools/focus/layout.js +119 -8
  10. package/dist/hoi4_agent_tools/focus/render.js +62 -0
  11. package/dist/hoi4_agent_tools/focus/scenario.js +99 -0
  12. package/dist/hoi4_agent_tools/focus/tool-operations.js +30 -4
  13. package/dist/hoi4_agent_tools/gui/assets.js +54 -4
  14. package/dist/hoi4_agent_tools/gui/layout.js +935 -104
  15. package/dist/hoi4_agent_tools/gui/renderer.js +47 -17
  16. package/dist/hoi4_agent_tools/gui/shader.js +515 -0
  17. package/dist/hoi4_agent_tools/gui/source-graph.js +166 -6
  18. package/dist/hoi4_agent_tools/gui/studio.js +245 -19
  19. package/dist/hoi4_agent_tools/gui/tool-operations.js +9 -0
  20. package/dist/hoi4_agent_tools/gui/validators.js +38 -12
  21. package/dist/hoi4_agent_tools/map/operations.js +70 -1
  22. package/dist/hoi4_agent_tools/map/render.js +70 -1
  23. package/dist/hoi4_agent_tools/map/service.js +30 -6
  24. package/dist/hoi4_agent_tools/map/tool-operations.js +67 -1
  25. package/dist/hoi4_agent_tools/mcp/tools/event.js +10 -1
  26. package/dist/hoi4_agent_tools/mcp/tools/focus.js +14 -4
  27. package/dist/hoi4_agent_tools/mcp/tools/gui.js +5 -1
  28. package/dist/hoi4_agent_tools/mcp/tools/map.js +12 -2
  29. package/dist/hoi4_agent_tools/mcp/tools/probability.js +5 -0
  30. package/dist/hoi4_agent_tools/probability/evaluation.js +3 -0
  31. package/dist/hoi4_agent_tools/probability/operations.js +8 -0
  32. package/dist/hoi4_agent_tools/probability/service.js +33 -12
  33. package/dist/hoi4_agent_tools/probability/source-analysis.js +1 -0
  34. package/dist/hoi4_agent_tools/schemas/event.js +8 -0
  35. package/dist/hoi4_agent_tools/schemas/focus-requests.js +40 -1
  36. package/dist/hoi4_agent_tools/schemas/gui-requests.js +8 -0
  37. package/dist/hoi4_agent_tools/schemas/map-requests.js +22 -1
  38. package/dist/hoi4_agent_tools/schemas/technology.js +7 -0
  39. package/dist/hoi4_agent_tools/technology/graph.js +25 -10
  40. package/dist/hoi4_agent_tools/technology/render.js +286 -31
  41. package/dist/hoi4_agent_tools/technology/service.js +55 -8
  42. package/dist/hoi4_agent_tools/technology/source-analysis.js +166 -3
  43. package/dist/hoi4_agent_tools/version.js +1 -1
  44. package/docs/events.md +3 -1
  45. package/docs/focus.md +10 -0
  46. package/docs/gui.md +49 -3
  47. package/docs/map.md +8 -0
  48. package/docs/probability.md +3 -1
  49. package/docs/technology.md +7 -2
  50. package/package.json +1 -1
  51. package/schemas/configuration.schema.json +1 -1
  52. package/schemas/continuous-focus-palette.schema.json +1 -1
  53. package/schemas/custom-weighted-pool.schema.json +1 -1
  54. package/schemas/decision-inspect-input.schema.json +1 -1
  55. package/schemas/event-feature-manifest.schema.json +1 -1
  56. package/schemas/focus-plan.schema.json +1 -1
  57. package/schemas/focus-planning-sidecar.schema.json +1 -1
  58. package/schemas/gui-animation-source.schema.json +1 -1
  59. package/schemas/gui-helper.schema.json +1 -1
  60. package/schemas/gui-scenario.schema.json +1 -1
  61. package/schemas/helper-expansion-request.schema.json +1 -1
  62. package/schemas/helper-expansion-summary.schema.json +1 -1
  63. package/schemas/impact-inspect-input.schema.json +1 -1
  64. package/schemas/map-operation.schema.json +1 -1
  65. package/schemas/mechanic-test-input.schema.json +1 -1
  66. package/schemas/operation-result.schema.json +1 -1
  67. package/schemas/package-check-input.schema.json +1 -1
  68. package/schemas/package-manifest.schema.json +1 -1
  69. package/schemas/probability-analysis-result.schema.json +1 -1
  70. package/schemas/probability-compare-input.schema.json +1 -1
  71. package/schemas/probability-evaluate-input.schema.json +1 -1
  72. package/schemas/probability-inspect-input.schema.json +1 -1
  73. package/schemas/probability-render-input.schema.json +1 -1
  74. package/schemas/probability-scenario-set.schema.json +1 -1
  75. package/schemas/probability-sequence-input.schema.json +1 -1
  76. package/schemas/probability-simulate-input.schema.json +1 -1
  77. package/schemas/probability-sweep-input.schema.json +1 -1
  78. package/schemas/scenario-suite.schema.json +1 -1
  79. package/schemas/scenario-test-input.schema.json +1 -1
  80. package/schemas/shared-scenario.schema.json +1 -1
  81. package/server.json +2 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 3.4.0 - 2026-09-20
6
+
7
+ - Add branch-local focus cleanup with pinned anchors and symmetry groups, connector measurements, source-linked crops, and scenario-backed structural route diagnostics.
8
+ - Report GUI visibility and enabled branches, measured label and background boxes, and matched source-baseline renders alongside scenario comparisons.
9
+ - Expose complete source-backed probability catalogs, actionable missing scenario inputs, and eligible, excluded, and unresolved inventories.
10
+ - Add selected event-chain comparisons and revision-bound bounded helper expansion while retaining explicit lifecycle coverage limits.
11
+ - 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.
12
+ - Resolve source `GFX_*_medium` artwork and design-team overlays at native item size for chemical and biological warfare folders.
13
+ - Compose installed GUI backgrounds, focus inlays, container scrolling, native button and progress shaders, and scenario-driven rows from source assets and control states.
14
+ - Bound focus connector and overlap diagnostics on anchored trees with thousands of nodes.
15
+ - Add focused map lookups, reusable render tiles, affected-area output, and typed external script-reference updates for map renumbering.
16
+
5
17
  ## 3.3.0 - 2026-09-20
6
18
 
7
19
  - 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') {
@@ -278,7 +283,7 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
278
283
  ? scenario.actor
279
284
  : (scopeContext.binding.actor ?? scopeContext.binding.id);
280
285
  if (actor === undefined)
281
- return unresolved(assignment, candidate, `Scenario does not declare actor for ${key}`);
286
+ return unresolved(assignment, candidate, `Scenario does not declare actor for ${key}`, 'actor');
282
287
  return {
283
288
  state: compare(actor, right, assignment.operator.text) ? 'true' : 'false',
284
289
  unresolved: [],
@@ -286,7 +291,7 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
286
291
  }
287
292
  if (key === 'date') {
288
293
  if (scenario.date === undefined)
289
- return unresolved(assignment, candidate, 'Scenario does not declare date');
294
+ return unresolved(assignment, candidate, 'Scenario does not declare date', 'date');
290
295
  const dateNumber = (value) => {
291
296
  const parts = /^(\d+)\.(\d{1,2})\.(\d{1,2})$/u.exec(value);
292
297
  if (parts === null)
@@ -336,7 +341,7 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
336
341
  const declared = scopeStateValue(scenario, key, scopeContext) ??
337
342
  numberValue(key, scenario, scopeContext, definitions, candidate.provenance[0]?.path);
338
343
  if (declared === undefined)
339
- return unresolved(assignment, candidate, `Scenario does not declare trigger ${key}`);
344
+ return unresolved(assignment, candidate, `Scenario does not declare trigger ${key}`, key);
340
345
  const expectedBoolean = bool(right);
341
346
  const actualBoolean = bool(declared);
342
347
  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 = {
@@ -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
  }
@@ -130,6 +130,15 @@ export async function compareEvents(viewer, input) {
130
130
  ...(compared.render === undefined ? {} : { renderHashes: compared.render.hashes }),
131
131
  counts: {
132
132
  changes: comparison.changes.length,
133
+ ...(compared.selection === undefined
134
+ ? {}
135
+ : {
136
+ selectedChanges: compared.selection.selectedChangeCount,
137
+ omittedChanges: compared.selection.omittedChangeCount,
138
+ unattributedChanges: compared.selection.unattributedChangeIds.length,
139
+ selectedBeforeNodes: compared.selection.beforeNodeIds.length,
140
+ selectedAfterNodes: compared.selection.afterNodeIds.length,
141
+ }),
133
142
  addedNodes: comparison.addedNodeIds.length,
134
143
  removedNodes: comparison.removedNodeIds.length,
135
144
  changedNodes: comparison.changedNodeIds.length,
@@ -155,6 +164,13 @@ export async function compareEvents(viewer, input) {
155
164
  },
156
165
  boundary: {
157
166
  proposedSources: input.proposedSources?.length ?? 0,
167
+ ...(compared.selection === undefined
168
+ ? {}
169
+ : {
170
+ selector: compared.selection.selector,
171
+ maxChainNodes: compared.selection.maxChainNodes,
172
+ selectionTruncated: compared.selection.truncated,
173
+ }),
158
174
  render: input.render ?? true,
159
175
  maxRenderNodes: input.maxRenderNodes ?? 120,
160
176
  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: projection.selectedNodeIds,
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,