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.
Files changed (44) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/README.md +6 -7
  3. package/dist/hoi4_agent_tools/core/artifacts.js +109 -4
  4. package/dist/hoi4_agent_tools/core/diagnostic-collector.js +28 -8
  5. package/dist/hoi4_agent_tools/core/diff.js +162 -4
  6. package/dist/hoi4_agent_tools/core/engine.js +2 -0
  7. package/dist/hoi4_agent_tools/core/index.js +17 -6
  8. package/dist/hoi4_agent_tools/core/render-budget.js +5 -3
  9. package/dist/hoi4_agent_tools/core/scanner.js +3 -2
  10. package/dist/hoi4_agent_tools/core/source/limits.js +4 -1
  11. package/dist/hoi4_agent_tools/core/workspace.js +31 -3
  12. package/dist/hoi4_agent_tools/event/graph.js +28 -6
  13. package/dist/hoi4_agent_tools/event/limits.js +1 -1
  14. package/dist/hoi4_agent_tools/event/service.js +103 -7
  15. package/dist/hoi4_agent_tools/focus/continuous-render.js +17 -8
  16. package/dist/hoi4_agent_tools/focus/presentation.js +96 -41
  17. package/dist/hoi4_agent_tools/focus/render.js +27 -16
  18. package/dist/hoi4_agent_tools/gui/assets.js +9 -2
  19. package/dist/hoi4_agent_tools/gui/inspection-artifact.js +99 -0
  20. package/dist/hoi4_agent_tools/gui/layout.js +126 -27
  21. package/dist/hoi4_agent_tools/gui/renderer.js +16 -0
  22. package/dist/hoi4_agent_tools/gui/source-graph.js +61 -28
  23. package/dist/hoi4_agent_tools/gui/studio.js +23 -8
  24. package/dist/hoi4_agent_tools/map/diagnostic-limit.js +90 -9
  25. package/dist/hoi4_agent_tools/map/model.js +22 -4
  26. package/dist/hoi4_agent_tools/map/service.js +4 -3
  27. package/dist/hoi4_agent_tools/map/validation.js +3 -2
  28. package/dist/hoi4_agent_tools/mcp/server/create.js +1 -3
  29. package/dist/hoi4_agent_tools/mcp/server/output-schemas.js +1 -1
  30. package/dist/hoi4_agent_tools/mcp/tools/event.js +6 -6
  31. package/dist/hoi4_agent_tools/mcp/tools/focus.js +149 -224
  32. package/dist/hoi4_agent_tools/mcp/tools/gui.js +35 -25
  33. package/dist/hoi4_agent_tools/mcp/tools/map.js +6 -3
  34. package/dist/hoi4_agent_tools/runtime.js +112 -2
  35. package/dist/hoi4_agent_tools/schemas/common.js +5 -1
  36. package/dist/hoi4_agent_tools/version.js +1 -1
  37. package/docs/events.md +4 -5
  38. package/docs/focus.md +5 -4
  39. package/docs/gui.md +5 -1
  40. package/docs/map.md +1 -1
  41. package/docs/setup.md +9 -6
  42. package/package.json +1 -1
  43. package/server.json +2 -2
  44. 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, sha256Bytes } from '../../core/canonical.js';
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, renderContinuousFocusPalette, renderFocusTree, resolveFocusPresentation, enrichFocusPlanFromSidecar, } from '../../focus/index.js';
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 for creation and cleanup review as deterministic HTML, SVG, PNG, JSON, and source-map artifacts. Omit mode for national behavior. National trees with at least 200 focuses use reviewScale 0.5 by default; set 0.25 through 1.0 explicitly when needed.',
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
- }, async (input, extra) => {
670
- const { workspaceId, relativePath } = input;
671
- try {
672
- const progress = progressReporter(extra);
673
- await progress.report(0, 3, 'Importing and indexing focus source');
674
- const snapshot = await engine.scan(workspaceId, {}, context.principal, progress.signal);
675
- const workspace = engine.resolver.get(workspaceId, context.principal);
676
- const renderBudget = new RenderBudget();
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
- const currentDiagnostics = currentPlan === undefined
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: { sourceRevision: snapshot.revision, proposedPlanHash },
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 beforePng = currentRender?.png ??
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
- const currentDiagnostics = currentPlan === undefined || currentLayout === undefined
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 beforePng = currentRender?.png ??
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 shared index and GUI inspection');
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 [shared, inspected] = await Promise.all([
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 artifact = await engine.artifacts.putChunked(workspace, `gui-inspect.${shared.revision.slice(0, 16)}.json`, 'application/json', `${canonicalJson({
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: shared.revision,
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: shared.revision,
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
- }, 'Scripted GUI graph, diagnostics, and optional scenario fidelity inspection', progress.signal);
202
- const diagnostics = [
203
- ...shared.diagnostics,
204
- ...(linted === undefined ? graph.diagnostics : linted.validation.diagnostics),
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: shared.revision,
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 { workspaceId, windowName } = input;
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 shared index and GUI source graph');
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
- const diagnostics = [...shared.diagnostics, ...rendered.validation.diagnostics];
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, shared.files.map(({ displayPath }) => displayPath));
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 { workspaceId, relativePath, windowName, scenario } = input;
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);