hoi4-agent-tools 2.0.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 (37) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.md +4 -3
  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/event/graph.js +28 -6
  12. package/dist/hoi4_agent_tools/event/limits.js +1 -1
  13. package/dist/hoi4_agent_tools/event/service.js +103 -7
  14. package/dist/hoi4_agent_tools/focus/continuous-render.js +17 -8
  15. package/dist/hoi4_agent_tools/focus/presentation.js +96 -41
  16. package/dist/hoi4_agent_tools/focus/render.js +27 -16
  17. package/dist/hoi4_agent_tools/gui/assets.js +9 -2
  18. package/dist/hoi4_agent_tools/gui/inspection-artifact.js +99 -0
  19. package/dist/hoi4_agent_tools/gui/layout.js +7 -3
  20. package/dist/hoi4_agent_tools/gui/source-graph.js +61 -28
  21. package/dist/hoi4_agent_tools/gui/studio.js +23 -8
  22. package/dist/hoi4_agent_tools/map/diagnostic-limit.js +90 -9
  23. package/dist/hoi4_agent_tools/map/model.js +22 -4
  24. package/dist/hoi4_agent_tools/map/service.js +4 -3
  25. package/dist/hoi4_agent_tools/map/validation.js +3 -2
  26. package/dist/hoi4_agent_tools/mcp/server/create.js +1 -1
  27. package/dist/hoi4_agent_tools/mcp/server/output-schemas.js +1 -1
  28. package/dist/hoi4_agent_tools/mcp/tools/focus.js +145 -223
  29. package/dist/hoi4_agent_tools/mcp/tools/gui.js +29 -22
  30. package/dist/hoi4_agent_tools/version.js +1 -1
  31. package/docs/events.md +4 -5
  32. package/docs/focus.md +5 -4
  33. package/docs/gui.md +4 -2
  34. package/docs/map.md +1 -1
  35. package/docs/setup.md +1 -1
  36. package/package.json +1 -1
  37. package/server.json +2 -2
@@ -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,
@@ -490,6 +607,7 @@ export function registerFocusTools(server, engine, context) {
490
607
  index: snapshot.index,
491
608
  scanner: engine.scanner,
492
609
  workspace,
610
+ decodeIcons: false,
493
611
  signal: progress.signal,
494
612
  });
495
613
  diagnostics.push(...presentation.diagnostics);
@@ -589,6 +707,7 @@ export function registerFocusTools(server, engine, context) {
589
707
  index: snapshot.index,
590
708
  scanner: engine.scanner,
591
709
  workspace,
710
+ decodeIcons: false,
592
711
  signal: progress.signal,
593
712
  });
594
713
  diagnostics.push(...presentation.diagnostics);
@@ -663,123 +782,18 @@ export function registerFocusTools(server, engine, context) {
663
782
  });
664
783
  server.registerTool('hoi4.focus_render', {
665
784
  title: 'Render focus review artifacts',
666
- 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.',
667
786
  inputSchema: focusRenderInput,
668
787
  outputSchema: focusRenderOutputSchema,
669
788
  annotations: artifactProducing,
670
- }, async (input, extra) => {
671
- const { workspaceId: requestedWorkspaceId, relativePath } = input;
672
- const workspaceId = engine.resolver.resolveWorkspaceId(requestedWorkspaceId, context.principal);
673
- try {
674
- const progress = progressReporter(extra);
675
- await progress.report(0, 3, 'Importing and indexing focus source');
676
- const snapshot = await engine.scan(workspaceId, {}, context.principal, progress.signal);
677
- const workspace = engine.resolver.get(workspaceId, context.principal);
678
- const renderBudget = new RenderBudget();
679
- if (input.mode === 'continuous') {
680
- selectedContinuousFocusFile(snapshot, relativePath);
681
- const imported = await workbench.importContinuousPath(workspaceId, relativePath, context.principal);
682
- const plan = selectContinuousPlan(imported.result, input.paletteId);
683
- const presentation = await resolveFocusPresentation({
684
- plans: [],
685
- palettes: [plan],
686
- files: snapshot.files,
687
- index: snapshot.index,
688
- scanner: engine.scanner,
689
- workspace,
690
- budget: renderBudget,
691
- signal: progress.signal,
692
- });
693
- await progress.report(1, 3, 'Rendering continuous focus artifacts');
694
- const rendered = await workbench.renderContinuousAndStore(workspaceId, plan, {
695
- ...(context.principal === undefined ? {} : { principal: context.principal }),
696
- presentation,
697
- sourceHashes: presentation.sourceHashes,
698
- ...(input.columns === undefined ? {} : { columns: input.columns }),
699
- ...(input.padding === undefined ? {} : { padding: input.padding }),
700
- renderProfile: { sourceRevision: snapshot.revision },
701
- budget: renderBudget,
702
- signal: progress.signal,
703
- });
704
- const diagnostics = [...imported.result.diagnostics, ...rendered.diagnostics];
705
- const result = emptyServiceResult(workspaceId, {
706
- mode: 'continuous',
707
- paletteId: plan.id,
708
- focusCount: plan.focuses.length,
709
- hashes: rendered.bundle.hashes,
710
- width: rendered.bundle.width,
711
- height: rendered.bundle.height,
712
- });
713
- result.code = 'CONTINUOUS_FOCUS_RENDERED';
714
- setInlineFilesScanned(result, [...new Set([plan.provenance.sourcePath, ...presentation.filesScanned])].sort((left, right) => compareCodeUnits(left, right)));
715
- result.diagnostics = diagnostics.slice(0, 100);
716
- result.artifacts = rendered.artifacts.map(publicArtifactLink);
717
- result.validation = validationFromDiagnostics(diagnostics);
718
- await progress.report(3, 3, 'Continuous focus render complete');
719
- return toolResult(result);
720
- }
721
- const paletteImport = continuousPalettes(snapshot);
722
- const imported = importFocusFile(snapshot, selectedFocusFile(snapshot, workspace.registration.roots.focus, relativePath), paletteImport.palettes);
723
- const plan = selectPlan(imported, input.treeId);
724
- const linkedPalettes = paletteImport.palettes.filter(({ id }) => plan.continuousFocusPaletteIds.includes(id));
725
- const presentation = await resolveFocusPresentation({
726
- plans: [plan],
727
- palettes: linkedPalettes,
728
- files: snapshot.files,
729
- index: snapshot.index,
730
- scanner: engine.scanner,
731
- workspace,
732
- budget: renderBudget,
733
- signal: progress.signal,
734
- });
735
- await progress.report(1, 3, 'Rendering focus artifacts');
736
- const effectiveReviewScale = input.reviewScale ?? automaticFocusRenderScale(plan.focuses.length);
737
- const rendered = await workbench.renderAndStore(workspaceId, plan, {
738
- ...(context.principal === undefined ? {} : { principal: context.principal }),
739
- index: snapshot.index,
740
- references: referenceCatalog(snapshot),
741
- presentation,
742
- sourceHashes: presentation.sourceHashes,
743
- ...(input.horizontalSpacing === undefined
744
- ? {}
745
- : { horizontalSpacing: input.horizontalSpacing }),
746
- ...(input.verticalSpacing === undefined
747
- ? {}
748
- : { verticalSpacing: input.verticalSpacing }),
749
- ...(input.padding === undefined ? {} : { padding: input.padding }),
750
- outputScale: effectiveReviewScale,
751
- renderProfile: {
752
- sourceRevision: snapshot.revision,
753
- reviewScale: effectiveReviewScale,
754
- },
755
- budget: renderBudget,
756
- signal: progress.signal,
757
- });
758
- const diagnostics = [
759
- ...paletteImport.diagnostics,
760
- ...imported.diagnostics,
761
- ...rendered.diagnostics,
762
- ];
763
- const result = emptyServiceResult(workspaceId, {
764
- mode: 'national',
765
- treeId: plan.id,
766
- layoutHash: rendered.layout.layoutHash,
767
- hashes: rendered.bundle.hashes,
768
- width: rendered.bundle.width,
769
- height: rendered.bundle.height,
770
- });
771
- result.code = 'FOCUS_RENDERED';
772
- setInlineFilesScanned(result, [...new Set([plan.provenance.sourcePath, ...presentation.filesScanned])].sort((left, right) => compareCodeUnits(left, right)));
773
- result.diagnostics = diagnostics.slice(0, 100);
774
- result.artifacts = rendered.artifacts.map(publicArtifactLink);
775
- result.validation = validationFromDiagnostics(diagnostics);
776
- await progress.report(3, 3, 'Focus render complete');
777
- return toolResult(result);
778
- }
779
- catch (error) {
780
- return errorResult(error, workspaceId);
781
- }
782
- });
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));
783
797
  server.registerTool('hoi4.focus_rewrite', {
784
798
  title: 'Create or clean up focus content',
785
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.',
@@ -831,20 +845,11 @@ export function registerFocusTools(server, engine, context) {
831
845
  index: snapshot.index,
832
846
  scanner: engine.scanner,
833
847
  workspace,
848
+ decodeIcons: false,
834
849
  budget: renderBudget,
835
850
  signal: progress.signal,
836
851
  });
837
- const currentDiagnostics = currentPlan === undefined
838
- ? []
839
- : [...(imported?.result.diagnostics ?? []), ...workbench.lintContinuous(currentPlan)];
840
- await progress.report(1, 5, 'Rendering before and proposed continuous focus plans');
841
- const currentRender = currentPlan === undefined
842
- ? undefined
843
- : await renderContinuousFocusPalette(currentPlan, currentDiagnostics, {
844
- presentation,
845
- signal: progress.signal,
846
- budget: renderBudget,
847
- });
852
+ await progress.report(1, 5, 'Building proposed continuous focus review');
848
853
  const proposedPlanHash = focusPlanHash(plan);
849
854
  const reviewPlan = {
850
855
  ...plan,
@@ -858,47 +863,16 @@ export function registerFocusTools(server, engine, context) {
858
863
  ...(context.principal === undefined ? {} : { principal: context.principal }),
859
864
  presentation,
860
865
  sourceHashes: presentation.sourceHashes,
861
- renderProfile: { sourceRevision: snapshot.revision, proposedPlanHash },
866
+ renderProfile: {
867
+ sourceRevision: snapshot.revision,
868
+ proposedPlanHash,
869
+ output: 'vector',
870
+ },
871
+ rasterize: false,
862
872
  budget: renderBudget,
863
873
  signal: progress.signal,
864
874
  });
865
- const beforePng = currentRender?.png ??
866
- (await createTransparentPng(proposed.bundle.width, proposed.bundle.height, 'new continuous focus baseline', renderBudget));
867
- const comparison = await comparePngImages(beforePng, proposed.bundle.png, 8, progress.signal, renderBudget);
868
- const stem = plan.id.replace(/[^A-Za-z0-9._-]/gu, '_').slice(0, 80) || 'continuous';
869
- const provenance = {
870
- toolVersion: PACKAGE_VERSION,
871
- schemaVersion: 'continuous-focus-visual-diff.v1',
872
- sourceHashes: {
873
- before: sourceFile?.sha256 ?? sha256Bytes(Buffer.alloc(0)),
874
- proposedPlan: proposedPlanHash,
875
- },
876
- renderProfile: { offline: true, kind: 'continuous-focus-palette' },
877
- };
878
- const comparisonArtifacts = await engine.artifacts.withAtomicWrites(workspace, [
879
- {
880
- name: `${stem}.continuous-focus-before.png`,
881
- mimeType: 'image/png',
882
- content: beforePng,
883
- provenance: { ...provenance, kind: 'continuous-focus-before-render' },
884
- },
885
- {
886
- name: `${stem}.continuous-focus-visual-diff.png`,
887
- mimeType: 'image/png',
888
- content: comparison.png,
889
- provenance: { ...provenance, kind: 'continuous-focus-visual-diff' },
890
- },
891
- {
892
- name: `${stem}.continuous-focus-visual-diff.json`,
893
- mimeType: 'application/json',
894
- content: comparison.json,
895
- provenance: { ...provenance, kind: 'continuous-focus-visual-diff-json' },
896
- },
897
- ], (stored) => Promise.resolve([...stored]), progress.signal);
898
- const reviewArtifacts = [
899
- ...proposed.artifacts.map(publicArtifactLink),
900
- ...comparisonArtifacts.map(publicArtifactLink),
901
- ];
875
+ const reviewArtifacts = proposed.artifacts.map(publicArtifactLink);
902
876
  await progress.report(3, 5, 'Preparing the continuous focus rewrite');
903
877
  const planned = await workbench.planContinuousChanges({
904
878
  workspaceId,
@@ -1029,6 +1003,7 @@ export function registerFocusTools(server, engine, context) {
1029
1003
  index: snapshot.index,
1030
1004
  scanner: engine.scanner,
1031
1005
  workspace,
1006
+ decodeIcons: false,
1032
1007
  budget: renderBudget,
1033
1008
  signal: progress.signal,
1034
1009
  });
@@ -1037,22 +1012,7 @@ export function registerFocusTools(server, engine, context) {
1037
1012
  : currentPlan === compactSourcePlan && compactPlanning !== undefined
1038
1013
  ? compactPlanning.currentLayout
1039
1014
  : await workbench.layoutAsync(currentPlan, { signal: progress.signal });
1040
- const currentDiagnostics = currentPlan === undefined || currentLayout === undefined
1041
- ? []
1042
- : workbench.lint(currentPlan, {
1043
- index: snapshot.index,
1044
- references: catalog,
1045
- layout: currentLayout,
1046
- });
1047
- await progress.report(1, 5, 'Rendering before and proposed focus plans');
1048
- const currentRender = currentPlan === undefined || currentLayout === undefined
1049
- ? undefined
1050
- : await renderFocusTree(currentPlan, currentLayout, currentDiagnostics, {
1051
- presentation,
1052
- ...reviewRenderOptions,
1053
- signal: progress.signal,
1054
- budget: renderBudget,
1055
- });
1015
+ await progress.report(1, 5, 'Building proposed focus review');
1056
1016
  const proposedPlanHash = focusPlanHash(plan);
1057
1017
  const reviewPlan = {
1058
1018
  ...plan,
@@ -1074,56 +1034,18 @@ export function registerFocusTools(server, engine, context) {
1074
1034
  presentation,
1075
1035
  sourceHashes: presentation.sourceHashes,
1076
1036
  ...reviewRenderOptions,
1037
+ rasterize: false,
1077
1038
  renderProfile: {
1078
1039
  sourceRevision: snapshot.revision,
1079
1040
  proposedPlanHash,
1080
1041
  reviewRenderProfile,
1042
+ output: 'vector',
1081
1043
  },
1082
1044
  budget: renderBudget,
1083
1045
  signal: progress.signal,
1084
1046
  layout: proposedLayout,
1085
1047
  });
1086
- const beforePng = currentRender?.png ??
1087
- (await createTransparentPng(proposed.bundle.width, proposed.bundle.height, 'new national focus baseline', renderBudget));
1088
- const comparison = await comparePngImages(beforePng, proposed.bundle.png, 8, progress.signal, renderBudget);
1089
- const stem = plan.id.replace(/[^A-Za-z0-9._-]/gu, '_').slice(0, 80) || 'focus-tree';
1090
- const provenance = {
1091
- toolVersion: PACKAGE_VERSION,
1092
- schemaVersion: 'focus-visual-diff.v1',
1093
- sourceHashes: {
1094
- before: focusFile?.sha256 ?? sha256Bytes(Buffer.alloc(0)),
1095
- proposedPlan: proposedPlanHash,
1096
- },
1097
- renderProfile: {
1098
- offline: true,
1099
- layoutHash: proposed.layout.layoutHash,
1100
- reviewRenderProfile,
1101
- },
1102
- };
1103
- const comparisonArtifacts = await engine.artifacts.withAtomicWrites(workspace, [
1104
- {
1105
- name: `${stem}.focus-before.png`,
1106
- mimeType: 'image/png',
1107
- content: beforePng,
1108
- provenance: { ...provenance, kind: 'focus-before-render' },
1109
- },
1110
- {
1111
- name: `${stem}.focus-visual-diff.png`,
1112
- mimeType: 'image/png',
1113
- content: comparison.png,
1114
- provenance: { ...provenance, kind: 'focus-visual-diff' },
1115
- },
1116
- {
1117
- name: `${stem}.focus-visual-diff.json`,
1118
- mimeType: 'application/json',
1119
- content: comparison.json,
1120
- provenance: { ...provenance, kind: 'focus-visual-diff-json' },
1121
- },
1122
- ], (stored) => Promise.resolve([...stored]), progress.signal);
1123
- const reviewArtifacts = [
1124
- ...proposed.artifacts.map(publicArtifactLink),
1125
- ...comparisonArtifacts.map(publicArtifactLink),
1126
- ];
1048
+ const reviewArtifacts = proposed.artifacts.map(publicArtifactLink);
1127
1049
  await progress.report(3, 5, 'Preparing the focus-tree rewrite');
1128
1050
  const planned = await workbench.planChanges({
1129
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';
@@ -153,7 +154,7 @@ export function registerGuiTools(server, engine, context) {
153
154
  const workspaceId = engine.resolver.resolveWorkspaceId(requestedWorkspaceId, context.principal);
154
155
  try {
155
156
  const progress = progressReporter(extra);
156
- await progress.report(0, 3, 'Building shared index and GUI inspection');
157
+ await progress.report(0, 3, 'Building GUI source graph');
157
158
  const inspection = windowName === undefined || scenario === undefined
158
159
  ? studio
159
160
  .scan(workspaceId, context.principal, progress.signal)
@@ -168,18 +169,21 @@ export function registerGuiTools(server, engine, context) {
168
169
  signal: progress.signal,
169
170
  })
170
171
  .then((linted) => ({ graph: linted.graph, linted }));
171
- const [shared, inspected] = await Promise.all([
172
- engine.scan(workspaceId, {}, context.principal, progress.signal),
173
- inspection,
174
- ]);
172
+ const inspected = await inspection;
175
173
  const { graph, linted } = inspected;
174
+ const sharedRevision = hashCanonical(graph.sourceHashes);
176
175
  const workspace = engine.resolver.get(workspaceId, context.principal);
177
176
  const sourceEvidence = boundedSourceHashEvidence(graph.sourceHashes);
178
- 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({
179
180
  schemaVersion: 1,
180
- sharedRevision: shared.revision,
181
+ sharedRevision,
181
182
  offline: true,
182
- graph,
183
+ graph: artifactGraph.graph,
184
+ ...(artifactGraph.projection === undefined
185
+ ? {}
186
+ : { graphProjection: artifactGraph.projection }),
183
187
  ...(linted === undefined
184
188
  ? {}
185
189
  : {
@@ -187,25 +191,29 @@ export function registerGuiTools(server, engine, context) {
187
191
  fidelity: linted.scene.fidelity,
188
192
  validation: linted.validation,
189
193
  }),
190
- })}\n`, {
194
+ })}\n`;
195
+ const encodedInspection = await encodeGuiInspectionArtifact(inspectionName, inspectionJson);
196
+ const artifact = await engine.artifacts.putChunked(workspace, encodedInspection.name, encodedInspection.mimeType, encodedInspection.content, {
191
197
  kind: 'gui-inspect',
192
198
  toolVersion: PACKAGE_VERSION,
193
199
  schemaVersion: 'gui-inspect.v1',
194
200
  sourceHashes: sourceEvidence.sourceHashes,
195
201
  metadata: {
196
- sharedRevision: shared.revision,
202
+ sharedRevision,
197
203
  offline: true,
198
204
  complete: graph.complete,
199
205
  skippedSourceCount: graph.skippedSourceCount,
200
206
  sourceHashInventory: sourceEvidence.inventory,
207
+ compressed: encodedInspection.compressed,
208
+ uncompressedBytes: encodedInspection.uncompressedBytes,
209
+ graphProjection: artifactGraph.projection?.mode ?? 'full',
201
210
  },
202
- }, 'Scripted GUI graph, diagnostics, and optional scenario fidelity inspection', progress.signal);
203
- const diagnostics = [
204
- ...shared.diagnostics,
205
- ...(linted === undefined ? graph.diagnostics : linted.validation.diagnostics),
206
- ];
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;
207
215
  const result = emptyServiceResult(workspaceId, {
208
- sharedRevision: shared.revision,
216
+ sharedRevision,
209
217
  complete: graph.complete,
210
218
  skippedSourceCount: graph.skippedSourceCount,
211
219
  skippedSources: graph.skippedSources,
@@ -258,9 +266,7 @@ export function registerGuiTools(server, engine, context) {
258
266
  const { windowName } = input;
259
267
  try {
260
268
  const progress = progressReporter(extra);
261
- await progress.report(0, 4, 'Building shared index and GUI source graph');
262
- const shared = await engine.scan(workspaceId, {}, context.principal, progress.signal);
263
- await progress.report(1, 4, 'Rendering offline GUI artifacts');
269
+ await progress.report(0, 4, 'Building GUI source graph');
264
270
  const rendered = await studio.renderAndStore({
265
271
  workspaceId,
266
272
  windowName,
@@ -281,7 +287,8 @@ export function registerGuiTools(server, engine, context) {
281
287
  ...(context.principal === undefined ? {} : { principal: context.principal }),
282
288
  signal: progress.signal,
283
289
  });
284
- const diagnostics = [...shared.diagnostics, ...rendered.validation.diagnostics];
290
+ await progress.report(3, 4, 'Preparing GUI result');
291
+ const diagnostics = rendered.validation.diagnostics;
285
292
  const result = emptyServiceResult(workspaceId, {
286
293
  windowName,
287
294
  scenarioId: rendered.render.scene.scenario.id,
@@ -302,7 +309,7 @@ export function registerGuiTools(server, engine, context) {
302
309
  offlineRepresentation: true,
303
310
  });
304
311
  result.code = code;
305
- setInlineFilesScanned(result, shared.files.map(({ displayPath }) => displayPath));
312
+ setInlineFilesScanned(result, rendered.filesScanned);
306
313
  result.diagnostics = diagnostics.slice(0, 100);
307
314
  result.artifacts = rendered.artifacts.map(publicArtifactLink);
308
315
  result.validation = validationSummary({
@@ -1,6 +1,6 @@
1
1
  import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
2
2
  export const PACKAGE_NAME = 'hoi4-agent-tools';
3
- export const PACKAGE_VERSION = '2.0.0';
3
+ export const PACKAGE_VERSION = '2.1.0';
4
4
  export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
5
5
  export const CONFIG_VERSION = 1;
6
6
  export const TRANSACTION_VERSION = 1;
package/docs/events.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The Event Chain Viewer analyzes event definitions and call sites across the active mod, configured dependencies, and vanilla sources. It is read-only: it does not rewrite event files or simulate the game.
4
4
 
5
- Use the three event tools directly; omit `workspaceId` when the MCP working directory is inside the target mod.
5
+ Use the three event tools directly from the target mod.
6
6
 
7
7
  ## Inspect
8
8
 
@@ -16,7 +16,9 @@ Use the three event tools directly; omit `workspaceId` when the MCP working dire
16
16
  - `lint`: find missing targets, unreachable events, suspicious cycles, conflicting definitions, invalid timing, and unresolved dynamic calls.
17
17
  - `impact`: identify callers, descendants, state dependencies, and files affected by a proposed event change.
18
18
 
19
- Use narrow identifiers, direction, and depth limits when the task concerns one chain. The compact response summarizes the result; complete graphs and diagnostics are returned as linked resources.
19
+ Use narrow identifiers, direction, and depth limits when the task concerns one chain. Broad scans build the structural event graph without expanding every scripted helper into duplicate paths. Focused trace, path, state, impact, and render calls expand helper relationships when needed.
20
+
21
+ The compact response summarizes the result. Small scan resources contain the complete graph; very large scan resources contain exact totals, grouped inventories, representative diagnostics, and the revision needed for focused follow-up queries. This keeps an agent's prompt and artifact storage bounded without reducing the results of those focused queries.
20
22
 
21
23
  Selectors use one of these forms:
22
24
 
@@ -37,7 +39,6 @@ Example `hoi4.event_inspect` arguments:
37
39
 
38
40
  ```json
39
41
  {
40
- "workspaceId": "my-mod",
41
42
  "mode": "trace",
42
43
  "selector": { "kind": "event", "eventId": "namespace.1" },
43
44
  "direction": "downstream",
@@ -61,7 +62,6 @@ Example `hoi4.event_render` arguments:
61
62
 
62
63
  ```json
63
64
  {
64
- "workspaceId": "my-mod",
65
65
  "view": "neighborhood",
66
66
  "selector": { "kind": "event", "eventId": "namespace.1" },
67
67
  "direction": "both",
@@ -83,7 +83,6 @@ Example `hoi4.event_compare` arguments:
83
83
 
84
84
  ```json
85
85
  {
86
- "workspaceId": "my-mod",
87
86
  "proposedSources": [
88
87
  {
89
88
  "relativePath": "events/example.txt",