hoi4-agent-tools 3.0.8 → 3.0.9

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 (36) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/hoi4_agent_tools/core/condition-evaluator.js +399 -0
  3. package/dist/hoi4_agent_tools/core/condition-model.js +1 -0
  4. package/dist/hoi4_agent_tools/core/condition-schema.js +42 -0
  5. package/dist/hoi4_agent_tools/core/scenario-state.js +97 -0
  6. package/dist/hoi4_agent_tools/core/shared-request-capacity.js +34 -17
  7. package/dist/hoi4_agent_tools/gui/scenario-conditions.js +119 -0
  8. package/dist/hoi4_agent_tools/gui/scenario-generator.js +59 -73
  9. package/dist/hoi4_agent_tools/gui/scenario.js +3 -0
  10. package/dist/hoi4_agent_tools/gui/studio.js +5 -2
  11. package/dist/hoi4_agent_tools/probability/scenario-state.js +2 -95
  12. package/dist/hoi4_agent_tools/probability/trigger-evaluator.js +1 -287
  13. package/dist/hoi4_agent_tools/version.js +1 -1
  14. package/docs/gui.md +3 -1
  15. package/package.json +1 -1
  16. package/schemas/configuration.schema.json +1 -1
  17. package/schemas/continuous-focus-palette.schema.json +1 -1
  18. package/schemas/custom-weighted-pool.schema.json +1 -1
  19. package/schemas/event-feature-manifest.schema.json +1 -1
  20. package/schemas/focus-plan.schema.json +1 -1
  21. package/schemas/focus-planning-sidecar.schema.json +1 -1
  22. package/schemas/gui-animation-source.schema.json +1 -1
  23. package/schemas/gui-helper.schema.json +1 -1
  24. package/schemas/gui-scenario.schema.json +189 -33
  25. package/schemas/map-operation.schema.json +1 -1
  26. package/schemas/operation-result.schema.json +1 -1
  27. package/schemas/probability-analysis-result.schema.json +1 -1
  28. package/schemas/probability-compare-input.schema.json +1 -1
  29. package/schemas/probability-evaluate-input.schema.json +1 -1
  30. package/schemas/probability-inspect-input.schema.json +1 -1
  31. package/schemas/probability-render-input.schema.json +1 -1
  32. package/schemas/probability-scenario-set.schema.json +1 -1
  33. package/schemas/probability-sequence-input.schema.json +1 -1
  34. package/schemas/probability-simulate-input.schema.json +1 -1
  35. package/schemas/probability-sweep-input.schema.json +1 -1
  36. package/server.json +2 -2
@@ -0,0 +1,119 @@
1
+ import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.js';
2
+ import { hasBlockingDiagnostics } from '../core/diagnostics.js';
3
+ import { evaluateTriggerBlock } from '../core/condition-evaluator.js';
4
+ import { resolveScopeContext, rootScopeContext } from '../core/scenario-state.js';
5
+ import { assignments, firstScalar, parseClausewitz } from '../core/source/index.js';
6
+ /** Keep declared scoped scalars out of exploratory value generation. */
7
+ export function guiExplicitConditionValues(base) {
8
+ const values = { ...base.stateValues, ...base.variables, ...base.values };
9
+ for (const [key, value] of Object.entries(values)) {
10
+ if (key.includes('.'))
11
+ continue;
12
+ values[`ROOT.${key}`] ??= value;
13
+ values[`THIS.${key}`] ??= value;
14
+ }
15
+ for (const [scope, binding] of Object.entries(base.scopes ?? {})) {
16
+ for (const [key, value] of Object.entries(binding.state)) {
17
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
18
+ values[`${scope}.${key}`] ??= value;
19
+ }
20
+ }
21
+ return values;
22
+ }
23
+ function parsedCondition(expression) {
24
+ const text = expression.trim();
25
+ const document = parseClausewitz(Buffer.from(`condition = ${text.startsWith('{') ? text : `{ ${text} }`}`), 'scenario:condition');
26
+ const assignment = assignments(document.root, 'condition')[0];
27
+ return {
28
+ ...(assignment?.value.type === 'block' ? { block: assignment.value } : {}),
29
+ invalid: hasBlockingDiagnostics(document.diagnostics) || assignment?.value.type !== 'block',
30
+ };
31
+ }
32
+ export function guiConditionScenario(base, values = base.values) {
33
+ const state = { ...base.stateValues, ...base.variables, ...values };
34
+ for (const [key, value] of Object.entries(base.flags))
35
+ state[`flag.${key}`] = value;
36
+ const scopes = structuredClone(base.scopes ?? {});
37
+ for (const [key, value] of Object.entries(state)) {
38
+ const match = /^(ROOT|THIS|FROM(?:\.FROM)*|PREV)\.(.+)$/u.exec(key);
39
+ if (match === null)
40
+ continue;
41
+ if (match[1] === 'ROOT' || match[1] === 'THIS')
42
+ state[match[2]] ??= value;
43
+ else {
44
+ const scope = (scopes[match[1]] ??= { id: match[1], state: {} });
45
+ scope.state[match[2]] ??= value;
46
+ }
47
+ }
48
+ const actor = base.country?.tag ?? base.country?.countryTag ?? base.country?.GetTag;
49
+ return {
50
+ id: base.id,
51
+ state,
52
+ scopes,
53
+ closedFlags: false,
54
+ ...(typeof actor === 'string' ? { actor } : {}),
55
+ };
56
+ }
57
+ export function evaluateGuiCondition(expression, scenario, subject, definitions = new ClausewitzEvaluationDefinitions(), scope = 'ROOT') {
58
+ const context = resolveScopeContext(scenario, scope, rootScopeContext(scenario));
59
+ const parsed = expression === undefined ? { invalid: false } : parsedCondition(expression);
60
+ if (parsed.invalid || context === undefined)
61
+ return {
62
+ state: 'unresolved',
63
+ unresolved: [
64
+ {
65
+ code: 'GUI_CONDITION_UNRESOLVED',
66
+ message: parsed.invalid
67
+ ? 'Condition could not be parsed'
68
+ : `Scenario does not declare scope ${scope}`,
69
+ candidateId: subject.id,
70
+ },
71
+ ],
72
+ };
73
+ return evaluateTriggerBlock(parsed.block, scenario, subject, definitions, [], context);
74
+ }
75
+ /** Find operands structurally for exploratory previews, without asserting branch eligibility. */
76
+ export function guiConditionVariables(expression, definitions) {
77
+ if (expression === undefined)
78
+ return [];
79
+ const parsed = parsedCondition(expression);
80
+ if (parsed.invalid || parsed.block === undefined)
81
+ return [];
82
+ const output = new Set();
83
+ const visit = (block, scope, stack) => {
84
+ const add = (key) => {
85
+ // Scoped operands need actual bindings; fabricated flat values can shadow ROOT/PREV.
86
+ if (scope === '' &&
87
+ key !== undefined &&
88
+ !key.includes('.') &&
89
+ !key.startsWith('@') &&
90
+ !key.startsWith('constant:') &&
91
+ !Number.isFinite(Number(key)))
92
+ output.add(key);
93
+ };
94
+ for (const assignment of assignments(block)) {
95
+ const key = assignment.key.value;
96
+ if (key === 'check_variable' && assignment.value.type === 'block') {
97
+ const short = assignments(assignment.value).find(({ key }) => !['var', 'value', 'compare', 'tooltip'].includes(key.value));
98
+ add(firstScalar(assignment.value, 'var')?.value ?? short?.key.value);
99
+ add(firstScalar(assignment.value, 'value')?.value ??
100
+ (short?.value.type === 'scalar' ? short.value.value : undefined));
101
+ }
102
+ else if (assignment.value.type === 'block') {
103
+ const prefix = ['ROOT', 'THIS', 'FROM', 'PREV'].includes(key) ? `${key}.` : scope;
104
+ visit(assignment.value, prefix, stack);
105
+ }
106
+ else if (assignment.operator.text === '>' || assignment.operator.text === '<') {
107
+ add(key);
108
+ add(assignment.value.value);
109
+ }
110
+ else {
111
+ const helper = definitions.scriptedTriggers.get(key);
112
+ if (helper !== undefined && !stack.has(key))
113
+ visit(helper.value, scope, new Set([...stack, key]));
114
+ }
115
+ }
116
+ };
117
+ visit(parsed.block, '', new Set());
118
+ return [...output].sort();
119
+ }
@@ -1,4 +1,6 @@
1
1
  import { sha256Bytes } from '../core/canonical.js';
2
+ import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.js';
3
+ import { evaluateGuiCondition, guiConditionScenario, guiConditionVariables, guiExplicitConditionValues, } from './scenario-conditions.js';
2
4
  import { parsePreviewScenario } from './scenario.js';
3
5
  const countryNames = ['Germany', 'France', 'Poland', 'Italy', 'Brazil', 'India', 'Canada'];
4
6
  const countryTags = ['GER', 'FRA', 'POL', 'ITA', 'BRA', 'RAJ', 'CAN'];
@@ -257,70 +259,46 @@ function generatedNumberForKey(key, options, random, inferred = {}) {
257
259
  function setIfMissing(target, key, value) {
258
260
  target[key] ??= value;
259
261
  }
260
- function scriptedLocalisationValue(graph, token, language, random, kind, values) {
262
+ function scriptedLocalisationValue(graph, token, language, kind, values, base, definitions, results) {
261
263
  const shortToken = token.slice(token.lastIndexOf('.') + 1);
262
- const definitions = graph.scriptedLocalisation.filter(({ name }) => name === token || name === shortToken);
263
- if (definitions.length === 0)
264
+ const definition = graph.scriptedLocalisation
265
+ .filter(({ name }) => name === token || name === shortToken)
266
+ .at(-1);
267
+ if (definition === undefined)
264
268
  return undefined;
265
- const sprites = new Set(graph.sprites.map(({ name }) => name.toLocaleLowerCase('en-US')));
266
- const choices = definitions.flatMap(({ choices }) => choices);
267
- const eligible = choices.filter(({ localisationKey }) => kind === 'image'
268
- ? sprites.has(localisationKey.toLocaleLowerCase('en-US'))
269
- : localisationText(graph, language, localisationKey) !== undefined);
270
- if (eligible.length === 0)
271
- return undefined;
272
- const evaluated = eligible.map((choice) => ({
273
- choice,
274
- result: evaluateSimpleTrigger(choice.triggerExpression, values),
275
- }));
276
- const matched = evaluated.find(({ result }) => result === true)?.choice;
277
- const firstThreshold = evaluated
278
- .map(({ choice }) => simpleThreshold(choice.triggerExpression))
279
- .find((threshold) => threshold !== undefined && typeof values[threshold.variable] === 'number');
280
- const unknownExists = evaluated.some(({ result }) => result === undefined);
281
- const ranked = matched === undefined && unknownExists && firstThreshold !== undefined
282
- ? eligible[Math.min(eligible.length - 1, Math.max(0, Math.round((1 - Math.max(0, Math.min(100, Number(values[firstThreshold.variable]))) / 100) *
283
- (eligible.length - 1))))]
284
- : undefined;
285
- const fallback = evaluated.find(({ result }) => result === 'fallback')?.choice;
286
- const selected = matched ?? ranked ?? fallback ?? random.pick(eligible);
287
- if (kind === 'image')
288
- return selected.localisationKey;
289
- return localisationText(graph, language, selected.localisationKey);
290
- }
291
- function simpleThreshold(triggerExpression) {
292
- if (triggerExpression === undefined)
293
- return undefined;
294
- const direct = /([A-Za-z_][A-Za-z0-9_.:^]*)\s*([<>])\s*(-?(?:\d+\.?\d*|\.\d+))/u.exec(triggerExpression);
295
- if (direct !== null)
296
- return {
297
- variable: direct[1],
298
- operator: direct[2],
299
- threshold: Number(direct[3]),
300
- };
301
- const variable = /\bvar\s*=\s*([A-Za-z_][A-Za-z0-9_.:^]*)/u.exec(triggerExpression)?.[1];
302
- const threshold = /\bvalue\s*=\s*(-?(?:\d+\.?\d*|\.\d+))/u.exec(triggerExpression)?.[1];
303
- const compare = /\bcompare\s*=\s*(greater_than|less_than)/u.exec(triggerExpression)?.[1];
304
- if (variable === undefined || threshold === undefined || compare === undefined)
305
- return undefined;
306
- return {
307
- variable,
308
- operator: compare === 'greater_than' ? '>' : '<',
309
- threshold: Number(threshold),
269
+ const scenario = guiConditionScenario(base, values);
270
+ const subject = {
271
+ id: token,
272
+ provenance: [
273
+ {
274
+ path: definition.sourcePath,
275
+ rootKind: 'source',
276
+ loadOrder: 0,
277
+ sourceHash: graph.sourceHashes[definition.sourcePath] ?? '',
278
+ ...(definition.location === undefined ? {} : { location: definition.location }),
279
+ },
280
+ ],
310
281
  };
311
- }
312
- function evaluateSimpleTrigger(triggerExpression, values) {
313
- if (triggerExpression === undefined || /\balways\s*=\s*yes\b/u.test(triggerExpression))
314
- return 'fallback';
315
- if (/\balways\s*=\s*no\b/u.test(triggerExpression))
316
- return false;
317
- const threshold = simpleThreshold(triggerExpression);
318
- if (threshold === undefined)
319
- return undefined;
320
- const value = values[threshold.variable];
321
- if (typeof value !== 'number')
322
- return undefined;
323
- return threshold.operator === '>' ? value > threshold.threshold : value < threshold.threshold;
282
+ const prefix = token.includes('.') ? token.slice(0, token.lastIndexOf('.')) : 'ROOT';
283
+ for (const choice of definition.choices) {
284
+ const result = evaluateGuiCondition(choice.triggerExpression, scenario, subject, definitions, prefix);
285
+ results.push({
286
+ token,
287
+ sourcePath: definition.sourcePath,
288
+ state: result.state,
289
+ localisationKey: choice.localisationKey,
290
+ unresolved: result.unresolved.map(({ message }) => message),
291
+ });
292
+ // An earlier unknown branch may be selected by the game; a later fallback cannot prove it false.
293
+ if (result.state === 'unresolved')
294
+ return undefined;
295
+ if (result.state === 'false')
296
+ continue;
297
+ return kind === 'image'
298
+ ? choice.localisationKey
299
+ : localisationText(graph, language, choice.localisationKey);
300
+ }
301
+ return undefined;
324
302
  }
325
303
  function materializeDynamicText(template, values) {
326
304
  const valueFor = (key) => {
@@ -481,7 +459,7 @@ function generatedRows(count, random, options) {
481
459
  };
482
460
  });
483
461
  }
484
- export function generateGuiPreviewScenarios(graph, windowName, base, options) {
462
+ export function generateGuiPreviewScenarios(graph, windowName, base, options, definitions = new ClausewitzEvaluationDefinitions()) {
485
463
  if (!options.enabled)
486
464
  return [];
487
465
  const scripted = relatedScriptedGuis(graph, windowName);
@@ -519,9 +497,8 @@ export function generateGuiPreviewScenarios(graph, windowName, base, options) {
519
497
  if (![...relevantDynamicTokens].some((token) => token === definition.name || token.endsWith(`.${definition.name}`)))
520
498
  continue;
521
499
  for (const choice of definition.choices) {
522
- const threshold = simpleThreshold(choice.triggerExpression);
523
- if (threshold !== undefined)
524
- tokens.numeric.add(threshold.variable);
500
+ for (const variable of guiConditionVariables(choice.triggerExpression, definitions))
501
+ tokens.numeric.add(variable);
525
502
  }
526
503
  }
527
504
  const progressRanges = new Map();
@@ -545,7 +522,10 @@ export function generateGuiPreviewScenarios(graph, windowName, base, options) {
545
522
  const sourceSeed = options.seed === 'auto' ? `${windowName}:${base.id}:${hashSourceGraph(graph)}` : options.seed;
546
523
  return Array.from({ length: options.count }, (_unused, scenarioIndex) => {
547
524
  const random = new ScenarioRandom(`${sourceSeed}:${scenarioIndex}`);
548
- const values = { ...base.values };
525
+ const explicitValues = guiExplicitConditionValues(base);
526
+ const values = { ...explicitValues };
527
+ const conditionResults = [];
528
+ const scriptedTokens = new Set([...tokens.textual].filter((token) => graph.scriptedLocalisation.some(({ name }) => name === token || token.endsWith(`.${name}`))));
549
529
  const lists = Object.fromEntries(Object.entries(base.lists).map(([key, rows]) => [key, rows.map((row) => ({ ...row }))]));
550
530
  const visibility = generatedVisibility(graph, scripted, options, random, base.visibility);
551
531
  const elementStates = { ...base.elementStates };
@@ -571,7 +551,7 @@ export function generateGuiPreviewScenarios(graph, windowName, base, options) {
571
551
  setIfMissing(values, startValueToken, value);
572
552
  }
573
553
  for (const key of tokens.numeric) {
574
- if (!key.endsWith('_bar') || base.values[key] !== undefined)
554
+ if (!key.endsWith('_bar') || explicitValues[key] !== undefined)
575
555
  continue;
576
556
  const sourceKey = key.slice(0, -'_bar'.length);
577
557
  const sourceValue = values[sourceKey];
@@ -579,7 +559,7 @@ export function generateGuiPreviewScenarios(graph, windowName, base, options) {
579
559
  values[key] = sourceValue;
580
560
  }
581
561
  for (const { value: valueKey, limit: limitKey } of numericConstraints.relationships) {
582
- if (base.values[valueKey] !== undefined)
562
+ if (explicitValues[valueKey] !== undefined)
583
563
  continue;
584
564
  const value = values[valueKey];
585
565
  const limit = values[limitKey];
@@ -589,22 +569,27 @@ export function generateGuiPreviewScenarios(graph, windowName, base, options) {
589
569
  values[valueKey] = Math.max(minimum, Math.min(value, limit));
590
570
  }
591
571
  for (const key of tokens.textual)
592
- setIfMissing(values, key, plausibleText(key, random, options.textSamples, scopedCountries));
572
+ if (!scriptedTokens.has(key))
573
+ setIfMissing(values, key, plausibleText(key, random, options.textSamples, scopedCountries));
593
574
  const scriptedTextTemplates = new Map();
594
575
  for (const key of tokens.textual) {
595
- const scriptedValue = scriptedLocalisationValue(graph, key, base.language, random, 'text', values);
596
- if (scriptedValue !== undefined && base.values[key] === undefined)
576
+ if (explicitValues[key] !== undefined)
577
+ continue;
578
+ const scriptedValue = scriptedLocalisationValue(graph, key, base.language, 'text', values, base, definitions, conditionResults);
579
+ if (scriptedValue !== undefined)
597
580
  scriptedTextTemplates.set(key, scriptedValue);
598
581
  }
599
582
  for (let pass = 0; pass < 3; pass += 1)
600
583
  for (const [key, template] of scriptedTextTemplates)
601
584
  values[key] = materializeDynamicText(template, values);
602
585
  for (const key of imageTokens) {
586
+ if (explicitValues[key] !== undefined)
587
+ continue;
603
588
  if (/(?:^|\.)GetFlag$/u.test(key)) {
604
589
  setIfMissing(values, key, generatedCountryTag);
605
590
  continue;
606
591
  }
607
- const sprite = scriptedLocalisationValue(graph, key, base.language, random, 'image', values);
592
+ const sprite = scriptedLocalisationValue(graph, key, base.language, 'image', values, base, definitions, conditionResults);
608
593
  if (sprite !== undefined)
609
594
  setIfMissing(values, key, sprite);
610
595
  }
@@ -625,7 +610,8 @@ export function generateGuiPreviewScenarios(graph, windowName, base, options) {
625
610
  return parsePreviewScenario({
626
611
  ...base,
627
612
  id: `${base.id}-${options.idPrefix}-${scenarioIndex + 1}`,
628
- description: `Generated source-aware scenario ${scenarioIndex + 1} (seed ${sourceSeed})`,
613
+ description: `Generated exploratory preview ${scenarioIndex + 1} (seed ${sourceSeed}; not verified campaign state)`,
614
+ conditionResults,
629
615
  values,
630
616
  country: [...imageTokens].some((key) => /(?:^|\.)GetFlag$/u.test(key))
631
617
  ? {
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { ConditionScopeBindingSchema, ConditionResultSchema } from '../core/condition-schema.js';
2
3
  import { renderDimensionViolation, RENDER_MAX_DIMENSION } from '../core/render-budget.js';
3
4
  import { ServiceError } from '../core/result.js';
4
5
  import { GUI_SCENARIO_MAX_KEYS, GUI_SCENARIO_MAX_LIST_KEYS, GUI_SCENARIO_MAX_ROWS, GUI_SCENARIO_MAX_STRING_CHARACTERS, } from './limits.js';
@@ -120,6 +121,8 @@ const GuiPreviewScenarioBodySchema = z
120
121
  visibleTimeSeconds: z.number().min(0).max(86_400).optional(),
121
122
  country: objectSchema.optional(),
122
123
  stateValues: objectSchema.optional(),
124
+ scopes: z.record(z.string().max(256), ConditionScopeBindingSchema).optional(),
125
+ conditionResults: z.array(ConditionResultSchema).max(10_000).optional(),
123
126
  variables: z.record(z.string(), z.number()).default({}),
124
127
  flags: z.record(z.string(), z.boolean()).default({}),
125
128
  lists: z.record(z.string(), z.array(objectSchema).max(10_000)).default({}),
@@ -6,6 +6,7 @@ import { compareCodeUnits, canonicalJson, deterministicId, hashCanonical, sha256
6
6
  import { DiagnosticCollector } from '../core/diagnostic-collector.js';
7
7
  import { sortDiagnostics } from '../core/diagnostics.js';
8
8
  import { CoreEngine } from '../core/engine.js';
9
+ import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.js';
9
10
  import { assertRenderDimensions, RenderBudget, RENDER_MAX_PIXELS } from '../core/render-budget.js';
10
11
  import { ServiceError } from '../core/result.js';
11
12
  import { WorkspaceScanner } from '../core/scanner.js';
@@ -25,6 +26,8 @@ import { buildGuiSourceGraph } from './source-graph.js';
25
26
  import { assertGuiSourcePatchesSafe } from './source-patch.js';
26
27
  import { validateGuiScene, validateResolutionDrift, validateStateMatrix } from './validators.js';
27
28
  const staticGuiDefinitionPatterns = [
29
+ 'common/scripted_triggers/**/*.txt',
30
+ 'common/script_constants/**/*.txt',
28
31
  'common/scripted_localisation/**/*.txt',
29
32
  'common/decisions/**/*.txt',
30
33
  'common/decision_categories/**/*.txt',
@@ -814,7 +817,7 @@ export class ScriptedGuiStudio {
814
817
  const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [placeholderScenario.language, ...explicitRelatedScenarios.map(({ language }) => language)], scenarioSpriteNames([placeholderScenario, ...explicitRelatedScenarios], generatedOptions?.textSamples));
815
818
  const generatedScenarios = generatedOptions === undefined
816
819
  ? []
817
- : generateGuiPreviewScenarios(scanned.graph, input.windowName, placeholderScenario, generatedOptions);
820
+ : generateGuiPreviewScenarios(scanned.graph, input.windowName, placeholderScenario, generatedOptions, ClausewitzEvaluationDefinitions.build(scanned));
818
821
  const scenario = generatedScenarios[0] ?? placeholderScenario;
819
822
  const relatedScenarios = [
820
823
  ...(generatedOptions?.preservePlaceholder === true && generatedScenarios.length > 0
@@ -893,7 +896,7 @@ export class ScriptedGuiStudio {
893
896
  ], generatedOptions?.textSamples));
894
897
  const generatedScenarios = generatedOptions === undefined
895
898
  ? []
896
- : generateGuiPreviewScenarios(scanned.graph, input.windowName, placeholderScenario, generatedOptions);
899
+ : generateGuiPreviewScenarios(scanned.graph, input.windowName, placeholderScenario, generatedOptions, ClausewitzEvaluationDefinitions.build(scanned));
897
900
  const scenario = generatedScenarios[0] ?? placeholderScenario;
898
901
  const relatedScenarios = [
899
902
  ...(generatedOptions?.preservePlaceholder === true && generatedScenarios.length > 0
@@ -1,98 +1,5 @@
1
- function caseInsensitiveEntry(record, key) {
2
- if (record === undefined)
3
- return undefined;
4
- if (Object.hasOwn(record, key))
5
- return record[key];
6
- const normalized = key.toLowerCase();
7
- const found = Object.entries(record).find(([candidate]) => candidate.toLowerCase() === normalized);
8
- return found?.[1];
9
- }
10
- export function rootScopeContext(scenario) {
11
- return {
12
- expression: 'ROOT',
13
- binding: {
14
- id: scenario.actor ?? 'ROOT',
15
- ...(scenario.actor === undefined ? {} : { actor: scenario.actor }),
16
- state: scenario.state,
17
- ...(scenario.flags === undefined ? {} : { flags: scenario.flags }),
18
- ...(scenario.eventTargets === undefined ? {} : { eventTargets: scenario.eventTargets }),
19
- },
20
- };
21
- }
22
- function explicitScope(scenario, expression, parent) {
23
- const binding = caseInsensitiveEntry(scenario.scopes, expression);
24
- return binding === undefined ? undefined : { expression, binding, parent };
25
- }
26
- export function resolveScopeContext(scenario, expression, current) {
27
- const upper = expression.toUpperCase();
28
- if (upper === 'ROOT')
29
- return rootScopeContext(scenario);
30
- if (upper === 'THIS')
31
- return current;
32
- if (upper === 'PREV')
33
- return current.parent ?? rootScopeContext(scenario);
34
- const chained = current.expression === 'ROOT' || current.expression === 'THIS'
35
- ? expression
36
- : `${current.expression}.${expression}`;
37
- const chainedBinding = explicitScope(scenario, chained, current);
38
- if (chainedBinding !== undefined)
39
- return chainedBinding;
40
- const direct = explicitScope(scenario, expression, current);
41
- if (direct !== undefined)
42
- return direct;
43
- const targetMatch = /^(?:event_target|scope):(.+)$/iu.exec(expression);
44
- const targetId = targetMatch === null
45
- ? undefined
46
- : (caseInsensitiveEntry(current.binding.eventTargets, targetMatch[1]) ??
47
- caseInsensitiveEntry(scenario.eventTargets, targetMatch[1]));
48
- if (targetId !== undefined) {
49
- const declaredBinding = Object.values(scenario.scopes ?? {}).find((binding) => binding.id === targetId || binding.actor === targetId);
50
- return {
51
- expression,
52
- binding: declaredBinding ?? { id: targetId, actor: targetId, state: {} },
53
- parent: current,
54
- };
55
- }
56
- return undefined;
57
- }
58
- export function scopeStateValue(scenario, key, current) {
59
- if (Object.hasOwn(current.binding.state, key))
60
- return current.binding.state[key];
61
- if (Object.hasOwn(current.binding.state, `trigger.${key}`))
62
- return current.binding.state[`trigger.${key}`];
63
- if (current.expression !== 'ROOT')
64
- return undefined;
65
- if (Object.hasOwn(scenario.state, key))
66
- return scenario.state[key];
67
- if (Object.hasOwn(scenario.state, `trigger.${key}`))
68
- return scenario.state[`trigger.${key}`];
69
- return undefined;
70
- }
71
- export function rootStateValue(scenario, key) {
72
- if (Object.hasOwn(scenario.state, key))
73
- return scenario.state[key];
74
- if (Object.hasOwn(scenario.state, `trigger.${key}`))
75
- return scenario.state[`trigger.${key}`];
76
- return undefined;
77
- }
78
- export function scopeIdentity(current) {
79
- return current.binding.actor ?? current.binding.id;
80
- }
81
- export function scopedExpressionValue(scenario, expression) {
82
- const scopes = Object.entries(scenario.scopes ?? {}).sort(([left], [right]) => right.length - left.length);
83
- for (const [scope, binding] of scopes) {
84
- for (const prefix of [`${scope}.`, `scope.${scope}.`, `scopes.${scope}.`]) {
85
- if (!expression.toLowerCase().startsWith(prefix.toLowerCase()))
86
- continue;
87
- const key = expression.slice(prefix.length).replace(/^state\./u, '');
88
- if (Object.hasOwn(binding.state, key))
89
- return binding.state[key];
90
- if (Object.hasOwn(binding.state, `variable.${key}`))
91
- return binding.state[`variable.${key}`];
92
- }
93
- }
94
- return undefined;
95
- }
1
+ import { rootStateValue, scopedExpressionValue } from '../core/scenario-state.js';
2
+ export { rootScopeContext, resolveScopeContext, scopeStateValue, rootStateValue, scopeIdentity, scopedExpressionValue, } from '../core/scenario-state.js';
96
3
  function updateScopeState(scenario, path, value) {
97
4
  const scopes = Object.entries(scenario.scopes ?? {}).sort(([left], [right]) => right.length - left.length);
98
5
  for (const [scope, binding] of scopes) {