hoi4-agent-tools 3.0.9 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/README.md +10 -2
  3. package/dist/bin/stdio.js +23 -12
  4. package/dist/hoi4_agent_tools/core/analysis-checkpoints.js +82 -0
  5. package/dist/hoi4_agent_tools/core/analysis-evidence.js +9 -0
  6. package/dist/hoi4_agent_tools/core/analysis-job-operations.js +38 -0
  7. package/dist/hoi4_agent_tools/core/artifact-resource.js +173 -0
  8. package/dist/hoi4_agent_tools/core/artifacts.js +84 -2
  9. package/dist/hoi4_agent_tools/core/condition-evaluator.js +16 -0
  10. package/dist/hoi4_agent_tools/core/dependency-pages.js +216 -0
  11. package/dist/hoi4_agent_tools/core/dependency-walk.js +266 -0
  12. package/dist/hoi4_agent_tools/core/domain-validation.js +188 -0
  13. package/dist/hoi4_agent_tools/core/engine.js +48 -5
  14. package/dist/hoi4_agent_tools/core/helper-expansion.js +186 -0
  15. package/dist/hoi4_agent_tools/core/impact-comparison.js +49 -0
  16. package/dist/hoi4_agent_tools/core/impact-graph.js +301 -0
  17. package/dist/hoi4_agent_tools/core/impact-semantic.js +256 -0
  18. package/dist/hoi4_agent_tools/core/index-segments.js +137 -0
  19. package/dist/hoi4_agent_tools/core/index.js +47 -4
  20. package/dist/hoi4_agent_tools/core/job-controls.js +109 -0
  21. package/dist/hoi4_agent_tools/core/job-executor.js +190 -0
  22. package/dist/hoi4_agent_tools/core/job-service.js +112 -0
  23. package/dist/hoi4_agent_tools/core/job-store.js +641 -0
  24. package/dist/hoi4_agent_tools/core/job-transactions.js +126 -0
  25. package/dist/hoi4_agent_tools/core/job-worker-host.js +202 -0
  26. package/dist/hoi4_agent_tools/core/job-worker.js +64 -0
  27. package/dist/hoi4_agent_tools/core/operation-context.js +31 -0
  28. package/dist/hoi4_agent_tools/core/operation-result.js +412 -0
  29. package/dist/hoi4_agent_tools/core/operation-tasks.js +387 -0
  30. package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +383 -0
  31. package/dist/hoi4_agent_tools/core/result.js +1 -0
  32. package/dist/hoi4_agent_tools/core/scanner.js +15 -75
  33. package/dist/hoi4_agent_tools/core/semantic-dependencies.js +233 -0
  34. package/dist/hoi4_agent_tools/core/shared-request-capacity.js +43 -11
  35. package/dist/hoi4_agent_tools/core/source/cache.js +107 -0
  36. package/dist/hoi4_agent_tools/core/source/parser.js +11 -1
  37. package/dist/hoi4_agent_tools/core/source-overlay.js +125 -0
  38. package/dist/hoi4_agent_tools/core/transaction-execution.js +134 -0
  39. package/dist/hoi4_agent_tools/core/transactions.js +80 -27
  40. package/dist/hoi4_agent_tools/decision/ai.js +104 -0
  41. package/dist/hoi4_agent_tools/decision/comparison.js +106 -0
  42. package/dist/hoi4_agent_tools/decision/cost.js +177 -0
  43. package/dist/hoi4_agent_tools/decision/evaluation.js +256 -0
  44. package/dist/hoi4_agent_tools/decision/lifecycle.js +114 -0
  45. package/dist/hoi4_agent_tools/decision/service.js +172 -0
  46. package/dist/hoi4_agent_tools/decision/source-inventory.js +182 -0
  47. package/dist/hoi4_agent_tools/event/graph.js +214 -290
  48. package/dist/hoi4_agent_tools/event/helper-expansion.js +33 -0
  49. package/dist/hoi4_agent_tools/event/job-operations.js +51 -0
  50. package/dist/hoi4_agent_tools/event/operations.js +179 -0
  51. package/dist/hoi4_agent_tools/event/service.js +67 -15
  52. package/dist/hoi4_agent_tools/event/source-analysis.js +5 -4
  53. package/dist/hoi4_agent_tools/focus/job-operations.js +77 -0
  54. package/dist/hoi4_agent_tools/focus/tool-operations.js +669 -0
  55. package/dist/hoi4_agent_tools/focus/tool-support.js +123 -0
  56. package/dist/hoi4_agent_tools/gui/job-operations.js +81 -0
  57. package/dist/hoi4_agent_tools/gui/tool-operations.js +270 -0
  58. package/dist/hoi4_agent_tools/impact/scenario-suites.js +74 -0
  59. package/dist/hoi4_agent_tools/impact/service.js +127 -0
  60. package/dist/hoi4_agent_tools/map/job-operations.js +76 -0
  61. package/dist/hoi4_agent_tools/map/tool-operations.js +493 -0
  62. package/dist/hoi4_agent_tools/mcp/prompts/probability.js +25 -0
  63. package/dist/hoi4_agent_tools/mcp/prompts/register-modern.js +29 -0
  64. package/dist/hoi4_agent_tools/mcp/resources/register-modern.js +34 -0
  65. package/dist/hoi4_agent_tools/mcp/resources/register.js +4 -150
  66. package/dist/hoi4_agent_tools/mcp/server/base-tools.js +3 -219
  67. package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +188 -0
  68. package/dist/hoi4_agent_tools/mcp/server/create.js +18 -6
  69. package/dist/hoi4_agent_tools/mcp/server/instructions.js +2 -0
  70. package/dist/hoi4_agent_tools/mcp/server/job-task-store.js +67 -0
  71. package/dist/hoi4_agent_tools/mcp/server/legacy-task-tools.js +20 -0
  72. package/dist/hoi4_agent_tools/mcp/server/modern-tasks.js +168 -0
  73. package/dist/hoi4_agent_tools/mcp/server/progress.js +17 -15
  74. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +45 -0
  75. package/dist/hoi4_agent_tools/mcp/server/result.js +2 -412
  76. package/dist/hoi4_agent_tools/mcp/server/task-request-context.js +9 -0
  77. package/dist/hoi4_agent_tools/mcp/server/task-tool-catalog.js +16 -0
  78. package/dist/hoi4_agent_tools/mcp/server/task-tool-definition.js +1 -0
  79. package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js +1 -123
  80. package/dist/hoi4_agent_tools/mcp/tools/analysis.js +65 -0
  81. package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +38 -21
  82. package/dist/hoi4_agent_tools/mcp/tools/event.js +24 -315
  83. package/dist/hoi4_agent_tools/mcp/tools/focus.js +34 -1039
  84. package/dist/hoi4_agent_tools/mcp/tools/gui.js +19 -504
  85. package/dist/hoi4_agent_tools/mcp/tools/job.js +46 -0
  86. package/dist/hoi4_agent_tools/mcp/tools/map.js +21 -600
  87. package/dist/hoi4_agent_tools/mcp/tools/probability.js +39 -463
  88. package/dist/hoi4_agent_tools/mcp/tools/technology.js +40 -304
  89. package/dist/hoi4_agent_tools/mcp/transports/http.js +58 -1
  90. package/dist/hoi4_agent_tools/mcp/transports/modern-task-routing.js +109 -0
  91. package/dist/hoi4_agent_tools/mcp/transports/negotiated-stdio.js +121 -0
  92. package/dist/hoi4_agent_tools/probability/job-operations.js +58 -0
  93. package/dist/hoi4_agent_tools/probability/operations.js +183 -0
  94. package/dist/hoi4_agent_tools/probability/service.js +24 -3
  95. package/dist/hoi4_agent_tools/schemas/analysis.js +228 -0
  96. package/dist/hoi4_agent_tools/schemas/common.js +2 -0
  97. package/dist/hoi4_agent_tools/schemas/event.js +72 -1
  98. package/dist/hoi4_agent_tools/schemas/focus-requests.js +189 -0
  99. package/dist/hoi4_agent_tools/schemas/gui-requests.js +193 -0
  100. package/dist/hoi4_agent_tools/schemas/helper-expansion.js +58 -0
  101. package/dist/hoi4_agent_tools/schemas/map-requests.js +85 -0
  102. package/dist/hoi4_agent_tools/schemas/probability-requests.js +168 -0
  103. package/dist/hoi4_agent_tools/schemas/technology.js +90 -1
  104. package/dist/hoi4_agent_tools/schemas/transaction.js +1 -0
  105. package/dist/hoi4_agent_tools/technology/graph.js +83 -78
  106. package/dist/hoi4_agent_tools/technology/helper-expansion.js +33 -0
  107. package/dist/hoi4_agent_tools/technology/job-operations.js +60 -0
  108. package/dist/hoi4_agent_tools/technology/operations.js +125 -0
  109. package/dist/hoi4_agent_tools/technology/service.js +89 -15
  110. package/dist/hoi4_agent_tools/technology/source-analysis.js +2 -1
  111. package/dist/hoi4_agent_tools/version.js +1 -1
  112. package/docs/README.md +3 -0
  113. package/docs/analysis.md +100 -0
  114. package/docs/development.md +2 -0
  115. package/docs/events.md +7 -2
  116. package/docs/helper-expansion.md +78 -0
  117. package/docs/http.md +2 -0
  118. package/docs/jobs.md +86 -0
  119. package/docs/technology.md +6 -2
  120. package/package.json +15 -6
  121. package/schemas/configuration.schema.json +1 -1
  122. package/schemas/continuous-focus-palette.schema.json +1 -1
  123. package/schemas/custom-weighted-pool.schema.json +1 -1
  124. package/schemas/decision-inspect-input.schema.json +290 -0
  125. package/schemas/event-feature-manifest.schema.json +1 -1
  126. package/schemas/focus-plan.schema.json +1 -1
  127. package/schemas/focus-planning-sidecar.schema.json +1 -1
  128. package/schemas/gui-animation-source.schema.json +1 -1
  129. package/schemas/gui-helper.schema.json +1 -1
  130. package/schemas/gui-scenario.schema.json +1 -1
  131. package/schemas/helper-expansion-request.schema.json +44 -0
  132. package/schemas/helper-expansion-summary.schema.json +70 -0
  133. package/schemas/impact-inspect-input.schema.json +203 -0
  134. package/schemas/map-operation.schema.json +1 -1
  135. package/schemas/operation-result.schema.json +1 -1
  136. package/schemas/probability-analysis-result.schema.json +1 -1
  137. package/schemas/probability-compare-input.schema.json +1 -1
  138. package/schemas/probability-evaluate-input.schema.json +1 -1
  139. package/schemas/probability-inspect-input.schema.json +1 -1
  140. package/schemas/probability-render-input.schema.json +1 -1
  141. package/schemas/probability-scenario-set.schema.json +1 -1
  142. package/schemas/probability-sequence-input.schema.json +1 -1
  143. package/schemas/probability-simulate-input.schema.json +1 -1
  144. package/schemas/probability-sweep-input.schema.json +1 -1
  145. package/server.json +3 -3
@@ -1,7 +1,9 @@
1
1
  import { compareCodeUnits, hashCanonical } from '../core/canonical.js';
2
2
  import { sortDiagnostics } from '../core/diagnostics.js';
3
3
  import { ServiceError } from '../core/result.js';
4
- import { EVENT_GRAPH_MAX_EDGES, EVENT_GRAPH_MAX_HELPER_DEPTH, EVENT_GRAPH_MAX_HELPER_PROJECTIONS, EVENT_GRAPH_MAX_HELPER_STATE_PROJECTIONS, EVENT_GRAPH_MAX_ISSUES, EVENT_GRAPH_MAX_NODES, EVENT_GRAPH_MAX_STATE_ACCESSES, EVENT_GRAPH_MAX_STATE_LINK_CANDIDATES, EVENT_GRAPH_MAX_STATE_LINKS, EVENT_GRAPH_MAX_UNRESOLVED, EventAnalysisBudget, } from './limits.js';
4
+ import { beginSemanticAnalysis, semanticFragment, SemanticCatalog, } from '../core/semantic-dependencies.js';
5
+ import { runDependencyAnalysis, runDependencyAnalysisAsync, } from '../core/dependency-walk.js';
6
+ import { EVENT_GRAPH_MAX_EDGES, EVENT_GRAPH_MAX_HELPER_DEPTH, EVENT_GRAPH_MAX_HELPER_PROJECTIONS, EVENT_GRAPH_MAX_HELPER_STATE_PROJECTIONS, EVENT_GRAPH_MAX_ISSUES, EVENT_GRAPH_MAX_NODES, EVENT_GRAPH_MAX_STATE_ACCESSES, EVENT_GRAPH_MAX_STATE_LINK_CANDIDATES, EVENT_GRAPH_MAX_STATE_LINKS, EVENT_GRAPH_MAX_UNRESOLVED, EVENT_GRAPH_WORK_LIMIT, EventAnalysisBudget, } from './limits.js';
5
7
  import { EVENT_GRAPH_PARSER_VERSION, EVENT_GRAPH_SCHEMA_VERSION, } from './model.js';
6
8
  import { iterativeStronglyConnectedComponents } from './scc.js';
7
9
  import { analyzeEventSource, eventSemanticFragmentCacheKey } from './source-analysis.js';
@@ -338,297 +340,209 @@ function helperName(node) {
338
340
  return name;
339
341
  return node.label || node.id.replace(/^helper:/u, '');
340
342
  }
341
- function helperProjections(graph, budget) {
343
+ function helperWalkInput(graph, revision) {
342
344
  const nodes = nodeById(graph.nodes);
343
- const optionParent = optionParents(graph.edges);
344
- const outgoing = new Map();
345
- for (const edge of graph.edges) {
346
- const values = outgoing.get(edge.from) ?? [];
347
- values.push(edge);
348
- outgoing.set(edge.from, values);
349
- }
350
- for (const values of outgoing.values()) {
351
- values.sort((left, right) => compareCodeUnits(left.id, right.id));
352
- }
353
- const starts = graph.edges
354
- .filter((edge) => !edge.derived &&
355
- nodes.get(edge.to)?.kind === 'helper' &&
356
- nodes.get(edge.from)?.kind !== 'helper')
345
+ const edges = graph.edges
346
+ .filter(({ derived }) => !derived)
357
347
  .sort((left, right) => compareCodeUnits(left.id, right.id));
348
+ return {
349
+ domain: 'event-helper-projections',
350
+ sourceRevision: revision,
351
+ edges: edges.map(({ id, from, to }) => ({ id, from, to })),
352
+ roots: edges
353
+ .filter((edge) => nodes.get(edge.to)?.kind === 'helper' && nodes.get(edge.from)?.kind !== 'helper')
354
+ .map(({ id }) => id),
355
+ branches: graph.nodes
356
+ .filter(({ kind }) => kind === 'helper')
357
+ .map(({ id }) => id)
358
+ .sort(compareCodeUnits),
359
+ maximumDepth: EVENT_GRAPH_MAX_HELPER_DEPTH,
360
+ maximumSteps: EVENT_GRAPH_WORK_LIMIT,
361
+ visitPolicy: 'node',
362
+ reverseEdges: true,
363
+ };
364
+ }
365
+ function helperCallerScope(nodes, parents, start) {
366
+ const caller = nodes.get(parents.get(start.from) ?? start.from);
367
+ const candidate = start.scope?.source ?? caller?.metadata.expectedScope ?? caller?.metadata.scope;
368
+ return candidate === 'country' ||
369
+ candidate === 'state' ||
370
+ candidate === 'unit_leader' ||
371
+ candidate === 'operative' ||
372
+ candidate === 'character' ||
373
+ candidate === 'global'
374
+ ? candidate
375
+ : 'unknown';
376
+ }
377
+ function helperProjections(graph, budget, walk) {
378
+ const edges = new Map(graph.edges.map((edge) => [edge.id, edge]));
379
+ const nodes = nodeById(graph.nodes);
358
380
  const projected = [];
381
+ const parents = optionParents(graph.edges);
359
382
  const projectedKeys = new Set();
360
- const reportedCycles = new Set();
361
- const reportedDepth = new Set();
362
- for (const start of starts) {
363
- const first = nodes.get(start.to);
364
- if (first === undefined)
383
+ const reported = new Set();
384
+ const callerScopes = new Map();
385
+ for (const record of walk.state.records) {
386
+ budget.spend('helper_projection');
387
+ if (record.kind === 'visit')
365
388
  continue;
366
- const caller = nodes.get(optionParent.get(start.from) ?? start.from);
367
- const callerScopeCandidate = start.scope?.source ?? caller?.metadata.expectedScope ?? caller?.metadata.scope;
368
- const callerScope = callerScopeCandidate === 'country' ||
369
- callerScopeCandidate === 'state' ||
370
- callerScopeCandidate === 'unit_leader' ||
371
- callerScopeCandidate === 'operative' ||
372
- callerScopeCandidate === 'character' ||
373
- callerScopeCandidate === 'global'
374
- ? callerScopeCandidate
375
- : 'unknown';
376
- const reachedHelpers = new Set([start.to]);
377
- const stack = [
378
- {
379
- currentId: start.to,
380
- helperStack: [helperName(first)],
381
- edgeStack: [start],
382
- visited: new Set([start.to]),
383
- },
384
- ];
385
- while (stack.length > 0) {
386
- budget.spend('helper_projection');
387
- const frame = stack.pop();
388
- const candidates = outgoing.get(frame.currentId) ?? [];
389
- for (const edge of [...candidates].reverse()) {
390
- if (edge.derived)
391
- continue;
392
- const target = nodes.get(edge.to);
393
- if (target?.kind === 'helper') {
394
- if (frame.visited.has(target.id)) {
395
- const cycleKey = `${start.id}:${target.id}`;
396
- if (!reportedCycles.has(cycleKey)) {
397
- reportedCycles.add(cycleKey);
398
- addIssue(graph.issues, {
399
- code: 'EVENT_HELPER_CYCLE',
400
- classification: 'unresolved_analysis',
401
- severity: 'warning',
402
- message: 'A scripted-effect cycle prevents complete helper expansion.',
403
- confidence: 'unresolved',
404
- location: edge.location,
405
- blockers: [
406
- {
407
- code: 'HELPER_CYCLE',
408
- message: 'Static expansion stopped at the repeated scripted effect.',
409
- location: edge.location,
410
- },
411
- ],
412
- subjectIds: [start.from, ...frame.visited, target.id],
413
- });
414
- }
415
- continue;
416
- }
417
- if (frame.helperStack.length >= EVENT_GRAPH_MAX_HELPER_DEPTH) {
418
- const depthKey = `${start.id}:${target.id}`;
419
- if (!reportedDepth.has(depthKey)) {
420
- reportedDepth.add(depthKey);
421
- addIssue(graph.issues, {
422
- code: 'EVENT_HELPER_DEPTH_LIMIT',
423
- classification: 'unresolved_analysis',
424
- severity: 'blocker',
425
- message: 'Scripted-effect expansion reached the fixed nesting ceiling.',
426
- confidence: 'unresolved',
427
- location: edge.location,
428
- blockers: [
429
- {
430
- code: 'HELPER_DEPTH_LIMIT',
431
- message: 'The helper chain is deeper than the supported static boundary.',
432
- location: edge.location,
433
- details: { maximumDepth: EVENT_GRAPH_MAX_HELPER_DEPTH },
434
- },
435
- ],
436
- subjectIds: [start.from, target.id],
437
- });
438
- }
439
- continue;
440
- }
441
- // The expanded structural graph retains every alternate helper
442
- // route. The collapsed projection needs one deterministic proof per
443
- // call site and reachable helper, otherwise diamond-shaped helper
444
- // graphs grow exponentially on real vanilla workspaces.
445
- if (reachedHelpers.has(target.id))
446
- continue;
447
- reachedHelpers.add(target.id);
448
- stack.push({
449
- currentId: target.id,
450
- helperStack: [...frame.helperStack, helperName(target)],
451
- edgeStack: [...frame.edgeStack, edge],
452
- visited: new Set([...frame.visited, target.id]),
453
- });
454
- continue;
455
- }
456
- const structural = [...frame.edgeStack, edge];
457
- const final = structural.at(-1);
458
- const projectionKey = `${start.id}\0${edge.to}`;
459
- if (projectedKeys.has(projectionKey))
460
- continue;
461
- projectedKeys.add(projectionKey);
462
- if (projected.length >= EVENT_GRAPH_MAX_HELPER_PROJECTIONS) {
463
- addIssue(graph.issues, {
464
- code: 'EVENT_HELPER_PROJECTION_LIMIT',
465
- classification: 'unresolved_analysis',
466
- severity: 'blocker',
467
- message: 'Collapsed helper projection reached its materialization ceiling; structural helper calls remain available.',
468
- confidence: 'unresolved',
389
+ const structural = walk.path(record.path).map(({ id }) => edges.get(id));
390
+ const start = structural[0];
391
+ const final = structural.at(-1);
392
+ const helpers = structural.slice(0, -1).map(({ to }) => nodes.get(to));
393
+ if (record.kind === 'cycle' || record.kind === 'depth') {
394
+ const key = `${record.kind}:${start.id}:${final.to}`;
395
+ if (reported.has(key))
396
+ continue;
397
+ reported.add(key);
398
+ const cycle = record.kind === 'cycle';
399
+ addIssue(graph.issues, {
400
+ code: cycle ? 'EVENT_HELPER_CYCLE' : 'EVENT_HELPER_DEPTH_LIMIT',
401
+ classification: 'unresolved_analysis',
402
+ severity: cycle ? 'warning' : 'blocker',
403
+ message: cycle
404
+ ? 'A scripted-effect cycle prevents complete helper expansion.'
405
+ : 'Scripted-effect expansion reached the fixed nesting ceiling.',
406
+ confidence: 'unresolved',
407
+ location: final.location,
408
+ blockers: [
409
+ {
410
+ code: cycle ? 'HELPER_CYCLE' : 'HELPER_DEPTH_LIMIT',
411
+ message: cycle
412
+ ? 'Static expansion stopped at the repeated scripted effect.'
413
+ : 'The helper chain is deeper than the supported static boundary.',
414
+ location: final.location,
415
+ ...(cycle ? {} : { details: { maximumDepth: EVENT_GRAPH_MAX_HELPER_DEPTH } }),
416
+ },
417
+ ],
418
+ subjectIds: cycle
419
+ ? [start.from, ...helpers.map(({ id }) => id), final.to]
420
+ : [start.from, final.to],
421
+ });
422
+ continue;
423
+ }
424
+ const projectionKey = `${start.id}\0${final.to}`;
425
+ if (projectedKeys.has(projectionKey))
426
+ continue;
427
+ projectedKeys.add(projectionKey);
428
+ if (projected.length >= EVENT_GRAPH_MAX_HELPER_PROJECTIONS) {
429
+ addIssue(graph.issues, {
430
+ code: 'EVENT_HELPER_PROJECTION_LIMIT',
431
+ classification: 'unresolved_analysis',
432
+ severity: 'blocker',
433
+ message: 'Collapsed helper projection reached its materialization ceiling; structural helper calls remain available.',
434
+ confidence: 'unresolved',
435
+ location: start.location,
436
+ blockers: [
437
+ {
438
+ code: 'HELPER_PROJECTION_LIMIT',
439
+ message: 'Use a bounded helper-expanded trace for paths omitted from the collapsed workspace graph.',
469
440
  location: start.location,
470
- blockers: [
471
- {
472
- code: 'HELPER_PROJECTION_LIMIT',
473
- message: 'Use a bounded helper-expanded trace for paths omitted from the collapsed workspace graph.',
474
- location: start.location,
475
- details: { maximum: EVENT_GRAPH_MAX_HELPER_PROJECTIONS },
476
- },
477
- ],
478
- subjectIds: [start.from, start.to],
479
- });
480
- return stableUnique(projected);
481
- }
482
- projected.push({
483
- id: `edge:helper-projection:${hashCanonical({
484
- from: start.from,
485
- to: edge.to,
486
- edges: structural.map(({ id }) => id),
487
- }).slice(0, 32)}`,
488
- from: start.from,
489
- to: edge.to,
490
- reason: 'scripted_effect_expansion',
491
- conditions: structural.flatMap(({ conditions }) => conditions),
492
- helperStack: frame.helperStack,
493
- location: start.location,
494
- provenance: structural.flatMap(({ provenance }) => provenance),
495
- confidence: worstConfidence(...structural.map(({ confidence }) => confidence)),
496
- derived: true,
497
- ...(final.timing === undefined ? {} : { timing: final.timing }),
498
- ...(final.weight === undefined ? {} : { weight: final.weight }),
499
- ...(final.scope === undefined
500
- ? {}
501
- : {
502
- scope: final.scope.source === 'unknown' && callerScope !== 'unknown'
503
- ? { ...final.scope, source: callerScope, confidence: 'high' }
504
- : final.scope,
505
- }),
506
- metadata: {
507
- structuralEdgeIds: structural.map(({ id }) => id),
508
- terminalDispatchLocation: locationKey(final.location),
441
+ details: { maximum: EVENT_GRAPH_MAX_HELPER_PROJECTIONS },
509
442
  },
510
- });
511
- }
443
+ ],
444
+ subjectIds: [start.from, start.to],
445
+ });
446
+ break;
512
447
  }
448
+ let callerScope = callerScopes.get(start.id);
449
+ if (callerScope === undefined) {
450
+ callerScope = helperCallerScope(nodes, parents, start);
451
+ callerScopes.set(start.id, callerScope);
452
+ }
453
+ projected.push({
454
+ id: `edge:helper-projection:${hashCanonical({ from: start.from, to: final.to, edges: structural.map(({ id }) => id) }).slice(0, 32)}`,
455
+ from: start.from,
456
+ to: final.to,
457
+ reason: 'scripted_effect_expansion',
458
+ conditions: structural.flatMap(({ conditions }) => conditions),
459
+ helperStack: helpers.map(helperName),
460
+ location: start.location,
461
+ provenance: structural.flatMap(({ provenance }) => provenance),
462
+ confidence: worstConfidence(...structural.map(({ confidence }) => confidence)),
463
+ derived: true,
464
+ ...(final.timing === undefined ? {} : { timing: final.timing }),
465
+ ...(final.weight === undefined ? {} : { weight: final.weight }),
466
+ ...(final.scope === undefined
467
+ ? {}
468
+ : {
469
+ scope: final.scope.source === 'unknown' && callerScope !== 'unknown'
470
+ ? { ...final.scope, source: callerScope, confidence: 'high' }
471
+ : final.scope,
472
+ }),
473
+ metadata: {
474
+ structuralEdgeIds: structural.map(({ id }) => id),
475
+ terminalDispatchLocation: locationKey(final.location),
476
+ },
477
+ });
513
478
  }
514
479
  return stableUnique(projected);
515
480
  }
516
- function projectHelperStateAccesses(graph, budget) {
481
+ function projectHelperStateAccesses(graph, budget, walk) {
517
482
  const nodes = nodeById(graph.nodes);
518
- const optionParent = optionParents(graph.edges);
483
+ const edges = new Map(graph.edges.map((edge) => [edge.id, edge]));
519
484
  const accesses = new Map();
485
+ const parents = optionParents(graph.edges);
520
486
  for (const access of graph.stateAccesses) {
521
487
  const values = accesses.get(access.ownerId) ?? [];
522
488
  values.push(access);
523
489
  accesses.set(access.ownerId, values);
524
490
  }
525
- const outgoing = new Map();
526
- for (const edge of graph.edges) {
527
- if (edge.derived)
528
- continue;
529
- const values = outgoing.get(edge.from) ?? [];
530
- values.push(edge);
531
- outgoing.set(edge.from, values);
532
- }
533
- for (const values of outgoing.values())
534
- values.sort((a, b) => compareCodeUnits(a.id, b.id));
535
- const starts = graph.edges
536
- .filter((edge) => !edge.derived &&
537
- nodes.get(edge.to)?.kind === 'helper' &&
538
- nodes.get(edge.from)?.kind !== 'helper')
539
- .sort((left, right) => compareCodeUnits(left.id, right.id));
540
491
  const projected = [];
541
- for (const start of starts) {
542
- const first = nodes.get(start.to);
543
- if (first === undefined)
492
+ const projectedKeys = new Set();
493
+ const callerScopes = new Map();
494
+ for (const record of walk.state.records) {
495
+ if (record.kind !== 'visit')
544
496
  continue;
545
- const caller = nodes.get(optionParent.get(start.from) ?? start.from);
546
- const callerScopeCandidate = start.scope?.source ?? caller?.metadata.expectedScope ?? caller?.metadata.scope;
547
- const callerScope = callerScopeCandidate === 'country' ||
548
- callerScopeCandidate === 'state' ||
549
- callerScopeCandidate === 'unit_leader' ||
550
- callerScopeCandidate === 'operative' ||
551
- callerScopeCandidate === 'character' ||
552
- callerScopeCandidate === 'global'
553
- ? callerScopeCandidate
554
- : 'unknown';
555
- const reachedHelpers = new Set([first.id]);
556
- const projectedAccesses = new Set();
557
- const stack = [
558
- {
559
- helperId: first.id,
560
- helperStack: [helperName(first)],
561
- callEdges: [start],
562
- visited: new Set([first.id]),
563
- },
564
- ];
565
- while (stack.length > 0) {
566
- budget.spend('helper_state_projection');
567
- const frame = stack.pop();
568
- for (const access of accesses.get(frame.helperId) ?? []) {
569
- const projectionKey = `${start.id}\0${access.id}`;
570
- if (projectedAccesses.has(projectionKey))
571
- continue;
572
- projectedAccesses.add(projectionKey);
573
- if (projected.length >= EVENT_GRAPH_MAX_HELPER_STATE_PROJECTIONS ||
574
- graph.stateAccesses.length + projected.length >= EVENT_GRAPH_MAX_STATE_ACCESSES) {
575
- addIssue(graph.issues, {
576
- code: 'EVENT_HELPER_STATE_PROJECTION_LIMIT',
577
- classification: 'unresolved_analysis',
578
- severity: 'blocker',
579
- message: 'Helper state-flow projection reached its fixed materialization ceiling; structural helper evidence remains available.',
580
- confidence: 'unresolved',
581
- location: start.location,
582
- blockers: [
583
- {
584
- code: 'HELPER_STATE_PROJECTION_LIMIT',
585
- message: 'Use an expanded bounded helper trace for state evidence omitted from the collapsed workspace graph.',
586
- location: start.location,
587
- details: { maximum: EVENT_GRAPH_MAX_HELPER_STATE_PROJECTIONS },
588
- },
589
- ],
590
- subjectIds: [start.from, start.to],
591
- });
592
- return projected;
593
- }
594
- projected.push({
595
- ...access,
596
- id: `state:helper-projection:${hashCanonical({
597
- caller: start.from,
598
- access: access.id,
599
- edges: frame.callEdges.map(({ id }) => id),
600
- }).slice(0, 32)}`,
601
- ownerId: start.from,
602
- scope: access.scope === 'unknown' ? callerScope : access.scope,
603
- confidence: worstConfidence(access.confidence, ...frame.callEdges.map(({ confidence }) => confidence)),
604
- helperStack: frame.helperStack,
605
- conditions: [
606
- ...frame.callEdges.flatMap(({ conditions }) => conditions),
607
- ...access.conditions,
608
- ],
609
- metadata: {
610
- ...access.metadata,
611
- projectedFromAccessId: access.id,
612
- structuralEdgeIds: frame.callEdges.map(({ id }) => id),
613
- },
614
- });
615
- }
616
- if (frame.helperStack.length >= EVENT_GRAPH_MAX_HELPER_DEPTH)
497
+ budget.spend('helper_state_projection');
498
+ const calls = walk.path(record.path).map(({ id }) => edges.get(id));
499
+ const start = calls[0];
500
+ const target = calls.at(-1).to;
501
+ let callerScope = callerScopes.get(start.id);
502
+ if (callerScope === undefined) {
503
+ callerScope = helperCallerScope(nodes, parents, start);
504
+ callerScopes.set(start.id, callerScope);
505
+ }
506
+ for (const access of accesses.get(target) ?? []) {
507
+ const key = `${start.id}\0${access.id}`;
508
+ if (projectedKeys.has(key))
617
509
  continue;
618
- for (const edge of [...(outgoing.get(frame.helperId) ?? [])].reverse()) {
619
- const target = nodes.get(edge.to);
620
- if (target?.kind !== 'helper' ||
621
- frame.visited.has(target.id) ||
622
- reachedHelpers.has(target.id))
623
- continue;
624
- reachedHelpers.add(target.id);
625
- stack.push({
626
- helperId: target.id,
627
- helperStack: [...frame.helperStack, helperName(target)],
628
- callEdges: [...frame.callEdges, edge],
629
- visited: new Set([...frame.visited, target.id]),
510
+ projectedKeys.add(key);
511
+ if (projected.length >= EVENT_GRAPH_MAX_HELPER_STATE_PROJECTIONS ||
512
+ graph.stateAccesses.length + projected.length >= EVENT_GRAPH_MAX_STATE_ACCESSES) {
513
+ addIssue(graph.issues, {
514
+ code: 'EVENT_HELPER_STATE_PROJECTION_LIMIT',
515
+ classification: 'unresolved_analysis',
516
+ severity: 'blocker',
517
+ message: 'Helper state-flow projection reached its fixed materialization ceiling; structural helper evidence remains available.',
518
+ confidence: 'unresolved',
519
+ location: start.location,
520
+ blockers: [
521
+ {
522
+ code: 'HELPER_STATE_PROJECTION_LIMIT',
523
+ message: 'Use an expanded bounded helper trace for state evidence omitted from the collapsed workspace graph.',
524
+ location: start.location,
525
+ details: { maximum: EVENT_GRAPH_MAX_HELPER_STATE_PROJECTIONS },
526
+ },
527
+ ],
528
+ subjectIds: [start.from, start.to],
630
529
  });
530
+ return projected;
631
531
  }
532
+ projected.push({
533
+ ...access,
534
+ id: `state:helper-projection:${hashCanonical({ caller: start.from, access: access.id, edges: calls.map(({ id }) => id) }).slice(0, 32)}`,
535
+ ownerId: start.from,
536
+ scope: access.scope === 'unknown' ? callerScope : access.scope,
537
+ confidence: worstConfidence(access.confidence, ...calls.map(({ confidence }) => confidence)),
538
+ helperStack: calls.map(({ to }) => helperName(nodes.get(to))),
539
+ conditions: [...calls.flatMap(({ conditions }) => conditions), ...access.conditions],
540
+ metadata: {
541
+ ...access.metadata,
542
+ projectedFromAccessId: access.id,
543
+ structuralEdgeIds: calls.map(({ id }) => id),
544
+ },
545
+ });
632
546
  }
633
547
  }
634
548
  return stableUnique(projected);
@@ -1450,6 +1364,22 @@ function unreachableIssues(graph, budget) {
1450
1364
  }
1451
1365
  function analyzeFragments(snapshot, files, catalog, options, budget) {
1452
1366
  const fragments = [];
1367
+ const dependencies = beginSemanticAnalysis(options.cache, snapshot, 'event', options.workspaceIdentity);
1368
+ const reads = new SemanticCatalog();
1369
+ const context = {
1370
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
1371
+ budget,
1372
+ knownEventIds: reads.bind('events', 'event', catalog.eventIds),
1373
+ knownEventTypes: reads.bind('event-types', 'event', catalog.eventTypes),
1374
+ knownHelperIds: reads.bind('helpers', 'scripted_effect', catalog.helperIds),
1375
+ knownDecisionIds: reads.bind('decisions', 'decision', catalog.decisionIds),
1376
+ activeEventPaths: reads.bind('active-events', 'event', catalog.activeEventPaths),
1377
+ retainedEventPaths: reads.bind('retained-events', 'event', catalog.retainedEventPaths),
1378
+ activeHelperPaths: reads.bind('active-helpers', 'scripted_effect', catalog.activeHelperPaths),
1379
+ activeDecisionPaths: reads.bind('active-decisions', 'decision', catalog.activeDecisionPaths),
1380
+ inventoryComplete: snapshot.complete,
1381
+ catalogFingerprint: catalog.fingerprint,
1382
+ };
1453
1383
  for (const file of files) {
1454
1384
  budget.spend('source_fragment');
1455
1385
  if (!shouldAnalyzeEventFile(file))
@@ -1457,24 +1387,7 @@ function analyzeFragments(snapshot, files, catalog, options, budget) {
1457
1387
  if (snapshot.index.isSourceSkipped(file.displayPath))
1458
1388
  continue;
1459
1389
  const cacheKey = eventSemanticFragmentCacheKey(file, catalog.fingerprint);
1460
- let fragment = options.cache?.get(cacheKey);
1461
- if (fragment === undefined) {
1462
- fragment = analyzeEventSource(file, {
1463
- ...(options.signal === undefined ? {} : { signal: options.signal }),
1464
- budget,
1465
- knownEventIds: catalog.eventIds,
1466
- knownEventTypes: catalog.eventTypes,
1467
- knownHelperIds: catalog.helperIds,
1468
- knownDecisionIds: catalog.decisionIds,
1469
- activeEventPaths: catalog.activeEventPaths,
1470
- retainedEventPaths: catalog.retainedEventPaths,
1471
- activeHelperPaths: catalog.activeHelperPaths,
1472
- activeDecisionPaths: catalog.activeDecisionPaths,
1473
- inventoryComplete: snapshot.complete,
1474
- catalogFingerprint: catalog.fingerprint,
1475
- });
1476
- options.cache?.set(cacheKey, fragment, file.bytes.length);
1477
- }
1390
+ const fragment = semanticFragment(dependencies, options.cache, file, cacheKey, reads, () => analyzeEventSource(file, context));
1478
1391
  fragments.push(fragment);
1479
1392
  }
1480
1393
  return fragments.sort((left, right) => compareCodeUnits(left.sourcePath, right.sourcePath) ||
@@ -1543,6 +1456,12 @@ function graphStatistics(nodes, edges, stateAccesses, issues) {
1543
1456
  * Parsing remains file-local; this stage owns cross-file resolution and static projections.
1544
1457
  */
1545
1458
  export function buildEventGraph(snapshot, options = {}) {
1459
+ return runDependencyAnalysis(eventGraphAnalysis(snapshot, options), options.signal);
1460
+ }
1461
+ export function buildEventGraphAsync(snapshot, options = {}) {
1462
+ return runDependencyAnalysisAsync(eventGraphAnalysis(snapshot, options), options.signal);
1463
+ }
1464
+ function* eventGraphAnalysis(snapshot, options) {
1546
1465
  options.signal?.throwIfAborted();
1547
1466
  const budget = new EventAnalysisBudget(options.signal);
1548
1467
  const files = activeFiles(snapshot);
@@ -1560,9 +1479,10 @@ export function buildEventGraph(snapshot, options = {}) {
1560
1479
  normalizeCrossFileEdges(graph);
1561
1480
  ensureReferencedNodes(graph);
1562
1481
  if (options.projectHelpers !== false) {
1563
- for (const edge of helperProjections(graph, budget))
1482
+ const walk = yield helperWalkInput(graph, snapshot.revision);
1483
+ for (const edge of helperProjections(graph, budget, walk))
1564
1484
  graph.edges.push(edge);
1565
- for (const access of projectHelperStateAccesses(graph, budget))
1485
+ for (const access of projectHelperStateAccesses(graph, budget, walk))
1566
1486
  graph.stateAccesses.push(access);
1567
1487
  }
1568
1488
  addTerminalNodes(graph);
@@ -1606,7 +1526,11 @@ export function buildEventGraph(snapshot, options = {}) {
1606
1526
  const diagnostics = sortDiagnostics([...snapshot.diagnostics, ...partial.diagnostics]);
1607
1527
  // Coverage completeness is independent from defects discovered in the source. A full,
1608
1528
  // untruncated analysis remains complete when it correctly reports missing or dynamic links.
1609
- const complete = !focused && snapshot.complete;
1529
+ const complete = !focused &&
1530
+ snapshot.complete &&
1531
+ !graph.issues.some(({ code }) => code === 'EVENT_HELPER_DEPTH_LIMIT' ||
1532
+ code === 'EVENT_HELPER_PROJECTION_LIMIT' ||
1533
+ code === 'EVENT_HELPER_STATE_PROJECTION_LIMIT');
1610
1534
  const nodes = sortNodes(graph.nodes);
1611
1535
  const edges = stableUnique(graph.edges);
1612
1536
  const stateAccesses = stableUnique(graph.stateAccesses);
@@ -0,0 +1,33 @@
1
+ import { compareCodeUnits } from '../core/canonical.js';
2
+ export function eventHelperInventory(graph) {
3
+ const helpers = new Set(graph.nodes.filter(({ kind }) => kind === 'helper').map(({ id }) => id));
4
+ const structural = graph.edges.filter(({ derived }) => !derived);
5
+ return {
6
+ domain: 'event',
7
+ sourceRevision: graph.revision,
8
+ sourceComplete: graph.complete,
9
+ sourceHashes: graph.sourceHashes,
10
+ unresolved: graph.unresolved,
11
+ edges: [
12
+ ...structural.map((edge) => ({
13
+ id: edge.id,
14
+ from: edge.from,
15
+ to: edge.to,
16
+ evidence: { kind: 'event_edge', edge },
17
+ })),
18
+ ...graph.stateAccesses
19
+ .filter(({ ownerId }) => helpers.has(ownerId))
20
+ .map((access) => ({
21
+ id: `state-access:${access.id}`,
22
+ from: access.ownerId,
23
+ to: `state-access:${access.id}`,
24
+ evidence: { kind: 'state_access', access },
25
+ })),
26
+ ].sort((left, right) => compareCodeUnits(left.id, right.id)),
27
+ roots: structural
28
+ .filter(({ from, to }) => !helpers.has(from) && helpers.has(to))
29
+ .map(({ id }) => id)
30
+ .sort(compareCodeUnits),
31
+ branches: [...helpers].sort(compareCodeUnits),
32
+ };
33
+ }