hoi4-agent-tools 3.0.9 → 3.1.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 (124) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.md +4 -0
  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/artifact-resource.js +173 -0
  6. package/dist/hoi4_agent_tools/core/artifacts.js +84 -2
  7. package/dist/hoi4_agent_tools/core/dependency-pages.js +216 -0
  8. package/dist/hoi4_agent_tools/core/dependency-walk.js +266 -0
  9. package/dist/hoi4_agent_tools/core/domain-validation.js +188 -0
  10. package/dist/hoi4_agent_tools/core/engine.js +48 -5
  11. package/dist/hoi4_agent_tools/core/helper-expansion.js +186 -0
  12. package/dist/hoi4_agent_tools/core/index-segments.js +137 -0
  13. package/dist/hoi4_agent_tools/core/index.js +47 -4
  14. package/dist/hoi4_agent_tools/core/job-controls.js +109 -0
  15. package/dist/hoi4_agent_tools/core/job-executor.js +190 -0
  16. package/dist/hoi4_agent_tools/core/job-service.js +112 -0
  17. package/dist/hoi4_agent_tools/core/job-store.js +641 -0
  18. package/dist/hoi4_agent_tools/core/job-transactions.js +126 -0
  19. package/dist/hoi4_agent_tools/core/job-worker-host.js +200 -0
  20. package/dist/hoi4_agent_tools/core/job-worker.js +62 -0
  21. package/dist/hoi4_agent_tools/core/operation-context.js +31 -0
  22. package/dist/hoi4_agent_tools/core/operation-result.js +412 -0
  23. package/dist/hoi4_agent_tools/core/operation-tasks.js +382 -0
  24. package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +383 -0
  25. package/dist/hoi4_agent_tools/core/result.js +1 -0
  26. package/dist/hoi4_agent_tools/core/scanner.js +15 -75
  27. package/dist/hoi4_agent_tools/core/semantic-dependencies.js +233 -0
  28. package/dist/hoi4_agent_tools/core/shared-request-capacity.js +43 -11
  29. package/dist/hoi4_agent_tools/core/source/cache.js +107 -0
  30. package/dist/hoi4_agent_tools/core/source/parser.js +11 -1
  31. package/dist/hoi4_agent_tools/core/transaction-execution.js +134 -0
  32. package/dist/hoi4_agent_tools/core/transactions.js +80 -27
  33. package/dist/hoi4_agent_tools/event/graph.js +214 -290
  34. package/dist/hoi4_agent_tools/event/helper-expansion.js +33 -0
  35. package/dist/hoi4_agent_tools/event/job-operations.js +51 -0
  36. package/dist/hoi4_agent_tools/event/operations.js +179 -0
  37. package/dist/hoi4_agent_tools/event/service.js +67 -15
  38. package/dist/hoi4_agent_tools/event/source-analysis.js +5 -4
  39. package/dist/hoi4_agent_tools/focus/job-operations.js +77 -0
  40. package/dist/hoi4_agent_tools/focus/tool-operations.js +669 -0
  41. package/dist/hoi4_agent_tools/focus/tool-support.js +123 -0
  42. package/dist/hoi4_agent_tools/gui/job-operations.js +81 -0
  43. package/dist/hoi4_agent_tools/gui/tool-operations.js +270 -0
  44. package/dist/hoi4_agent_tools/map/job-operations.js +76 -0
  45. package/dist/hoi4_agent_tools/map/tool-operations.js +493 -0
  46. package/dist/hoi4_agent_tools/mcp/prompts/probability.js +25 -0
  47. package/dist/hoi4_agent_tools/mcp/prompts/register-modern.js +29 -0
  48. package/dist/hoi4_agent_tools/mcp/resources/register-modern.js +34 -0
  49. package/dist/hoi4_agent_tools/mcp/resources/register.js +4 -150
  50. package/dist/hoi4_agent_tools/mcp/server/base-tools.js +3 -219
  51. package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +188 -0
  52. package/dist/hoi4_agent_tools/mcp/server/create.js +16 -6
  53. package/dist/hoi4_agent_tools/mcp/server/instructions.js +2 -0
  54. package/dist/hoi4_agent_tools/mcp/server/job-task-store.js +67 -0
  55. package/dist/hoi4_agent_tools/mcp/server/legacy-task-tools.js +20 -0
  56. package/dist/hoi4_agent_tools/mcp/server/modern-tasks.js +168 -0
  57. package/dist/hoi4_agent_tools/mcp/server/progress.js +17 -15
  58. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +43 -0
  59. package/dist/hoi4_agent_tools/mcp/server/result.js +2 -412
  60. package/dist/hoi4_agent_tools/mcp/server/task-request-context.js +9 -0
  61. package/dist/hoi4_agent_tools/mcp/server/task-tool-catalog.js +14 -0
  62. package/dist/hoi4_agent_tools/mcp/server/task-tool-definition.js +1 -0
  63. package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js +1 -123
  64. package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +38 -21
  65. package/dist/hoi4_agent_tools/mcp/tools/event.js +24 -315
  66. package/dist/hoi4_agent_tools/mcp/tools/focus.js +34 -1039
  67. package/dist/hoi4_agent_tools/mcp/tools/gui.js +19 -504
  68. package/dist/hoi4_agent_tools/mcp/tools/job.js +46 -0
  69. package/dist/hoi4_agent_tools/mcp/tools/map.js +21 -600
  70. package/dist/hoi4_agent_tools/mcp/tools/probability.js +39 -463
  71. package/dist/hoi4_agent_tools/mcp/tools/technology.js +40 -304
  72. package/dist/hoi4_agent_tools/mcp/transports/http.js +58 -1
  73. package/dist/hoi4_agent_tools/mcp/transports/modern-task-routing.js +109 -0
  74. package/dist/hoi4_agent_tools/mcp/transports/negotiated-stdio.js +121 -0
  75. package/dist/hoi4_agent_tools/probability/job-operations.js +58 -0
  76. package/dist/hoi4_agent_tools/probability/operations.js +183 -0
  77. package/dist/hoi4_agent_tools/probability/service.js +24 -3
  78. package/dist/hoi4_agent_tools/schemas/common.js +2 -0
  79. package/dist/hoi4_agent_tools/schemas/event.js +72 -1
  80. package/dist/hoi4_agent_tools/schemas/focus-requests.js +189 -0
  81. package/dist/hoi4_agent_tools/schemas/gui-requests.js +193 -0
  82. package/dist/hoi4_agent_tools/schemas/helper-expansion.js +58 -0
  83. package/dist/hoi4_agent_tools/schemas/map-requests.js +85 -0
  84. package/dist/hoi4_agent_tools/schemas/probability-requests.js +168 -0
  85. package/dist/hoi4_agent_tools/schemas/technology.js +90 -1
  86. package/dist/hoi4_agent_tools/schemas/transaction.js +1 -0
  87. package/dist/hoi4_agent_tools/technology/graph.js +83 -78
  88. package/dist/hoi4_agent_tools/technology/helper-expansion.js +33 -0
  89. package/dist/hoi4_agent_tools/technology/job-operations.js +60 -0
  90. package/dist/hoi4_agent_tools/technology/operations.js +125 -0
  91. package/dist/hoi4_agent_tools/technology/service.js +89 -15
  92. package/dist/hoi4_agent_tools/technology/source-analysis.js +2 -1
  93. package/dist/hoi4_agent_tools/version.js +1 -1
  94. package/docs/README.md +2 -0
  95. package/docs/development.md +2 -0
  96. package/docs/events.md +7 -2
  97. package/docs/helper-expansion.md +78 -0
  98. package/docs/http.md +2 -0
  99. package/docs/jobs.md +85 -0
  100. package/docs/technology.md +6 -2
  101. package/package.json +11 -5
  102. package/schemas/configuration.schema.json +1 -1
  103. package/schemas/continuous-focus-palette.schema.json +1 -1
  104. package/schemas/custom-weighted-pool.schema.json +1 -1
  105. package/schemas/event-feature-manifest.schema.json +1 -1
  106. package/schemas/focus-plan.schema.json +1 -1
  107. package/schemas/focus-planning-sidecar.schema.json +1 -1
  108. package/schemas/gui-animation-source.schema.json +1 -1
  109. package/schemas/gui-helper.schema.json +1 -1
  110. package/schemas/gui-scenario.schema.json +1 -1
  111. package/schemas/helper-expansion-request.schema.json +44 -0
  112. package/schemas/helper-expansion-summary.schema.json +70 -0
  113. package/schemas/map-operation.schema.json +1 -1
  114. package/schemas/operation-result.schema.json +1 -1
  115. package/schemas/probability-analysis-result.schema.json +1 -1
  116. package/schemas/probability-compare-input.schema.json +1 -1
  117. package/schemas/probability-evaluate-input.schema.json +1 -1
  118. package/schemas/probability-inspect-input.schema.json +1 -1
  119. package/schemas/probability-render-input.schema.json +1 -1
  120. package/schemas/probability-scenario-set.schema.json +1 -1
  121. package/schemas/probability-sequence-input.schema.json +1 -1
  122. package/schemas/probability-simulate-input.schema.json +1 -1
  123. package/schemas/probability-sweep-input.schema.json +1 -1
  124. package/server.json +2 -2
@@ -0,0 +1,168 @@
1
+ import { z } from 'zod/v4';
2
+ import { customWeightedPoolManifestSchema, probabilityAcceptanceBandSchema, probabilityAdapterIdSchema, probabilityDiagnosticThresholdsSchema, probabilityMetricSchema, probabilityOutputSchema, probabilityRenderFilterSchema, probabilityScenarioSetSchema, probabilitySamplingMethodSchema, probabilitySourceSchema, } from './probability.js';
3
+ import { workspaceIdSchema } from './common.js';
4
+ function compact(schema, description) {
5
+ return z.unknown().describe(description).pipe(schema);
6
+ }
7
+ const nestedSource = compact(probabilitySourceSchema, 'Source selector or proposed source.');
8
+ const nestedScenarios = compact(probabilityScenarioSetSchema, 'Explicit world-state scenarios.');
9
+ const nestedManifest = compact(customWeightedPoolManifestSchema, 'Declared custom weighted pool and state transitions.');
10
+ const nestedRenderFilter = compact(probabilityRenderFilterSchema, 'Optional scenario, candidate, metric, diagnostic, source, and page filter.');
11
+ const nestedAcceptanceBands = compact(z.array(probabilityAcceptanceBandSchema).max(100_000), 'Optional named metric acceptance bands.');
12
+ const nestedDiagnosticThresholds = compact(probabilityDiagnosticThresholdsSchema, 'Optional diagnostic thresholds.');
13
+ const candidatePool = z.array(z.string().min(1).max(512)).max(100_000).optional();
14
+ const outputs = z.array(probabilityOutputSchema).max(10).optional();
15
+ const commonShape = {
16
+ workspaceId: workspaceIdSchema,
17
+ adapter: probabilityAdapterIdSchema.optional(),
18
+ scenarioSet: nestedScenarios,
19
+ candidatePool,
20
+ horizonDays: z.number().positive().max(1_000_000).optional(),
21
+ metrics: z.array(probabilityMetricSchema).min(1).max(4).optional(),
22
+ acceptanceBands: nestedAcceptanceBands.optional(),
23
+ diagnosticThresholds: nestedDiagnosticThresholds.optional(),
24
+ outputs,
25
+ refresh: z.boolean().optional(),
26
+ };
27
+ export const probabilityInspectRequestSchema = z
28
+ .object({
29
+ workspaceId: workspaceIdSchema,
30
+ adapter: probabilityAdapterIdSchema.optional(),
31
+ source: nestedSource.optional(),
32
+ customPoolManifest: nestedManifest.optional(),
33
+ candidatePool,
34
+ refresh: z.boolean().optional(),
35
+ })
36
+ .strict()
37
+ .superRefine((value, context) => {
38
+ if (value.source !== undefined && value.customPoolManifest !== undefined)
39
+ context.addIssue({ code: 'custom', message: 'Provide either source or customPoolManifest' });
40
+ if (value.adapter !== undefined &&
41
+ value.source === undefined &&
42
+ value.customPoolManifest === undefined)
43
+ context.addIssue({
44
+ code: 'custom',
45
+ message: 'An adapter requires a source; provide a source alone to discover compatible adapters',
46
+ });
47
+ if (value.customPoolManifest !== undefined &&
48
+ value.adapter !== undefined &&
49
+ value.adapter !== 'custom_weighted_pool')
50
+ context.addIssue({
51
+ code: 'custom',
52
+ path: ['adapter'],
53
+ message: 'customPoolManifest requires adapter custom_weighted_pool',
54
+ });
55
+ });
56
+ export const probabilityEvaluateRequestSchema = z
57
+ .object({
58
+ ...commonShape,
59
+ source: nestedSource.optional(),
60
+ customPoolManifest: nestedManifest.optional(),
61
+ })
62
+ .strict()
63
+ .superRefine((value, context) => {
64
+ if ((value.source === undefined) === (value.customPoolManifest === undefined))
65
+ context.addIssue({
66
+ code: 'custom',
67
+ message: 'Provide exactly one source or customPoolManifest',
68
+ });
69
+ if (value.customPoolManifest !== undefined &&
70
+ value.adapter !== undefined &&
71
+ value.adapter !== 'custom_weighted_pool')
72
+ context.addIssue({
73
+ code: 'custom',
74
+ path: ['adapter'],
75
+ message: 'customPoolManifest requires adapter custom_weighted_pool',
76
+ });
77
+ });
78
+ export const probabilitySweepRequestSchema = z
79
+ .object({
80
+ ...commonShape,
81
+ source: nestedSource,
82
+ sweep: z
83
+ .object({
84
+ paths: z.array(z.string().min(1).max(1024)).min(1).max(32),
85
+ steps: z.number().int().min(2).max(10_000).default(25),
86
+ pairwise: z.boolean().default(false),
87
+ findRankReversals: z.boolean().default(true),
88
+ })
89
+ .strict(),
90
+ })
91
+ .strict();
92
+ export const probabilitySimulateRequestSchema = z
93
+ .object({
94
+ ...commonShape,
95
+ source: nestedSource,
96
+ samples: z.number().int().min(100).max(10_000_000).default(100_000),
97
+ seed: z.number().int().min(-2_147_483_648).max(2_147_483_647).default(1),
98
+ confidenceLevel: z.number().min(0.5).max(0.9999).default(0.95),
99
+ samplingMethod: probabilitySamplingMethodSchema.default('latin_hypercube'),
100
+ })
101
+ .strict();
102
+ export const probabilitySequenceRequestSchema = z
103
+ .object({
104
+ workspaceId: workspaceIdSchema,
105
+ scenarioSet: nestedScenarios,
106
+ customPoolManifest: nestedManifest,
107
+ horizonDays: z.number().positive().max(1_000_000),
108
+ maxSteps: z.number().int().min(1).max(100_000).default(1_000),
109
+ samples: z.number().int().min(100).max(10_000_000).default(100_000),
110
+ seed: z.number().int().min(-2_147_483_648).max(2_147_483_647).default(1),
111
+ confidenceLevel: z.number().min(0.5).max(0.9999).default(0.95),
112
+ acceptanceBands: nestedAcceptanceBands.optional(),
113
+ diagnosticThresholds: nestedDiagnosticThresholds.optional(),
114
+ outputs,
115
+ })
116
+ .strict();
117
+ export const probabilityCompareRequestSchema = z
118
+ .object({
119
+ workspaceId: workspaceIdSchema,
120
+ adapter: probabilityAdapterIdSchema.optional(),
121
+ before: nestedSource.optional(),
122
+ after: nestedSource.optional(),
123
+ beforeManifest: nestedManifest.optional(),
124
+ afterManifest: nestedManifest.optional(),
125
+ scenarioSet: nestedScenarios,
126
+ candidatePool,
127
+ horizonDays: z.number().positive().max(1_000_000).optional(),
128
+ acceptanceBands: nestedAcceptanceBands.optional(),
129
+ diagnosticThresholds: nestedDiagnosticThresholds.optional(),
130
+ outputs,
131
+ refresh: z.boolean().optional(),
132
+ })
133
+ .strict()
134
+ .superRefine((value, context) => {
135
+ const sourceMode = value.before !== undefined || value.after !== undefined;
136
+ const manifestMode = value.beforeManifest !== undefined || value.afterManifest !== undefined;
137
+ if (sourceMode === manifestMode)
138
+ context.addIssue({
139
+ code: 'custom',
140
+ message: 'Compare either before/after sources or beforeManifest/afterManifest',
141
+ });
142
+ if (sourceMode && (value.before === undefined || value.after === undefined))
143
+ context.addIssue({ code: 'custom', message: 'Source comparison requires before and after' });
144
+ if (manifestMode && (value.beforeManifest === undefined || value.afterManifest === undefined))
145
+ context.addIssue({
146
+ code: 'custom',
147
+ message: 'Custom-pool comparison requires beforeManifest and afterManifest',
148
+ });
149
+ if (manifestMode && value.adapter !== undefined && value.adapter !== 'custom_weighted_pool')
150
+ context.addIssue({
151
+ code: 'custom',
152
+ path: ['adapter'],
153
+ message: 'Manifest comparison requires adapter custom_weighted_pool',
154
+ });
155
+ });
156
+ export const probabilityRenderRequestSchema = z
157
+ .object({
158
+ workspaceId: workspaceIdSchema,
159
+ analysisId: z.string().min(1).max(256),
160
+ expectedScenarioHash: z
161
+ .string()
162
+ .regex(/^[a-f0-9]{64}$/u)
163
+ .optional(),
164
+ outputs: z.array(probabilityOutputSchema).min(1).max(10),
165
+ includeHtml: z.boolean().default(false),
166
+ filter: nestedRenderFilter.optional(),
167
+ })
168
+ .strict();
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod/v4';
2
2
  import { SOURCE_MAX_BYTES } from '../core/source/index.js';
3
- import { workspaceRelativePathSchema } from './common.js';
3
+ import { workspaceIdSchema, workspaceRelativePathSchema } from './common.js';
4
+ import { helperExpansionRequestSchema, validateHelperExpansionMode } from './helper-expansion.js';
4
5
  export const technologyIdSchema = z.string().min(1).max(512);
5
6
  export const technologyDirectionSchema = z.enum(['prerequisites', 'descendants', 'both']);
6
7
  export const technologyUnlockKindSchema = z.enum([
@@ -75,3 +76,91 @@ export const technologyProposedSourceSchema = z
75
76
  .optional(),
76
77
  })
77
78
  .strict();
79
+ export const technologyAnalysisModeSchema = z.enum([
80
+ 'scan',
81
+ 'folders',
82
+ 'trace',
83
+ 'explain',
84
+ 'unlocks',
85
+ 'bonus_coverage',
86
+ 'lint',
87
+ 'impact',
88
+ 'helper_expansion',
89
+ ]);
90
+ export function validateTechnologyInspectRequest(value, context) {
91
+ validateHelperExpansionMode(value, context);
92
+ if ((value.mode === 'trace' || value.mode === 'explain') && value.technologyId === undefined)
93
+ context.addIssue({
94
+ code: 'custom',
95
+ path: ['technologyId'],
96
+ message: `${value.mode} requires technologyId`,
97
+ });
98
+ if (value.mode === 'impact' && value.impact === undefined)
99
+ context.addIssue({
100
+ code: 'custom',
101
+ path: ['impact'],
102
+ message: 'Impact mode requires impact',
103
+ });
104
+ }
105
+ export const technologyInspectRequestSchema = z
106
+ .object({
107
+ workspaceId: workspaceIdSchema,
108
+ mode: technologyAnalysisModeSchema,
109
+ folderId: technologyIdSchema.optional(),
110
+ technologyId: technologyIdSchema.optional(),
111
+ categoryId: technologyIdSchema.optional(),
112
+ targetKind: technologyUnlockKindSchema.optional(),
113
+ targetId: technologyIdSchema.optional(),
114
+ direction: technologyDirectionSchema.optional(),
115
+ maxDepth: z.number().int().min(1).max(256).optional(),
116
+ maxNodes: z.number().int().min(1).max(25_000).optional(),
117
+ includeSubTechnologies: z.boolean().optional(),
118
+ classifications: z.array(technologyDefectClassSchema).max(4).optional(),
119
+ codes: z.array(z.string().min(1).max(256)).max(100).optional(),
120
+ impact: technologyImpactSchema.optional(),
121
+ helperExpansion: helperExpansionRequestSchema.optional(),
122
+ refresh: z.boolean().optional(),
123
+ })
124
+ .strict()
125
+ .superRefine(validateTechnologyInspectRequest);
126
+ export const technologyRenderRequestSchema = z
127
+ .object({
128
+ workspaceId: workspaceIdSchema,
129
+ view: technologyRenderViewSchema.exclude(['comparison']),
130
+ folderId: technologyIdSchema.optional(),
131
+ technologyId: technologyIdSchema.optional(),
132
+ categoryId: technologyIdSchema.optional(),
133
+ targetId: technologyIdSchema.optional(),
134
+ maxNodes: z.number().int().min(1).max(2_000).optional(),
135
+ includeHtml: z.boolean().optional(),
136
+ refresh: z.boolean().optional(),
137
+ })
138
+ .strict()
139
+ .superRefine((value, context) => {
140
+ if (value.view === 'folder' && value.folderId === undefined)
141
+ context.addIssue({
142
+ code: 'custom',
143
+ path: ['folderId'],
144
+ message: 'Folder view requires folderId',
145
+ });
146
+ });
147
+ export function validateTechnologyCompareRequest(value, context) {
148
+ if (value.after !== undefined && value.proposedSources !== undefined)
149
+ context.addIssue({
150
+ code: 'custom',
151
+ path: ['after'],
152
+ message: 'after conflicts with proposedSources',
153
+ });
154
+ }
155
+ export const technologyCompareRequestSchema = z
156
+ .object({
157
+ workspaceId: workspaceIdSchema,
158
+ before: technologyGraphReferenceSchema.optional(),
159
+ after: technologyGraphReferenceSchema.optional(),
160
+ proposedSources: z.array(technologyProposedSourceSchema).min(1).max(128).optional(),
161
+ render: z.boolean().optional(),
162
+ maxRenderNodes: z.number().int().min(1).max(2_000).optional(),
163
+ refresh: z.boolean().optional(),
164
+ })
165
+ .strict()
166
+ .superRefine(validateTechnologyCompareRequest);
@@ -96,6 +96,7 @@ export const transactionManifestSchema = z
96
96
  diagnostics: z.array(diagnosticSchema).max(TRANSACTION_MAX_DIAGNOSTICS),
97
97
  validation: validationSummarySchema,
98
98
  artifacts: z.array(artifactLinkSchema).max(TRANSACTION_MAX_ARTIFACTS),
99
+ executionArtifacts: z.array(artifactLinkSchema).max(TRANSACTION_MAX_ARTIFACTS).optional(),
99
100
  appliedFiles: z.array(workspaceRelativePathSchema).max(TRANSACTION_MAX_APPLIED_FILES),
100
101
  rollbackStatus: z.enum(['available', 'applied', 'failed']),
101
102
  failure: z.object({ code: identifierSchema, message: summarySchema }).strict().optional(),
@@ -1,6 +1,8 @@
1
1
  import { canonicalJson, compareCodeUnits, deterministicId, hashCanonical, } from '../core/canonical.js';
2
2
  import { sortDiagnostics } from '../core/diagnostics.js';
3
3
  import { ServiceError } from '../core/result.js';
4
+ import { beginSemanticAnalysis, semanticFragment, SemanticCatalog, } from '../core/semantic-dependencies.js';
5
+ import { runDependencyAnalysis, runDependencyAnalysisAsync, } from '../core/dependency-walk.js';
4
6
  import { analyzeTechnologySource, technologyAnalysisDiagnostics, technologySourceFiles, technologySourceFragmentCacheKey, } from './source-analysis.js';
5
7
  import { TECHNOLOGY_GRAPH_SCHEMA_VERSION, TECHNOLOGY_PARSER_VERSION, } from './model.js';
6
8
  const MAX_TECHNOLOGIES = 100_000;
@@ -374,7 +376,24 @@ function stronglyConnectedComponents(nodeIds, edges, signal) {
374
376
  }
375
377
  return components.sort((left, right) => compareCodeUnits(left[0] ?? '', right[0] ?? ''));
376
378
  }
377
- function expandHelperReferences(references, calls, unresolved, analysisMode, signal) {
379
+ function technologyHelperWalk(calls, sourceRevision) {
380
+ return {
381
+ domain: 'technology-helper-references',
382
+ sourceRevision,
383
+ edges: calls.map(({ id, sourceKind, sourceId, helperId }) => ({
384
+ id,
385
+ from: `${sourceKind}:${sourceId}`,
386
+ to: `scripted_effect:${helperId}`,
387
+ })),
388
+ roots: calls.filter(({ sourceKind }) => sourceKind !== 'scripted_effect').map(({ id }) => id),
389
+ branches: [...new Set(calls.map(({ helperId }) => `scripted_effect:${helperId}`))].sort(compareCodeUnits),
390
+ maximumDepth: MAX_HELPER_DEPTH,
391
+ maximumSteps: 10_000_000,
392
+ visitPolicy: 'path',
393
+ reverseEdges: false,
394
+ };
395
+ }
396
+ function expandHelperReferences(references, calls, unresolved, analysisMode, walk, signal) {
378
397
  const direct = references.filter(({ sourceKind }) => sourceKind === 'scripted_effect');
379
398
  const publicReferences = references.filter(({ sourceKind }) => sourceKind !== 'scripted_effect');
380
399
  if (analysisMode === 'focused') {
@@ -404,82 +423,59 @@ function expandHelperReferences(references, calls, unresolved, analysisMode, sig
404
423
  group.push(reference);
405
424
  refsByHelper.set(reference.sourceId, group);
406
425
  }
407
- const callsBySource = new Map();
408
- for (const call of calls) {
409
- const key = `${call.sourceKind}:${call.sourceId}`;
410
- const group = callsBySource.get(key) ?? [];
411
- group.push(call);
412
- callsBySource.set(key, group);
413
- }
414
426
  const projected = [...publicReferences, ...direct];
415
427
  let projectionCount = 0;
416
- for (const rootCall of calls.filter(({ sourceKind }) => sourceKind !== 'scripted_effect')) {
417
- const stack = [
418
- {
419
- helperId: rootCall.helperId,
420
- helperStack: [rootCall.helperId],
421
- confidence: rootCall.confidence,
422
- },
423
- ];
424
- const visited = new Set();
425
- while (stack.length > 0) {
426
- signal?.throwIfAborted();
427
- const current = stack.pop();
428
- const visitKey = current.helperStack.join('\0');
429
- if (visited.has(visitKey))
430
- continue;
431
- visited.add(visitKey);
432
- if (current.helperStack.length > MAX_HELPER_DEPTH) {
433
- unresolved.push({
434
- id: deterministicId('tech-unresolved', { rootCall, helperStack: current.helperStack }),
435
- kind: 'unsupported_construct',
436
- expression: current.helperStack.join(' -> '),
437
- ownerId: rootCall.sourceId,
438
- location: rootCall.location,
439
- confidence: 'unresolved',
440
- blockers: [
441
- {
442
- code: 'TECH_HELPER_DEPTH_BLOCKED',
443
- message: `Scripted-effect expansion exceeds depth ${MAX_HELPER_DEPTH}`,
444
- location: rootCall.location,
445
- },
446
- ],
447
- });
448
- continue;
449
- }
450
- for (const reference of refsByHelper.get(current.helperId) ?? []) {
451
- projectionCount += 1;
452
- if (projectionCount > MAX_HELPER_PROJECTIONS)
453
- break;
454
- projected.push({
455
- ...reference,
456
- id: deterministicId('tech-external-projection', {
457
- root: rootCall.id,
458
- reference: reference.id,
459
- helperStack: current.helperStack,
460
- }),
461
- sourceKind: rootCall.sourceKind,
462
- sourceId: rootCall.sourceId,
463
- helperStack: [...current.helperStack],
464
- confidence: worseConfidence(current.confidence, reference.confidence),
465
- metadata: {
466
- ...reference.metadata,
467
- projectedFromScriptedEffect: reference.sourceId,
468
- callLocation: canonicalJson(rootCall.location),
428
+ if (walk === undefined)
429
+ throw new Error('Full technology helper analysis requires a dependency traversal');
430
+ const roots = calls.filter(({ sourceKind }) => sourceKind !== 'scripted_effect');
431
+ const callById = new Map(calls.map((call) => [call.id, call]));
432
+ for (const record of walk.state.records) {
433
+ signal?.throwIfAborted();
434
+ if (record.kind !== 'visit' && record.kind !== 'depth')
435
+ continue;
436
+ const rootCall = roots[record.root];
437
+ const path = walk.path(record.path).map(({ id }) => callById.get(id));
438
+ const helperStack = path.map(({ helperId }) => helperId);
439
+ if (record.kind === 'depth') {
440
+ unresolved.push({
441
+ id: deterministicId('tech-unresolved', { rootCall, helperStack }),
442
+ kind: 'unsupported_construct',
443
+ expression: helperStack.join(' -> '),
444
+ ownerId: rootCall.sourceId,
445
+ location: rootCall.location,
446
+ confidence: 'unresolved',
447
+ blockers: [
448
+ {
449
+ code: 'TECH_HELPER_DEPTH_BLOCKED',
450
+ message: `Scripted-effect expansion exceeds depth ${MAX_HELPER_DEPTH}`,
451
+ location: rootCall.location,
469
452
  },
470
- });
471
- }
453
+ ],
454
+ });
455
+ continue;
456
+ }
457
+ const confidence = path.reduce((value, call) => worseConfidence(value, call.confidence), rootCall.confidence);
458
+ for (const reference of refsByHelper.get(path.at(-1).helperId) ?? []) {
459
+ projectionCount++;
472
460
  if (projectionCount > MAX_HELPER_PROJECTIONS)
473
461
  break;
474
- for (const call of callsBySource.get(`scripted_effect:${current.helperId}`) ?? []) {
475
- if (current.helperStack.includes(call.helperId))
476
- continue;
477
- stack.push({
478
- helperId: call.helperId,
479
- helperStack: [...current.helperStack, call.helperId],
480
- confidence: worseConfidence(current.confidence, call.confidence),
481
- });
482
- }
462
+ projected.push({
463
+ ...reference,
464
+ id: deterministicId('tech-external-projection', {
465
+ root: rootCall.id,
466
+ reference: reference.id,
467
+ helperStack,
468
+ }),
469
+ sourceKind: rootCall.sourceKind,
470
+ sourceId: rootCall.sourceId,
471
+ helperStack,
472
+ confidence: worseConfidence(confidence, reference.confidence),
473
+ metadata: {
474
+ ...reference.metadata,
475
+ projectedFromScriptedEffect: reference.sourceId,
476
+ callLocation: canonicalJson(rootCall.location),
477
+ },
478
+ });
483
479
  }
484
480
  if (projectionCount > MAX_HELPER_PROJECTIONS)
485
481
  break;
@@ -1123,14 +1119,13 @@ function fragmentsFor(snapshot, options) {
1123
1119
  };
1124
1120
  const fingerprint = helperCatalogFingerprint(snapshot);
1125
1121
  const fragments = [];
1122
+ const dependencies = beginSemanticAnalysis(options.cache, snapshot, 'technology', options.workspaceIdentity);
1123
+ const reads = new SemanticCatalog();
1124
+ context.helperIds = reads.bind('helpers', 'scripted_effect', helperIds);
1126
1125
  for (const file of technologySourceFiles(snapshot.files)) {
1127
1126
  options.signal?.throwIfAborted();
1128
1127
  const key = technologySourceFragmentCacheKey(file, fingerprint);
1129
- let fragment = options.cache?.get(key);
1130
- if (fragment === undefined) {
1131
- fragment = analyzeTechnologySource(file, context);
1132
- options.cache?.set(key, fragment, file.bytes.length);
1133
- }
1128
+ const fragment = semanticFragment(dependencies, options.cache, file, key, reads, () => analyzeTechnologySource(file, context));
1134
1129
  fragments.push(fragment);
1135
1130
  }
1136
1131
  return fragments;
@@ -1143,6 +1138,12 @@ function resolveUnlocks(unlocks, targets, complete) {
1143
1138
  }));
1144
1139
  }
1145
1140
  export function buildTechnologyGraph(snapshot, options) {
1141
+ return runDependencyAnalysis(technologyGraphAnalysis(snapshot, options), options.signal);
1142
+ }
1143
+ export function buildTechnologyGraphAsync(snapshot, options) {
1144
+ return runDependencyAnalysisAsync(technologyGraphAnalysis(snapshot, options), options.signal);
1145
+ }
1146
+ function* technologyGraphAnalysis(snapshot, options) {
1146
1147
  options.signal?.throwIfAborted();
1147
1148
  const fragments = fragmentsFor(snapshot, options);
1148
1149
  const assets = assetMap(options.assetFiles ?? []);
@@ -1167,7 +1168,10 @@ export function buildTechnologyGraph(snapshot, options) {
1167
1168
  const placements = enrichPlacements(basePlacements, edges, gridboxes, technologies, unlocks, itemLayouts, options.signal);
1168
1169
  const unresolved = fragments.flatMap(({ unresolved }) => unresolved);
1169
1170
  const helperCalls = fragments.flatMap(({ helperCalls }) => helperCalls);
1170
- const externalReferences = expandHelperReferences(fragments.flatMap(({ externalReferences }) => externalReferences), helperCalls, unresolved, options.analysisMode ?? 'full', options.signal);
1171
+ const walk = options.analysisMode === 'focused'
1172
+ ? undefined
1173
+ : yield technologyHelperWalk(helperCalls, snapshot.revision);
1174
+ const externalReferences = expandHelperReferences(fragments.flatMap(({ externalReferences }) => externalReferences), helperCalls, unresolved, options.analysisMode ?? 'full', walk, options.signal);
1171
1175
  const issues = diagnose(technologies, folders, placements, gridboxes, edges, categories, doctrineDefinitions, unlocks, externalReferences, selectedTechnologies.duplicates.map((group) => group.map(({ iconSprite, effectSignatureFields, ...technology }) => ({
1172
1176
  ...technology,
1173
1177
  icon: iconStatus(iconSprite, snapshot.index, assets),
@@ -1199,6 +1203,7 @@ export function buildTechnologyGraph(snapshot, options) {
1199
1203
  revision,
1200
1204
  complete: snapshot.complete &&
1201
1205
  unresolved.every(({ kind }) => kind !== 'partial_source') &&
1206
+ !unresolved.some(({ blockers }) => blockers.some(({ code }) => code === 'TECH_HELPER_DEPTH_BLOCKED' || code === 'TECH_HELPER_PROJECTION_LIMIT')) &&
1202
1207
  !(options.analysisMode === 'focused' && helperCalls.length > 0),
1203
1208
  analysisBoundary: {
1204
1209
  staticAnalysis: true,
@@ -0,0 +1,33 @@
1
+ import { compareCodeUnits } from '../core/canonical.js';
2
+ export function technologyHelperInventory(graph, sourceComplete) {
3
+ return {
4
+ domain: 'technology',
5
+ sourceRevision: graph.revision,
6
+ sourceComplete,
7
+ sourceHashes: graph.sourceHashes,
8
+ unresolved: graph.unresolved.filter(({ id }) => id !== 'tech-unresolved-helper-expansion-deferred'),
9
+ edges: [
10
+ ...graph.helperCalls.map((call) => ({
11
+ id: call.id,
12
+ from: `${call.sourceKind}:${call.sourceId}`,
13
+ to: `scripted_effect:${call.helperId}`,
14
+ evidence: { kind: 'helper_call', call },
15
+ })),
16
+ ...graph.externalReferences
17
+ .filter(({ sourceKind }) => sourceKind === 'scripted_effect')
18
+ .map((reference) => ({
19
+ id: `reference:${reference.id}`,
20
+ from: `scripted_effect:${reference.sourceId}`,
21
+ to: `reference:${reference.id}`,
22
+ evidence: { kind: 'technology_reference', reference },
23
+ })),
24
+ ].sort((left, right) => compareCodeUnits(left.id, right.id)),
25
+ roots: graph.helperCalls
26
+ .filter(({ sourceKind }) => sourceKind !== 'scripted_effect')
27
+ .map(({ id }) => id)
28
+ .sort(compareCodeUnits),
29
+ branches: [
30
+ ...new Set(graph.helperCalls.map(({ helperId }) => `scripted_effect:${helperId}`)),
31
+ ].sort(compareCodeUnits),
32
+ };
33
+ }
@@ -0,0 +1,60 @@
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 { technologyCompareRequestSchema, technologyInspectRequestSchema, technologyRenderRequestSchema, } from '../schemas/technology.js';
6
+ import { compareTechnologies, inspectTechnologies, normalizeTechnologyCompareRequest, normalizeTechnologyInspectionRequest, normalizeTechnologyRenderRequest, renderTechnologies, } from './operations.js';
7
+ import { TechnologyTreeViewer } from './service.js';
8
+ const wireResult = z.record(z.string(), z.json());
9
+ /** Registers typed technology operations without invoking transport handlers. */
10
+ export function registerTechnologyJobs(operations, engine) {
11
+ const viewer = new TechnologyTreeViewer(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({
27
+ completed: 3,
28
+ total: 3,
29
+ message: 'Technology operation complete',
30
+ });
31
+ const wire = wireResult.parse(JSON.parse(JSON.stringify(toolResult(result))));
32
+ const revision = result.data !== null &&
33
+ typeof result.data === 'object' &&
34
+ 'revision' in result.data &&
35
+ typeof result.data.revision === 'string'
36
+ ? result.data.revision
37
+ : result.data !== null &&
38
+ typeof result.data === 'object' &&
39
+ 'afterRevision' in result.data &&
40
+ typeof result.data.afterRevision === 'string'
41
+ ? result.data.afterRevision
42
+ : undefined;
43
+ if (revision !== undefined)
44
+ await context.stageResult(wire, { sourceRevision: revision });
45
+ return wire;
46
+ }
47
+ catch (error) {
48
+ if (context.signal.aborted)
49
+ throw error;
50
+ return wireResult.parse(JSON.parse(JSON.stringify(errorResult(error, context.workspaceId))));
51
+ }
52
+ finally {
53
+ release();
54
+ }
55
+ });
56
+ }
57
+ register('hoi4.tech_inspect', technologyInspectRequestSchema, normalizeTechnologyInspectionRequest, inspectTechnologies, 'Analyzing technology trees');
58
+ register('hoi4.tech_render', technologyRenderRequestSchema, normalizeTechnologyRenderRequest, renderTechnologies, 'Rendering technology-tree view');
59
+ register('hoi4.tech_compare', technologyCompareRequestSchema, normalizeTechnologyCompareRequest, compareTechnologies, 'Comparing technology-tree graphs');
60
+ }