hoi4-agent-tools 4.0.5 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +2 -0
- package/dist/hoi4_agent_tools/core/bounded-source.js +210 -0
- package/dist/hoi4_agent_tools/core/canonical.js +42 -0
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +12 -8
- package/dist/hoi4_agent_tools/core/dependency-walk.js +21 -2
- package/dist/hoi4_agent_tools/core/diagnostic-rollup.js +91 -0
- package/dist/hoi4_agent_tools/core/domain-validation.js +12 -0
- package/dist/hoi4_agent_tools/core/engine.js +2 -1
- package/dist/hoi4_agent_tools/core/impact-graph.js +6 -2
- package/dist/hoi4_agent_tools/core/impact-semantic.js +12 -2
- package/dist/hoi4_agent_tools/core/index.js +1 -0
- package/dist/hoi4_agent_tools/core/job-executor.js +9 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +12 -2
- package/dist/hoi4_agent_tools/core/managed-outputs.js +72 -0
- package/dist/hoi4_agent_tools/core/operation-result.js +28 -2
- package/dist/hoi4_agent_tools/core/scanner.js +17 -0
- package/dist/hoi4_agent_tools/core/server-state.js +9 -1
- package/dist/hoi4_agent_tools/core/temporary-directories.js +5 -1
- package/dist/hoi4_agent_tools/core/transaction-canonical.js +43 -0
- package/dist/hoi4_agent_tools/core/transaction-limits.js +7 -2
- package/dist/hoi4_agent_tools/core/transactions.js +355 -111
- package/dist/hoi4_agent_tools/decision/service.js +22 -1
- package/dist/hoi4_agent_tools/event/algorithms.js +3 -2
- package/dist/hoi4_agent_tools/event/graph.js +52 -35
- package/dist/hoi4_agent_tools/event/limits.js +4 -0
- package/dist/hoi4_agent_tools/event/operations.js +11 -0
- package/dist/hoi4_agent_tools/event/queries.js +13 -34
- package/dist/hoi4_agent_tools/event/render.js +4 -2
- package/dist/hoi4_agent_tools/event/service.js +93 -33
- package/dist/hoi4_agent_tools/event/source-analysis.js +94 -4
- package/dist/hoi4_agent_tools/event/state-keys.js +63 -0
- package/dist/hoi4_agent_tools/event/summary.js +130 -0
- package/dist/hoi4_agent_tools/impact/service.js +2 -1
- package/dist/hoi4_agent_tools/map/derived.js +21 -6
- package/dist/hoi4_agent_tools/map/job-operations.js +9 -3
- package/dist/hoi4_agent_tools/map/operations.js +72 -21
- package/dist/hoi4_agent_tools/map/reference-labels.js +238 -0
- package/dist/hoi4_agent_tools/map/reference-preparation.js +247 -0
- package/dist/hoi4_agent_tools/map/reference-world.js +1097 -0
- package/dist/hoi4_agent_tools/map/render.js +9 -1
- package/dist/hoi4_agent_tools/map/service.js +222 -18
- package/dist/hoi4_agent_tools/map/tool-operations.js +71 -2
- package/dist/hoi4_agent_tools/map/world-context.js +148 -0
- package/dist/hoi4_agent_tools/map/world-drainage.js +273 -0
- package/dist/hoi4_agent_tools/map/world-outputs.js +55 -0
- package/dist/hoi4_agent_tools/map/world-preview-style.js +157 -0
- package/dist/hoi4_agent_tools/map/world-preview.js +230 -0
- package/dist/hoi4_agent_tools/map/world-provinces.js +524 -0
- package/dist/hoi4_agent_tools/map/world.js +374 -253
- package/dist/hoi4_agent_tools/mcp/tools/analysis.js +1 -0
- package/dist/hoi4_agent_tools/mcp/tools/event.js +66 -0
- package/dist/hoi4_agent_tools/mcp/tools/map.js +15 -2
- package/dist/hoi4_agent_tools/reference/source-lookup.js +4 -2
- package/dist/hoi4_agent_tools/schemas/map-requests.js +4 -0
- package/dist/hoi4_agent_tools/schemas/map.js +103 -10
- package/dist/hoi4_agent_tools/schemas/transaction.js +20 -3
- package/dist/hoi4_agent_tools/version.js +2 -2
- package/docs/analysis.md +4 -0
- package/docs/events.md +12 -0
- package/docs/images/reference-maps/political.png +0 -0
- package/docs/images/reference-maps/sketch.png +0 -0
- package/docs/map.md +45 -0
- package/docs/third-party-notices.md +6 -0
- package/npm-shrinkwrap.json +124 -2
- package/package.json +4 -1
- package/schemas/configuration.schema.json +1 -1
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +1 -1
- package/schemas/error-log-input.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/helper-expansion-request.schema.json +1 -1
- package/schemas/helper-expansion-summary.schema.json +1 -1
- package/schemas/impact-inspect-input.schema.json +1 -1
- package/schemas/map-operation.schema.json +373 -70
- package/schemas/mechanic-test-input.schema.json +1 -1
- package/schemas/mod-index-input.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +1 -1
- package/schemas/package-manifest.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/schemas/reference-context-input.schema.json +1 -1
- package/schemas/reference-read-input.schema.json +1 -1
- package/schemas/reference-search-input.schema.json +1 -1
- package/schemas/scenario-suite.schema.json +1 -1
- package/schemas/scenario-test-input.schema.json +1 -1
- package/schemas/script-validate-input.schema.json +1 -1
- package/schemas/shared-scenario.schema.json +1 -1
- package/schemas/source-lookup-input.schema.json +1 -1
- package/server.json +2 -2
|
@@ -99,7 +99,20 @@ export class DecisionAnalyzer {
|
|
|
99
99
|
}));
|
|
100
100
|
const ai = input.mode === 'inventory'
|
|
101
101
|
? undefined
|
|
102
|
-
: await inspectDecisionAi(this.engine, before, input.id, scenarios, overlay, input.principal, input.signal)
|
|
102
|
+
: await inspectDecisionAi(this.engine, before, input.id, scenarios, overlay, input.principal, input.signal).catch((error) => {
|
|
103
|
+
// AI weights depend on adapters verified for one game version. A game patch leaves
|
|
104
|
+
// the AI score unresolved; visibility, availability and cost evidence still stand.
|
|
105
|
+
if (error instanceof ServiceError &&
|
|
106
|
+
(error.code === 'PROBABILITY_GAME_VERSION_UNSUPPORTED' ||
|
|
107
|
+
error.code === 'PROBABILITY_GAME_VERSION_UNVERIFIED'))
|
|
108
|
+
return {
|
|
109
|
+
status: 'unresolved',
|
|
110
|
+
code: error.code,
|
|
111
|
+
message: error.message,
|
|
112
|
+
details: error.details,
|
|
113
|
+
};
|
|
114
|
+
throw error;
|
|
115
|
+
});
|
|
103
116
|
const sourceComplete = input.mode === 'inventory'
|
|
104
117
|
? inventory.complete
|
|
105
118
|
: input.mode === 'compare'
|
|
@@ -180,6 +193,14 @@ export class DecisionAnalyzer {
|
|
|
180
193
|
: {}),
|
|
181
194
|
});
|
|
182
195
|
result.code = complete ? 'DECISION_ANALYZED' : 'DECISION_ANALYZED_PARTIAL';
|
|
196
|
+
if (ai !== undefined && 'code' in ai)
|
|
197
|
+
result.diagnostics.push({
|
|
198
|
+
category: 'configuration',
|
|
199
|
+
code: ai.code,
|
|
200
|
+
severity: 'warning',
|
|
201
|
+
message: `${ai.message}; AI weights are unresolved, the remaining evidence is unaffected`,
|
|
202
|
+
details: ai.details,
|
|
203
|
+
});
|
|
183
204
|
result.artifacts = [publicArtifactLink(artifact)];
|
|
184
205
|
result.proposedFiles = overlay?.changes.map(({ path }) => path).slice(0, 64) ?? [];
|
|
185
206
|
setInlineFilesScanned(result, before.files.map(({ displayPath }) => displayPath));
|
|
@@ -362,10 +362,11 @@ export function eventStronglyConnectedComponents(graph, expandHelpers = false, s
|
|
|
362
362
|
const budget = new EventAnalysisBudget(signal);
|
|
363
363
|
const components = iterativeStronglyConnectedComponents(retainedNodes, adjacency, () => budget.spend('strongly_connected_components'));
|
|
364
364
|
budget.check();
|
|
365
|
+
// Collect self-loops once: scanning every edge per component is quadratic on large mods.
|
|
366
|
+
const selfLoops = new Set(edges.filter(({ from, to }) => from === to).map(({ from }) => from));
|
|
365
367
|
return components
|
|
366
368
|
.map((nodeIds) => {
|
|
367
|
-
const
|
|
368
|
-
const selfLoop = edges.some(({ from, to }) => from === to && members.has(from));
|
|
369
|
+
const selfLoop = nodeIds.some((nodeId) => selfLoops.has(nodeId));
|
|
369
370
|
return {
|
|
370
371
|
id: `scc:${nodeIds.join('|')}`,
|
|
371
372
|
nodeIds,
|
|
@@ -5,6 +5,7 @@ import { beginSemanticAnalysis, semanticFragment, SemanticCatalog, } from '../co
|
|
|
5
5
|
import { runDependencyAnalysis, runDependencyAnalysisAsync, } from '../core/dependency-walk.js';
|
|
6
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';
|
|
7
7
|
import { EVENT_GRAPH_PARSER_VERSION, EVENT_GRAPH_SCHEMA_VERSION, } from './model.js';
|
|
8
|
+
import { compatibleProducerKeys, hasUnknownSavedScopeStorage, stateKey } from './state-keys.js';
|
|
8
9
|
import { iterativeStronglyConnectedComponents } from './scc.js';
|
|
9
10
|
import { analyzeEventSource, eventSemanticFragmentCacheKey } from './source-analysis.js';
|
|
10
11
|
const confidenceRank = {
|
|
@@ -117,11 +118,14 @@ function locationKey(location) {
|
|
|
117
118
|
return `${location?.path ?? ''}:${location?.start.offset ?? -1}:${location?.end.offset ?? -1}`;
|
|
118
119
|
}
|
|
119
120
|
function issueKey(issue) {
|
|
120
|
-
|
|
121
|
+
// One finding per owner and subject: repeated accesses to the same named state collapse,
|
|
122
|
+
// while different events, targets, flags or expressions in one owner stay separate findings.
|
|
123
|
+
const { subjectIds, accessId: _accessId, stateLinkId: _stateLinkId, ...subject } = issue.details;
|
|
121
124
|
return hashCanonical({
|
|
122
125
|
code: issue.code,
|
|
123
126
|
classification: issue.classification,
|
|
124
127
|
subjectIds: Array.isArray(subjectIds) ? subjectIds : [],
|
|
128
|
+
subject,
|
|
125
129
|
});
|
|
126
130
|
}
|
|
127
131
|
function stableIssues(values) {
|
|
@@ -360,6 +364,7 @@ function helperWalkInput(graph, revision) {
|
|
|
360
364
|
maximumSteps: EVENT_GRAPH_WORK_LIMIT,
|
|
361
365
|
visitPolicy: 'node',
|
|
362
366
|
reverseEdges: true,
|
|
367
|
+
onWorkLimit: 'truncate',
|
|
363
368
|
};
|
|
364
369
|
}
|
|
365
370
|
function helperCallerScope(nodes, parents, start) {
|
|
@@ -391,11 +396,17 @@ function helperProjections(graph, budget, walk) {
|
|
|
391
396
|
const final = structural.at(-1);
|
|
392
397
|
const helpers = structural.slice(0, -1).map(({ to }) => nodes.get(to));
|
|
393
398
|
if (record.kind === 'cycle' || record.kind === 'depth') {
|
|
394
|
-
const
|
|
399
|
+
const cycle = record.kind === 'cycle';
|
|
400
|
+
// One finding per recursive loop, identified by its member helpers, rather than one per
|
|
401
|
+
// caller that reaches it.
|
|
402
|
+
const chain = helpers.map(({ id }) => id);
|
|
403
|
+
const members = chain.slice(Math.max(0, chain.indexOf(final.to)));
|
|
404
|
+
const key = cycle
|
|
405
|
+
? `cycle:${[...members].sort(compareCodeUnits).join('\0')}`
|
|
406
|
+
: `depth:${start.id}:${final.to}`;
|
|
395
407
|
if (reported.has(key))
|
|
396
408
|
continue;
|
|
397
409
|
reported.add(key);
|
|
398
|
-
const cycle = record.kind === 'cycle';
|
|
399
410
|
addIssue(graph.issues, {
|
|
400
411
|
code: cycle ? 'EVENT_HELPER_CYCLE' : 'EVENT_HELPER_DEPTH_LIMIT',
|
|
401
412
|
classification: 'unresolved_analysis',
|
|
@@ -415,9 +426,7 @@ function helperProjections(graph, budget, walk) {
|
|
|
415
426
|
...(cycle ? {} : { details: { maximumDepth: EVENT_GRAPH_MAX_HELPER_DEPTH } }),
|
|
416
427
|
},
|
|
417
428
|
],
|
|
418
|
-
subjectIds: cycle
|
|
419
|
-
? [start.from, ...helpers.map(({ id }) => id), final.to]
|
|
420
|
-
: [start.from, final.to],
|
|
429
|
+
subjectIds: cycle ? members : [start.from, final.to],
|
|
421
430
|
});
|
|
422
431
|
continue;
|
|
423
432
|
}
|
|
@@ -617,32 +626,6 @@ function addTerminalNodes(graph) {
|
|
|
617
626
|
});
|
|
618
627
|
}
|
|
619
628
|
}
|
|
620
|
-
function stateKey(value) {
|
|
621
|
-
const scope = value.kind === 'variable' || value.kind === 'array' ? `:${value.scope}` : '';
|
|
622
|
-
const storage = value.kind === 'saved_scope'
|
|
623
|
-
? `:${value.metadata.storage === 'global' || value.metadata.storage === 'local'
|
|
624
|
-
? value.metadata.storage
|
|
625
|
-
: 'unknown'}`
|
|
626
|
-
: '';
|
|
627
|
-
return `${value.kind}:${value.name}${scope}${storage}`;
|
|
628
|
-
}
|
|
629
|
-
function compatibleProducerKeys(value) {
|
|
630
|
-
if (value.kind !== 'saved_scope' ||
|
|
631
|
-
value.metadata.storage === 'global' ||
|
|
632
|
-
value.metadata.storage === 'local') {
|
|
633
|
-
return [stateKey(value)];
|
|
634
|
-
}
|
|
635
|
-
return [
|
|
636
|
-
`saved_scope:${value.name}:global`,
|
|
637
|
-
`saved_scope:${value.name}:local`,
|
|
638
|
-
`saved_scope:${value.name}:unknown`,
|
|
639
|
-
];
|
|
640
|
-
}
|
|
641
|
-
function hasUnknownSavedScopeStorage(value) {
|
|
642
|
-
return (value.kind === 'saved_scope' &&
|
|
643
|
-
value.metadata.storage !== 'global' &&
|
|
644
|
-
value.metadata.storage !== 'local');
|
|
645
|
-
}
|
|
646
629
|
function collapsedFlowEdges(nodes, edges) {
|
|
647
630
|
return edges
|
|
648
631
|
.filter((edge) => {
|
|
@@ -999,9 +982,23 @@ function structuralCallIssues(graph, budget) {
|
|
|
999
982
|
for (const values of cyclicOutgoing.values())
|
|
1000
983
|
values.sort(compareCodeUnits);
|
|
1001
984
|
const flowComponents = iterativeStronglyConnectedComponents(collapsed.flatMap(({ from, to }) => [from, to]), cyclicOutgoing, () => budget.spend('event_cycle'));
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
985
|
+
// Group each edge under its component once instead of filtering every edge per component.
|
|
986
|
+
const componentOf = new Map();
|
|
987
|
+
flowComponents.forEach((component, index) => {
|
|
988
|
+
for (const member of component)
|
|
989
|
+
componentOf.set(member, index);
|
|
990
|
+
});
|
|
991
|
+
const internalEdges = new Map();
|
|
992
|
+
for (const edge of collapsed) {
|
|
993
|
+
const component = componentOf.get(edge.from);
|
|
994
|
+
if (component === undefined || componentOf.get(edge.to) !== component)
|
|
995
|
+
continue;
|
|
996
|
+
const values = internalEdges.get(component) ?? [];
|
|
997
|
+
values.push(edge);
|
|
998
|
+
internalEdges.set(component, values);
|
|
999
|
+
}
|
|
1000
|
+
for (const [componentIndex, component] of flowComponents.entries()) {
|
|
1001
|
+
const internal = internalEdges.get(componentIndex) ?? [];
|
|
1005
1002
|
const cyclic = component.length > 1 || internal.some(({ from, to }) => from === to);
|
|
1006
1003
|
if (!cyclic)
|
|
1007
1004
|
continue;
|
|
@@ -1487,6 +1484,26 @@ function* eventGraphAnalysis(snapshot, options) {
|
|
|
1487
1484
|
ensureReferencedNodes(graph);
|
|
1488
1485
|
if (options.projectHelpers !== false) {
|
|
1489
1486
|
const walk = yield helperWalkInput(graph, snapshot.revision);
|
|
1487
|
+
if (walk.state.truncated === true)
|
|
1488
|
+
addIssue(graph.issues, {
|
|
1489
|
+
code: 'EVENT_HELPER_PROJECTION_LIMIT',
|
|
1490
|
+
classification: 'unresolved_analysis',
|
|
1491
|
+
severity: 'blocker',
|
|
1492
|
+
message: 'Helper expansion reached its fixed work ceiling; helper calls from the remaining roots stay structural.',
|
|
1493
|
+
confidence: 'unresolved',
|
|
1494
|
+
blockers: [
|
|
1495
|
+
{
|
|
1496
|
+
code: 'HELPER_PROJECTION_LIMIT',
|
|
1497
|
+
message: 'Select a namespace, event or file, or use helper_expansion mode, for paths beyond the ceiling.',
|
|
1498
|
+
details: {
|
|
1499
|
+
maximum: EVENT_GRAPH_WORK_LIMIT,
|
|
1500
|
+
completedRoots: walk.state.root,
|
|
1501
|
+
totalRoots: walk.input.roots.length,
|
|
1502
|
+
},
|
|
1503
|
+
},
|
|
1504
|
+
],
|
|
1505
|
+
details: { completedRoots: walk.state.root, totalRoots: walk.input.roots.length },
|
|
1506
|
+
});
|
|
1490
1507
|
for (const edge of helperProjections(graph, budget, walk))
|
|
1491
1508
|
graph.edges.push(edge);
|
|
1492
1509
|
for (const access of projectHelperStateAccesses(graph, budget, walk))
|
|
@@ -17,6 +17,10 @@ export const EVENT_GRAPH_WORK_LIMIT = 10_000_000;
|
|
|
17
17
|
export const EVENT_FRAGMENT_CACHE_MAX_ENTRIES = 10_000;
|
|
18
18
|
export const EVENT_FRAGMENT_CACHE_MAX_SOURCE_BYTES = 134_217_728;
|
|
19
19
|
export const EVENT_PROPOSED_SOURCE_MAX_FILES = 64;
|
|
20
|
+
// The full graph snapshot of a scan is retained only up to this size. Larger graphs, such as a
|
|
21
|
+
// mod with several thousand events and hundreds of thousands of state accesses, would exceed a
|
|
22
|
+
// JavaScript string and the artifact preparation ceiling.
|
|
23
|
+
export const EVENT_GRAPH_SNAPSHOT_MAX_BYTES = 134_217_728;
|
|
20
24
|
export const EVENT_PROPOSED_SOURCE_MAX_BYTES = 67_108_864;
|
|
21
25
|
/** One cooperative, deterministic work ceiling shared by extraction and graph construction. */
|
|
22
26
|
export class EventAnalysisBudget {
|
|
@@ -4,6 +4,7 @@ import { publicArtifactLink } from '../core/artifacts.js';
|
|
|
4
4
|
import { setInlineFilesScanned } from '../core/operation-result.js';
|
|
5
5
|
import { emptyServiceResult } from '../core/result.js';
|
|
6
6
|
import { eventInspectRequestSchema, splitEventInspectRequest, eventRenderRequestSchema, eventCompareRequestSchema, } from '../schemas/event.js';
|
|
7
|
+
import { EVENT_GRAPH_SNAPSHOT_MAX_BYTES } from './limits.js';
|
|
7
8
|
import { eventDiagnostics, } from './service.js';
|
|
8
9
|
export function normalizeEventInspectionRequest(input) {
|
|
9
10
|
// Validated request fields are JSON-only. Match wire semantics by removing optional
|
|
@@ -87,6 +88,7 @@ export async function inspectEvents(viewer, input) {
|
|
|
87
88
|
...(inspected.helperExpansion === undefined
|
|
88
89
|
? {}
|
|
89
90
|
: { helperExpansion: inspected.helperExpansion }),
|
|
91
|
+
...(inspected.summary === undefined ? {} : { summary: inspected.summary }),
|
|
90
92
|
boundary: {
|
|
91
93
|
direction: input.direction ?? 'both',
|
|
92
94
|
maxDepth: inspected.helperExpansion?.maxDepth ?? input.maxDepth ?? 8,
|
|
@@ -103,6 +105,14 @@ export async function inspectEvents(viewer, input) {
|
|
|
103
105
|
: 'EVENT_INSPECTED_PARTIAL';
|
|
104
106
|
setInlineFilesScanned(result, inspected.graph.filesScanned);
|
|
105
107
|
result.artifacts = inspected.artifacts.map(publicArtifactLink);
|
|
108
|
+
if (inspected.graphSnapshotOmitted === true)
|
|
109
|
+
result.diagnostics.push({
|
|
110
|
+
category: 'configuration',
|
|
111
|
+
code: 'EVENT_GRAPH_SNAPSHOT_OMITTED',
|
|
112
|
+
severity: 'info',
|
|
113
|
+
message: 'The workspace graph is too large to retain as a snapshot artifact; the scan report is linked, and comparisons use revisions held by this server',
|
|
114
|
+
details: { maximumBytes: EVENT_GRAPH_SNAPSHOT_MAX_BYTES },
|
|
115
|
+
});
|
|
106
116
|
result.validation = eventGraphValidation(inspected.graph);
|
|
107
117
|
if (inspected.helperExpansion !== undefined)
|
|
108
118
|
result.validation = helperExpansionValidation(inspected.helperExpansion);
|
|
@@ -122,6 +132,7 @@ export async function renderEvents(viewer, input) {
|
|
|
122
132
|
selectedNodes: rendered.render.selectedNodeIds.length,
|
|
123
133
|
omittedNodes: rendered.render.omittedNodeCount,
|
|
124
134
|
branchRenders: rendered.branches.length,
|
|
135
|
+
omittedBranchRenders: rendered.budgetOmittedBranches,
|
|
125
136
|
},
|
|
126
137
|
boundary: {
|
|
127
138
|
direction: input.direction ?? 'both',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { compareCodeUnits } from '../core/canonical.js';
|
|
2
2
|
import { eventFlowEdges, eventStronglyConnectedComponents, reachableEventNodeIds, shortestEventPath, traceEventGraph, } from './algorithms.js';
|
|
3
|
+
import { compatibleProducerKeys, hasUnknownSavedScopeStorage, stateKey } from './state-keys.js';
|
|
3
4
|
function reachableFromRootsWithout(graph, rootIds, removedNodeIds, signal) {
|
|
4
5
|
signal?.throwIfAborted();
|
|
5
6
|
const retainedNodeIds = new Set(graph.nodes.filter(({ id }) => !removedNodeIds.has(id)).map(({ id }) => id));
|
|
@@ -348,32 +349,6 @@ export function explainEventPath(graph, from, to, boundary, signal) {
|
|
|
348
349
|
boundary,
|
|
349
350
|
};
|
|
350
351
|
}
|
|
351
|
-
function stateKey(access) {
|
|
352
|
-
const scope = access.kind === 'variable' || access.kind === 'array' ? `:${access.scope}` : '';
|
|
353
|
-
const storage = access.kind === 'saved_scope'
|
|
354
|
-
? `:${access.metadata.storage === 'global' || access.metadata.storage === 'local'
|
|
355
|
-
? access.metadata.storage
|
|
356
|
-
: 'unknown'}`
|
|
357
|
-
: '';
|
|
358
|
-
return `${access.kind}:${access.name}${scope}${storage}`;
|
|
359
|
-
}
|
|
360
|
-
function compatibleProducerKeys(access) {
|
|
361
|
-
if (access.kind !== 'saved_scope' ||
|
|
362
|
-
access.metadata.storage === 'global' ||
|
|
363
|
-
access.metadata.storage === 'local') {
|
|
364
|
-
return [stateKey(access)];
|
|
365
|
-
}
|
|
366
|
-
return [
|
|
367
|
-
`saved_scope:${access.name}:global`,
|
|
368
|
-
`saved_scope:${access.name}:local`,
|
|
369
|
-
`saved_scope:${access.name}:unknown`,
|
|
370
|
-
];
|
|
371
|
-
}
|
|
372
|
-
function hasUnknownSavedScopeStorage(access) {
|
|
373
|
-
return (access.kind === 'saved_scope' &&
|
|
374
|
-
access.metadata.storage !== 'global' &&
|
|
375
|
-
access.metadata.storage !== 'local');
|
|
376
|
-
}
|
|
377
352
|
function compatibleProducers(producers, consumer) {
|
|
378
353
|
const compatible = new Map();
|
|
379
354
|
for (const key of compatibleProducerKeys(consumer)) {
|
|
@@ -544,20 +519,24 @@ export function lintEventGraph(graph, selector, signal) {
|
|
|
544
519
|
if (selector === undefined)
|
|
545
520
|
return [...graph.issues];
|
|
546
521
|
const selected = selectedOwnershipIds(graph, selector, signal);
|
|
547
|
-
|
|
522
|
+
// Source ranges of the selected nodes, grouped by file so each issue checks only its own file.
|
|
523
|
+
const selectedRanges = new Map();
|
|
524
|
+
for (const [index, { id, location }] of graph.nodes.entries()) {
|
|
548
525
|
if ((index & 255) === 0)
|
|
549
526
|
signal?.throwIfAborted();
|
|
550
|
-
|
|
551
|
-
|
|
527
|
+
if (location === undefined || !selected.has(id))
|
|
528
|
+
continue;
|
|
529
|
+
const ranges = selectedRanges.get(location.path) ?? [];
|
|
530
|
+
ranges.push({ start: location.start.offset, end: location.end.offset });
|
|
531
|
+
selectedRanges.set(location.path, ranges);
|
|
532
|
+
}
|
|
552
533
|
return graph.issues.filter((issue, index) => {
|
|
553
534
|
if ((index & 255) === 0)
|
|
554
535
|
signal?.throwIfAborted();
|
|
536
|
+
const location = issue.location;
|
|
555
537
|
return (issueSubjectIds(issue).some((id) => selected.has(id)) ||
|
|
556
|
-
(
|
|
557
|
-
|
|
558
|
-
location.path === issue.location?.path &&
|
|
559
|
-
issue.location.start.offset >= location.start.offset &&
|
|
560
|
-
issue.location.end.offset <= location.end.offset)));
|
|
538
|
+
(location !== undefined &&
|
|
539
|
+
(selectedRanges.get(location.path) ?? []).some(({ start, end }) => location.start.offset >= start && location.end.offset <= end)));
|
|
561
540
|
});
|
|
562
541
|
}
|
|
563
542
|
export function issueSubjectIds(issue) {
|
|
@@ -103,12 +103,14 @@ function boundedSelection(graph, options) {
|
|
|
103
103
|
else if (options.view === 'terminals') {
|
|
104
104
|
const terminals = withinSelector(graph.nodes.filter(({ kind }) => kind === 'terminal').map(({ id }) => id));
|
|
105
105
|
add(terminals);
|
|
106
|
-
|
|
106
|
+
const terminalIds = new Set(terminals);
|
|
107
|
+
add(graph.edges.filter(({ to }) => terminalIds.has(to)).flatMap(({ from, to }) => [from, to]));
|
|
107
108
|
}
|
|
108
109
|
else if (options.view === 'unresolved') {
|
|
109
110
|
const unresolved = withinSelector(graph.nodes.filter(({ kind }) => kind === 'unresolved').map(({ id }) => id));
|
|
110
111
|
add(unresolved);
|
|
111
|
-
|
|
112
|
+
const unresolvedIds = new Set(unresolved);
|
|
113
|
+
add(graph.edges.filter(({ to }) => unresolvedIds.has(to)).flatMap(({ from, to }) => [from, to]));
|
|
112
114
|
}
|
|
113
115
|
else if (options.view === 'neighborhood' || options.view === 'reachability') {
|
|
114
116
|
add(selectorTrace);
|
|
@@ -2,7 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { inspectHelperExpansion } from '../core/helper-expansion.js';
|
|
3
3
|
import { eventHelperInventory } from './helper-expansion.js';
|
|
4
4
|
import { clearSemanticDependencies } from '../core/semantic-dependencies.js';
|
|
5
|
-
import { canonicalJson, compareCodeUnits, hashCanonical, sha256Bytes } from '../core/canonical.js';
|
|
5
|
+
import { canonicalJson, canonicalJsonByteLength, compareCodeUnits, hashCanonical, sha256Bytes, } from '../core/canonical.js';
|
|
6
6
|
import { boundedSourceHashEvidence, publicArtifactLink, } from '../core/artifacts.js';
|
|
7
7
|
import { ServiceError } from '../core/result.js';
|
|
8
8
|
import { RenderBudget } from '../core/render-budget.js';
|
|
@@ -10,7 +10,8 @@ import { SOURCE_MAX_BYTES } from '../core/source/index.js';
|
|
|
10
10
|
import { isPortablePathSegment } from '../core/workspace.js';
|
|
11
11
|
import { PACKAGE_VERSION } from '../version.js';
|
|
12
12
|
import { buildEventGraph, buildEventGraphAsync } from './graph.js';
|
|
13
|
-
import {
|
|
13
|
+
import { summarizeEventImpact, summarizeEventIssues, summarizeEventPath, summarizeEventRoots, summarizeEventTrace, } from './summary.js';
|
|
14
|
+
import { EVENT_FRAGMENT_CACHE_MAX_ENTRIES, EVENT_FRAGMENT_CACHE_MAX_SOURCE_BYTES, EVENT_GRAPH_SNAPSHOT_MAX_BYTES, EVENT_PROPOSED_SOURCE_MAX_BYTES, EVENT_PROPOSED_SOURCE_MAX_FILES, } from './limits.js';
|
|
14
15
|
import { analyzeEventImpact, discoverEventRoots, explainEventPath, inspectEventStateFlow, lintEventGraph, traceSelectedEvents, } from './queries.js';
|
|
15
16
|
import { compareEventGraphs, eventGraphHash } from './compare.js';
|
|
16
17
|
import { selectEventComparison } from './comparison-selection.js';
|
|
@@ -210,9 +211,23 @@ async function scanEventSources(engine, workspaceId, workspace, focused = false,
|
|
|
210
211
|
complete: index.complete,
|
|
211
212
|
skippedSourceCount: index.skippedSourceCount,
|
|
212
213
|
skippedSources: index.skippedSources,
|
|
213
|
-
diagnostics: index
|
|
214
|
+
diagnostics: diagnosticsWithinScannedKinds(index),
|
|
214
215
|
};
|
|
215
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Event analysis indexes only event-relevant folders, so it holds no sprite, interface or
|
|
219
|
+
* scripted-trigger definitions. A reference to a kind with no definition in this file set says
|
|
220
|
+
* nothing about the workspace and is left to the full index, which scans those folders.
|
|
221
|
+
*/
|
|
222
|
+
function diagnosticsWithinScannedKinds(index) {
|
|
223
|
+
const indexedKinds = new Set(index.symbols.map(({ kind }) => kind));
|
|
224
|
+
return index.diagnostics.filter(({ code, details }) => {
|
|
225
|
+
if (code !== 'INDEX_UNRESOLVED_REFERENCE' && code !== 'INDEX_UNRESOLVED_REFERENCE_PARTIAL')
|
|
226
|
+
return true;
|
|
227
|
+
const toKind = details?.toKind;
|
|
228
|
+
return typeof toKind !== 'string' || indexedKinds.has(toKind);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
216
231
|
function rememberGraph(state, workspaceId, graph) {
|
|
217
232
|
const history = state.history.get(workspaceId) ?? new Map();
|
|
218
233
|
history.delete(graph.revision);
|
|
@@ -808,17 +823,21 @@ export class EventChainViewer {
|
|
|
808
823
|
refresh: input.refresh ?? input.mode === 'scan',
|
|
809
824
|
};
|
|
810
825
|
let report;
|
|
826
|
+
let summary;
|
|
811
827
|
switch (input.mode) {
|
|
812
828
|
case 'scan':
|
|
813
829
|
report = eventScanReport(graph);
|
|
830
|
+
summary = summarizeEventIssues(graph.issues);
|
|
814
831
|
break;
|
|
815
832
|
case 'roots':
|
|
816
833
|
{
|
|
817
834
|
const unresolved = boundedUnresolved(graph);
|
|
835
|
+
const roots = discoverEventRoots(graph, input.signal);
|
|
836
|
+
summary = summarizeEventRoots(roots);
|
|
818
837
|
report = {
|
|
819
838
|
graphRevision: graph.revision,
|
|
820
839
|
sourceHashes: graph.sourceHashes,
|
|
821
|
-
roots
|
|
840
|
+
roots,
|
|
822
841
|
unresolved: unresolved.values,
|
|
823
842
|
...(unresolved.inventory === undefined
|
|
824
843
|
? {}
|
|
@@ -838,6 +857,10 @@ export class EventChainViewer {
|
|
|
838
857
|
expandHelpers: input.expandHelpers ?? false,
|
|
839
858
|
}, input.signal);
|
|
840
859
|
const unresolved = boundedUnresolved(graph, new Set(trace.nodes.map(({ id }) => id)));
|
|
860
|
+
summary = {
|
|
861
|
+
trace: summarizeEventTrace(trace),
|
|
862
|
+
...summarizeEventIssues(lintEventGraph(graph, input.selector, input.signal)),
|
|
863
|
+
};
|
|
841
864
|
report = {
|
|
842
865
|
graphRevision: graph.revision,
|
|
843
866
|
sourceHashes: graph.sourceHashes,
|
|
@@ -852,16 +875,22 @@ export class EventChainViewer {
|
|
|
852
875
|
case 'explain_path':
|
|
853
876
|
if (input.from === undefined || input.to === undefined)
|
|
854
877
|
throw new ServiceError('EVENT_PATH_SELECTORS_REQUIRED', 'Path explanation requires from and to selectors');
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
878
|
+
{
|
|
879
|
+
const explanation = explainEventPath(graph, input.from, input.to, { maxDepth, maxNodes, expandHelpers: input.expandHelpers ?? true }, input.signal);
|
|
880
|
+
summary = summarizeEventPath(explanation);
|
|
881
|
+
report = {
|
|
882
|
+
graphRevision: graph.revision,
|
|
883
|
+
sourceHashes: graph.sourceHashes,
|
|
884
|
+
explanation,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
860
887
|
break;
|
|
861
888
|
case 'state_flow':
|
|
862
889
|
{
|
|
863
890
|
const unresolved = boundedUnresolved(graph);
|
|
864
|
-
const
|
|
891
|
+
const stateIssues = lintEventGraph(graph, input.selector, input.signal).filter(({ code }) => /(?:STATE|FLAG|VARIABLE|ARRAY|TARGET|SCOPE|WRITER)/u.test(code));
|
|
892
|
+
summary = summarizeEventIssues(stateIssues);
|
|
893
|
+
const issues = boundedEvidence(stateIssues);
|
|
865
894
|
report = {
|
|
866
895
|
graphRevision: graph.revision,
|
|
867
896
|
sourceHashes: graph.sourceHashes,
|
|
@@ -877,7 +906,9 @@ export class EventChainViewer {
|
|
|
877
906
|
break;
|
|
878
907
|
case 'lint':
|
|
879
908
|
{
|
|
880
|
-
const
|
|
909
|
+
const selectedIssues = lintEventGraph(graph, input.selector, input.signal);
|
|
910
|
+
summary = summarizeEventIssues(selectedIssues);
|
|
911
|
+
const issues = boundedEvidence(selectedIssues);
|
|
881
912
|
const unresolved = boundedUnresolved(graph);
|
|
882
913
|
report = {
|
|
883
914
|
graphRevision: graph.revision,
|
|
@@ -897,10 +928,12 @@ export class EventChainViewer {
|
|
|
897
928
|
throw new ServiceError('EVENT_IMPACT_SUBJECT_REQUIRED', 'Impact analysis requires a subject');
|
|
898
929
|
{
|
|
899
930
|
const unresolved = boundedUnresolved(graph);
|
|
931
|
+
const impact = analyzeEventImpact(graph, input.impactSubject, input.signal);
|
|
932
|
+
summary = summarizeEventImpact(impact);
|
|
900
933
|
report = {
|
|
901
934
|
graphRevision: graph.revision,
|
|
902
935
|
sourceHashes: graph.sourceHashes,
|
|
903
|
-
impact
|
|
936
|
+
impact,
|
|
904
937
|
unresolved: unresolved.values,
|
|
905
938
|
...(unresolved.inventory === undefined
|
|
906
939
|
? {}
|
|
@@ -912,6 +945,13 @@ export class EventChainViewer {
|
|
|
912
945
|
const graphHash = cachedEventGraphHash(graph, input.signal);
|
|
913
946
|
const name = `${safeSlug(`event-${input.mode}`)}-${graph.revision.slice(0, 12)}.json`;
|
|
914
947
|
const graphSnapshotName = `event-graph-${graph.revision}.json`;
|
|
948
|
+
// A large workspace graph cannot be held as one JSON string or artifact. Measure it first and
|
|
949
|
+
// retain it only when it fits; comparisons then use the in-memory revision history.
|
|
950
|
+
const graphSnapshot = input.mode === 'scan' ? { schemaVersion: 'event-graph-snapshot.v1', graph } : undefined;
|
|
951
|
+
const graphSnapshotBytes = graphSnapshot === undefined
|
|
952
|
+
? undefined
|
|
953
|
+
: canonicalJsonByteLength(graphSnapshot, EVENT_GRAPH_SNAPSHOT_MAX_BYTES);
|
|
954
|
+
const snapshotOmitted = graphSnapshot !== undefined && graphSnapshotBytes === undefined;
|
|
915
955
|
const projectedScan = input.mode === 'scan' &&
|
|
916
956
|
typeof report === 'object' &&
|
|
917
957
|
report !== null &&
|
|
@@ -949,10 +989,13 @@ export class EventChainViewer {
|
|
|
949
989
|
: {}),
|
|
950
990
|
resources: [
|
|
951
991
|
{ name, mimeType: 'application/json' },
|
|
952
|
-
...(input.mode === 'scan'
|
|
992
|
+
...(input.mode === 'scan' && !snapshotOmitted
|
|
953
993
|
? [{ name: graphSnapshotName, mimeType: 'application/json' }]
|
|
954
994
|
: []),
|
|
955
995
|
],
|
|
996
|
+
...(snapshotOmitted
|
|
997
|
+
? { graphSnapshotOmitted: { maximumBytes: EVENT_GRAPH_SNAPSHOT_MAX_BYTES } }
|
|
998
|
+
: {}),
|
|
956
999
|
report,
|
|
957
1000
|
})}\n`;
|
|
958
1001
|
const workspace = this.engine.resolver.get(input.workspaceId, input.principal);
|
|
@@ -960,8 +1003,8 @@ export class EventChainViewer {
|
|
|
960
1003
|
mode: input.mode,
|
|
961
1004
|
...(input.selector === undefined ? {} : { selector: input.selector }),
|
|
962
1005
|
}, input.signal), `Authoritative Event Chain Viewer ${input.mode.replaceAll('_', ' ')} report`, input.signal);
|
|
963
|
-
const graphArtifact =
|
|
964
|
-
? await this.engine.artifacts.putChunked(workspace, graphSnapshotName, 'application/json', `${canonicalJson(
|
|
1006
|
+
const graphArtifact = graphSnapshot !== undefined && !snapshotOmitted
|
|
1007
|
+
? await this.engine.artifacts.putChunked(workspace, graphSnapshotName, 'application/json', `${canonicalJson(graphSnapshot)}\n`, eventProvenance(graph, 'event-graph-snapshot', { revision: graph.revision, graphHash }, input.signal), 'Revision-addressed Event Chain Viewer graph snapshot', input.signal)
|
|
965
1008
|
: undefined;
|
|
966
1009
|
return {
|
|
967
1010
|
graph,
|
|
@@ -970,6 +1013,8 @@ export class EventChainViewer {
|
|
|
970
1013
|
report,
|
|
971
1014
|
reportJson,
|
|
972
1015
|
artifacts: [artifact, ...(graphArtifact === undefined ? [] : [graphArtifact])],
|
|
1016
|
+
...(snapshotOmitted ? { graphSnapshotOmitted: true } : {}),
|
|
1017
|
+
summary,
|
|
973
1018
|
};
|
|
974
1019
|
}
|
|
975
1020
|
async renderAndStore(input) {
|
|
@@ -1001,25 +1046,38 @@ export class EventChainViewer {
|
|
|
1001
1046
|
graph.nodes.length > render.selectedNodeIds.length
|
|
1002
1047
|
? coherentBranchPartitions(graph, branchNodeLimit, 64, input.signal)
|
|
1003
1048
|
: { groups: [], omittedNodeIds: [] };
|
|
1004
|
-
|
|
1005
|
-
|
|
1049
|
+
// Branch images share the request's raster budget with the overview. When it is spent, the
|
|
1050
|
+
// remaining branches are listed as omitted instead of failing the whole render.
|
|
1051
|
+
let budgetOmittedBranches = 0;
|
|
1052
|
+
for (const [branchIndex, nodeIds] of partitions.groups.entries()) {
|
|
1006
1053
|
input.signal?.throwIfAborted();
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1054
|
+
let branch;
|
|
1055
|
+
try {
|
|
1056
|
+
branch = await renderEventGraph(graph, {
|
|
1057
|
+
view: 'neighborhood',
|
|
1058
|
+
selector: {
|
|
1059
|
+
kind: 'manifest',
|
|
1060
|
+
manifest: { id: `overview-branch-${branchIndex + 1}`, nodeIds },
|
|
1061
|
+
},
|
|
1062
|
+
direction: 'both',
|
|
1063
|
+
maxDepth: 0,
|
|
1064
|
+
maxNodes: branchNodeLimit,
|
|
1065
|
+
expandHelpers: input.expandHelpers ?? false,
|
|
1066
|
+
includeHtml: false,
|
|
1067
|
+
compactLayout: true,
|
|
1068
|
+
budget,
|
|
1069
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
catch (error) {
|
|
1073
|
+
if (!(error instanceof ServiceError) || error.code !== 'RENDER_AGGREGATE_BLOCKED')
|
|
1074
|
+
throw error;
|
|
1075
|
+
budgetOmittedBranches = partitions.groups.length - branchIndex;
|
|
1076
|
+
break;
|
|
1077
|
+
}
|
|
1078
|
+
branches.push(branch);
|
|
1022
1079
|
}
|
|
1080
|
+
const branchGroups = partitions.groups.slice(0, branches.length);
|
|
1023
1081
|
const prefix = safeSlug(`event-${input.view}-${graph.revision.slice(0, 12)}`);
|
|
1024
1082
|
const manifestName = `${prefix}-manifest.json`;
|
|
1025
1083
|
const resourceNames = {
|
|
@@ -1165,7 +1223,8 @@ export class EventChainViewer {
|
|
|
1165
1223
|
omittedNodeCount: partitions.omittedNodeIds.length,
|
|
1166
1224
|
omittedNodeIds: partitions.omittedNodeIds,
|
|
1167
1225
|
omittedNodeHash: hashCanonical(partitions.omittedNodeIds),
|
|
1168
|
-
|
|
1226
|
+
budgetOmittedBranches,
|
|
1227
|
+
truncated: partitions.omittedNodeIds.length > 0 || budgetOmittedBranches > 0,
|
|
1169
1228
|
},
|
|
1170
1229
|
})}\n`;
|
|
1171
1230
|
const manifestArtifact = await this.engine.artifacts.putChunked(workspace, manifestName, 'application/json', manifestJson, provenance, `Event-chain ${input.view} render and bounded branch manifest`, input.signal);
|
|
@@ -1175,6 +1234,7 @@ export class EventChainViewer {
|
|
|
1175
1234
|
graphHash: cachedEventGraphHash(graph, input.signal),
|
|
1176
1235
|
render,
|
|
1177
1236
|
branches,
|
|
1237
|
+
budgetOmittedBranches,
|
|
1178
1238
|
manifestJson,
|
|
1179
1239
|
artifacts,
|
|
1180
1240
|
};
|