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.
- package/CHANGELOG.md +32 -1
- package/README.md +10 -2
- package/dist/bin/stdio.js +23 -12
- package/dist/hoi4_agent_tools/core/analysis-checkpoints.js +82 -0
- package/dist/hoi4_agent_tools/core/analysis-evidence.js +9 -0
- package/dist/hoi4_agent_tools/core/analysis-job-operations.js +38 -0
- package/dist/hoi4_agent_tools/core/artifact-resource.js +173 -0
- package/dist/hoi4_agent_tools/core/artifacts.js +84 -2
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +16 -0
- package/dist/hoi4_agent_tools/core/dependency-pages.js +216 -0
- package/dist/hoi4_agent_tools/core/dependency-walk.js +266 -0
- package/dist/hoi4_agent_tools/core/domain-validation.js +188 -0
- package/dist/hoi4_agent_tools/core/engine.js +48 -5
- package/dist/hoi4_agent_tools/core/helper-expansion.js +186 -0
- package/dist/hoi4_agent_tools/core/impact-comparison.js +49 -0
- package/dist/hoi4_agent_tools/core/impact-graph.js +301 -0
- package/dist/hoi4_agent_tools/core/impact-semantic.js +256 -0
- package/dist/hoi4_agent_tools/core/index-segments.js +137 -0
- package/dist/hoi4_agent_tools/core/index.js +47 -4
- package/dist/hoi4_agent_tools/core/job-controls.js +109 -0
- package/dist/hoi4_agent_tools/core/job-executor.js +190 -0
- package/dist/hoi4_agent_tools/core/job-service.js +112 -0
- package/dist/hoi4_agent_tools/core/job-store.js +641 -0
- package/dist/hoi4_agent_tools/core/job-transactions.js +126 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +202 -0
- package/dist/hoi4_agent_tools/core/job-worker.js +64 -0
- package/dist/hoi4_agent_tools/core/operation-context.js +31 -0
- package/dist/hoi4_agent_tools/core/operation-result.js +412 -0
- package/dist/hoi4_agent_tools/core/operation-tasks.js +387 -0
- package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +383 -0
- package/dist/hoi4_agent_tools/core/result.js +1 -0
- package/dist/hoi4_agent_tools/core/scanner.js +15 -75
- package/dist/hoi4_agent_tools/core/semantic-dependencies.js +233 -0
- package/dist/hoi4_agent_tools/core/shared-request-capacity.js +43 -11
- package/dist/hoi4_agent_tools/core/source/cache.js +107 -0
- package/dist/hoi4_agent_tools/core/source/parser.js +11 -1
- package/dist/hoi4_agent_tools/core/source-overlay.js +125 -0
- package/dist/hoi4_agent_tools/core/transaction-execution.js +134 -0
- package/dist/hoi4_agent_tools/core/transactions.js +80 -27
- package/dist/hoi4_agent_tools/decision/ai.js +104 -0
- package/dist/hoi4_agent_tools/decision/comparison.js +106 -0
- package/dist/hoi4_agent_tools/decision/cost.js +177 -0
- package/dist/hoi4_agent_tools/decision/evaluation.js +256 -0
- package/dist/hoi4_agent_tools/decision/lifecycle.js +114 -0
- package/dist/hoi4_agent_tools/decision/service.js +172 -0
- package/dist/hoi4_agent_tools/decision/source-inventory.js +182 -0
- package/dist/hoi4_agent_tools/event/graph.js +214 -290
- package/dist/hoi4_agent_tools/event/helper-expansion.js +33 -0
- package/dist/hoi4_agent_tools/event/job-operations.js +51 -0
- package/dist/hoi4_agent_tools/event/operations.js +179 -0
- package/dist/hoi4_agent_tools/event/service.js +67 -15
- package/dist/hoi4_agent_tools/event/source-analysis.js +5 -4
- package/dist/hoi4_agent_tools/focus/job-operations.js +77 -0
- package/dist/hoi4_agent_tools/focus/tool-operations.js +669 -0
- package/dist/hoi4_agent_tools/focus/tool-support.js +123 -0
- package/dist/hoi4_agent_tools/gui/job-operations.js +81 -0
- package/dist/hoi4_agent_tools/gui/tool-operations.js +270 -0
- package/dist/hoi4_agent_tools/impact/scenario-suites.js +74 -0
- package/dist/hoi4_agent_tools/impact/service.js +127 -0
- package/dist/hoi4_agent_tools/map/job-operations.js +76 -0
- package/dist/hoi4_agent_tools/map/tool-operations.js +493 -0
- package/dist/hoi4_agent_tools/mcp/prompts/probability.js +25 -0
- package/dist/hoi4_agent_tools/mcp/prompts/register-modern.js +29 -0
- package/dist/hoi4_agent_tools/mcp/resources/register-modern.js +34 -0
- package/dist/hoi4_agent_tools/mcp/resources/register.js +4 -150
- package/dist/hoi4_agent_tools/mcp/server/base-tools.js +3 -219
- package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +188 -0
- package/dist/hoi4_agent_tools/mcp/server/create.js +18 -6
- package/dist/hoi4_agent_tools/mcp/server/instructions.js +2 -0
- package/dist/hoi4_agent_tools/mcp/server/job-task-store.js +67 -0
- package/dist/hoi4_agent_tools/mcp/server/legacy-task-tools.js +20 -0
- package/dist/hoi4_agent_tools/mcp/server/modern-tasks.js +168 -0
- package/dist/hoi4_agent_tools/mcp/server/progress.js +17 -15
- package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +45 -0
- package/dist/hoi4_agent_tools/mcp/server/result.js +2 -412
- package/dist/hoi4_agent_tools/mcp/server/task-request-context.js +9 -0
- package/dist/hoi4_agent_tools/mcp/server/task-tool-catalog.js +16 -0
- package/dist/hoi4_agent_tools/mcp/server/task-tool-definition.js +1 -0
- package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js +1 -123
- package/dist/hoi4_agent_tools/mcp/tools/analysis.js +65 -0
- package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +38 -21
- package/dist/hoi4_agent_tools/mcp/tools/event.js +24 -315
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +34 -1039
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +19 -504
- package/dist/hoi4_agent_tools/mcp/tools/job.js +46 -0
- package/dist/hoi4_agent_tools/mcp/tools/map.js +21 -600
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +39 -463
- package/dist/hoi4_agent_tools/mcp/tools/technology.js +40 -304
- package/dist/hoi4_agent_tools/mcp/transports/http.js +58 -1
- package/dist/hoi4_agent_tools/mcp/transports/modern-task-routing.js +109 -0
- package/dist/hoi4_agent_tools/mcp/transports/negotiated-stdio.js +121 -0
- package/dist/hoi4_agent_tools/probability/job-operations.js +58 -0
- package/dist/hoi4_agent_tools/probability/operations.js +183 -0
- package/dist/hoi4_agent_tools/probability/service.js +24 -3
- package/dist/hoi4_agent_tools/schemas/analysis.js +228 -0
- package/dist/hoi4_agent_tools/schemas/common.js +2 -0
- package/dist/hoi4_agent_tools/schemas/event.js +72 -1
- package/dist/hoi4_agent_tools/schemas/focus-requests.js +189 -0
- package/dist/hoi4_agent_tools/schemas/gui-requests.js +193 -0
- package/dist/hoi4_agent_tools/schemas/helper-expansion.js +58 -0
- package/dist/hoi4_agent_tools/schemas/map-requests.js +85 -0
- package/dist/hoi4_agent_tools/schemas/probability-requests.js +168 -0
- package/dist/hoi4_agent_tools/schemas/technology.js +90 -1
- package/dist/hoi4_agent_tools/schemas/transaction.js +1 -0
- package/dist/hoi4_agent_tools/technology/graph.js +83 -78
- package/dist/hoi4_agent_tools/technology/helper-expansion.js +33 -0
- package/dist/hoi4_agent_tools/technology/job-operations.js +60 -0
- package/dist/hoi4_agent_tools/technology/operations.js +125 -0
- package/dist/hoi4_agent_tools/technology/service.js +89 -15
- package/dist/hoi4_agent_tools/technology/source-analysis.js +2 -1
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +3 -0
- package/docs/analysis.md +100 -0
- package/docs/development.md +2 -0
- package/docs/events.md +7 -2
- package/docs/helper-expansion.md +78 -0
- package/docs/http.md +2 -0
- package/docs/jobs.md +86 -0
- package/docs/technology.md +6 -2
- package/package.json +15 -6
- package/schemas/configuration.schema.json +1 -1
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +290 -0
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/helper-expansion-request.schema.json +44 -0
- package/schemas/helper-expansion-summary.schema.json +70 -0
- package/schemas/impact-inspect-input.schema.json +203 -0
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/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
|
+
}
|