hoi4-agent-tools 1.2.0 → 2.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.
- package/CHANGELOG.md +21 -1
- package/README.md +6 -7
- package/dist/hoi4_agent_tools/core/artifacts.js +109 -4
- package/dist/hoi4_agent_tools/core/diagnostic-collector.js +28 -8
- package/dist/hoi4_agent_tools/core/diff.js +162 -4
- package/dist/hoi4_agent_tools/core/engine.js +2 -0
- package/dist/hoi4_agent_tools/core/index.js +17 -6
- package/dist/hoi4_agent_tools/core/render-budget.js +5 -3
- package/dist/hoi4_agent_tools/core/scanner.js +3 -2
- package/dist/hoi4_agent_tools/core/source/limits.js +4 -1
- package/dist/hoi4_agent_tools/core/workspace.js +31 -3
- package/dist/hoi4_agent_tools/event/graph.js +28 -6
- package/dist/hoi4_agent_tools/event/limits.js +1 -1
- package/dist/hoi4_agent_tools/event/service.js +103 -7
- package/dist/hoi4_agent_tools/focus/continuous-render.js +17 -8
- package/dist/hoi4_agent_tools/focus/presentation.js +96 -41
- package/dist/hoi4_agent_tools/focus/render.js +27 -16
- package/dist/hoi4_agent_tools/gui/assets.js +9 -2
- package/dist/hoi4_agent_tools/gui/inspection-artifact.js +99 -0
- package/dist/hoi4_agent_tools/gui/layout.js +126 -27
- package/dist/hoi4_agent_tools/gui/renderer.js +16 -0
- package/dist/hoi4_agent_tools/gui/source-graph.js +61 -28
- package/dist/hoi4_agent_tools/gui/studio.js +23 -8
- package/dist/hoi4_agent_tools/map/diagnostic-limit.js +90 -9
- package/dist/hoi4_agent_tools/map/model.js +22 -4
- package/dist/hoi4_agent_tools/map/service.js +4 -3
- package/dist/hoi4_agent_tools/map/validation.js +3 -2
- package/dist/hoi4_agent_tools/mcp/server/create.js +1 -3
- package/dist/hoi4_agent_tools/mcp/server/output-schemas.js +1 -1
- package/dist/hoi4_agent_tools/mcp/tools/event.js +6 -6
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +149 -224
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +35 -25
- package/dist/hoi4_agent_tools/mcp/tools/map.js +6 -3
- package/dist/hoi4_agent_tools/runtime.js +112 -2
- package/dist/hoi4_agent_tools/schemas/common.js +5 -1
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/events.md +4 -5
- package/docs/focus.md +5 -4
- package/docs/gui.md +5 -1
- package/docs/map.md +1 -1
- package/docs/setup.md +9 -6
- package/package.json +1 -1
- package/server.json +2 -2
- package/dist/hoi4_agent_tools/mcp/tools/mods.js +0 -54
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod/v4';
|
|
2
2
|
import { boundedSourceHashEvidence, publicArtifactLink } from '../../core/artifacts.js';
|
|
3
|
-
import { compareCodeUnits, canonicalJson
|
|
4
|
-
import { comparePngImages, createTransparentPng } from '../../core/image-diff.js';
|
|
3
|
+
import { compareCodeUnits, canonicalJson } from '../../core/canonical.js';
|
|
5
4
|
import { RenderBudget } from '../../core/render-budget.js';
|
|
6
5
|
import { emptyServiceResult, ServiceError } from '../../core/result.js';
|
|
7
6
|
import { parseClausewitz } from '../../core/source/index.js';
|
|
8
7
|
import { readDependenciesFromScannedFiles } from '../../core/transactions.js';
|
|
9
|
-
import { FocusWorkbench, FOCUS_RENDER_MAX_OUTPUT_SCALE, FOCUS_RENDER_MIN_OUTPUT_SCALE, assertCompactLayoutQuality, compactFocusTreePlanAsync, focusPlanHash, focusPresentationEvidence, focusPlanningSidecarPath, importContinuousFocusPalettes, importFocusTrees, linkContinuousFocusPalettes, parseFocusPlanningSidecar,
|
|
8
|
+
import { FocusWorkbench, FOCUS_RENDER_MAX_OUTPUT_SCALE, FOCUS_RENDER_MIN_OUTPUT_SCALE, assertCompactLayoutQuality, compactFocusTreePlanAsync, focusPlanHash, focusPresentationEvidence, focusPlanningSidecarPath, importContinuousFocusPalettes, importFocusTrees, linkContinuousFocusPalettes, parseFocusPlanningSidecar, resolveFocusPresentation, enrichFocusPlanFromSidecar, } from '../../focus/index.js';
|
|
10
9
|
import { continuousFocusPaletteSchema, focusLayoutMetricsSchema, focusLayoutSchema, focusTreePlanSchema, } from '../../schemas/focus.js';
|
|
11
10
|
import { workspaceIdSchema, workspaceRelativePathSchema } from '../../schemas/common.js';
|
|
12
11
|
import { PACKAGE_VERSION } from '../../version.js';
|
|
@@ -308,6 +307,124 @@ const LARGE_FOCUS_RENDER_SCALE = 0.5;
|
|
|
308
307
|
function automaticFocusRenderScale(focusCount) {
|
|
309
308
|
return focusCount >= LARGE_FOCUS_RENDER_NODE_THRESHOLD ? LARGE_FOCUS_RENDER_SCALE : 1;
|
|
310
309
|
}
|
|
310
|
+
async function executeFocusVisualTool(engine, workbench, context, input, extra, rasterize) {
|
|
311
|
+
const { workspaceId: requestedWorkspaceId, relativePath } = input;
|
|
312
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(requestedWorkspaceId, context.principal);
|
|
313
|
+
const outputName = rasterize ? 'raster' : 'render';
|
|
314
|
+
try {
|
|
315
|
+
const progress = progressReporter(extra);
|
|
316
|
+
await progress.report(0, 4, 'Importing and indexing focus source');
|
|
317
|
+
const snapshot = await engine.scan(workspaceId, {}, context.principal, progress.signal);
|
|
318
|
+
await progress.report(1, 4, 'Focus source index complete');
|
|
319
|
+
const workspace = engine.resolver.get(workspaceId, context.principal);
|
|
320
|
+
const renderBudget = new RenderBudget();
|
|
321
|
+
if (input.mode === 'continuous') {
|
|
322
|
+
selectedContinuousFocusFile(snapshot, relativePath);
|
|
323
|
+
const imported = await workbench.importContinuousPath(workspaceId, relativePath, context.principal);
|
|
324
|
+
const plan = selectContinuousPlan(imported.result, input.paletteId);
|
|
325
|
+
const presentation = await resolveFocusPresentation({
|
|
326
|
+
plans: [],
|
|
327
|
+
palettes: [plan],
|
|
328
|
+
files: snapshot.files,
|
|
329
|
+
index: snapshot.index,
|
|
330
|
+
scanner: engine.scanner,
|
|
331
|
+
workspace,
|
|
332
|
+
decodeIcons: rasterize,
|
|
333
|
+
budget: renderBudget,
|
|
334
|
+
signal: progress.signal,
|
|
335
|
+
});
|
|
336
|
+
await progress.report(2, 4, `Producing continuous focus ${outputName} artifacts`);
|
|
337
|
+
const rendered = await workbench.renderContinuousAndStore(workspaceId, plan, {
|
|
338
|
+
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
339
|
+
presentation,
|
|
340
|
+
sourceHashes: presentation.sourceHashes,
|
|
341
|
+
...(input.columns === undefined ? {} : { columns: input.columns }),
|
|
342
|
+
...(input.padding === undefined ? {} : { padding: input.padding }),
|
|
343
|
+
renderProfile: { sourceRevision: snapshot.revision, output: outputName },
|
|
344
|
+
rasterize,
|
|
345
|
+
budget: renderBudget,
|
|
346
|
+
signal: progress.signal,
|
|
347
|
+
});
|
|
348
|
+
const diagnostics = [...imported.result.diagnostics, ...rendered.diagnostics];
|
|
349
|
+
const result = emptyServiceResult(workspaceId, {
|
|
350
|
+
mode: 'continuous',
|
|
351
|
+
paletteId: plan.id,
|
|
352
|
+
focusCount: plan.focuses.length,
|
|
353
|
+
hashes: rendered.bundle.hashes,
|
|
354
|
+
width: rendered.bundle.width,
|
|
355
|
+
height: rendered.bundle.height,
|
|
356
|
+
});
|
|
357
|
+
result.code = rasterize ? 'CONTINUOUS_FOCUS_RASTERIZED' : 'CONTINUOUS_FOCUS_RENDERED';
|
|
358
|
+
setInlineFilesScanned(result, [...new Set([plan.provenance.sourcePath, ...presentation.filesScanned])].sort((left, right) => compareCodeUnits(left, right)));
|
|
359
|
+
result.diagnostics = diagnostics.slice(0, 100);
|
|
360
|
+
result.artifacts = rendered.artifacts.map(publicArtifactLink);
|
|
361
|
+
result.validation = validationFromDiagnostics(diagnostics);
|
|
362
|
+
await progress.report(4, 4, `Continuous focus ${outputName} complete`);
|
|
363
|
+
return toolResult(result);
|
|
364
|
+
}
|
|
365
|
+
const paletteImport = continuousPalettes(snapshot);
|
|
366
|
+
const imported = importFocusFile(snapshot, selectedFocusFile(snapshot, workspace.registration.roots.focus, relativePath), paletteImport.palettes);
|
|
367
|
+
const plan = selectPlan(imported, input.treeId);
|
|
368
|
+
const linkedPalettes = paletteImport.palettes.filter(({ id }) => plan.continuousFocusPaletteIds.includes(id));
|
|
369
|
+
const presentation = await resolveFocusPresentation({
|
|
370
|
+
plans: [plan],
|
|
371
|
+
palettes: linkedPalettes,
|
|
372
|
+
files: snapshot.files,
|
|
373
|
+
index: snapshot.index,
|
|
374
|
+
scanner: engine.scanner,
|
|
375
|
+
workspace,
|
|
376
|
+
decodeIcons: rasterize,
|
|
377
|
+
budget: renderBudget,
|
|
378
|
+
signal: progress.signal,
|
|
379
|
+
});
|
|
380
|
+
await progress.report(2, 4, `Producing focus ${outputName} artifacts`);
|
|
381
|
+
const effectiveReviewScale = input.reviewScale ?? automaticFocusRenderScale(plan.focuses.length);
|
|
382
|
+
const rendered = await workbench.renderAndStore(workspaceId, plan, {
|
|
383
|
+
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
384
|
+
index: snapshot.index,
|
|
385
|
+
references: referenceCatalog(snapshot),
|
|
386
|
+
presentation,
|
|
387
|
+
sourceHashes: presentation.sourceHashes,
|
|
388
|
+
...(input.horizontalSpacing === undefined
|
|
389
|
+
? {}
|
|
390
|
+
: { horizontalSpacing: input.horizontalSpacing }),
|
|
391
|
+
...(input.verticalSpacing === undefined ? {} : { verticalSpacing: input.verticalSpacing }),
|
|
392
|
+
...(input.padding === undefined ? {} : { padding: input.padding }),
|
|
393
|
+
outputScale: effectiveReviewScale,
|
|
394
|
+
rasterize,
|
|
395
|
+
renderProfile: {
|
|
396
|
+
sourceRevision: snapshot.revision,
|
|
397
|
+
reviewScale: effectiveReviewScale,
|
|
398
|
+
output: outputName,
|
|
399
|
+
},
|
|
400
|
+
budget: renderBudget,
|
|
401
|
+
signal: progress.signal,
|
|
402
|
+
});
|
|
403
|
+
const diagnostics = [
|
|
404
|
+
...paletteImport.diagnostics,
|
|
405
|
+
...imported.diagnostics,
|
|
406
|
+
...rendered.diagnostics,
|
|
407
|
+
];
|
|
408
|
+
const result = emptyServiceResult(workspaceId, {
|
|
409
|
+
mode: 'national',
|
|
410
|
+
treeId: plan.id,
|
|
411
|
+
layoutHash: rendered.layout.layoutHash,
|
|
412
|
+
hashes: rendered.bundle.hashes,
|
|
413
|
+
width: rendered.bundle.width,
|
|
414
|
+
height: rendered.bundle.height,
|
|
415
|
+
});
|
|
416
|
+
result.code = rasterize ? 'FOCUS_RASTERIZED' : 'FOCUS_RENDERED';
|
|
417
|
+
setInlineFilesScanned(result, [...new Set([plan.provenance.sourcePath, ...presentation.filesScanned])].sort((left, right) => compareCodeUnits(left, right)));
|
|
418
|
+
result.diagnostics = diagnostics.slice(0, 100);
|
|
419
|
+
result.artifacts = rendered.artifacts.map(publicArtifactLink);
|
|
420
|
+
result.validation = validationFromDiagnostics(diagnostics);
|
|
421
|
+
await progress.report(4, 4, `Focus ${outputName} complete`);
|
|
422
|
+
return toolResult(result);
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
return errorResult(error, workspaceId);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
311
428
|
function compactDrift(drift) {
|
|
312
429
|
return {
|
|
313
430
|
status: drift.status,
|
|
@@ -451,7 +568,8 @@ export function registerFocusTools(server, engine, context) {
|
|
|
451
568
|
outputSchema: focusInspectOutputSchema,
|
|
452
569
|
annotations: artifactProducing,
|
|
453
570
|
}, async (input, extra) => {
|
|
454
|
-
const { workspaceId, relativePath } = input;
|
|
571
|
+
const { workspaceId: requestedWorkspaceId, relativePath } = input;
|
|
572
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(requestedWorkspaceId, context.principal);
|
|
455
573
|
try {
|
|
456
574
|
const progress = progressReporter(extra);
|
|
457
575
|
await progress.report(0, 3, 'Building shared workspace index');
|
|
@@ -489,6 +607,7 @@ export function registerFocusTools(server, engine, context) {
|
|
|
489
607
|
index: snapshot.index,
|
|
490
608
|
scanner: engine.scanner,
|
|
491
609
|
workspace,
|
|
610
|
+
decodeIcons: false,
|
|
492
611
|
signal: progress.signal,
|
|
493
612
|
});
|
|
494
613
|
diagnostics.push(...presentation.diagnostics);
|
|
@@ -588,6 +707,7 @@ export function registerFocusTools(server, engine, context) {
|
|
|
588
707
|
index: snapshot.index,
|
|
589
708
|
scanner: engine.scanner,
|
|
590
709
|
workspace,
|
|
710
|
+
decodeIcons: false,
|
|
591
711
|
signal: progress.signal,
|
|
592
712
|
});
|
|
593
713
|
diagnostics.push(...presentation.diagnostics);
|
|
@@ -662,122 +782,18 @@ export function registerFocusTools(server, engine, context) {
|
|
|
662
782
|
});
|
|
663
783
|
server.registerTool('hoi4.focus_render', {
|
|
664
784
|
title: 'Render focus review artifacts',
|
|
665
|
-
description: 'Render a national tree or continuous palette
|
|
785
|
+
description: 'Render a national tree or continuous palette as fast deterministic HTML, SVG, JSON, and source-map artifacts. Use hoi4.focus_raster when decoded icons and PNG output are needed.',
|
|
666
786
|
inputSchema: focusRenderInput,
|
|
667
787
|
outputSchema: focusRenderOutputSchema,
|
|
668
788
|
annotations: artifactProducing,
|
|
669
|
-
},
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
if (input.mode === 'continuous') {
|
|
678
|
-
selectedContinuousFocusFile(snapshot, relativePath);
|
|
679
|
-
const imported = await workbench.importContinuousPath(workspaceId, relativePath, context.principal);
|
|
680
|
-
const plan = selectContinuousPlan(imported.result, input.paletteId);
|
|
681
|
-
const presentation = await resolveFocusPresentation({
|
|
682
|
-
plans: [],
|
|
683
|
-
palettes: [plan],
|
|
684
|
-
files: snapshot.files,
|
|
685
|
-
index: snapshot.index,
|
|
686
|
-
scanner: engine.scanner,
|
|
687
|
-
workspace,
|
|
688
|
-
budget: renderBudget,
|
|
689
|
-
signal: progress.signal,
|
|
690
|
-
});
|
|
691
|
-
await progress.report(1, 3, 'Rendering continuous focus artifacts');
|
|
692
|
-
const rendered = await workbench.renderContinuousAndStore(workspaceId, plan, {
|
|
693
|
-
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
694
|
-
presentation,
|
|
695
|
-
sourceHashes: presentation.sourceHashes,
|
|
696
|
-
...(input.columns === undefined ? {} : { columns: input.columns }),
|
|
697
|
-
...(input.padding === undefined ? {} : { padding: input.padding }),
|
|
698
|
-
renderProfile: { sourceRevision: snapshot.revision },
|
|
699
|
-
budget: renderBudget,
|
|
700
|
-
signal: progress.signal,
|
|
701
|
-
});
|
|
702
|
-
const diagnostics = [...imported.result.diagnostics, ...rendered.diagnostics];
|
|
703
|
-
const result = emptyServiceResult(workspaceId, {
|
|
704
|
-
mode: 'continuous',
|
|
705
|
-
paletteId: plan.id,
|
|
706
|
-
focusCount: plan.focuses.length,
|
|
707
|
-
hashes: rendered.bundle.hashes,
|
|
708
|
-
width: rendered.bundle.width,
|
|
709
|
-
height: rendered.bundle.height,
|
|
710
|
-
});
|
|
711
|
-
result.code = 'CONTINUOUS_FOCUS_RENDERED';
|
|
712
|
-
setInlineFilesScanned(result, [...new Set([plan.provenance.sourcePath, ...presentation.filesScanned])].sort((left, right) => compareCodeUnits(left, right)));
|
|
713
|
-
result.diagnostics = diagnostics.slice(0, 100);
|
|
714
|
-
result.artifacts = rendered.artifacts.map(publicArtifactLink);
|
|
715
|
-
result.validation = validationFromDiagnostics(diagnostics);
|
|
716
|
-
await progress.report(3, 3, 'Continuous focus render complete');
|
|
717
|
-
return toolResult(result);
|
|
718
|
-
}
|
|
719
|
-
const paletteImport = continuousPalettes(snapshot);
|
|
720
|
-
const imported = importFocusFile(snapshot, selectedFocusFile(snapshot, workspace.registration.roots.focus, relativePath), paletteImport.palettes);
|
|
721
|
-
const plan = selectPlan(imported, input.treeId);
|
|
722
|
-
const linkedPalettes = paletteImport.palettes.filter(({ id }) => plan.continuousFocusPaletteIds.includes(id));
|
|
723
|
-
const presentation = await resolveFocusPresentation({
|
|
724
|
-
plans: [plan],
|
|
725
|
-
palettes: linkedPalettes,
|
|
726
|
-
files: snapshot.files,
|
|
727
|
-
index: snapshot.index,
|
|
728
|
-
scanner: engine.scanner,
|
|
729
|
-
workspace,
|
|
730
|
-
budget: renderBudget,
|
|
731
|
-
signal: progress.signal,
|
|
732
|
-
});
|
|
733
|
-
await progress.report(1, 3, 'Rendering focus artifacts');
|
|
734
|
-
const effectiveReviewScale = input.reviewScale ?? automaticFocusRenderScale(plan.focuses.length);
|
|
735
|
-
const rendered = await workbench.renderAndStore(workspaceId, plan, {
|
|
736
|
-
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
737
|
-
index: snapshot.index,
|
|
738
|
-
references: referenceCatalog(snapshot),
|
|
739
|
-
presentation,
|
|
740
|
-
sourceHashes: presentation.sourceHashes,
|
|
741
|
-
...(input.horizontalSpacing === undefined
|
|
742
|
-
? {}
|
|
743
|
-
: { horizontalSpacing: input.horizontalSpacing }),
|
|
744
|
-
...(input.verticalSpacing === undefined
|
|
745
|
-
? {}
|
|
746
|
-
: { verticalSpacing: input.verticalSpacing }),
|
|
747
|
-
...(input.padding === undefined ? {} : { padding: input.padding }),
|
|
748
|
-
outputScale: effectiveReviewScale,
|
|
749
|
-
renderProfile: {
|
|
750
|
-
sourceRevision: snapshot.revision,
|
|
751
|
-
reviewScale: effectiveReviewScale,
|
|
752
|
-
},
|
|
753
|
-
budget: renderBudget,
|
|
754
|
-
signal: progress.signal,
|
|
755
|
-
});
|
|
756
|
-
const diagnostics = [
|
|
757
|
-
...paletteImport.diagnostics,
|
|
758
|
-
...imported.diagnostics,
|
|
759
|
-
...rendered.diagnostics,
|
|
760
|
-
];
|
|
761
|
-
const result = emptyServiceResult(workspaceId, {
|
|
762
|
-
mode: 'national',
|
|
763
|
-
treeId: plan.id,
|
|
764
|
-
layoutHash: rendered.layout.layoutHash,
|
|
765
|
-
hashes: rendered.bundle.hashes,
|
|
766
|
-
width: rendered.bundle.width,
|
|
767
|
-
height: rendered.bundle.height,
|
|
768
|
-
});
|
|
769
|
-
result.code = 'FOCUS_RENDERED';
|
|
770
|
-
setInlineFilesScanned(result, [...new Set([plan.provenance.sourcePath, ...presentation.filesScanned])].sort((left, right) => compareCodeUnits(left, right)));
|
|
771
|
-
result.diagnostics = diagnostics.slice(0, 100);
|
|
772
|
-
result.artifacts = rendered.artifacts.map(publicArtifactLink);
|
|
773
|
-
result.validation = validationFromDiagnostics(diagnostics);
|
|
774
|
-
await progress.report(3, 3, 'Focus render complete');
|
|
775
|
-
return toolResult(result);
|
|
776
|
-
}
|
|
777
|
-
catch (error) {
|
|
778
|
-
return errorResult(error, workspaceId);
|
|
779
|
-
}
|
|
780
|
-
});
|
|
789
|
+
}, (input, extra) => executeFocusVisualTool(engine, workbench, context, input, extra, false));
|
|
790
|
+
server.registerTool('hoi4.focus_raster', {
|
|
791
|
+
title: 'Rasterize focus review artifacts',
|
|
792
|
+
description: 'Produce the high-fidelity focus review with decoded source icons and deterministic PNG output. Use focus_render for the faster structural HTML, SVG, and JSON view.',
|
|
793
|
+
inputSchema: focusRenderInput,
|
|
794
|
+
outputSchema: focusRenderOutputSchema,
|
|
795
|
+
annotations: artifactProducing,
|
|
796
|
+
}, (input, extra) => executeFocusVisualTool(engine, workbench, context, input, extra, true));
|
|
781
797
|
server.registerTool('hoi4.focus_rewrite', {
|
|
782
798
|
title: 'Create or clean up focus content',
|
|
783
799
|
description: 'Create or clean up a national tree or continuous palette, validate it, and apply it in one call. Supply a complete plan; set layoutMode compact for automatic arrangement. Existing national trees can omit the plan and use treeId plus layoutMode compact. Set createIfMissing for a new file.',
|
|
@@ -790,7 +806,8 @@ export function registerFocusTools(server, engine, context) {
|
|
|
790
806
|
openWorldHint: false,
|
|
791
807
|
},
|
|
792
808
|
}, async (input, extra) => {
|
|
793
|
-
const { workspaceId, relativePath, createIfMissing, horizontalSpacing, verticalSpacing, padding, reviewScale, } = input;
|
|
809
|
+
const { workspaceId: requestedWorkspaceId, relativePath, createIfMissing, horizontalSpacing, verticalSpacing, padding, reviewScale, } = input;
|
|
810
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(requestedWorkspaceId, context.principal);
|
|
794
811
|
try {
|
|
795
812
|
requireServerScope(context, 'hoi4:write');
|
|
796
813
|
const progress = progressReporter(extra);
|
|
@@ -828,20 +845,11 @@ export function registerFocusTools(server, engine, context) {
|
|
|
828
845
|
index: snapshot.index,
|
|
829
846
|
scanner: engine.scanner,
|
|
830
847
|
workspace,
|
|
848
|
+
decodeIcons: false,
|
|
831
849
|
budget: renderBudget,
|
|
832
850
|
signal: progress.signal,
|
|
833
851
|
});
|
|
834
|
-
|
|
835
|
-
? []
|
|
836
|
-
: [...(imported?.result.diagnostics ?? []), ...workbench.lintContinuous(currentPlan)];
|
|
837
|
-
await progress.report(1, 5, 'Rendering before and proposed continuous focus plans');
|
|
838
|
-
const currentRender = currentPlan === undefined
|
|
839
|
-
? undefined
|
|
840
|
-
: await renderContinuousFocusPalette(currentPlan, currentDiagnostics, {
|
|
841
|
-
presentation,
|
|
842
|
-
signal: progress.signal,
|
|
843
|
-
budget: renderBudget,
|
|
844
|
-
});
|
|
852
|
+
await progress.report(1, 5, 'Building proposed continuous focus review');
|
|
845
853
|
const proposedPlanHash = focusPlanHash(plan);
|
|
846
854
|
const reviewPlan = {
|
|
847
855
|
...plan,
|
|
@@ -855,47 +863,16 @@ export function registerFocusTools(server, engine, context) {
|
|
|
855
863
|
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
856
864
|
presentation,
|
|
857
865
|
sourceHashes: presentation.sourceHashes,
|
|
858
|
-
renderProfile: {
|
|
866
|
+
renderProfile: {
|
|
867
|
+
sourceRevision: snapshot.revision,
|
|
868
|
+
proposedPlanHash,
|
|
869
|
+
output: 'vector',
|
|
870
|
+
},
|
|
871
|
+
rasterize: false,
|
|
859
872
|
budget: renderBudget,
|
|
860
873
|
signal: progress.signal,
|
|
861
874
|
});
|
|
862
|
-
const
|
|
863
|
-
(await createTransparentPng(proposed.bundle.width, proposed.bundle.height, 'new continuous focus baseline', renderBudget));
|
|
864
|
-
const comparison = await comparePngImages(beforePng, proposed.bundle.png, 8, progress.signal, renderBudget);
|
|
865
|
-
const stem = plan.id.replace(/[^A-Za-z0-9._-]/gu, '_').slice(0, 80) || 'continuous';
|
|
866
|
-
const provenance = {
|
|
867
|
-
toolVersion: PACKAGE_VERSION,
|
|
868
|
-
schemaVersion: 'continuous-focus-visual-diff.v1',
|
|
869
|
-
sourceHashes: {
|
|
870
|
-
before: sourceFile?.sha256 ?? sha256Bytes(Buffer.alloc(0)),
|
|
871
|
-
proposedPlan: proposedPlanHash,
|
|
872
|
-
},
|
|
873
|
-
renderProfile: { offline: true, kind: 'continuous-focus-palette' },
|
|
874
|
-
};
|
|
875
|
-
const comparisonArtifacts = await engine.artifacts.withAtomicWrites(workspace, [
|
|
876
|
-
{
|
|
877
|
-
name: `${stem}.continuous-focus-before.png`,
|
|
878
|
-
mimeType: 'image/png',
|
|
879
|
-
content: beforePng,
|
|
880
|
-
provenance: { ...provenance, kind: 'continuous-focus-before-render' },
|
|
881
|
-
},
|
|
882
|
-
{
|
|
883
|
-
name: `${stem}.continuous-focus-visual-diff.png`,
|
|
884
|
-
mimeType: 'image/png',
|
|
885
|
-
content: comparison.png,
|
|
886
|
-
provenance: { ...provenance, kind: 'continuous-focus-visual-diff' },
|
|
887
|
-
},
|
|
888
|
-
{
|
|
889
|
-
name: `${stem}.continuous-focus-visual-diff.json`,
|
|
890
|
-
mimeType: 'application/json',
|
|
891
|
-
content: comparison.json,
|
|
892
|
-
provenance: { ...provenance, kind: 'continuous-focus-visual-diff-json' },
|
|
893
|
-
},
|
|
894
|
-
], (stored) => Promise.resolve([...stored]), progress.signal);
|
|
895
|
-
const reviewArtifacts = [
|
|
896
|
-
...proposed.artifacts.map(publicArtifactLink),
|
|
897
|
-
...comparisonArtifacts.map(publicArtifactLink),
|
|
898
|
-
];
|
|
875
|
+
const reviewArtifacts = proposed.artifacts.map(publicArtifactLink);
|
|
899
876
|
await progress.report(3, 5, 'Preparing the continuous focus rewrite');
|
|
900
877
|
const planned = await workbench.planContinuousChanges({
|
|
901
878
|
workspaceId,
|
|
@@ -1026,6 +1003,7 @@ export function registerFocusTools(server, engine, context) {
|
|
|
1026
1003
|
index: snapshot.index,
|
|
1027
1004
|
scanner: engine.scanner,
|
|
1028
1005
|
workspace,
|
|
1006
|
+
decodeIcons: false,
|
|
1029
1007
|
budget: renderBudget,
|
|
1030
1008
|
signal: progress.signal,
|
|
1031
1009
|
});
|
|
@@ -1034,22 +1012,7 @@ export function registerFocusTools(server, engine, context) {
|
|
|
1034
1012
|
: currentPlan === compactSourcePlan && compactPlanning !== undefined
|
|
1035
1013
|
? compactPlanning.currentLayout
|
|
1036
1014
|
: await workbench.layoutAsync(currentPlan, { signal: progress.signal });
|
|
1037
|
-
|
|
1038
|
-
? []
|
|
1039
|
-
: workbench.lint(currentPlan, {
|
|
1040
|
-
index: snapshot.index,
|
|
1041
|
-
references: catalog,
|
|
1042
|
-
layout: currentLayout,
|
|
1043
|
-
});
|
|
1044
|
-
await progress.report(1, 5, 'Rendering before and proposed focus plans');
|
|
1045
|
-
const currentRender = currentPlan === undefined || currentLayout === undefined
|
|
1046
|
-
? undefined
|
|
1047
|
-
: await renderFocusTree(currentPlan, currentLayout, currentDiagnostics, {
|
|
1048
|
-
presentation,
|
|
1049
|
-
...reviewRenderOptions,
|
|
1050
|
-
signal: progress.signal,
|
|
1051
|
-
budget: renderBudget,
|
|
1052
|
-
});
|
|
1015
|
+
await progress.report(1, 5, 'Building proposed focus review');
|
|
1053
1016
|
const proposedPlanHash = focusPlanHash(plan);
|
|
1054
1017
|
const reviewPlan = {
|
|
1055
1018
|
...plan,
|
|
@@ -1071,56 +1034,18 @@ export function registerFocusTools(server, engine, context) {
|
|
|
1071
1034
|
presentation,
|
|
1072
1035
|
sourceHashes: presentation.sourceHashes,
|
|
1073
1036
|
...reviewRenderOptions,
|
|
1037
|
+
rasterize: false,
|
|
1074
1038
|
renderProfile: {
|
|
1075
1039
|
sourceRevision: snapshot.revision,
|
|
1076
1040
|
proposedPlanHash,
|
|
1077
1041
|
reviewRenderProfile,
|
|
1042
|
+
output: 'vector',
|
|
1078
1043
|
},
|
|
1079
1044
|
budget: renderBudget,
|
|
1080
1045
|
signal: progress.signal,
|
|
1081
1046
|
layout: proposedLayout,
|
|
1082
1047
|
});
|
|
1083
|
-
const
|
|
1084
|
-
(await createTransparentPng(proposed.bundle.width, proposed.bundle.height, 'new national focus baseline', renderBudget));
|
|
1085
|
-
const comparison = await comparePngImages(beforePng, proposed.bundle.png, 8, progress.signal, renderBudget);
|
|
1086
|
-
const stem = plan.id.replace(/[^A-Za-z0-9._-]/gu, '_').slice(0, 80) || 'focus-tree';
|
|
1087
|
-
const provenance = {
|
|
1088
|
-
toolVersion: PACKAGE_VERSION,
|
|
1089
|
-
schemaVersion: 'focus-visual-diff.v1',
|
|
1090
|
-
sourceHashes: {
|
|
1091
|
-
before: focusFile?.sha256 ?? sha256Bytes(Buffer.alloc(0)),
|
|
1092
|
-
proposedPlan: proposedPlanHash,
|
|
1093
|
-
},
|
|
1094
|
-
renderProfile: {
|
|
1095
|
-
offline: true,
|
|
1096
|
-
layoutHash: proposed.layout.layoutHash,
|
|
1097
|
-
reviewRenderProfile,
|
|
1098
|
-
},
|
|
1099
|
-
};
|
|
1100
|
-
const comparisonArtifacts = await engine.artifacts.withAtomicWrites(workspace, [
|
|
1101
|
-
{
|
|
1102
|
-
name: `${stem}.focus-before.png`,
|
|
1103
|
-
mimeType: 'image/png',
|
|
1104
|
-
content: beforePng,
|
|
1105
|
-
provenance: { ...provenance, kind: 'focus-before-render' },
|
|
1106
|
-
},
|
|
1107
|
-
{
|
|
1108
|
-
name: `${stem}.focus-visual-diff.png`,
|
|
1109
|
-
mimeType: 'image/png',
|
|
1110
|
-
content: comparison.png,
|
|
1111
|
-
provenance: { ...provenance, kind: 'focus-visual-diff' },
|
|
1112
|
-
},
|
|
1113
|
-
{
|
|
1114
|
-
name: `${stem}.focus-visual-diff.json`,
|
|
1115
|
-
mimeType: 'application/json',
|
|
1116
|
-
content: comparison.json,
|
|
1117
|
-
provenance: { ...provenance, kind: 'focus-visual-diff-json' },
|
|
1118
|
-
},
|
|
1119
|
-
], (stored) => Promise.resolve([...stored]), progress.signal);
|
|
1120
|
-
const reviewArtifacts = [
|
|
1121
|
-
...proposed.artifacts.map(publicArtifactLink),
|
|
1122
|
-
...comparisonArtifacts.map(publicArtifactLink),
|
|
1123
|
-
];
|
|
1048
|
+
const reviewArtifacts = proposed.artifacts.map(publicArtifactLink);
|
|
1124
1049
|
await progress.report(3, 5, 'Preparing the focus-tree rewrite');
|
|
1125
1050
|
const planned = await workbench.planChanges({
|
|
1126
1051
|
workspaceId,
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod/v4';
|
|
2
2
|
import { boundedSourceHashEvidence, publicArtifactLink } from '../../core/artifacts.js';
|
|
3
|
-
import { canonicalJson, compareCodeUnits } from '../../core/canonical.js';
|
|
3
|
+
import { canonicalJson, compareCodeUnits, hashCanonical } from '../../core/canonical.js';
|
|
4
4
|
import { renderDimensionViolation, RenderBudget, RENDER_MAX_DIMENSION, } from '../../core/render-budget.js';
|
|
5
5
|
import { emptyServiceResult, ServiceError } from '../../core/result.js';
|
|
6
6
|
import { SOURCE_MAX_BYTES } from '../../core/source/index.js';
|
|
7
7
|
import { GuiHelperDocumentSchema, GuiPreviewScenarioSchema, GUI_TEXT_PACKAGE_MAX_FILES, ScriptedGuiStudio, compileGuiHelpers, } from '../../gui/index.js';
|
|
8
|
+
import { encodeGuiInspectionArtifact, projectGuiGraphForArtifact, } from '../../gui/inspection-artifact.js';
|
|
8
9
|
import { workspaceIdSchema, workspaceRelativePathSchema } from '../../schemas/common.js';
|
|
9
10
|
import { PACKAGE_VERSION } from '../../version.js';
|
|
10
11
|
import { requireServerScope } from '../server/base-tools.js';
|
|
@@ -149,10 +150,11 @@ export function registerGuiTools(server, engine, context) {
|
|
|
149
150
|
inputSchema: guiInspectInput,
|
|
150
151
|
outputSchema: guiScanOutputSchema,
|
|
151
152
|
annotations: artifactProducing,
|
|
152
|
-
}, async ({ workspaceId, windowName, scenario, relatedScenarios }, extra) => {
|
|
153
|
+
}, async ({ workspaceId: requestedWorkspaceId, windowName, scenario, relatedScenarios }, extra) => {
|
|
154
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(requestedWorkspaceId, context.principal);
|
|
153
155
|
try {
|
|
154
156
|
const progress = progressReporter(extra);
|
|
155
|
-
await progress.report(0, 3, 'Building
|
|
157
|
+
await progress.report(0, 3, 'Building GUI source graph');
|
|
156
158
|
const inspection = windowName === undefined || scenario === undefined
|
|
157
159
|
? studio
|
|
158
160
|
.scan(workspaceId, context.principal, progress.signal)
|
|
@@ -167,18 +169,21 @@ export function registerGuiTools(server, engine, context) {
|
|
|
167
169
|
signal: progress.signal,
|
|
168
170
|
})
|
|
169
171
|
.then((linted) => ({ graph: linted.graph, linted }));
|
|
170
|
-
const
|
|
171
|
-
engine.scan(workspaceId, {}, context.principal, progress.signal),
|
|
172
|
-
inspection,
|
|
173
|
-
]);
|
|
172
|
+
const inspected = await inspection;
|
|
174
173
|
const { graph, linted } = inspected;
|
|
174
|
+
const sharedRevision = hashCanonical(graph.sourceHashes);
|
|
175
175
|
const workspace = engine.resolver.get(workspaceId, context.principal);
|
|
176
176
|
const sourceEvidence = boundedSourceHashEvidence(graph.sourceHashes);
|
|
177
|
-
const
|
|
177
|
+
const artifactGraph = projectGuiGraphForArtifact(graph, linted?.scene.elements.map(({ sourceId }) => sourceId));
|
|
178
|
+
const inspectionName = `gui-inspect.${sharedRevision.slice(0, 16)}.json`;
|
|
179
|
+
const inspectionJson = `${canonicalJson({
|
|
178
180
|
schemaVersion: 1,
|
|
179
|
-
sharedRevision
|
|
181
|
+
sharedRevision,
|
|
180
182
|
offline: true,
|
|
181
|
-
graph,
|
|
183
|
+
graph: artifactGraph.graph,
|
|
184
|
+
...(artifactGraph.projection === undefined
|
|
185
|
+
? {}
|
|
186
|
+
: { graphProjection: artifactGraph.projection }),
|
|
182
187
|
...(linted === undefined
|
|
183
188
|
? {}
|
|
184
189
|
: {
|
|
@@ -186,25 +191,29 @@ export function registerGuiTools(server, engine, context) {
|
|
|
186
191
|
fidelity: linted.scene.fidelity,
|
|
187
192
|
validation: linted.validation,
|
|
188
193
|
}),
|
|
189
|
-
})}\n
|
|
194
|
+
})}\n`;
|
|
195
|
+
const encodedInspection = await encodeGuiInspectionArtifact(inspectionName, inspectionJson);
|
|
196
|
+
const artifact = await engine.artifacts.putChunked(workspace, encodedInspection.name, encodedInspection.mimeType, encodedInspection.content, {
|
|
190
197
|
kind: 'gui-inspect',
|
|
191
198
|
toolVersion: PACKAGE_VERSION,
|
|
192
199
|
schemaVersion: 'gui-inspect.v1',
|
|
193
200
|
sourceHashes: sourceEvidence.sourceHashes,
|
|
194
201
|
metadata: {
|
|
195
|
-
sharedRevision
|
|
202
|
+
sharedRevision,
|
|
196
203
|
offline: true,
|
|
197
204
|
complete: graph.complete,
|
|
198
205
|
skippedSourceCount: graph.skippedSourceCount,
|
|
199
206
|
sourceHashInventory: sourceEvidence.inventory,
|
|
207
|
+
compressed: encodedInspection.compressed,
|
|
208
|
+
uncompressedBytes: encodedInspection.uncompressedBytes,
|
|
209
|
+
graphProjection: artifactGraph.projection?.mode ?? 'full',
|
|
200
210
|
},
|
|
201
|
-
},
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
];
|
|
211
|
+
}, encodedInspection.compressed
|
|
212
|
+
? 'Gzip-compressed scripted GUI graph, diagnostics, and optional scenario fidelity inspection'
|
|
213
|
+
: 'Scripted GUI graph, diagnostics, and optional scenario fidelity inspection', progress.signal);
|
|
214
|
+
const diagnostics = linted === undefined ? graph.diagnostics : linted.validation.diagnostics;
|
|
206
215
|
const result = emptyServiceResult(workspaceId, {
|
|
207
|
-
sharedRevision
|
|
216
|
+
sharedRevision,
|
|
208
217
|
complete: graph.complete,
|
|
209
218
|
skippedSourceCount: graph.skippedSourceCount,
|
|
210
219
|
skippedSources: graph.skippedSources,
|
|
@@ -253,12 +262,11 @@ export function registerGuiTools(server, engine, context) {
|
|
|
253
262
|
}
|
|
254
263
|
});
|
|
255
264
|
const render = async (input, extra, code) => {
|
|
256
|
-
const
|
|
265
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(input.workspaceId, context.principal);
|
|
266
|
+
const { windowName } = input;
|
|
257
267
|
try {
|
|
258
268
|
const progress = progressReporter(extra);
|
|
259
|
-
await progress.report(0, 4, 'Building
|
|
260
|
-
const shared = await engine.scan(workspaceId, {}, context.principal, progress.signal);
|
|
261
|
-
await progress.report(1, 4, 'Rendering offline GUI artifacts');
|
|
269
|
+
await progress.report(0, 4, 'Building GUI source graph');
|
|
262
270
|
const rendered = await studio.renderAndStore({
|
|
263
271
|
workspaceId,
|
|
264
272
|
windowName,
|
|
@@ -279,7 +287,8 @@ export function registerGuiTools(server, engine, context) {
|
|
|
279
287
|
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
280
288
|
signal: progress.signal,
|
|
281
289
|
});
|
|
282
|
-
|
|
290
|
+
await progress.report(3, 4, 'Preparing GUI result');
|
|
291
|
+
const diagnostics = rendered.validation.diagnostics;
|
|
283
292
|
const result = emptyServiceResult(workspaceId, {
|
|
284
293
|
windowName,
|
|
285
294
|
scenarioId: rendered.render.scene.scenario.id,
|
|
@@ -300,7 +309,7 @@ export function registerGuiTools(server, engine, context) {
|
|
|
300
309
|
offlineRepresentation: true,
|
|
301
310
|
});
|
|
302
311
|
result.code = code;
|
|
303
|
-
setInlineFilesScanned(result,
|
|
312
|
+
setInlineFilesScanned(result, rendered.filesScanned);
|
|
304
313
|
result.diagnostics = diagnostics.slice(0, 100);
|
|
305
314
|
result.artifacts = rendered.artifacts.map(publicArtifactLink);
|
|
306
315
|
result.validation = validationSummary({
|
|
@@ -444,7 +453,8 @@ export function registerGuiTools(server, engine, context) {
|
|
|
444
453
|
openWorldHint: false,
|
|
445
454
|
},
|
|
446
455
|
}, async (input, extra) => {
|
|
447
|
-
const
|
|
456
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(input.workspaceId, context.principal);
|
|
457
|
+
const { relativePath, windowName, scenario } = input;
|
|
448
458
|
try {
|
|
449
459
|
requireServerScope(context, 'hoi4:write');
|
|
450
460
|
const progress = progressReporter(extra);
|
|
@@ -364,7 +364,8 @@ export function registerMapTools(server, engine, context) {
|
|
|
364
364
|
.strict(),
|
|
365
365
|
outputSchema: mapWorkspaceInspectOutputSchema,
|
|
366
366
|
annotations: artifactProducing,
|
|
367
|
-
}, async ({ workspaceId, provinceIds, stateIds, regionIds, allocationRequests }, extra) => {
|
|
367
|
+
}, async ({ workspaceId: requestedWorkspaceId, provinceIds, stateIds, regionIds, allocationRequests }, extra) => {
|
|
368
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(requestedWorkspaceId, context.principal);
|
|
368
369
|
try {
|
|
369
370
|
const progress = progressReporter(extra);
|
|
370
371
|
await progress.report(0, 3, 'Inspecting and validating the map');
|
|
@@ -512,7 +513,8 @@ export function registerMapTools(server, engine, context) {
|
|
|
512
513
|
.strict(),
|
|
513
514
|
outputSchema: mapRenderOutputSchema,
|
|
514
515
|
annotations: artifactProducing,
|
|
515
|
-
}, async ({ workspaceId, layer, overlays, scale }, extra) => {
|
|
516
|
+
}, async ({ workspaceId: requestedWorkspaceId, layer, overlays, scale }, extra) => {
|
|
517
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(requestedWorkspaceId, context.principal);
|
|
516
518
|
try {
|
|
517
519
|
const progress = progressReporter(extra);
|
|
518
520
|
await progress.report(0, 3, 'Rendering offline map layer');
|
|
@@ -557,7 +559,8 @@ export function registerMapTools(server, engine, context) {
|
|
|
557
559
|
idempotentHint: false,
|
|
558
560
|
openWorldHint: false,
|
|
559
561
|
},
|
|
560
|
-
}, async ({ workspaceId, operations, diffScale }, extra) => {
|
|
562
|
+
}, async ({ workspaceId: requestedWorkspaceId, operations, diffScale }, extra) => {
|
|
563
|
+
const workspaceId = engine.resolver.resolveWorkspaceId(requestedWorkspaceId, context.principal);
|
|
561
564
|
try {
|
|
562
565
|
requireServerScope(context, 'hoi4:write');
|
|
563
566
|
const progress = progressReporter(extra);
|