hoi4-agent-tools 3.0.9 → 3.2.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 (145) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/README.md +10 -2
  3. package/dist/bin/stdio.js +23 -12
  4. package/dist/hoi4_agent_tools/core/analysis-checkpoints.js +82 -0
  5. package/dist/hoi4_agent_tools/core/analysis-evidence.js +9 -0
  6. package/dist/hoi4_agent_tools/core/analysis-job-operations.js +38 -0
  7. package/dist/hoi4_agent_tools/core/artifact-resource.js +173 -0
  8. package/dist/hoi4_agent_tools/core/artifacts.js +84 -2
  9. package/dist/hoi4_agent_tools/core/condition-evaluator.js +16 -0
  10. package/dist/hoi4_agent_tools/core/dependency-pages.js +216 -0
  11. package/dist/hoi4_agent_tools/core/dependency-walk.js +266 -0
  12. package/dist/hoi4_agent_tools/core/domain-validation.js +188 -0
  13. package/dist/hoi4_agent_tools/core/engine.js +48 -5
  14. package/dist/hoi4_agent_tools/core/helper-expansion.js +186 -0
  15. package/dist/hoi4_agent_tools/core/impact-comparison.js +49 -0
  16. package/dist/hoi4_agent_tools/core/impact-graph.js +301 -0
  17. package/dist/hoi4_agent_tools/core/impact-semantic.js +256 -0
  18. package/dist/hoi4_agent_tools/core/index-segments.js +137 -0
  19. package/dist/hoi4_agent_tools/core/index.js +47 -4
  20. package/dist/hoi4_agent_tools/core/job-controls.js +109 -0
  21. package/dist/hoi4_agent_tools/core/job-executor.js +190 -0
  22. package/dist/hoi4_agent_tools/core/job-service.js +112 -0
  23. package/dist/hoi4_agent_tools/core/job-store.js +641 -0
  24. package/dist/hoi4_agent_tools/core/job-transactions.js +126 -0
  25. package/dist/hoi4_agent_tools/core/job-worker-host.js +202 -0
  26. package/dist/hoi4_agent_tools/core/job-worker.js +64 -0
  27. package/dist/hoi4_agent_tools/core/operation-context.js +31 -0
  28. package/dist/hoi4_agent_tools/core/operation-result.js +412 -0
  29. package/dist/hoi4_agent_tools/core/operation-tasks.js +387 -0
  30. package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +383 -0
  31. package/dist/hoi4_agent_tools/core/result.js +1 -0
  32. package/dist/hoi4_agent_tools/core/scanner.js +15 -75
  33. package/dist/hoi4_agent_tools/core/semantic-dependencies.js +233 -0
  34. package/dist/hoi4_agent_tools/core/shared-request-capacity.js +43 -11
  35. package/dist/hoi4_agent_tools/core/source/cache.js +107 -0
  36. package/dist/hoi4_agent_tools/core/source/parser.js +11 -1
  37. package/dist/hoi4_agent_tools/core/source-overlay.js +125 -0
  38. package/dist/hoi4_agent_tools/core/transaction-execution.js +134 -0
  39. package/dist/hoi4_agent_tools/core/transactions.js +80 -27
  40. package/dist/hoi4_agent_tools/decision/ai.js +104 -0
  41. package/dist/hoi4_agent_tools/decision/comparison.js +106 -0
  42. package/dist/hoi4_agent_tools/decision/cost.js +177 -0
  43. package/dist/hoi4_agent_tools/decision/evaluation.js +256 -0
  44. package/dist/hoi4_agent_tools/decision/lifecycle.js +114 -0
  45. package/dist/hoi4_agent_tools/decision/service.js +172 -0
  46. package/dist/hoi4_agent_tools/decision/source-inventory.js +182 -0
  47. package/dist/hoi4_agent_tools/event/graph.js +214 -290
  48. package/dist/hoi4_agent_tools/event/helper-expansion.js +33 -0
  49. package/dist/hoi4_agent_tools/event/job-operations.js +51 -0
  50. package/dist/hoi4_agent_tools/event/operations.js +179 -0
  51. package/dist/hoi4_agent_tools/event/service.js +67 -15
  52. package/dist/hoi4_agent_tools/event/source-analysis.js +5 -4
  53. package/dist/hoi4_agent_tools/focus/job-operations.js +77 -0
  54. package/dist/hoi4_agent_tools/focus/tool-operations.js +669 -0
  55. package/dist/hoi4_agent_tools/focus/tool-support.js +123 -0
  56. package/dist/hoi4_agent_tools/gui/job-operations.js +81 -0
  57. package/dist/hoi4_agent_tools/gui/tool-operations.js +270 -0
  58. package/dist/hoi4_agent_tools/impact/scenario-suites.js +74 -0
  59. package/dist/hoi4_agent_tools/impact/service.js +127 -0
  60. package/dist/hoi4_agent_tools/map/job-operations.js +76 -0
  61. package/dist/hoi4_agent_tools/map/tool-operations.js +493 -0
  62. package/dist/hoi4_agent_tools/mcp/prompts/probability.js +25 -0
  63. package/dist/hoi4_agent_tools/mcp/prompts/register-modern.js +29 -0
  64. package/dist/hoi4_agent_tools/mcp/resources/register-modern.js +34 -0
  65. package/dist/hoi4_agent_tools/mcp/resources/register.js +4 -150
  66. package/dist/hoi4_agent_tools/mcp/server/base-tools.js +3 -219
  67. package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +188 -0
  68. package/dist/hoi4_agent_tools/mcp/server/create.js +18 -6
  69. package/dist/hoi4_agent_tools/mcp/server/instructions.js +2 -0
  70. package/dist/hoi4_agent_tools/mcp/server/job-task-store.js +67 -0
  71. package/dist/hoi4_agent_tools/mcp/server/legacy-task-tools.js +20 -0
  72. package/dist/hoi4_agent_tools/mcp/server/modern-tasks.js +168 -0
  73. package/dist/hoi4_agent_tools/mcp/server/progress.js +17 -15
  74. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +45 -0
  75. package/dist/hoi4_agent_tools/mcp/server/result.js +2 -412
  76. package/dist/hoi4_agent_tools/mcp/server/task-request-context.js +9 -0
  77. package/dist/hoi4_agent_tools/mcp/server/task-tool-catalog.js +16 -0
  78. package/dist/hoi4_agent_tools/mcp/server/task-tool-definition.js +1 -0
  79. package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js +1 -123
  80. package/dist/hoi4_agent_tools/mcp/tools/analysis.js +65 -0
  81. package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +38 -21
  82. package/dist/hoi4_agent_tools/mcp/tools/event.js +24 -315
  83. package/dist/hoi4_agent_tools/mcp/tools/focus.js +34 -1039
  84. package/dist/hoi4_agent_tools/mcp/tools/gui.js +19 -504
  85. package/dist/hoi4_agent_tools/mcp/tools/job.js +46 -0
  86. package/dist/hoi4_agent_tools/mcp/tools/map.js +21 -600
  87. package/dist/hoi4_agent_tools/mcp/tools/probability.js +39 -463
  88. package/dist/hoi4_agent_tools/mcp/tools/technology.js +40 -304
  89. package/dist/hoi4_agent_tools/mcp/transports/http.js +58 -1
  90. package/dist/hoi4_agent_tools/mcp/transports/modern-task-routing.js +109 -0
  91. package/dist/hoi4_agent_tools/mcp/transports/negotiated-stdio.js +121 -0
  92. package/dist/hoi4_agent_tools/probability/job-operations.js +58 -0
  93. package/dist/hoi4_agent_tools/probability/operations.js +183 -0
  94. package/dist/hoi4_agent_tools/probability/service.js +24 -3
  95. package/dist/hoi4_agent_tools/schemas/analysis.js +228 -0
  96. package/dist/hoi4_agent_tools/schemas/common.js +2 -0
  97. package/dist/hoi4_agent_tools/schemas/event.js +72 -1
  98. package/dist/hoi4_agent_tools/schemas/focus-requests.js +189 -0
  99. package/dist/hoi4_agent_tools/schemas/gui-requests.js +193 -0
  100. package/dist/hoi4_agent_tools/schemas/helper-expansion.js +58 -0
  101. package/dist/hoi4_agent_tools/schemas/map-requests.js +85 -0
  102. package/dist/hoi4_agent_tools/schemas/probability-requests.js +168 -0
  103. package/dist/hoi4_agent_tools/schemas/technology.js +90 -1
  104. package/dist/hoi4_agent_tools/schemas/transaction.js +1 -0
  105. package/dist/hoi4_agent_tools/technology/graph.js +83 -78
  106. package/dist/hoi4_agent_tools/technology/helper-expansion.js +33 -0
  107. package/dist/hoi4_agent_tools/technology/job-operations.js +60 -0
  108. package/dist/hoi4_agent_tools/technology/operations.js +125 -0
  109. package/dist/hoi4_agent_tools/technology/service.js +89 -15
  110. package/dist/hoi4_agent_tools/technology/source-analysis.js +2 -1
  111. package/dist/hoi4_agent_tools/version.js +1 -1
  112. package/docs/README.md +3 -0
  113. package/docs/analysis.md +100 -0
  114. package/docs/development.md +2 -0
  115. package/docs/events.md +7 -2
  116. package/docs/helper-expansion.md +78 -0
  117. package/docs/http.md +2 -0
  118. package/docs/jobs.md +86 -0
  119. package/docs/technology.md +6 -2
  120. package/package.json +15 -6
  121. package/schemas/configuration.schema.json +1 -1
  122. package/schemas/continuous-focus-palette.schema.json +1 -1
  123. package/schemas/custom-weighted-pool.schema.json +1 -1
  124. package/schemas/decision-inspect-input.schema.json +290 -0
  125. package/schemas/event-feature-manifest.schema.json +1 -1
  126. package/schemas/focus-plan.schema.json +1 -1
  127. package/schemas/focus-planning-sidecar.schema.json +1 -1
  128. package/schemas/gui-animation-source.schema.json +1 -1
  129. package/schemas/gui-helper.schema.json +1 -1
  130. package/schemas/gui-scenario.schema.json +1 -1
  131. package/schemas/helper-expansion-request.schema.json +44 -0
  132. package/schemas/helper-expansion-summary.schema.json +70 -0
  133. package/schemas/impact-inspect-input.schema.json +203 -0
  134. package/schemas/map-operation.schema.json +1 -1
  135. package/schemas/operation-result.schema.json +1 -1
  136. package/schemas/probability-analysis-result.schema.json +1 -1
  137. package/schemas/probability-compare-input.schema.json +1 -1
  138. package/schemas/probability-evaluate-input.schema.json +1 -1
  139. package/schemas/probability-inspect-input.schema.json +1 -1
  140. package/schemas/probability-render-input.schema.json +1 -1
  141. package/schemas/probability-scenario-set.schema.json +1 -1
  142. package/schemas/probability-sequence-input.schema.json +1 -1
  143. package/schemas/probability-simulate-input.schema.json +1 -1
  144. package/schemas/probability-sweep-input.schema.json +1 -1
  145. package/server.json +3 -3
@@ -0,0 +1,51 @@
1
+ import { z } from 'zod/v4';
2
+ import { IdleCacheLifetime } from '../core/idle-cache-lifetime.js';
3
+ import { errorResult, toolResult } from '../core/operation-result.js';
4
+ import { ServiceError } from '../core/result.js';
5
+ import { eventInspectRequestSchema, eventRenderRequestSchema, eventCompareRequestSchema, } from '../schemas/event.js';
6
+ import { inspectEvents, renderEvents, compareEvents, normalizeEventInspectionRequest, normalizeEventRenderRequest, normalizeEventCompareRequest, } from './operations.js';
7
+ import { EventChainViewer } from './service.js';
8
+ const wireResult = z.record(z.string(), z.json());
9
+ /** Registers typed event operations without invoking transport handlers. */
10
+ export function registerEventJobs(operations, engine) {
11
+ const viewer = new EventChainViewer(engine);
12
+ const lifetime = new IdleCacheLifetime(() => viewer.clearCaches());
13
+ function register(name, schema, normalize, execute, message) {
14
+ operations.registerRead(name, schema, async (input, context) => {
15
+ const release = lifetime.begin();
16
+ try {
17
+ if (input.workspaceId !== 'current' && input.workspaceId !== context.workspaceId)
18
+ throw new ServiceError('JOB_ARGUMENT_SCOPE_MISMATCH', 'The request workspace does not match the authorized job workspace');
19
+ await context.progress({ completed: 0, total: 3, message });
20
+ const result = await execute(viewer, {
21
+ ...normalize(input),
22
+ workspaceId: context.workspaceId,
23
+ ...(context.principal === undefined ? {} : { principal: context.principal }),
24
+ signal: context.signal,
25
+ });
26
+ await context.progress({ completed: 3, total: 3, message: 'Event operation complete' });
27
+ const wire = wireResult.parse(JSON.parse(JSON.stringify(toolResult(result))));
28
+ const revision = result.data !== null &&
29
+ typeof result.data === 'object' &&
30
+ 'revision' in result.data &&
31
+ typeof result.data.revision === 'string'
32
+ ? result.data.revision
33
+ : undefined;
34
+ if (revision !== undefined)
35
+ await context.stageResult(wire, { sourceRevision: revision });
36
+ return wire;
37
+ }
38
+ catch (error) {
39
+ if (context.signal.aborted)
40
+ throw error;
41
+ return wireResult.parse(JSON.parse(JSON.stringify(errorResult(error, context.workspaceId))));
42
+ }
43
+ finally {
44
+ release();
45
+ }
46
+ });
47
+ }
48
+ register('hoi4.event_inspect', eventInspectRequestSchema, normalizeEventInspectionRequest, inspectEvents, 'Analyzing event chain');
49
+ register('hoi4.event_render', eventRenderRequestSchema, normalizeEventRenderRequest, renderEvents, 'Rendering event-chain view');
50
+ register('hoi4.event_compare', eventCompareRequestSchema, normalizeEventCompareRequest, compareEvents, 'Comparing event-chain graphs');
51
+ }
@@ -0,0 +1,179 @@
1
+ import { compareCodeUnits } from '../core/canonical.js';
2
+ import { helperExpansionValidation } from '../core/helper-expansion.js';
3
+ import { publicArtifactLink } from '../core/artifacts.js';
4
+ import { setInlineFilesScanned } from '../core/operation-result.js';
5
+ import { emptyServiceResult } from '../core/result.js';
6
+ import { eventInspectRequestSchema, eventRenderRequestSchema, eventCompareRequestSchema, } from '../schemas/event.js';
7
+ import { eventDiagnostics, } from './service.js';
8
+ export function normalizeEventInspectionRequest(input) {
9
+ // Validated request fields are JSON-only. Match wire semantics by removing optional
10
+ // undefined fields, including nested source-selector columns, before service dispatch.
11
+ return JSON.parse(JSON.stringify(eventInspectRequestSchema.parse(input)));
12
+ }
13
+ export function normalizeEventRenderRequest(input) {
14
+ return JSON.parse(JSON.stringify(eventRenderRequestSchema.parse(input)));
15
+ }
16
+ export function normalizeEventCompareRequest(input) {
17
+ return JSON.parse(JSON.stringify(eventCompareRequestSchema.parse(input)));
18
+ }
19
+ export function eventGraphCounts(graph, artifacts) {
20
+ const diagnostics = eventDiagnostics(graph);
21
+ return {
22
+ events: graph.statistics.eventCount,
23
+ options: graph.statistics.optionCount,
24
+ entries: graph.statistics.entryCount,
25
+ helpers: graph.statistics.helperCount,
26
+ unresolvedNodes: graph.statistics.unresolvedNodeCount,
27
+ terminals: graph.statistics.terminalCount,
28
+ edges: graph.statistics.edgeCount,
29
+ derivedEdges: graph.statistics.derivedEdgeCount,
30
+ stateAccesses: graph.statistics.stateAccessCount,
31
+ issues: graph.statistics.issueCount,
32
+ diagnostics: diagnostics.length,
33
+ blockingDiagnostics: diagnostics.filter(({ severity }) => severity === 'error' || severity === 'blocker').length,
34
+ skippedSources: graph.skippedSourceCount,
35
+ artifacts,
36
+ };
37
+ }
38
+ export function eventGraphValidation(graph) {
39
+ const counts = eventGraphCounts(graph, 0);
40
+ return {
41
+ passed: graph.complete && counts.blockingDiagnostics === 0,
42
+ checks: [
43
+ {
44
+ id: 'event-analysis',
45
+ passed: graph.complete && counts.blockingDiagnostics === 0,
46
+ message: graph.analysisMode === 'focused'
47
+ ? 'Large workspace analysis deferred workspace-wide helper projections and lifecycle passes; direct evidence is linked'
48
+ : graph.complete
49
+ ? `${counts.blockingDiagnostics} blocking event-chain diagnostics; full evidence is linked`
50
+ : graph.issues.some(({ code }) => code === 'EVENT_HELPER_DEPTH_LIMIT' ||
51
+ code === 'EVENT_HELPER_PROJECTION_LIMIT' ||
52
+ code === 'EVENT_HELPER_STATE_PROJECTION_LIMIT')
53
+ ? 'Helper expansion reached a depth or materialization boundary; helper_expansion mode provides bounded source-linked continuation'
54
+ : `${graph.skippedSourceCount} event-analysis source(s) were skipped; full evidence is linked`,
55
+ },
56
+ ],
57
+ };
58
+ }
59
+ /** Transport-independent event inspection and its complete bounded result contract. */
60
+ export async function inspectEvents(viewer, input) {
61
+ const inspected = await viewer.inspect(input);
62
+ const result = emptyServiceResult(input.workspaceId, {
63
+ mode: inspected.mode,
64
+ analysisMode: inspected.graph.analysisMode ?? 'full',
65
+ revision: inspected.graph.revision,
66
+ graphHash: inspected.graphHash,
67
+ counts: eventGraphCounts(inspected.graph, inspected.artifacts.length),
68
+ ...(inspected.helperExpansion === undefined
69
+ ? {}
70
+ : { helperExpansion: inspected.helperExpansion }),
71
+ boundary: {
72
+ direction: input.direction ?? 'both',
73
+ maxDepth: inspected.helperExpansion?.maxDepth ?? input.maxDepth ?? 8,
74
+ maxNodes: inspected.helperExpansion === undefined ? (input.maxNodes ?? 500) : 0,
75
+ maxEdges: inspected.helperExpansion === undefined ? (input.maxEdges ?? 2_000) : 0,
76
+ expandHelpers: inspected.helperExpansion !== undefined ||
77
+ (input.expandHelpers ?? input.mode === 'explain_path'),
78
+ refresh: input.refresh ?? input.mode === 'scan',
79
+ },
80
+ });
81
+ result.code =
82
+ (inspected.helperExpansion?.complete ?? inspected.graph.complete)
83
+ ? 'EVENT_INSPECTED'
84
+ : 'EVENT_INSPECTED_PARTIAL';
85
+ setInlineFilesScanned(result, inspected.graph.filesScanned);
86
+ result.artifacts = inspected.artifacts.map(publicArtifactLink);
87
+ result.validation = eventGraphValidation(inspected.graph);
88
+ if (inspected.helperExpansion !== undefined)
89
+ result.validation = helperExpansionValidation(inspected.helperExpansion);
90
+ return result;
91
+ }
92
+ export async function renderEvents(viewer, input) {
93
+ const rendered = await viewer.renderAndStore(input);
94
+ const result = emptyServiceResult(input.workspaceId, {
95
+ view: rendered.render.view,
96
+ analysisMode: rendered.graph.analysisMode ?? 'full',
97
+ revision: rendered.graph.revision,
98
+ graphHash: rendered.graphHash,
99
+ layoutHash: rendered.render.layout.layoutHash,
100
+ hashes: rendered.render.hashes,
101
+ counts: {
102
+ ...eventGraphCounts(rendered.graph, rendered.artifacts.length),
103
+ selectedNodes: rendered.render.selectedNodeIds.length,
104
+ omittedNodes: rendered.render.omittedNodeCount,
105
+ branchRenders: rendered.branches.length,
106
+ },
107
+ boundary: {
108
+ direction: input.direction ?? 'both',
109
+ maxDepth: input.maxDepth ?? 4,
110
+ maxNodes: input.maxNodes ?? 120,
111
+ expandHelpers: input.expandHelpers ?? false,
112
+ includeHtml: input.includeHtml ?? false,
113
+ refresh: input.refresh ?? false,
114
+ },
115
+ });
116
+ result.code = rendered.graph.complete ? 'EVENT_RENDERED' : 'EVENT_RENDERED_PARTIAL';
117
+ setInlineFilesScanned(result, rendered.graph.filesScanned);
118
+ result.artifacts = rendered.artifacts.map(publicArtifactLink);
119
+ result.validation = eventGraphValidation(rendered.graph);
120
+ return result;
121
+ }
122
+ export async function compareEvents(viewer, input) {
123
+ const compared = await viewer.compareAndStore(input);
124
+ const { comparison } = compared;
125
+ const result = emptyServiceResult(input.workspaceId, {
126
+ beforeRevision: comparison.beforeRevision,
127
+ afterRevision: comparison.afterRevision,
128
+ beforeGraphHash: comparison.beforeGraphHash,
129
+ afterGraphHash: comparison.afterGraphHash,
130
+ ...(compared.render === undefined ? {} : { renderHashes: compared.render.hashes }),
131
+ counts: {
132
+ changes: comparison.changes.length,
133
+ addedNodes: comparison.addedNodeIds.length,
134
+ removedNodes: comparison.removedNodeIds.length,
135
+ changedNodes: comparison.changedNodeIds.length,
136
+ addedEdges: comparison.addedEdgeIds.length,
137
+ removedEdges: comparison.removedEdgeIds.length,
138
+ changedEdges: comparison.changedEdgeIds.length,
139
+ addedStateAccesses: comparison.addedStateAccessIds.length,
140
+ removedStateAccesses: comparison.removedStateAccessIds.length,
141
+ changedStateAccesses: comparison.changedStateAccessIds.length,
142
+ addedStateLinks: comparison.addedStateLinkIds.length,
143
+ removedStateLinks: comparison.removedStateLinkIds.length,
144
+ changedStateLinks: comparison.changedStateLinkIds.length,
145
+ addedDiagnostics: comparison.addedIssueIds.length,
146
+ resolvedDiagnostics: comparison.resolvedIssueIds.length,
147
+ addedUnresolved: comparison.addedUnresolvedIds.length,
148
+ resolvedUnresolved: comparison.resolvedUnresolvedIds.length,
149
+ disconnectedRoots: comparison.newlyDisconnectedRootIds.length,
150
+ disconnectedBranches: comparison.newlyDisconnectedBranchIds.length,
151
+ disconnectedTerminals: comparison.newlyDisconnectedTerminalIds.length,
152
+ beforeSkippedSources: compared.before.skippedSourceCount,
153
+ afterSkippedSources: compared.after.skippedSourceCount,
154
+ artifacts: compared.artifacts.length,
155
+ },
156
+ boundary: {
157
+ proposedSources: input.proposedSources?.length ?? 0,
158
+ render: input.render ?? true,
159
+ maxRenderNodes: input.maxRenderNodes ?? 120,
160
+ refresh: compared.refresh,
161
+ },
162
+ });
163
+ result.code =
164
+ compared.before.complete && compared.after.complete
165
+ ? 'EVENT_COMPARED'
166
+ : 'EVENT_COMPARED_PARTIAL';
167
+ setInlineFilesScanned(result, [...new Set([...compared.before.filesScanned, ...compared.after.filesScanned])].sort(compareCodeUnits));
168
+ result.artifacts = compared.artifacts.map(publicArtifactLink);
169
+ const beforeValidation = eventGraphValidation(compared.before);
170
+ const afterValidation = eventGraphValidation(compared.after);
171
+ result.validation = {
172
+ passed: beforeValidation.passed && afterValidation.passed,
173
+ checks: [...beforeValidation.checks, ...afterValidation.checks].map((check, index) => ({
174
+ ...check,
175
+ id: `${index === 0 ? 'before' : 'after'}-${check.id}`,
176
+ })),
177
+ };
178
+ return result;
179
+ }
@@ -1,4 +1,7 @@
1
1
  import path from 'node:path';
2
+ import { inspectHelperExpansion } from '../core/helper-expansion.js';
3
+ import { eventHelperInventory } from './helper-expansion.js';
4
+ import { clearSemanticDependencies } from '../core/semantic-dependencies.js';
2
5
  import { canonicalJson, compareCodeUnits, hashCanonical, sha256Bytes } from '../core/canonical.js';
3
6
  import { boundedSourceHashEvidence, publicArtifactLink, } from '../core/artifacts.js';
4
7
  import { ServiceError } from '../core/result.js';
@@ -6,7 +9,7 @@ import { RenderBudget } from '../core/render-budget.js';
6
9
  import { SOURCE_MAX_BYTES } from '../core/source/index.js';
7
10
  import { isPortablePathSegment } from '../core/workspace.js';
8
11
  import { PACKAGE_VERSION } from '../version.js';
9
- import { buildEventGraph } from './graph.js';
12
+ import { buildEventGraph, buildEventGraphAsync } from './graph.js';
10
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';
11
14
  import { analyzeEventImpact, discoverEventRoots, explainEventPath, inspectEventStateFlow, lintEventGraph, traceSelectedEvents, } from './queries.js';
12
15
  import { compareEventGraphs, eventGraphHash } from './compare.js';
@@ -42,6 +45,7 @@ class BoundedEventFragmentCache {
42
45
  }
43
46
  }
44
47
  clear() {
48
+ clearSemanticDependencies(this);
45
49
  this.#entries.clear();
46
50
  this.#sourceBytes = 0;
47
51
  }
@@ -717,14 +721,18 @@ export class EventChainViewer {
717
721
  const cacheProjectHelpers = cacheAnalysisMode === 'focused' ? false : requestedProjectHelpers;
718
722
  const cacheKey = eventGraphCacheKey(workspaceId, cacheProjectHelpers, cacheAnalysisMode);
719
723
  const cached = this.#state.current.get(cacheKey);
720
- if (options.refresh !== true && cached?.generation === generation) {
721
- return cached.graph;
722
- }
724
+ // An engine generation observes server-owned edits, not changes made by
725
+ // another agent or process. Verify source identity before reusing a graph.
723
726
  if (options.refresh !== true && requestedAnalysisMode === 'focused') {
724
- const fullSibling = this.#state.current.get(eventGraphCacheKey(workspaceId, cacheProjectHelpers, 'full')) ??
725
- this.#state.current.get(eventGraphCacheKey(workspaceId, !cacheProjectHelpers, 'full'));
726
- if (fullSibling?.generation === generation)
727
- return fullSibling.graph;
727
+ for (const expanded of [cacheProjectHelpers, !cacheProjectHelpers]) {
728
+ const sibling = this.#state.current.get(eventGraphCacheKey(workspaceId, expanded, 'full'));
729
+ if (sibling?.generation === generation)
730
+ return this.scan(workspaceId, {
731
+ ...options,
732
+ analysisMode: 'full',
733
+ projectHelpers: expanded,
734
+ });
735
+ }
728
736
  }
729
737
  const scanFocused = requestedAnalysisMode === 'focused';
730
738
  const snapshot = await scanEventSources(this.engine, workspaceId, workspace, scanFocused, options.signal);
@@ -733,7 +741,7 @@ export class EventChainViewer {
733
741
  return cached.graph;
734
742
  const buildAnalysisMode = requestedAnalysisMode;
735
743
  const projectHelpers = buildAnalysisMode === 'focused' ? false : requestedProjectHelpers;
736
- const graph = buildEventGraph(snapshot, {
744
+ const graph = await buildEventGraphAsync(snapshot, {
737
745
  ...(options.signal === undefined ? {} : { signal: options.signal }),
738
746
  cache: this.#state.fragments,
739
747
  workspaceIdentity: workspace.workspaceIdentity,
@@ -747,6 +755,24 @@ export class EventChainViewer {
747
755
  }
748
756
  async inspect(input) {
749
757
  input.signal?.throwIfAborted();
758
+ if (input.mode === 'helper_expansion') {
759
+ const graph = await this.scan(input.workspaceId, {
760
+ projectHelpers: false,
761
+ analysisMode: 'full',
762
+ ...(input.principal === undefined ? {} : { principal: input.principal }),
763
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
764
+ });
765
+ const expansion = await inspectHelperExpansion(this.engine, input.workspaceId, eventHelperInventory(graph), input.helperExpansion, input.principal, input.signal);
766
+ return {
767
+ graph,
768
+ graphHash: cachedEventGraphHash(graph, input.signal),
769
+ mode: input.mode,
770
+ report: expansion.report,
771
+ reportJson: expansion.reportJson,
772
+ artifacts: expansion.artifacts,
773
+ helperExpansion: expansion.summary,
774
+ };
775
+ }
750
776
  const focused = input.mode === 'trace' ||
751
777
  input.mode === 'explain_path' ||
752
778
  (input.selector !== undefined &&
@@ -884,6 +910,7 @@ export class EventChainViewer {
884
910
  }
885
911
  const graphHash = cachedEventGraphHash(graph, input.signal);
886
912
  const name = `${safeSlug(`event-${input.mode}`)}-${graph.revision.slice(0, 12)}.json`;
913
+ const graphSnapshotName = `event-graph-${graph.revision}.json`;
887
914
  const projectedScan = input.mode === 'scan' &&
888
915
  typeof report === 'object' &&
889
916
  report !== null &&
@@ -919,7 +946,12 @@ export class EventChainViewer {
919
946
  },
920
947
  }
921
948
  : {}),
922
- resources: [{ name, mimeType: 'application/json' }],
949
+ resources: [
950
+ { name, mimeType: 'application/json' },
951
+ ...(input.mode === 'scan'
952
+ ? [{ name: graphSnapshotName, mimeType: 'application/json' }]
953
+ : []),
954
+ ],
923
955
  report,
924
956
  })}\n`;
925
957
  const workspace = this.engine.resolver.get(input.workspaceId, input.principal);
@@ -927,13 +959,16 @@ export class EventChainViewer {
927
959
  mode: input.mode,
928
960
  ...(input.selector === undefined ? {} : { selector: input.selector }),
929
961
  }, input.signal), `Authoritative Event Chain Viewer ${input.mode.replaceAll('_', ' ')} report`, input.signal);
962
+ const graphArtifact = input.mode === 'scan'
963
+ ? await this.engine.artifacts.putChunked(workspace, graphSnapshotName, 'application/json', `${canonicalJson({ schemaVersion: 'event-graph-snapshot.v1', graph })}\n`, eventProvenance(graph, 'event-graph-snapshot', { revision: graph.revision, graphHash }, input.signal), 'Revision-addressed Event Chain Viewer graph snapshot', input.signal)
964
+ : undefined;
930
965
  return {
931
966
  graph,
932
967
  graphHash,
933
968
  mode: input.mode,
934
969
  report,
935
970
  reportJson,
936
- artifacts: [artifact],
971
+ artifacts: [artifact, ...(graphArtifact === undefined ? [] : [graphArtifact])],
937
972
  };
938
973
  }
939
974
  async renderAndStore(input) {
@@ -1243,10 +1278,27 @@ export class EventChainViewer {
1243
1278
  return selected;
1244
1279
  }
1245
1280
  if (reference?.revision !== undefined) {
1246
- const selected = this.#state.history.get(workspaceId)?.get(reference.revision);
1247
- if (selected === undefined)
1248
- throw new ServiceError('EVENT_REVISION_NOT_CACHED', 'Requested event graph revision is not cached');
1249
- return selected;
1281
+ const cached = this.#state.history.get(workspaceId)?.get(reference.revision);
1282
+ if (cached !== undefined)
1283
+ return cached;
1284
+ const workspace = this.engine.resolver.get(workspaceId, principal);
1285
+ const names = new Set([
1286
+ `event-graph-${reference.revision}.json`,
1287
+ `event-scan-${reference.revision.slice(0, 12)}.json`,
1288
+ ]);
1289
+ const candidates = (await this.engine.artifacts.list(workspace, signal)).filter(({ name }) => names.has(name));
1290
+ for (const artifact of candidates) {
1291
+ const selected = parseGraphArtifact(await readLogicalArtifact(this.engine, workspaceId, artifact.uri, principal, signal), signal);
1292
+ if (selected.revision === reference.revision &&
1293
+ selected.workspaceId === workspaceId &&
1294
+ selected.workspaceIdentity === current.workspaceIdentity) {
1295
+ rememberGraph(this.#state, workspaceId, selected);
1296
+ return selected;
1297
+ }
1298
+ }
1299
+ if (candidates.length === 0)
1300
+ throw new ServiceError('EVENT_REVISION_NOT_CACHED', 'Requested event graph revision is not cached or retained as a graph artifact');
1301
+ throw new ServiceError('EVENT_GRAPH_WORKSPACE_MISMATCH', 'Retained event graph evidence does not match the requested revision and workspace topology');
1250
1302
  }
1251
1303
  return current;
1252
1304
  }
@@ -1,4 +1,5 @@
1
1
  import { compareCodeUnits, deterministicId } from '../core/canonical.js';
2
+ import { semanticSourceIdentity } from '../core/semantic-dependencies.js';
2
3
  import { assignments, childBlocks, firstScalar, nodeLocation, parseClausewitz, parseLocalisation, sourcePartialLimitDiagnostics, } from '../core/source/index.js';
3
4
  import { nativeEffectKeys } from '../focus/native-effects.js';
4
5
  import { EVENT_FRAGMENT_CACHE_MAX_ENTRIES, EVENT_FRAGMENT_CACHE_MAX_SOURCE_BYTES, EVENT_GRAPH_MAX_CONDITION_TEXT, EventAnalysisBudget, } from './limits.js';
@@ -303,8 +304,7 @@ function issueFromDiagnostic(diagnostic) {
303
304
  export function eventSemanticFragmentCacheKey(file, catalogFingerprint = '') {
304
305
  return deterministicId('event_fragment', {
305
306
  parser: 'clausewitz-cst.v1',
306
- sourcePath: file.displayPath,
307
- sourceHash: file.sha256,
307
+ source: semanticSourceIdentity(file),
308
308
  catalogFingerprint,
309
309
  });
310
310
  }
@@ -634,7 +634,8 @@ class FragmentAnalyzer {
634
634
  for (const assignment of assignments(this.document.root)) {
635
635
  if (assignment.value.type !== 'block')
636
636
  continue;
637
- if (this.#knownHelpers.size > 0 && !this.#knownHelpers.has(assignment.key.value))
637
+ if (this.context.knownHelperIds !== undefined &&
638
+ !this.#knownHelpers.has(assignment.key.value))
638
639
  continue;
639
640
  const name = assignment.key.value;
640
641
  if (this.context.activeHelperPaths !== undefined &&
@@ -1050,7 +1051,7 @@ class FragmentAnalyzer {
1050
1051
  this.recordDynamicEvent(id, node, context.ownerId, context.semanticPath);
1051
1052
  return;
1052
1053
  }
1053
- const known = this.#knownEvents.size === 0 || this.#knownEvents.has(id);
1054
+ const known = this.context.knownEventIds === undefined || this.#knownEvents.has(id);
1054
1055
  const target = known
1055
1056
  ? this.eventNodeId(id)
1056
1057
  : this.unresolvedNode(this.context.inventoryComplete === false ? 'partial_source' : 'missing_event', id, node, context.ownerId, context.semanticPath);
@@ -0,0 +1,77 @@
1
+ import { z } from 'zod/v4';
2
+ import { IdleCacheLifetime } from '../core/idle-cache-lifetime.js';
3
+ import { errorResult, toolResult } from '../core/operation-result.js';
4
+ import { ServiceError } from '../core/result.js';
5
+ import { focusInspectRequestSchema, focusRenderRequestSchema, focusRewriteRequestSchema, } from '../schemas/focus-requests.js';
6
+ import { FocusWorkbench } from './index.js';
7
+ import { inspectFocus, completeFocusRewrite, normalizeFocusInspectRequest, normalizeFocusRenderRequest, normalizeFocusRewriteRequest, prepareFocusRewrite, renderFocus, } from './tool-operations.js';
8
+ const wireResult = z.record(z.string(), z.json());
9
+ /** Registers read-only focus operations without invoking transport handlers. */
10
+ export function registerFocusJobs(operations, engine) {
11
+ const workbench = new FocusWorkbench(engine.resolver, engine.transactions, engine.artifacts);
12
+ const lifetime = new IdleCacheLifetime(() => engine.releaseScanCaches());
13
+ function register(name, schema, normalize, execute) {
14
+ operations.registerRead(name, schema, async (input, context) => {
15
+ const release = lifetime.begin();
16
+ try {
17
+ if (input.workspaceId !== 'current' && input.workspaceId !== context.workspaceId)
18
+ throw new ServiceError('JOB_ARGUMENT_SCOPE_MISMATCH', 'The request workspace does not match the authorized job workspace');
19
+ const execution = await execute(normalize(input), {
20
+ workspaceId: context.workspaceId,
21
+ ...(context.principal === undefined ? {} : { principal: context.principal }),
22
+ signal: context.signal,
23
+ progress: (completed, total, message) => context.progress({ completed, total, message }),
24
+ });
25
+ const wire = wireResult.parse(JSON.parse(JSON.stringify(toolResult(execution.result))));
26
+ await context.stageResult(wire, { sourceRevision: execution.sourceRevision });
27
+ return wire;
28
+ }
29
+ catch (error) {
30
+ if (context.signal.aborted)
31
+ throw error;
32
+ return wireResult.parse(JSON.parse(JSON.stringify(errorResult(error, context.workspaceId))));
33
+ }
34
+ finally {
35
+ release();
36
+ }
37
+ });
38
+ }
39
+ register('hoi4.focus_inspect', focusInspectRequestSchema, normalizeFocusInspectRequest, (input, context) => inspectFocus(engine, workbench, input, context));
40
+ register('hoi4.focus_render', focusRenderRequestSchema, normalizeFocusRenderRequest, (input, context) => renderFocus(engine, workbench, input, context, false));
41
+ register('hoi4.focus_raster', focusRenderRequestSchema, normalizeFocusRenderRequest, (input, context) => renderFocus(engine, workbench, input, context, true));
42
+ operations.registerWrite('hoi4.focus_rewrite', focusRewriteRequestSchema, async (input, context) => {
43
+ if (input.workspaceId !== 'current' && input.workspaceId !== context.workspaceId)
44
+ throw new ServiceError('JOB_ARGUMENT_SCOPE_MISMATCH', 'The request workspace does not match the authorized job workspace');
45
+ const release = lifetime.begin();
46
+ try {
47
+ return await prepareFocusRewrite(engine, workbench, normalizeFocusRewriteRequest(input), {
48
+ workspaceId: context.workspaceId,
49
+ ...(context.principal === undefined ? {} : { principal: context.principal }),
50
+ signal: context.signal,
51
+ progress: (completed, total, message) => context.progress({ completed, total, message }),
52
+ });
53
+ }
54
+ finally {
55
+ release();
56
+ }
57
+ }, async (input, execution, _reconciled, recipe, context) => {
58
+ const result = completeFocusRewrite(context.workspaceId, execution, recipe);
59
+ const continuous = input.mode === 'continuous';
60
+ await context.progress({
61
+ completed: 5,
62
+ total: 5,
63
+ message: execution.outcome === 'applied'
64
+ ? continuous
65
+ ? 'Continuous focus rewrite complete'
66
+ : 'Focus rewrite complete'
67
+ : execution.outcome === 'unchanged'
68
+ ? continuous
69
+ ? 'Continuous focus content already satisfied the plan'
70
+ : 'Focus content already satisfied the plan'
71
+ : continuous
72
+ ? 'Continuous focus rewrite blocked'
73
+ : 'Focus rewrite blocked',
74
+ });
75
+ return wireResult.parse(JSON.parse(JSON.stringify(toolResult(result))));
76
+ });
77
+ }