hoi4-agent-tools 3.3.0 → 3.4.1
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 +22 -0
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +20 -8
- package/dist/hoi4_agent_tools/core/helper-expansion.js +3 -0
- package/dist/hoi4_agent_tools/core/index.js +1 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +19 -1
- package/dist/hoi4_agent_tools/core/operation-result.js +4 -1
- package/dist/hoi4_agent_tools/event/comparison-selection.js +78 -0
- package/dist/hoi4_agent_tools/event/helper-expansion.js +48 -0
- package/dist/hoi4_agent_tools/event/operations.js +32 -10
- package/dist/hoi4_agent_tools/event/service.js +21 -2
- package/dist/hoi4_agent_tools/focus/compact.js +241 -27
- package/dist/hoi4_agent_tools/focus/layout.js +119 -8
- package/dist/hoi4_agent_tools/focus/render.js +62 -0
- package/dist/hoi4_agent_tools/focus/scenario.js +99 -0
- package/dist/hoi4_agent_tools/focus/tool-operations.js +30 -4
- package/dist/hoi4_agent_tools/gui/assets.js +54 -4
- package/dist/hoi4_agent_tools/gui/layout.js +935 -104
- package/dist/hoi4_agent_tools/gui/renderer.js +47 -17
- package/dist/hoi4_agent_tools/gui/shader.js +515 -0
- package/dist/hoi4_agent_tools/gui/source-graph.js +166 -6
- package/dist/hoi4_agent_tools/gui/studio.js +245 -19
- package/dist/hoi4_agent_tools/gui/tool-operations.js +9 -0
- package/dist/hoi4_agent_tools/gui/validators.js +41 -12
- package/dist/hoi4_agent_tools/map/operations.js +70 -1
- package/dist/hoi4_agent_tools/map/render.js +70 -1
- package/dist/hoi4_agent_tools/map/service.js +30 -6
- package/dist/hoi4_agent_tools/map/tool-operations.js +67 -1
- package/dist/hoi4_agent_tools/mcp/tools/event.js +10 -1
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +14 -4
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +5 -1
- package/dist/hoi4_agent_tools/mcp/tools/map.js +12 -2
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +5 -0
- package/dist/hoi4_agent_tools/mechanic/interpreter.js +91 -30
- package/dist/hoi4_agent_tools/mechanic/source.js +19 -4
- package/dist/hoi4_agent_tools/probability/evaluation.js +3 -0
- package/dist/hoi4_agent_tools/probability/operations.js +8 -0
- package/dist/hoi4_agent_tools/probability/service.js +33 -12
- package/dist/hoi4_agent_tools/probability/source-analysis.js +1 -0
- package/dist/hoi4_agent_tools/schemas/event.js +8 -0
- package/dist/hoi4_agent_tools/schemas/focus-requests.js +40 -1
- package/dist/hoi4_agent_tools/schemas/gui-requests.js +8 -0
- package/dist/hoi4_agent_tools/schemas/map-requests.js +22 -1
- package/dist/hoi4_agent_tools/schemas/technology.js +7 -0
- package/dist/hoi4_agent_tools/technology/graph.js +25 -10
- package/dist/hoi4_agent_tools/technology/operations.js +29 -1
- package/dist/hoi4_agent_tools/technology/render.js +287 -31
- package/dist/hoi4_agent_tools/technology/service.js +56 -8
- package/dist/hoi4_agent_tools/technology/source-analysis.js +166 -3
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/events.md +4 -2
- package/docs/focus.md +10 -0
- package/docs/gui.md +50 -3
- package/docs/jobs.md +1 -0
- package/docs/map.md +8 -0
- package/docs/mechanics.md +2 -1
- package/docs/probability.md +3 -1
- package/docs/technology.md +11 -2
- package/package.json +1 -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/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 +1 -1
- package/schemas/mechanic-test-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/scenario-suite.schema.json +1 -1
- package/schemas/scenario-test-input.schema.json +1 -1
- package/schemas/shared-scenario.schema.json +1 -1
- package/server.json +2 -2
|
@@ -357,7 +357,50 @@ export async function renderMap(index, options = {}) {
|
|
|
357
357
|
throw new ServiceError('MAP_RENDER_SCALE_INVALID', 'Map render scale must be an integer from 1 through 16');
|
|
358
358
|
const budget = options.budget ?? new RenderBudget();
|
|
359
359
|
assertRenderDimensions(raster.width, raster.height, 'map RGBA source plane');
|
|
360
|
-
|
|
360
|
+
let tile = options.tile;
|
|
361
|
+
if (options.area !== undefined) {
|
|
362
|
+
if (tile !== undefined)
|
|
363
|
+
throw new ServiceError('MAP_RENDER_AREA_CONFLICT', 'Choose a map tile or entity area');
|
|
364
|
+
const ids = new Set(options.area.provinceIds ?? []);
|
|
365
|
+
for (const stateId of options.area.stateIds ?? []) {
|
|
366
|
+
const state = index.statesById.get(stateId);
|
|
367
|
+
if (state === undefined)
|
|
368
|
+
throw new ServiceError('MAP_AREA_STATE_MISSING', 'Map area state ID is missing', {
|
|
369
|
+
stateId,
|
|
370
|
+
});
|
|
371
|
+
for (const id of state.provinces)
|
|
372
|
+
ids.add(id);
|
|
373
|
+
}
|
|
374
|
+
for (const regionId of options.area.regionIds ?? []) {
|
|
375
|
+
const region = index.regionsById.get(regionId);
|
|
376
|
+
if (region === undefined)
|
|
377
|
+
throw new ServiceError('MAP_AREA_REGION_MISSING', 'Map area region ID is missing', {
|
|
378
|
+
regionId,
|
|
379
|
+
});
|
|
380
|
+
for (const id of region.provinces)
|
|
381
|
+
ids.add(id);
|
|
382
|
+
}
|
|
383
|
+
if (ids.size === 0)
|
|
384
|
+
throw new ServiceError('MAP_AREA_EMPTY', 'Map area has no provinces');
|
|
385
|
+
const geometry = [...ids].map((id) => {
|
|
386
|
+
const province = raster.geometry.get(id);
|
|
387
|
+
if (province === undefined)
|
|
388
|
+
throw new ServiceError('MAP_AREA_PROVINCE_MISSING', 'Map area province ID has no raster geometry', { provinceId: id });
|
|
389
|
+
return province;
|
|
390
|
+
});
|
|
391
|
+
const padding = options.area.padding ?? 32;
|
|
392
|
+
const x = Math.max(0, Math.min(...geometry.map(({ minX }) => minX)) - padding);
|
|
393
|
+
const y = Math.max(0, Math.min(...geometry.map(({ minY }) => minY)) - padding);
|
|
394
|
+
const right = Math.min(raster.width, Math.max(...geometry.map(({ maxX }) => maxX)) + padding + 1);
|
|
395
|
+
const bottom = Math.min(raster.height, Math.max(...geometry.map(({ maxY }) => maxY)) + padding + 1);
|
|
396
|
+
tile = { x, y, width: right - x, height: bottom - y };
|
|
397
|
+
if (tile.width > 2_048 || tile.height > 2_048)
|
|
398
|
+
throw new ServiceError('MAP_AREA_TOO_LARGE', 'Map area exceeds the tile limit; select fewer entities', { tile });
|
|
399
|
+
}
|
|
400
|
+
if (tile !== undefined &&
|
|
401
|
+
(tile.x + tile.width > raster.width || tile.y + tile.height > raster.height))
|
|
402
|
+
throw new ServiceError('MAP_TILE_OUT_OF_BOUNDS', 'Map tile extends beyond the province raster', { tile, rasterWidth: raster.width, rasterHeight: raster.height });
|
|
403
|
+
const outputDimensions = budget.reserve((tile?.width ?? raster.width) * resolved.scale, (tile?.height ?? raster.height) * resolved.scale, 'map PNG output');
|
|
361
404
|
const raw = Buffer.alloc(raster.width * raster.height * 4);
|
|
362
405
|
for (let offset = 0; offset < raster.provinceIds.length; offset += 1) {
|
|
363
406
|
if (offset % Math.max(raster.width * 32, 1) === 0)
|
|
@@ -371,6 +414,32 @@ export async function renderMap(index, options = {}) {
|
|
|
371
414
|
}
|
|
372
415
|
await applyOverlays(index, raw, new Set(resolved.overlays), options.signal);
|
|
373
416
|
options.signal?.throwIfAborted();
|
|
417
|
+
if (tile !== undefined) {
|
|
418
|
+
const tileRaw = Buffer.alloc(tile.width * tile.height * 4);
|
|
419
|
+
const provinceIds = new Set();
|
|
420
|
+
for (let row = 0; row < tile.height; row += 1) {
|
|
421
|
+
if (row % 32 === 0)
|
|
422
|
+
await renderCheckpoint(options.signal);
|
|
423
|
+
raw.copy(tileRaw, row * tile.width * 4, ((tile.y + row) * raster.width + tile.x) * 4, ((tile.y + row) * raster.width + tile.x + tile.width) * 4);
|
|
424
|
+
for (let column = 0; column < tile.width; column += 1) {
|
|
425
|
+
const id = raster.provinceIds[(tile.y + row) * raster.width + tile.x + column];
|
|
426
|
+
if (id !== undefined && id >= 0)
|
|
427
|
+
provinceIds.add(id);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const png = await encodePng(tileRaw, tile.width, tile.height, resolved.scale);
|
|
431
|
+
const json = `${canonicalJson({ schemaVersion: 'map-tile.v1', layer: resolved.layer, overlays: resolved.overlays, scale: resolved.scale, tile, raster: { width: raster.width, height: raster.height }, provinceIds: [...provinceIds].sort((left, right) => left - right) })}\n`;
|
|
432
|
+
const html = diffHtmlDocument(`HOI4 map tile - ${resolved.layer}`, png, json);
|
|
433
|
+
return {
|
|
434
|
+
width: outputDimensions.width,
|
|
435
|
+
height: outputDimensions.height,
|
|
436
|
+
tile,
|
|
437
|
+
png,
|
|
438
|
+
json,
|
|
439
|
+
html,
|
|
440
|
+
hashes: { png: sha256Bytes(png), json: sha256Bytes(json), html: sha256Bytes(html) },
|
|
441
|
+
};
|
|
442
|
+
}
|
|
374
443
|
const png = await encodePng(raw, raster.width, raster.height, resolved.scale);
|
|
375
444
|
options.signal?.throwIfAborted();
|
|
376
445
|
const json = `${canonicalJson(await renderMetadata(index, resolved, options.signal))}\n`;
|
|
@@ -481,7 +481,7 @@ export class AgentNudger {
|
|
|
481
481
|
this.scanner = this.engine.scanner;
|
|
482
482
|
this.transactions = this.engine.transactions;
|
|
483
483
|
}
|
|
484
|
-
async scan(workspaceId, principal, signal) {
|
|
484
|
+
async scan(workspaceId, principal, signal, includeExternalScriptReferences = false) {
|
|
485
485
|
signal?.throwIfAborted();
|
|
486
486
|
const workspace = this.resolver.get(workspaceId, principal);
|
|
487
487
|
const sourceRoots = {
|
|
@@ -493,8 +493,27 @@ export class AgentNudger {
|
|
|
493
493
|
const bitmapPatterns = provinceBitmapPatterns(selectorSnapshot.files, sourceRoots.map);
|
|
494
494
|
const contentSnapshot = await this.engine.scan(workspaceId, { patterns: [...sourceTextPatterns(sourceRoots), ...bitmapPatterns] }, principal, signal);
|
|
495
495
|
const locatorSnapshot = await this.engine.scan(workspaceId, { patterns: ['**/*.asset'], rootKinds: ['mod', 'fixture'] }, principal, signal);
|
|
496
|
+
const externalScriptSnapshot = includeExternalScriptReferences
|
|
497
|
+
? await this.engine.scan(workspaceId, {
|
|
498
|
+
patterns: [
|
|
499
|
+
'events/**/*.txt',
|
|
500
|
+
'common/national_focus/**/*.txt',
|
|
501
|
+
'common/decisions/**/*.txt',
|
|
502
|
+
'common/scripted_effects/**/*.txt',
|
|
503
|
+
'common/scripted_triggers/**/*.txt',
|
|
504
|
+
'common/on_actions/**/*.txt',
|
|
505
|
+
'common/ideas/**/*.txt',
|
|
506
|
+
'common/characters/**/*.txt',
|
|
507
|
+
'history/countries/**/*.txt',
|
|
508
|
+
],
|
|
509
|
+
}, principal, signal)
|
|
510
|
+
: undefined;
|
|
496
511
|
signal?.throwIfAborted();
|
|
497
|
-
const files = [
|
|
512
|
+
const files = [
|
|
513
|
+
...contentSnapshot.files,
|
|
514
|
+
...locatorSnapshot.files,
|
|
515
|
+
...(externalScriptSnapshot?.files ?? []),
|
|
516
|
+
].sort((left, right) => left.loadOrder - right.loadOrder || compareCodeUnits(left.displayPath, right.displayPath));
|
|
498
517
|
const revision = hashCanonical(files.map(({ displayPath, loadOrder, sha256 }) => ({ displayPath, loadOrder, sha256 })));
|
|
499
518
|
return {
|
|
500
519
|
workspaceId,
|
|
@@ -512,7 +531,7 @@ export class AgentNudger {
|
|
|
512
531
|
}
|
|
513
532
|
async prepareOperations(workspaceId, operations, principal, signal) {
|
|
514
533
|
signal?.throwIfAborted();
|
|
515
|
-
const snapshot = await this.scan(workspaceId, principal, signal);
|
|
534
|
+
const snapshot = await this.scan(workspaceId, principal, signal, operations.some(({ kind }) => kind === 'renumber_map_entity'));
|
|
516
535
|
const plan = await planMapOperationsAsync(snapshot.index, operations, signal);
|
|
517
536
|
const baselineValidation = await validateMapAsync(snapshot.index, signal === undefined ? {} : { signal });
|
|
518
537
|
const rawValidation = await validateMapAsync(plan.finalIndex, {
|
|
@@ -614,6 +633,9 @@ export class AgentNudger {
|
|
|
614
633
|
const bundle = mapBundleWithSourceHashes(rawBundle, completeSourceHashes);
|
|
615
634
|
const workspace = this.resolver.get(workspaceId, options.principal);
|
|
616
635
|
const layer = options.layer ?? 'province';
|
|
636
|
+
const tileSuffix = options.tile === undefined && options.area === undefined
|
|
637
|
+
? ''
|
|
638
|
+
: `-tile-${hashCanonical({ tile: bundle.tile, area: options.area, overlays: [...new Set(options.overlays ?? [])].sort(), scale: options.scale ?? 1 }).slice(0, 12)}`;
|
|
617
639
|
const provenance = {
|
|
618
640
|
kind: 'map-render',
|
|
619
641
|
toolVersion: PACKAGE_VERSION,
|
|
@@ -624,24 +646,26 @@ export class AgentNudger {
|
|
|
624
646
|
layer,
|
|
625
647
|
overlays: [...new Set(options.overlays ?? [])].sort(),
|
|
626
648
|
scale: options.scale ?? 1,
|
|
649
|
+
...(bundle.tile === undefined ? {} : { tile: bundle.tile }),
|
|
650
|
+
...(options.area === undefined ? {} : { area: options.area }),
|
|
627
651
|
},
|
|
628
652
|
metadata: { sourceHashInventory: sourceEvidence.inventory },
|
|
629
653
|
};
|
|
630
654
|
const writes = [
|
|
631
655
|
{
|
|
632
|
-
name: `map-${layer}.png`,
|
|
656
|
+
name: `map-${layer}${tileSuffix}.png`,
|
|
633
657
|
mimeType: 'image/png',
|
|
634
658
|
content: bundle.png,
|
|
635
659
|
provenance,
|
|
636
660
|
},
|
|
637
661
|
{
|
|
638
|
-
name: `map-${layer}.json`,
|
|
662
|
+
name: `map-${layer}${tileSuffix}.json`,
|
|
639
663
|
mimeType: 'application/json',
|
|
640
664
|
content: bundle.json,
|
|
641
665
|
provenance,
|
|
642
666
|
},
|
|
643
667
|
{
|
|
644
|
-
name: `map-${layer}.html`,
|
|
668
|
+
name: `map-${layer}${tileSuffix}.html`,
|
|
645
669
|
mimeType: 'text/html',
|
|
646
670
|
content: bundle.html,
|
|
647
671
|
provenance,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { boundedSourceHashEvidence, publicArtifactLink, } from '../core/artifacts.js';
|
|
2
2
|
import { canonicalJson, hashCanonical } from '../core/canonical.js';
|
|
3
3
|
import { autonomousResultArtifacts, } from '../core/transaction-execution.js';
|
|
4
|
-
import { emptyServiceResult } from '../core/result.js';
|
|
4
|
+
import { emptyServiceResult, ServiceError } from '../core/result.js';
|
|
5
5
|
import { setInlineFilesScanned } from '../core/operation-result.js';
|
|
6
6
|
import { PACKAGE_VERSION } from '../version.js';
|
|
7
7
|
import { allocateMapIdentifiersAsync, } from './operations.js';
|
|
@@ -299,6 +299,69 @@ export function completeMapRewrite(workspaceId, execution, recipe) {
|
|
|
299
299
|
}
|
|
300
300
|
export async function inspectMap(engine, nudger, input, context) {
|
|
301
301
|
await context.progress(0, 3, 'Inspecting and validating the map');
|
|
302
|
+
if (input.lookupOnly) {
|
|
303
|
+
if (input.allocationRequests.length > 0)
|
|
304
|
+
throw new ServiceError('MAP_LOOKUP_ALLOCATION_UNSUPPORTED', 'Use full map inspection for identifier allocation previews');
|
|
305
|
+
const snapshot = await nudger.scan(context.workspaceId, context.principal, context.signal);
|
|
306
|
+
const catalog = input.query === undefined ? undefined : buildMapCatalog(snapshot.index);
|
|
307
|
+
const queryMatches = catalog === undefined ? [] : searchMapCatalog(catalog, input.query, input.queryLimit);
|
|
308
|
+
const coordinateMatches = input.coordinates.map((coordinate) => lookupMapCoordinate(snapshot.index, coordinate));
|
|
309
|
+
const selected = selectedMapEntities(snapshot, input.provinceIds, input.stateIds, input.regionIds);
|
|
310
|
+
const workspace = engine.resolver.get(context.workspaceId, context.principal);
|
|
311
|
+
const sourceEvidence = mapArtifactSourceEvidence(snapshot.files);
|
|
312
|
+
const selectorHash = hashCanonical({
|
|
313
|
+
provinceIds: input.provinceIds,
|
|
314
|
+
stateIds: input.stateIds,
|
|
315
|
+
regionIds: input.regionIds,
|
|
316
|
+
query: input.query,
|
|
317
|
+
coordinates: input.coordinates,
|
|
318
|
+
}).slice(0, 16);
|
|
319
|
+
const artifact = await engine.artifacts.putChunked(workspace, `map-lookup.${snapshot.revision.slice(0, 16)}.${selectorHash}.json`, 'application/json', `${canonicalJson({ schemaVersion: 1, revision: snapshot.revision, lookupOnly: true, queryMatches, coordinateMatches, selected, sourceHashes: sourceEvidence.complete })}\n`, {
|
|
320
|
+
kind: 'map-lookup',
|
|
321
|
+
toolVersion: PACKAGE_VERSION,
|
|
322
|
+
schemaVersion: 'map-lookup.v1',
|
|
323
|
+
sourceHashes: sourceEvidence.bounded.sourceHashes,
|
|
324
|
+
metadata: { sourceHashInventory: sourceEvidence.bounded.inventory },
|
|
325
|
+
}, 'Bounded map coordinate, catalog, and entity lookup without full validation or overview rendering', context.signal);
|
|
326
|
+
const result = emptyServiceResult(context.workspaceId, {
|
|
327
|
+
revision: snapshot.revision,
|
|
328
|
+
sharedRevision: snapshot.revision,
|
|
329
|
+
width: snapshot.index.raster?.width ?? null,
|
|
330
|
+
height: snapshot.index.raster?.height ?? null,
|
|
331
|
+
definitions: snapshot.index.definitions.length,
|
|
332
|
+
states: snapshot.index.states.length,
|
|
333
|
+
regions: snapshot.index.regions.length,
|
|
334
|
+
ports: snapshot.index.ports.length,
|
|
335
|
+
inspectedProvinceCount: selected.provinces.length,
|
|
336
|
+
inspectedStateCount: selected.states.length,
|
|
337
|
+
inspectedRegionCount: selected.regions.length,
|
|
338
|
+
allocationCount: 0,
|
|
339
|
+
queryMatchCount: queryMatches.length,
|
|
340
|
+
coordinateMatchCount: coordinateMatches.length,
|
|
341
|
+
overviewRendered: false,
|
|
342
|
+
provinceGeometryCount: 0,
|
|
343
|
+
provinceGeometryPixelCount: 0,
|
|
344
|
+
provinceGeometryRowRunCount: 0,
|
|
345
|
+
unknownProvinceIds: [],
|
|
346
|
+
missingGeometryProvinceIds: [],
|
|
347
|
+
lookupOnly: true,
|
|
348
|
+
});
|
|
349
|
+
result.code = 'MAP_LOOKUP_COMPLETED';
|
|
350
|
+
result.validation = {
|
|
351
|
+
passed: true,
|
|
352
|
+
checks: [
|
|
353
|
+
{
|
|
354
|
+
id: 'lookup-only',
|
|
355
|
+
passed: true,
|
|
356
|
+
message: 'Lookup completed; full map validation was not requested',
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
};
|
|
360
|
+
setInlineFilesScanned(result, snapshot.files.map(({ displayPath }) => displayPath));
|
|
361
|
+
result.artifacts = [publicArtifactLink(artifact)];
|
|
362
|
+
await context.progress(3, 3, 'Map lookup complete');
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
302
365
|
const { snapshot, validation } = await nudger.validate(context.workspaceId, context.principal, context.signal);
|
|
303
366
|
const sharedRevision = snapshot.revision;
|
|
304
367
|
const catalog = buildMapCatalog(snapshot.index);
|
|
@@ -474,6 +537,8 @@ export async function renderMapView(nudger, input, context) {
|
|
|
474
537
|
...(input.layer === undefined ? {} : { layer: input.layer }),
|
|
475
538
|
...(input.overlays === undefined ? {} : { overlays: input.overlays }),
|
|
476
539
|
...(input.scale === undefined ? {} : { scale: input.scale }),
|
|
540
|
+
...(input.tile === undefined ? {} : { tile: input.tile }),
|
|
541
|
+
...(input.area === undefined ? {} : { area: input.area }),
|
|
477
542
|
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
478
543
|
...(context.signal === undefined ? {} : { signal: context.signal }),
|
|
479
544
|
});
|
|
@@ -483,6 +548,7 @@ export async function renderMapView(nudger, input, context) {
|
|
|
483
548
|
height: rendered.bundle.height,
|
|
484
549
|
hashes: rendered.bundle.hashes,
|
|
485
550
|
offlineRepresentation: true,
|
|
551
|
+
...(rendered.bundle.tile === undefined ? {} : { tile: rendered.bundle.tile }),
|
|
486
552
|
});
|
|
487
553
|
result.code = 'MAP_RENDERED';
|
|
488
554
|
setInlineFilesScanned(result, rendered.filesScanned);
|
|
@@ -31,6 +31,7 @@ const eventRenderInputSchema = z
|
|
|
31
31
|
const eventCompareInputSchema = z
|
|
32
32
|
.object({
|
|
33
33
|
...eventCompareRequestSchema.shape,
|
|
34
|
+
selector: nestedSelectorSchema.optional(),
|
|
34
35
|
before: nestedGraphReferenceSchema.optional(),
|
|
35
36
|
after: nestedGraphReferenceSchema.optional(),
|
|
36
37
|
proposedSources: z.array(nestedProposedSourceSchema).min(1).max(64).optional(),
|
|
@@ -119,6 +120,11 @@ const eventCompareOutputSchema = strictOperationResultSchema(z
|
|
|
119
120
|
counts: z
|
|
120
121
|
.object({
|
|
121
122
|
changes: nonNegativeIntegerSchema,
|
|
123
|
+
selectedChanges: nonNegativeIntegerSchema.optional(),
|
|
124
|
+
omittedChanges: nonNegativeIntegerSchema.optional(),
|
|
125
|
+
unattributedChanges: nonNegativeIntegerSchema.optional(),
|
|
126
|
+
selectedBeforeNodes: nonNegativeIntegerSchema.optional(),
|
|
127
|
+
selectedAfterNodes: nonNegativeIntegerSchema.optional(),
|
|
122
128
|
addedNodes: nonNegativeIntegerSchema,
|
|
123
129
|
removedNodes: nonNegativeIntegerSchema,
|
|
124
130
|
changedNodes: nonNegativeIntegerSchema,
|
|
@@ -146,6 +152,9 @@ const eventCompareOutputSchema = strictOperationResultSchema(z
|
|
|
146
152
|
boundary: z
|
|
147
153
|
.object({
|
|
148
154
|
proposedSources: nonNegativeIntegerSchema,
|
|
155
|
+
selector: eventSelectorSchema.optional(),
|
|
156
|
+
maxChainNodes: nonNegativeIntegerSchema.optional(),
|
|
157
|
+
selectionTruncated: z.boolean().optional(),
|
|
149
158
|
render: z.boolean(),
|
|
150
159
|
maxRenderNodes: nonNegativeIntegerSchema,
|
|
151
160
|
refresh: z.boolean(),
|
|
@@ -179,7 +188,7 @@ export const eventTaskTools = [
|
|
|
179
188
|
{
|
|
180
189
|
name: 'hoi4.event_compare',
|
|
181
190
|
title: 'Compare event chains',
|
|
182
|
-
description: 'Compare cached, artifact-backed, current, or in-memory proposed event graphs
|
|
191
|
+
description: 'Compare cached, artifact-backed, current, or in-memory proposed event graphs, optionally selecting one downstream chain. Full changes and explicit selection coverage are resources.',
|
|
183
192
|
inputSchema: eventCompareInputSchema,
|
|
184
193
|
outputSchema: eventCompareOutputSchema,
|
|
185
194
|
annotations: readOnlyEventTool,
|
|
@@ -30,6 +30,16 @@ const focusInspectOutputSchema = strictOperationResultSchema(z
|
|
|
30
30
|
layoutDecisionCount: nonNegativeIntegerSchema,
|
|
31
31
|
layoutMetrics: focusLayoutMetricsSchema,
|
|
32
32
|
diagnosticCount: nonNegativeIntegerSchema,
|
|
33
|
+
scenarioSummary: z
|
|
34
|
+
.object({
|
|
35
|
+
completed: nonNegativeIntegerSchema,
|
|
36
|
+
blocked: nonNegativeIntegerSchema,
|
|
37
|
+
unresolved: nonNegativeIntegerSchema,
|
|
38
|
+
structuralCandidates: nonNegativeIntegerSchema,
|
|
39
|
+
choiceConflictCount: nonNegativeIntegerSchema,
|
|
40
|
+
})
|
|
41
|
+
.strict()
|
|
42
|
+
.optional(),
|
|
33
43
|
})
|
|
34
44
|
.strict())
|
|
35
45
|
.max(100),
|
|
@@ -125,7 +135,7 @@ export const focusTaskTools = [
|
|
|
125
135
|
{
|
|
126
136
|
name: 'hoi4.focus_inspect',
|
|
127
137
|
title: 'Inspect focus trees',
|
|
128
|
-
description: 'Inspect
|
|
138
|
+
description: 'Inspect focus trees and continuous palettes, including source references, diagnostics, and layout.',
|
|
129
139
|
inputSchema: focusInspectRequestSchema,
|
|
130
140
|
outputSchema: focusInspectOutputSchema,
|
|
131
141
|
annotations: artifactProducing,
|
|
@@ -133,7 +143,7 @@ export const focusTaskTools = [
|
|
|
133
143
|
{
|
|
134
144
|
name: 'hoi4.focus_render',
|
|
135
145
|
title: 'Render focus review artifacts',
|
|
136
|
-
description: 'Render
|
|
146
|
+
description: 'Render focus HTML, SVG, JSON, and source maps. Use hoi4.focus_raster for decoded icons and PNG.',
|
|
137
147
|
inputSchema: focusRenderRequestSchema,
|
|
138
148
|
outputSchema: focusRenderOutputSchema,
|
|
139
149
|
annotations: artifactProducing,
|
|
@@ -141,7 +151,7 @@ export const focusTaskTools = [
|
|
|
141
151
|
{
|
|
142
152
|
name: 'hoi4.focus_raster',
|
|
143
153
|
title: 'Rasterize focus review artifacts',
|
|
144
|
-
description: '
|
|
154
|
+
description: 'Render focus PNG with decoded source icons; also returns HTML, SVG, and JSON.',
|
|
145
155
|
inputSchema: focusRenderRequestSchema,
|
|
146
156
|
outputSchema: focusRenderOutputSchema,
|
|
147
157
|
annotations: artifactProducing,
|
|
@@ -149,7 +159,7 @@ export const focusTaskTools = [
|
|
|
149
159
|
{
|
|
150
160
|
name: 'hoi4.focus_rewrite',
|
|
151
161
|
title: 'Create or clean up focus content',
|
|
152
|
-
description: 'Create or
|
|
162
|
+
description: 'Create or compact a focus tree or palette from a complete plan. Use treeId for existing trees and createIfMissing for new files.',
|
|
153
163
|
inputSchema: focusRewriteRequestSchema,
|
|
154
164
|
outputSchema: focusPlanOutputSchema,
|
|
155
165
|
annotations: {
|
|
@@ -43,6 +43,10 @@ const guiRenderOutputSchema = strictOperationResultSchema(z
|
|
|
43
43
|
comparison: z
|
|
44
44
|
.object({ changedPixels: nonNegativeIntegerSchema, changedRatio: z.number().min(0).max(1) })
|
|
45
45
|
.strict(),
|
|
46
|
+
sourceComparison: z
|
|
47
|
+
.object({ changedPixels: nonNegativeIntegerSchema, changedRatio: z.number().min(0).max(1) })
|
|
48
|
+
.strict()
|
|
49
|
+
.optional(),
|
|
46
50
|
fidelityCounts: countRecordSchema,
|
|
47
51
|
offlineRepresentation: z.literal(true),
|
|
48
52
|
})
|
|
@@ -76,7 +80,7 @@ export const guiTaskTools = [
|
|
|
76
80
|
{
|
|
77
81
|
name: 'hoi4.gui_render',
|
|
78
82
|
title: 'Render scripted GUI artifacts',
|
|
79
|
-
description: 'Render GUI states, resolutions,
|
|
83
|
+
description: 'Render GUI states, resolutions, scenarios, hierarchy, comparisons, and diagnostics.',
|
|
80
84
|
inputSchema: guiRenderRequestSchema,
|
|
81
85
|
outputSchema: guiRenderOutputSchema,
|
|
82
86
|
annotations: artifactProducing,
|
|
@@ -75,6 +75,7 @@ const mapWorkspaceInspectOutputSchema = strictOperationResultSchema(z
|
|
|
75
75
|
provinceGeometryCount: nonNegativeIntegerSchema,
|
|
76
76
|
provinceGeometryPixelCount: nonNegativeIntegerSchema,
|
|
77
77
|
provinceGeometryRowRunCount: nonNegativeIntegerSchema,
|
|
78
|
+
lookupOnly: z.boolean().optional(),
|
|
78
79
|
unknownProvinceIds: z.array(nonNegativeIntegerSchema).max(512),
|
|
79
80
|
missingGeometryProvinceIds: z.array(nonNegativeIntegerSchema).max(512),
|
|
80
81
|
})
|
|
@@ -86,6 +87,15 @@ const mapRenderOutputSchema = strictOperationResultSchema(z
|
|
|
86
87
|
height: nonNegativeIntegerSchema,
|
|
87
88
|
hashes: bitmapRenderHashesSchema,
|
|
88
89
|
offlineRepresentation: z.literal(true),
|
|
90
|
+
tile: z
|
|
91
|
+
.object({
|
|
92
|
+
x: nonNegativeIntegerSchema,
|
|
93
|
+
y: nonNegativeIntegerSchema,
|
|
94
|
+
width: nonNegativeIntegerSchema,
|
|
95
|
+
height: nonNegativeIntegerSchema,
|
|
96
|
+
})
|
|
97
|
+
.strict()
|
|
98
|
+
.optional(),
|
|
89
99
|
})
|
|
90
100
|
.strict());
|
|
91
101
|
const mapPlanOutputSchema = strictOperationResultSchema(z
|
|
@@ -113,7 +123,7 @@ export const mapTaskTools = [
|
|
|
113
123
|
{
|
|
114
124
|
name: 'hoi4.map_inspect',
|
|
115
125
|
title: 'Inspect HOI4 map',
|
|
116
|
-
description: '
|
|
126
|
+
description: 'Validate and inspect the complete map, or use lookupOnly for bounded coordinate and entity lookups.',
|
|
117
127
|
inputSchema: mapInspectRequestSchema,
|
|
118
128
|
outputSchema: mapWorkspaceInspectOutputSchema,
|
|
119
129
|
annotations: artifactProducing,
|
|
@@ -121,7 +131,7 @@ export const mapTaskTools = [
|
|
|
121
131
|
{
|
|
122
132
|
name: 'hoi4.map_render',
|
|
123
133
|
title: 'Render map inspection artifacts',
|
|
124
|
-
description: 'Render searchable full-map PNG, JSON, and HTML.',
|
|
134
|
+
description: 'Render searchable full-map PNG, JSON, and HTML, or a reusable source-coordinate tile.',
|
|
125
135
|
inputSchema: mapRenderRequestSchema,
|
|
126
136
|
outputSchema: mapRenderOutputSchema,
|
|
127
137
|
annotations: artifactProducing,
|
|
@@ -16,6 +16,9 @@ const analysisDataSchema = z
|
|
|
16
16
|
cacheKey: sha256Schema,
|
|
17
17
|
scenarios: nonNegativeIntegerSchema,
|
|
18
18
|
candidates: nonNegativeIntegerSchema,
|
|
19
|
+
eligibleCandidates: nonNegativeIntegerSchema,
|
|
20
|
+
excludedCandidates: nonNegativeIntegerSchema,
|
|
21
|
+
unresolvedCandidates: nonNegativeIntegerSchema,
|
|
19
22
|
scopePools: nonNegativeIntegerSchema.optional(),
|
|
20
23
|
scopePoolCandidates: nonNegativeIntegerSchema.optional(),
|
|
21
24
|
unresolved: nonNegativeIntegerSchema,
|
|
@@ -61,6 +64,8 @@ const inspectDataSchema = z
|
|
|
61
64
|
candidateExamples: z.array(z.string().max(512)).max(10),
|
|
62
65
|
poolComplete: z.boolean().optional(),
|
|
63
66
|
requiredInputs: nonNegativeIntegerSchema,
|
|
67
|
+
requiredInputPaths: z.array(z.string().max(1024)).max(32),
|
|
68
|
+
requiredInputPathsTruncated: z.boolean(),
|
|
64
69
|
unresolved: nonNegativeIntegerSchema,
|
|
65
70
|
})
|
|
66
71
|
.strict();
|
|
@@ -13,6 +13,22 @@ const numericEffects = new Map([
|
|
|
13
13
|
['add_war_support', { key: 'war_support', mode: 'add', min: 0, max: 1 }],
|
|
14
14
|
]);
|
|
15
15
|
const harmlessEffects = new Set(['hidden_effect', 'custom_effect_tooltip', 'log', 'tooltip']);
|
|
16
|
+
const variableEffects = new Map([
|
|
17
|
+
['set_variable', 'set'],
|
|
18
|
+
['set_temp_variable', 'set'],
|
|
19
|
+
['add_to_variable', 'add'],
|
|
20
|
+
['add_to_temp_variable', 'add'],
|
|
21
|
+
['subtract_from_variable', 'subtract'],
|
|
22
|
+
['subtract_from_temp_variable', 'subtract'],
|
|
23
|
+
['multiply_variable', 'multiply'],
|
|
24
|
+
['multiply_temp_variable', 'multiply'],
|
|
25
|
+
['divide_variable', 'divide'],
|
|
26
|
+
['divide_temp_variable', 'divide'],
|
|
27
|
+
['clamp_variable', 'clamp'],
|
|
28
|
+
['clamp_temp_variable', 'clamp'],
|
|
29
|
+
['round_variable', 'round'],
|
|
30
|
+
['round_temp_variable', 'round'],
|
|
31
|
+
]);
|
|
16
32
|
function numeric(value) {
|
|
17
33
|
if (typeof value === 'number' && Number.isFinite(value))
|
|
18
34
|
return value;
|
|
@@ -175,6 +191,40 @@ export class MechanicInterpreter {
|
|
|
175
191
|
return (numeric(scenarioValue(this.state, key, targetScope)) ??
|
|
176
192
|
numeric(scenarioValue(this.state, `variable.${key}`, targetScope)));
|
|
177
193
|
}
|
|
194
|
+
scopeReference(expression, scope, stack) {
|
|
195
|
+
const scopeId = (name) => {
|
|
196
|
+
if (name === 'ROOT')
|
|
197
|
+
return this.state.scenario.actor ?? 'ROOT';
|
|
198
|
+
if (name === 'THIS')
|
|
199
|
+
return scope === 'ROOT'
|
|
200
|
+
? (this.state.scenario.actor ?? 'ROOT')
|
|
201
|
+
: this.state.scenario.scopes?.[scope]?.id;
|
|
202
|
+
if (name === 'PREV') {
|
|
203
|
+
const previous = stack.findLast((item) => item.startsWith('scope:'))?.slice(6);
|
|
204
|
+
return previous === undefined
|
|
205
|
+
? undefined
|
|
206
|
+
: previous === 'ROOT'
|
|
207
|
+
? (this.state.scenario.actor ?? 'ROOT')
|
|
208
|
+
: this.state.scenario.scopes?.[previous]?.id;
|
|
209
|
+
}
|
|
210
|
+
return this.state.scenario.scopes?.[name]?.id;
|
|
211
|
+
};
|
|
212
|
+
if (expression.startsWith('event_target:'))
|
|
213
|
+
return this.state.scenario.eventTargets?.[expression.slice(13)];
|
|
214
|
+
const identifier = /^([A-Za-z0-9_:]+)(?:\.id)?$/u.exec(expression)?.[1];
|
|
215
|
+
return identifier === undefined ? undefined : scopeId(identifier.toUpperCase());
|
|
216
|
+
}
|
|
217
|
+
value(expression, path, scope, stack) {
|
|
218
|
+
const numberValue = this.number(expression, path, scope);
|
|
219
|
+
if (numberValue !== undefined)
|
|
220
|
+
return numberValue;
|
|
221
|
+
const dotted = /^(ROOT|THIS|FROM|PREV)\.(.+)$/iu.exec(expression);
|
|
222
|
+
const targetScope = dotted?.[1]?.toUpperCase() === 'THIS' ? scope : (dotted?.[1]?.toUpperCase() ?? scope);
|
|
223
|
+
const key = dotted?.[2] ?? expression;
|
|
224
|
+
const existing = scenarioValue(this.state, key, targetScope) ??
|
|
225
|
+
scenarioValue(this.state, `variable.${key}`, targetScope);
|
|
226
|
+
return existing ?? this.scopeReference(expression, scope, stack);
|
|
227
|
+
}
|
|
178
228
|
mathExpression(block, path, scope) {
|
|
179
229
|
const entries = entriesOnly(block);
|
|
180
230
|
if (entries === undefined || entries[0]?.key.value !== 'value')
|
|
@@ -410,42 +460,53 @@ export class MechanicInterpreter {
|
|
|
410
460
|
}
|
|
411
461
|
return;
|
|
412
462
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
'
|
|
416
|
-
|
|
417
|
-
'
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
463
|
+
const variableMode = variableEffects.get(key);
|
|
464
|
+
if (variableMode !== undefined) {
|
|
465
|
+
const variableScope = key.includes('temp_variable') ? 'ROOT' : scope;
|
|
466
|
+
const scalarVariable = asText(entry.value);
|
|
467
|
+
if (variableMode === 'round') {
|
|
468
|
+
const variable = scalarVariable ?? (child === undefined ? undefined : firstScalar(child, 'var')?.value);
|
|
469
|
+
const before = variable === undefined
|
|
470
|
+
? undefined
|
|
471
|
+
: numeric(scenarioValue(this.state, variable, variableScope));
|
|
472
|
+
const after = before === undefined ? undefined : Math.round(before);
|
|
473
|
+
if (variable === undefined) {
|
|
474
|
+
this.unknown(key, path, scope, 'Variable must resolve');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
setScenarioValue(this.state, variable, after, variableScope);
|
|
478
|
+
this.note(key, path, scope, after === undefined ? 'unresolved' : 'applied', before, after, after === undefined ? 'Variable value is not numeric' : undefined, variable);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
421
481
|
if (child === undefined) {
|
|
422
482
|
this.unknown(key, path, scope, 'Variable effect requires a block');
|
|
423
483
|
return;
|
|
424
484
|
}
|
|
425
485
|
const short = assignments(child).find((item) => !['var', 'value', 'min', 'max', 'tooltip'].includes(item.key.value));
|
|
426
486
|
const variable = firstScalar(child, 'var')?.value ?? short?.key.value;
|
|
487
|
+
if (variable === undefined) {
|
|
488
|
+
this.unknown(key, path, scope, 'Variable must resolve');
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
427
491
|
const expression = firstScalar(child, 'value')?.value ??
|
|
428
492
|
(short === undefined ? undefined : asText(short.value));
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
this.
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (value !== undefined && key === 'divide_variable')
|
|
447
|
-
after = value === 0 ? undefined : before / value;
|
|
448
|
-
if (value !== undefined && key === 'clamp_variable') {
|
|
493
|
+
const operand = expression === undefined
|
|
494
|
+
? variableMode === 'set' && short?.value.type === 'block'
|
|
495
|
+
? this.mathExpression(short.value, path, scope)
|
|
496
|
+
: undefined
|
|
497
|
+
: this.value(expression, path, scope, stack);
|
|
498
|
+
const previous = scenarioValue(this.state, variable, variableScope);
|
|
499
|
+
const before = numeric(previous) ?? 0;
|
|
500
|
+
let after = variableMode === 'set' ? operand : numeric(operand);
|
|
501
|
+
if (variableMode === 'add' && typeof after === 'number')
|
|
502
|
+
after = before + after;
|
|
503
|
+
if (variableMode === 'subtract' && typeof after === 'number')
|
|
504
|
+
after = before - after;
|
|
505
|
+
if (variableMode === 'multiply' && typeof after === 'number')
|
|
506
|
+
after = before * after;
|
|
507
|
+
if (variableMode === 'divide' && typeof after === 'number')
|
|
508
|
+
after = after === 0 ? undefined : before / after;
|
|
509
|
+
if (variableMode === 'clamp') {
|
|
449
510
|
const minimum = firstScalar(child, 'min')?.value;
|
|
450
511
|
const maximum = firstScalar(child, 'max')?.value;
|
|
451
512
|
const min = minimum === undefined ? -Infinity : this.number(minimum, path, scope);
|
|
@@ -453,8 +514,8 @@ export class MechanicInterpreter {
|
|
|
453
514
|
after =
|
|
454
515
|
min === undefined || max === undefined ? undefined : Math.max(min, Math.min(max, before));
|
|
455
516
|
}
|
|
456
|
-
setScenarioValue(this.state, variable, after,
|
|
457
|
-
this.note(key, path, scope, after === undefined ? 'unresolved' : 'applied',
|
|
517
|
+
setScenarioValue(this.state, variable, after, variableScope);
|
|
518
|
+
this.note(key, path, scope, after === undefined ? 'unresolved' : 'applied', previous, after, after === undefined ? 'Numeric operand is unknown or division by zero' : undefined, variable);
|
|
458
519
|
return;
|
|
459
520
|
}
|
|
460
521
|
if (key === 'set_country_flag' ||
|
|
@@ -77,13 +77,28 @@ export function selectMechanicSource(snapshot, selector) {
|
|
|
77
77
|
}
|
|
78
78
|
export function scriptedEffectSources(snapshot) {
|
|
79
79
|
const result = new Map();
|
|
80
|
+
const symbolsByPath = new Map();
|
|
80
81
|
for (const symbol of snapshot.index.symbols) {
|
|
81
82
|
if (symbol.kind !== 'scripted_effect' || symbol.overridden || symbol.sourceShadowed)
|
|
82
83
|
continue;
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
const symbols = symbolsByPath.get(symbol.path) ?? [];
|
|
85
|
+
symbols.push(symbol);
|
|
86
|
+
symbolsByPath.set(symbol.path, symbols);
|
|
87
|
+
}
|
|
88
|
+
for (const [sourcePath, symbols] of symbolsByPath) {
|
|
89
|
+
const document = activeDocument(snapshot, sourcePath);
|
|
90
|
+
if (document === undefined)
|
|
91
|
+
continue;
|
|
92
|
+
const blocks = new Map();
|
|
93
|
+
for (const definition of assignments(document.root)) {
|
|
94
|
+
if (definition.value.type === 'block' && !blocks.has(definition.key.value))
|
|
95
|
+
blocks.set(definition.key.value, definition.value);
|
|
96
|
+
}
|
|
97
|
+
for (const symbol of symbols) {
|
|
98
|
+
const block = blocks.get(symbol.id);
|
|
99
|
+
if (block !== undefined)
|
|
100
|
+
result.set(symbol.id, { id: symbol.id, path: symbol.path, block });
|
|
101
|
+
}
|
|
87
102
|
}
|
|
88
103
|
return result;
|
|
89
104
|
}
|
|
@@ -114,6 +114,9 @@ export function evaluateExactCandidates(surface, scenario, definitions) {
|
|
|
114
114
|
...external.trace,
|
|
115
115
|
],
|
|
116
116
|
unresolved: [...eligibility.unresolved, ...value.unresolved, ...external.unresolved],
|
|
117
|
+
eligibilityUnresolved: eligibility.unresolved,
|
|
118
|
+
valueUnresolved: value.unresolved,
|
|
119
|
+
externalUnresolved: external.unresolved,
|
|
117
120
|
provenance: [
|
|
118
121
|
...candidate.provenance,
|
|
119
122
|
...(eligibility.helperProvenance ?? []),
|