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,123 @@
1
+ import { compareCodeUnits } from '../core/canonical.js';
2
+ import { ServiceError } from '../core/result.js';
3
+ import { parseClausewitz } from '../core/source/index.js';
4
+ import { enrichFocusPlanFromSidecar, focusPlanningSidecarPath, importContinuousFocusPalettes, importFocusTrees, linkContinuousFocusPalettes, parseFocusPlanningSidecar, } from './index.js';
5
+ function normalizedRoot(value) {
6
+ return value.replaceAll('\\', '/').replace(/^\.\//u, '').replace(/\/+$/u, '').toLowerCase();
7
+ }
8
+ export function underConfiguredRoot(relativePath, roots) {
9
+ const candidate = relativePath.replaceAll('\\', '/').toLowerCase();
10
+ return roots.some((root) => {
11
+ const normalized = normalizedRoot(root);
12
+ return candidate === normalized || candidate.startsWith(`${normalized}/`);
13
+ });
14
+ }
15
+ export function activeFocusFiles(snapshot, focusRoots) {
16
+ return snapshot.files.filter(({ relativePath, shadowedBy }) => shadowedBy === undefined &&
17
+ underConfiguredRoot(relativePath, focusRoots) &&
18
+ relativePath.toLowerCase().endsWith('.txt'));
19
+ }
20
+ export function activeContinuousFocusFiles(snapshot) {
21
+ return snapshot.files.filter(({ relativePath, shadowedBy }) => shadowedBy === undefined &&
22
+ relativePath.replaceAll('\\', '/').toLowerCase().startsWith('common/continuous_focus/') &&
23
+ relativePath.toLowerCase().endsWith('.txt'));
24
+ }
25
+ export function continuousPalettes(snapshot) {
26
+ const palettes = [];
27
+ const diagnostics = [];
28
+ for (const file of activeContinuousFocusFiles(snapshot)) {
29
+ const document = parseClausewitz(file.bytes, file.displayPath);
30
+ const imported = importContinuousFocusPalettes(document);
31
+ palettes.push(...imported.continuousFocusPalettes);
32
+ diagnostics.push(...imported.diagnostics);
33
+ }
34
+ return { palettes, diagnostics };
35
+ }
36
+ export function activeSidecar(snapshot, focusFile) {
37
+ const relativePath = focusPlanningSidecarPath(focusFile.relativePath).toLowerCase();
38
+ const file = snapshot.files.find((candidate) => candidate.shadowedBy === undefined && candidate.relativePath.toLowerCase() === relativePath);
39
+ return file === undefined ? undefined : { file, sidecar: parseFocusPlanningSidecar(file.bytes) };
40
+ }
41
+ export function importFocusFile(snapshot, file, palettes) {
42
+ const document = parseClausewitz(file.bytes, file.displayPath);
43
+ const imported = importFocusTrees(document, { references: referenceCatalog(snapshot) });
44
+ const sidecar = activeSidecar(snapshot, file);
45
+ imported.plans = imported.plans.map((plan) => {
46
+ const enriched = sidecar === undefined
47
+ ? { plan, diagnostics: [] }
48
+ : enrichFocusPlanFromSidecar(plan, sidecar.sidecar);
49
+ imported.diagnostics.push(...enriched.diagnostics);
50
+ return linkContinuousFocusPalettes(enriched.plan, palettes);
51
+ });
52
+ return imported;
53
+ }
54
+ export function selectedFocusFile(snapshot, focusRoots, relativePath) {
55
+ const file = activeFocusFiles(snapshot, focusRoots).find((candidate) => candidate.relativePath === relativePath);
56
+ if (file === undefined)
57
+ throw new ServiceError('FOCUS_SOURCE_NOT_FOUND', `Focus source was not found: ${relativePath}`);
58
+ return file;
59
+ }
60
+ export function selectedContinuousFocusFile(snapshot, relativePath) {
61
+ const file = activeContinuousFocusFiles(snapshot).find((candidate) => candidate.relativePath === relativePath);
62
+ if (file === undefined)
63
+ throw new ServiceError('CONTINUOUS_FOCUS_SOURCE_NOT_FOUND', `Continuous focus source was not found: ${relativePath}`);
64
+ return file;
65
+ }
66
+ export function selectPlan(result, treeId) {
67
+ if (result.plans.length === 0)
68
+ throw new ServiceError('FOCUS_TREE_NOT_FOUND', 'The source file contains no focus tree');
69
+ if (treeId === undefined) {
70
+ if (result.plans.length !== 1)
71
+ throw new ServiceError('FOCUS_TREE_ID_REQUIRED', 'The source file contains multiple focus trees; provide treeId', { treeIds: result.plans.map(({ id }) => id) });
72
+ return result.plans[0];
73
+ }
74
+ const plan = result.plans.find(({ id }) => id === treeId);
75
+ if (plan === undefined)
76
+ throw new ServiceError('FOCUS_TREE_NOT_FOUND', `Focus tree was not found: ${treeId}`);
77
+ return plan;
78
+ }
79
+ export function selectContinuousPlan(result, paletteId) {
80
+ if (result.continuousFocusPalettes.length === 0)
81
+ throw new ServiceError('CONTINUOUS_FOCUS_PALETTE_NOT_FOUND', 'The source file contains no continuous focus palette');
82
+ if (paletteId === undefined) {
83
+ if (result.continuousFocusPalettes.length !== 1)
84
+ throw new ServiceError('CONTINUOUS_FOCUS_PALETTE_ID_REQUIRED', 'The source file contains multiple continuous focus palettes; provide paletteId', { paletteIds: result.continuousFocusPalettes.map(({ id }) => id) });
85
+ return result.continuousFocusPalettes[0];
86
+ }
87
+ const plan = result.continuousFocusPalettes.find(({ id }) => id === paletteId);
88
+ if (plan === undefined)
89
+ throw new ServiceError('CONTINUOUS_FOCUS_PALETTE_NOT_FOUND', `Continuous focus palette was not found: ${paletteId}`);
90
+ return plan;
91
+ }
92
+ export function referenceCatalog(snapshot) {
93
+ const identifiers = (kind) => [
94
+ ...new Set(snapshot.index
95
+ .findAll(kind)
96
+ .filter(({ overridden }) => !overridden)
97
+ .map(({ id }) => id)),
98
+ ].sort((left, right) => compareCodeUnits(left, right));
99
+ return {
100
+ decision: identifiers('decision'),
101
+ decision_category: identifiers('decision_category'),
102
+ event: identifiers('event'),
103
+ idea: identifiers('idea'),
104
+ leader: identifiers('leader'),
105
+ formable: identifiers('formable'),
106
+ helper: identifiers('scripted_effect'),
107
+ };
108
+ }
109
+ export function validationFromDiagnostics(diagnostics) {
110
+ const blocking = diagnostics.filter(({ severity }) => severity === 'error' || severity === 'blocker');
111
+ return {
112
+ passed: blocking.length === 0,
113
+ checks: [
114
+ {
115
+ id: 'focus-diagnostics',
116
+ passed: blocking.length === 0,
117
+ message: blocking.length === 0
118
+ ? 'Focus source, references, and layout have no blocking diagnostics'
119
+ : `${blocking.length} blocking focus diagnostics`,
120
+ },
121
+ ],
122
+ };
123
+ }
@@ -0,0 +1,81 @@
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 { guiInspectRequestSchema, guiRenderRequestSchema, guiRewriteRequestSchema, } from '../schemas/gui-requests.js';
6
+ import { ScriptedGuiStudio } from './studio.js';
7
+ import { inspectGui, completeGuiRewrite, normalizeGuiInspectRequest, normalizeGuiRenderRequest, normalizeGuiRewriteRequest, prepareGuiRewrite, renderGui, } from './tool-operations.js';
8
+ const wireResult = z.record(z.string(), z.json());
9
+ /** Registers read-only GUI operations without invoking transport handlers. */
10
+ export function registerGuiJobs(operations, engine) {
11
+ const studio = new ScriptedGuiStudio(engine);
12
+ const lifetime = new IdleCacheLifetime(() => studio.clearCaches());
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 result = await execute(studio, 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(result))));
26
+ const revision = result.data !== null &&
27
+ typeof result.data === 'object' &&
28
+ 'sourceRevision' in result.data &&
29
+ typeof result.data.sourceRevision === 'string'
30
+ ? result.data.sourceRevision
31
+ : result.data !== null &&
32
+ typeof result.data === 'object' &&
33
+ 'sharedRevision' in result.data &&
34
+ typeof result.data.sharedRevision === 'string'
35
+ ? result.data.sharedRevision
36
+ : undefined;
37
+ if (revision !== undefined)
38
+ await context.stageResult(wire, { sourceRevision: revision });
39
+ return wire;
40
+ }
41
+ catch (error) {
42
+ if (context.signal.aborted)
43
+ throw error;
44
+ return wireResult.parse(JSON.parse(JSON.stringify(errorResult(error, context.workspaceId))));
45
+ }
46
+ finally {
47
+ release();
48
+ }
49
+ });
50
+ }
51
+ register('hoi4.gui_inspect', guiInspectRequestSchema, normalizeGuiInspectRequest, (studio, input, context) => inspectGui(engine, studio, input, context));
52
+ register('hoi4.gui_render', guiRenderRequestSchema, normalizeGuiRenderRequest, renderGui);
53
+ operations.registerWrite('hoi4.gui_rewrite', guiRewriteRequestSchema, async (input, context) => {
54
+ if (input.workspaceId !== 'current' && input.workspaceId !== context.workspaceId)
55
+ throw new ServiceError('JOB_ARGUMENT_SCOPE_MISMATCH', 'The request workspace does not match the authorized job workspace');
56
+ const release = lifetime.begin();
57
+ try {
58
+ return await prepareGuiRewrite(studio, normalizeGuiRewriteRequest(input), {
59
+ workspaceId: context.workspaceId,
60
+ ...(context.principal === undefined ? {} : { principal: context.principal }),
61
+ signal: context.signal,
62
+ progress: (completed, total, message) => context.progress({ completed, total, message }),
63
+ });
64
+ }
65
+ finally {
66
+ release();
67
+ }
68
+ }, async (_input, execution, _reconciled, recipe, context) => {
69
+ const result = completeGuiRewrite(context.workspaceId, execution, recipe);
70
+ await context.progress({
71
+ completed: 3,
72
+ total: 3,
73
+ message: execution.outcome === 'applied'
74
+ ? 'GUI rewrite complete'
75
+ : execution.outcome === 'unchanged'
76
+ ? 'GUI content already satisfied the rewrite'
77
+ : 'GUI rewrite blocked',
78
+ });
79
+ return wireResult.parse(JSON.parse(JSON.stringify(toolResult(result))));
80
+ });
81
+ }
@@ -0,0 +1,270 @@
1
+ import { z } from 'zod/v4';
2
+ import { boundedSourceHashEvidence, publicArtifactLink } from '../core/artifacts.js';
3
+ import { canonicalJson, compareCodeUnits, hashCanonical } from '../core/canonical.js';
4
+ import { autonomousResultArtifacts, } from '../core/transaction-execution.js';
5
+ import { emptyServiceResult } from '../core/result.js';
6
+ import { setInlineFilesScanned } from '../core/operation-result.js';
7
+ import { PACKAGE_VERSION } from '../version.js';
8
+ import { compileGuiHelpers } from './helpers.js';
9
+ import { encodeGuiInspectionArtifact, projectGuiGraphForArtifact } from './inspection-artifact.js';
10
+ const guiRewriteRecipeSchema = z
11
+ .object({
12
+ mode: z.enum(['source', 'helpers', 'patches']),
13
+ filesScanned: z.array(z.string().min(1).max(4096)),
14
+ compilation: z
15
+ .object({
16
+ nodeCount: z.number().int().nonnegative(),
17
+ templateInstanceCount: z.number().int().nonnegative(),
18
+ rawEscapeCount: z.number().int().nonnegative(),
19
+ })
20
+ .strict()
21
+ .optional(),
22
+ })
23
+ .strict();
24
+ function validationSummary(validation) {
25
+ return {
26
+ passed: validation.checks.every(({ passed }) => passed) &&
27
+ !validation.diagnostics.some(({ severity }) => severity === 'error' || severity === 'blocker'),
28
+ checks: validation.checks,
29
+ };
30
+ }
31
+ export function normalizeGuiInspectRequest(input) {
32
+ return input;
33
+ }
34
+ export function normalizeGuiRenderRequest(input) {
35
+ return input;
36
+ }
37
+ export function normalizeGuiRewriteRequest(input) {
38
+ return input;
39
+ }
40
+ export async function prepareGuiRewrite(studio, input, context) {
41
+ await context.progress(0, 3, 'Validating the requested GUI result');
42
+ const compilation = input.mode === 'helpers' ? compileGuiHelpers(input.helper) : undefined;
43
+ const transaction = await studio.planSource({
44
+ workspaceId: context.workspaceId,
45
+ relativePath: input.relativePath,
46
+ ...(input.mode === 'patches'
47
+ ? { expectedSourceHash: input.expectedSourceHash, patches: input.patches }
48
+ : {
49
+ source: input.mode === 'source' ? input.source : compilation.source,
50
+ ...(input.additionalFiles === undefined
51
+ ? {}
52
+ : { additionalFiles: input.additionalFiles }),
53
+ }),
54
+ windowName: input.windowName,
55
+ scenario: input.scenario,
56
+ ...(context.principal === undefined ? {} : { principal: context.principal }),
57
+ ...(context.signal === undefined ? {} : { signal: context.signal }),
58
+ });
59
+ await context.progress(2, 3, 'GUI rewrite prepared for journaled application');
60
+ return {
61
+ transaction,
62
+ recipe: guiRewriteRecipeSchema.parse({
63
+ mode: input.mode,
64
+ filesScanned: [
65
+ input.relativePath,
66
+ ...(input.additionalFiles ?? []).map(({ relativePath }) => relativePath),
67
+ ].sort(compareCodeUnits),
68
+ ...(compilation === undefined
69
+ ? {}
70
+ : {
71
+ compilation: {
72
+ nodeCount: compilation.nodeCount,
73
+ templateInstanceCount: compilation.templateInstanceCount,
74
+ rawEscapeCount: compilation.rawEscapeCount,
75
+ },
76
+ }),
77
+ }),
78
+ };
79
+ }
80
+ export function completeGuiRewrite(workspaceId, execution, recipe) {
81
+ const parsed = guiRewriteRecipeSchema.parse(recipe);
82
+ const transaction = execution.transaction;
83
+ const result = emptyServiceResult(workspaceId, {
84
+ mode: parsed.mode,
85
+ execution: execution.outcome,
86
+ fileCount: transaction.files.length,
87
+ artifactCount: transaction.artifacts.length,
88
+ ...(parsed.compilation ?? {}),
89
+ });
90
+ result.status = transaction.validation.passed ? 'ok' : 'blocked';
91
+ result.code =
92
+ execution.outcome === 'applied'
93
+ ? 'GUI_CHANGES_APPLIED'
94
+ : execution.outcome === 'unchanged'
95
+ ? 'GUI_CHANGES_UNCHANGED'
96
+ : 'GUI_CHANGES_BLOCKED';
97
+ setInlineFilesScanned(result, parsed.filesScanned);
98
+ result.proposedFiles = transaction.files.slice(0, 100).map(({ relativePath }) => relativePath);
99
+ result.changedFiles = transaction.appliedFiles.slice(0, 100);
100
+ result.diagnostics = transaction.diagnostics.slice(0, 100);
101
+ result.artifacts = autonomousResultArtifacts(execution);
102
+ result.validation = transaction.validation;
103
+ result.blockers = transaction.diagnostics
104
+ .filter(({ severity }) => severity === 'error' || severity === 'blocker')
105
+ .map(({ code, message, details }) => ({
106
+ code,
107
+ message,
108
+ ...(details === undefined ? {} : { details }),
109
+ }));
110
+ return result;
111
+ }
112
+ export async function inspectGui(engine, studio, input, context) {
113
+ await context.progress(0, 3, 'Building GUI source graph');
114
+ const inspected = input.windowName === undefined || input.scenario === undefined
115
+ ? {
116
+ graph: (await studio.scan(context.workspaceId, context.principal, context.signal)).graph,
117
+ linted: undefined,
118
+ }
119
+ : await (async () => {
120
+ const linted = await studio.lint({
121
+ workspaceId: context.workspaceId,
122
+ windowName: input.windowName,
123
+ scenario: input.scenario,
124
+ ...(input.relatedScenarios === undefined
125
+ ? {}
126
+ : { relatedScenarios: input.relatedScenarios }),
127
+ generatedScenarios: input.generatedScenarios ?? {},
128
+ ...(context.principal === undefined ? {} : { principal: context.principal }),
129
+ ...(context.signal === undefined ? {} : { signal: context.signal }),
130
+ });
131
+ return { graph: linted.graph, linted };
132
+ })();
133
+ const { graph, linted } = inspected;
134
+ const sharedRevision = hashCanonical(graph.sourceHashes);
135
+ const workspace = engine.resolver.get(context.workspaceId, context.principal);
136
+ const sourceEvidence = boundedSourceHashEvidence(graph.sourceHashes);
137
+ const artifactGraph = projectGuiGraphForArtifact(graph, linted?.scene.elements.map(({ sourceId }) => sourceId));
138
+ const inspectionName = `gui-inspect.${sharedRevision.slice(0, 16)}.json`;
139
+ const inspectionJson = `${canonicalJson({
140
+ schemaVersion: 1,
141
+ sharedRevision,
142
+ offline: true,
143
+ graph: artifactGraph.graph,
144
+ ...(artifactGraph.projection === undefined
145
+ ? {}
146
+ : { graphProjection: artifactGraph.projection }),
147
+ ...(linted === undefined
148
+ ? {}
149
+ : {
150
+ scenario: linted.scene.scenario,
151
+ fidelity: linted.scene.fidelity,
152
+ validation: linted.validation,
153
+ }),
154
+ })}\n`;
155
+ const encodedInspection = await encodeGuiInspectionArtifact(inspectionName, inspectionJson);
156
+ const artifact = await engine.artifacts.putChunked(workspace, encodedInspection.name, encodedInspection.mimeType, encodedInspection.content, {
157
+ kind: 'gui-inspect',
158
+ toolVersion: PACKAGE_VERSION,
159
+ schemaVersion: 'gui-inspect.v1',
160
+ sourceHashes: sourceEvidence.sourceHashes,
161
+ metadata: {
162
+ sharedRevision,
163
+ offline: true,
164
+ complete: graph.complete,
165
+ skippedSourceCount: graph.skippedSourceCount,
166
+ sourceHashInventory: sourceEvidence.inventory,
167
+ compressed: encodedInspection.compressed,
168
+ uncompressedBytes: encodedInspection.uncompressedBytes,
169
+ graphProjection: artifactGraph.projection?.mode ?? 'full',
170
+ },
171
+ }, encodedInspection.compressed
172
+ ? 'Gzip-compressed scripted GUI graph, diagnostics, and optional scenario fidelity inspection'
173
+ : 'Scripted GUI graph, diagnostics, and optional scenario fidelity inspection', context.signal);
174
+ const diagnostics = linted === undefined ? graph.diagnostics : linted.validation.diagnostics;
175
+ const result = emptyServiceResult(context.workspaceId, {
176
+ sharedRevision,
177
+ complete: graph.complete,
178
+ skippedSourceCount: graph.skippedSourceCount,
179
+ skippedSources: graph.skippedSources,
180
+ nodes: graph.nodes.length,
181
+ edges: graph.edges.length,
182
+ elements: graph.elements.length,
183
+ sprites: graph.sprites.length,
184
+ fonts: graph.fonts.length,
185
+ scriptedGuis: graph.scriptedGuis.length,
186
+ ...(linted === undefined
187
+ ? {}
188
+ : {
189
+ windowName: input.windowName,
190
+ scenarioId: linted.scene.scenario.id,
191
+ inspectedElementCount: linted.scene.elements.length,
192
+ fidelityCounts: Object.fromEntries(Object.entries(linted.scene.fidelity).map(([key, values]) => [key, values.length])),
193
+ }),
194
+ });
195
+ result.code = graph.complete ? 'GUI_INSPECTED' : 'GUI_INSPECTED_PARTIAL';
196
+ setInlineFilesScanned(result, graph.filesScanned);
197
+ result.diagnostics = diagnostics.slice(0, 100);
198
+ result.artifacts = [publicArtifactLink(artifact)];
199
+ result.validation =
200
+ linted === undefined
201
+ ? {
202
+ passed: !diagnostics.some(({ severity }) => severity === 'error' || severity === 'blocker'),
203
+ checks: [
204
+ {
205
+ id: 'gui-inspection',
206
+ passed: true,
207
+ message: graph.complete
208
+ ? `${graph.nodes.length} GUI graph nodes connected from a complete inventory`
209
+ : `${graph.nodes.length} GUI graph nodes connected; ${graph.skippedSourceCount} over-limit source(s) were skipped`,
210
+ },
211
+ ],
212
+ }
213
+ : validationSummary({ diagnostics, checks: linted.validation.checks });
214
+ await context.progress(3, 3, 'GUI inspection complete');
215
+ return result;
216
+ }
217
+ export async function renderGui(studio, input, context) {
218
+ await context.progress(0, 4, 'Building GUI source graph');
219
+ const rendered = await studio.renderAndStore({
220
+ workspaceId: context.workspaceId,
221
+ windowName: input.windowName,
222
+ scenario: input.scenario,
223
+ ...(input.states === undefined ? {} : { states: input.states }),
224
+ ...(input.resolutions === undefined
225
+ ? {}
226
+ : {
227
+ resolutions: input.resolutions.map(({ width, height, uiScale }) => ({
228
+ width,
229
+ height,
230
+ ...(uiScale === undefined ? {} : { uiScale }),
231
+ })),
232
+ }),
233
+ ...(input.relatedScenarios === undefined ? {} : { relatedScenarios: input.relatedScenarios }),
234
+ generatedScenarios: input.generatedScenarios ?? {},
235
+ ...(input.comparisonScenario === undefined
236
+ ? {}
237
+ : { comparisonScenario: input.comparisonScenario }),
238
+ ...(context.principal === undefined ? {} : { principal: context.principal }),
239
+ ...(context.signal === undefined ? {} : { signal: context.signal }),
240
+ });
241
+ await context.progress(3, 4, 'Preparing GUI result');
242
+ const diagnostics = rendered.validation.diagnostics;
243
+ const result = emptyServiceResult(context.workspaceId, {
244
+ windowName: input.windowName,
245
+ scenarioId: rendered.render.scene.scenario.id,
246
+ sourceRevision: rendered.render.scene.sourceRevision,
247
+ variantCount: rendered.render.images.length,
248
+ variants: rendered.render.images.slice(0, 64).map(({ variant, width, height }) => ({
249
+ variant,
250
+ width,
251
+ height,
252
+ })),
253
+ stateCount: rendered.stateScenes.length,
254
+ scenarioCount: rendered.scenarioScenes.length,
255
+ resolutionCount: rendered.resolutionScenes.length,
256
+ comparison: {
257
+ changedPixels: rendered.comparison.changedPixels,
258
+ changedRatio: rendered.comparison.changedRatio,
259
+ },
260
+ fidelityCounts: Object.fromEntries(Object.entries(rendered.render.fidelity).map(([key, values]) => [key, values.length])),
261
+ offlineRepresentation: true,
262
+ });
263
+ result.code = 'GUI_RENDERED';
264
+ setInlineFilesScanned(result, rendered.filesScanned);
265
+ result.diagnostics = diagnostics.slice(0, 100);
266
+ result.artifacts = rendered.artifacts.map(publicArtifactLink);
267
+ result.validation = validationSummary({ diagnostics, checks: rendered.validation.checks });
268
+ await context.progress(4, 4, 'GUI render complete');
269
+ return result;
270
+ }
@@ -0,0 +1,74 @@
1
+ import { readFile, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { compareCodeUnits, sha256Bytes } from '../core/canonical.js';
4
+ import { ServiceError } from '../core/result.js';
5
+ import { canonicalPath, isWithin } from '../core/workspace.js';
6
+ import { impactScenarioSuiteReferenceSchema } from '../schemas/analysis.js';
7
+ function selectorKey(selector) {
8
+ return `${selector.kind}:${selector.id}`;
9
+ }
10
+ /** Read bounded workspace-owned suite references without executing any case or assertion. */
11
+ export async function inspectImpactScenarioSuites(workspace, paths, graph, selected, signal) {
12
+ const result = {
13
+ matches: [],
14
+ sourceHashes: {},
15
+ unresolved: [],
16
+ scannedSuites: 0,
17
+ scannedCases: 0,
18
+ omittedMatches: 0,
19
+ complete: true,
20
+ };
21
+ const relevantSymbols = new Set([
22
+ ...selected.map(selectorKey),
23
+ ...graph.directConsumers.map(({ source }) => selectorKey(source)),
24
+ ...graph.transitiveConsumers.map(({ source }) => selectorKey(source)),
25
+ ]);
26
+ const relevantFiles = new Set(graph.affectedFiles);
27
+ for (const relativePath of [...new Set(paths)].sort(compareCodeUnits)) {
28
+ signal?.throwIfAborted();
29
+ const candidate = await canonicalPath(path.join(workspace.modRoot, relativePath), signal);
30
+ if (!isWithin(workspace.modRoot, candidate))
31
+ throw new ServiceError('IMPACT_SUITE_OUTSIDE_WORKSPACE', 'Scenario suite resolves outside the authorized mod root', {
32
+ relativePath,
33
+ });
34
+ try {
35
+ const details = await stat(candidate);
36
+ if (!details.isFile() || details.size > 4 * 1024 * 1024)
37
+ throw new RangeError('Suite must be a JSON file no larger than 4 MiB');
38
+ const bytes = await readFile(candidate);
39
+ if (bytes.length > 4 * 1024 * 1024)
40
+ throw new RangeError('Suite file exceeds 4 MiB');
41
+ const suite = impactScenarioSuiteReferenceSchema.parse(JSON.parse(bytes.toString('utf8')));
42
+ result.sourceHashes[`suite:${relativePath}`] = sha256Bytes(bytes);
43
+ result.scannedSuites += 1;
44
+ result.scannedCases += suite.cases.length;
45
+ for (const item of suite.cases) {
46
+ const via = [
47
+ ...item.sourceSelectors
48
+ .map(selectorKey)
49
+ .filter((key) => relevantSymbols.has(key))
50
+ .map((key) => `symbol:${key}`),
51
+ ...item.sourceFiles
52
+ .filter((file) => relevantFiles.has(file))
53
+ .map((file) => `file:${file}`),
54
+ ].sort(compareCodeUnits);
55
+ if (via.length === 0)
56
+ continue;
57
+ if (result.matches.length < 10_000)
58
+ result.matches.push({ suitePath: relativePath, suiteId: suite.id, caseId: item.id, via });
59
+ else
60
+ result.omittedMatches += 1;
61
+ }
62
+ }
63
+ catch (error) {
64
+ if (signal?.aborted)
65
+ throw error;
66
+ result.unresolved.push({
67
+ path: relativePath,
68
+ reason: error instanceof Error ? error.message : 'Scenario suite could not be read or parsed',
69
+ });
70
+ }
71
+ }
72
+ result.complete = result.unresolved.length === 0 && result.omittedMatches === 0;
73
+ return result;
74
+ }
@@ -0,0 +1,127 @@
1
+ import { analysisSourceEvidence } from '../core/analysis-evidence.js';
2
+ import { publicArtifactLink } from '../core/artifacts.js';
3
+ import { canonicalJson, hashCanonical } from '../core/canonical.js';
4
+ import { compareImpactGraphs } from '../core/impact-comparison.js';
5
+ import { inspectImpactGraph } from '../core/impact-graph.js';
6
+ import { setInlineFilesScanned } from '../core/operation-result.js';
7
+ import { emptyServiceResult, ServiceError } from '../core/result.js';
8
+ import { buildProposedSourceOverlay } from '../core/source-overlay.js';
9
+ import { PACKAGE_VERSION } from '../version.js';
10
+ import { inspectImpactScenarioSuites } from './scenario-suites.js';
11
+ /** Authorized, read-only impact operation shared by ordinary calls and persistent jobs. */
12
+ export class ImpactAnalyzer {
13
+ engine;
14
+ constructor(engine) {
15
+ this.engine = engine;
16
+ }
17
+ async inspect(input) {
18
+ const workspace = this.engine.resolver.get(input.workspaceId, input.principal);
19
+ if (input.refresh)
20
+ this.engine.invalidate(input.workspaceId);
21
+ const before = await this.engine.scan(input.workspaceId, {}, input.principal, input.signal);
22
+ if (input.expectedRevision !== undefined && before.revision !== input.expectedRevision)
23
+ throw new ServiceError('IMPACT_SOURCE_STALE', 'Impact source revision changed since selection', {
24
+ expectedRevision: input.expectedRevision,
25
+ observedRevision: before.revision,
26
+ });
27
+ const overlay = input.proposedSources === undefined
28
+ ? undefined
29
+ : buildProposedSourceOverlay(before, workspace, input.proposedSources);
30
+ const availablePaths = new Set([
31
+ ...before.files.map(({ displayPath }) => displayPath),
32
+ ...(overlay?.snapshot.files.map(({ displayPath }) => displayPath) ?? []),
33
+ ...(overlay?.changes.map(({ path }) => path) ?? []),
34
+ ]);
35
+ for (const file of input.changedFiles)
36
+ if (!availablePaths.has(file))
37
+ throw new ServiceError('IMPACT_SOURCE_UNKNOWN', 'Changed file is not in the authorized source inventory or proposed overlay', { file });
38
+ const query = {
39
+ symbols: input.symbols,
40
+ changedFiles: input.changedFiles.length > 0
41
+ ? input.changedFiles
42
+ : (overlay?.changes.map(({ path }) => path) ?? []),
43
+ ...(input.maxNodes === undefined ? {} : { maxNodes: input.maxNodes }),
44
+ ...(input.maxEdges === undefined ? {} : { maxEdges: input.maxEdges }),
45
+ ...(input.maxDepth === undefined ? {} : { maxDepth: input.maxDepth }),
46
+ };
47
+ const comparison = overlay === undefined ? undefined : compareImpactGraphs(before, overlay.snapshot, query);
48
+ const graph = comparison?.after ?? inspectImpactGraph(before, query);
49
+ const analysis = comparison ?? graph;
50
+ const suiteGraph = comparison === undefined
51
+ ? graph
52
+ : {
53
+ ...graph,
54
+ directConsumers: [...comparison.before.directConsumers, ...graph.directConsumers],
55
+ transitiveConsumers: [
56
+ ...comparison.before.transitiveConsumers,
57
+ ...graph.transitiveConsumers,
58
+ ],
59
+ affectedFiles: [
60
+ ...new Set([...comparison.before.affectedFiles, ...graph.affectedFiles]),
61
+ ],
62
+ };
63
+ const scenarioSuites = await inspectImpactScenarioSuites(workspace, input.scenarioSuites, suiteGraph, input.symbols, input.signal);
64
+ const complete = analysis.complete && scenarioSuites.complete;
65
+ const report = {
66
+ schemaVersion: 'impact-analysis.v1',
67
+ mode: overlay === undefined ? 'inspect' : 'compare',
68
+ query,
69
+ ...(overlay === undefined ? {} : { proposedChanges: overlay.changes }),
70
+ analysis,
71
+ scenarioSuites,
72
+ };
73
+ const reportHash = hashCanonical(report);
74
+ const artifactName = `impact-${reportHash.slice(0, 24)}.json`;
75
+ const provenanceSources = analysisSourceEvidence(before, overlay?.snapshot, scenarioSuites.sourceHashes);
76
+ const artifact = await this.engine.artifacts.putChunked(workspace, artifactName, 'application/json', `${canonicalJson(report)}\n`, {
77
+ kind: 'impact-analysis',
78
+ toolVersion: PACKAGE_VERSION,
79
+ schemaVersion: 'impact-analysis.v1',
80
+ sourceHashes: provenanceSources.sourceHashes,
81
+ metadata: {
82
+ beforeRevision: before.revision,
83
+ ...(overlay === undefined ? {} : { afterRevision: overlay.snapshot.revision }),
84
+ sourceInventory: provenanceSources.inventory,
85
+ },
86
+ }, 'Revision-pinned cross-system impact graph and source evidence', input.signal);
87
+ const result = emptyServiceResult(input.workspaceId, {
88
+ mode: overlay === undefined ? 'inspect' : 'compare',
89
+ sourceRevision: before.revision,
90
+ ...(overlay === undefined ? {} : { proposedRevision: overlay.snapshot.revision }),
91
+ reportHash,
92
+ complete,
93
+ definitions: graph.definitions.length,
94
+ directConsumers: graph.directConsumers.length,
95
+ transitiveConsumers: graph.transitiveConsumers.length,
96
+ affectedFiles: graph.affectedFiles.length,
97
+ cycles: graph.cycles.length,
98
+ scenarioSuites: scenarioSuites.scannedSuites,
99
+ affectedCases: scenarioSuites.matches.length,
100
+ unresolved: graph.unresolved.length + graph.dynamicReferences.length + scenarioSuites.unresolved.length,
101
+ ...(comparison === undefined
102
+ ? {}
103
+ : {
104
+ addedConsumers: comparison.addedConsumers.length,
105
+ removedConsumers: comparison.removedConsumers.length,
106
+ }),
107
+ });
108
+ result.code = complete ? 'IMPACT_ANALYZED' : 'IMPACT_ANALYZED_PARTIAL';
109
+ result.artifacts = [publicArtifactLink(artifact)];
110
+ result.proposedFiles = overlay?.changes.map(({ path }) => path).slice(0, 64) ?? [];
111
+ result.changedFiles = graph.affectedFiles.slice(0, 64);
112
+ setInlineFilesScanned(result, before.files.map(({ displayPath }) => displayPath));
113
+ result.validation = {
114
+ passed: complete,
115
+ checks: [
116
+ {
117
+ id: 'impact-coverage',
118
+ passed: complete,
119
+ message: complete
120
+ ? 'Selected impact graph is complete under the declared bounds'
121
+ : 'Impact graph has source, dynamic-reference, or traversal boundaries; inspect the linked report',
122
+ },
123
+ ],
124
+ };
125
+ return result;
126
+ }
127
+ }