hoi4-agent-tools 3.0.9 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -1
- package/README.md +10 -2
- package/dist/bin/stdio.js +23 -12
- package/dist/hoi4_agent_tools/core/analysis-checkpoints.js +82 -0
- package/dist/hoi4_agent_tools/core/analysis-evidence.js +9 -0
- package/dist/hoi4_agent_tools/core/analysis-job-operations.js +38 -0
- package/dist/hoi4_agent_tools/core/artifact-resource.js +173 -0
- package/dist/hoi4_agent_tools/core/artifacts.js +84 -2
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +16 -0
- package/dist/hoi4_agent_tools/core/dependency-pages.js +216 -0
- package/dist/hoi4_agent_tools/core/dependency-walk.js +266 -0
- package/dist/hoi4_agent_tools/core/domain-validation.js +188 -0
- package/dist/hoi4_agent_tools/core/engine.js +48 -5
- package/dist/hoi4_agent_tools/core/helper-expansion.js +186 -0
- package/dist/hoi4_agent_tools/core/impact-comparison.js +49 -0
- package/dist/hoi4_agent_tools/core/impact-graph.js +301 -0
- package/dist/hoi4_agent_tools/core/impact-semantic.js +256 -0
- package/dist/hoi4_agent_tools/core/index-segments.js +137 -0
- package/dist/hoi4_agent_tools/core/index.js +47 -4
- package/dist/hoi4_agent_tools/core/job-controls.js +109 -0
- package/dist/hoi4_agent_tools/core/job-executor.js +190 -0
- package/dist/hoi4_agent_tools/core/job-service.js +112 -0
- package/dist/hoi4_agent_tools/core/job-store.js +641 -0
- package/dist/hoi4_agent_tools/core/job-transactions.js +126 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +202 -0
- package/dist/hoi4_agent_tools/core/job-worker.js +64 -0
- package/dist/hoi4_agent_tools/core/operation-context.js +31 -0
- package/dist/hoi4_agent_tools/core/operation-result.js +412 -0
- package/dist/hoi4_agent_tools/core/operation-tasks.js +387 -0
- package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +383 -0
- package/dist/hoi4_agent_tools/core/result.js +1 -0
- package/dist/hoi4_agent_tools/core/scanner.js +15 -75
- package/dist/hoi4_agent_tools/core/semantic-dependencies.js +233 -0
- package/dist/hoi4_agent_tools/core/shared-request-capacity.js +43 -11
- package/dist/hoi4_agent_tools/core/source/cache.js +107 -0
- package/dist/hoi4_agent_tools/core/source/parser.js +11 -1
- package/dist/hoi4_agent_tools/core/source-overlay.js +125 -0
- package/dist/hoi4_agent_tools/core/transaction-execution.js +134 -0
- package/dist/hoi4_agent_tools/core/transactions.js +80 -27
- package/dist/hoi4_agent_tools/decision/ai.js +104 -0
- package/dist/hoi4_agent_tools/decision/comparison.js +106 -0
- package/dist/hoi4_agent_tools/decision/cost.js +177 -0
- package/dist/hoi4_agent_tools/decision/evaluation.js +256 -0
- package/dist/hoi4_agent_tools/decision/lifecycle.js +114 -0
- package/dist/hoi4_agent_tools/decision/service.js +172 -0
- package/dist/hoi4_agent_tools/decision/source-inventory.js +182 -0
- package/dist/hoi4_agent_tools/event/graph.js +214 -290
- package/dist/hoi4_agent_tools/event/helper-expansion.js +33 -0
- package/dist/hoi4_agent_tools/event/job-operations.js +51 -0
- package/dist/hoi4_agent_tools/event/operations.js +179 -0
- package/dist/hoi4_agent_tools/event/service.js +67 -15
- package/dist/hoi4_agent_tools/event/source-analysis.js +5 -4
- package/dist/hoi4_agent_tools/focus/job-operations.js +77 -0
- package/dist/hoi4_agent_tools/focus/tool-operations.js +669 -0
- package/dist/hoi4_agent_tools/focus/tool-support.js +123 -0
- package/dist/hoi4_agent_tools/gui/job-operations.js +81 -0
- package/dist/hoi4_agent_tools/gui/tool-operations.js +270 -0
- package/dist/hoi4_agent_tools/impact/scenario-suites.js +74 -0
- package/dist/hoi4_agent_tools/impact/service.js +127 -0
- package/dist/hoi4_agent_tools/map/job-operations.js +76 -0
- package/dist/hoi4_agent_tools/map/tool-operations.js +493 -0
- package/dist/hoi4_agent_tools/mcp/prompts/probability.js +25 -0
- package/dist/hoi4_agent_tools/mcp/prompts/register-modern.js +29 -0
- package/dist/hoi4_agent_tools/mcp/resources/register-modern.js +34 -0
- package/dist/hoi4_agent_tools/mcp/resources/register.js +4 -150
- package/dist/hoi4_agent_tools/mcp/server/base-tools.js +3 -219
- package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +188 -0
- package/dist/hoi4_agent_tools/mcp/server/create.js +18 -6
- package/dist/hoi4_agent_tools/mcp/server/instructions.js +2 -0
- package/dist/hoi4_agent_tools/mcp/server/job-task-store.js +67 -0
- package/dist/hoi4_agent_tools/mcp/server/legacy-task-tools.js +20 -0
- package/dist/hoi4_agent_tools/mcp/server/modern-tasks.js +168 -0
- package/dist/hoi4_agent_tools/mcp/server/progress.js +17 -15
- package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +45 -0
- package/dist/hoi4_agent_tools/mcp/server/result.js +2 -412
- package/dist/hoi4_agent_tools/mcp/server/task-request-context.js +9 -0
- package/dist/hoi4_agent_tools/mcp/server/task-tool-catalog.js +16 -0
- package/dist/hoi4_agent_tools/mcp/server/task-tool-definition.js +1 -0
- package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js +1 -123
- package/dist/hoi4_agent_tools/mcp/tools/analysis.js +65 -0
- package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +38 -21
- package/dist/hoi4_agent_tools/mcp/tools/event.js +24 -315
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +34 -1039
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +19 -504
- package/dist/hoi4_agent_tools/mcp/tools/job.js +46 -0
- package/dist/hoi4_agent_tools/mcp/tools/map.js +21 -600
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +39 -463
- package/dist/hoi4_agent_tools/mcp/tools/technology.js +40 -304
- package/dist/hoi4_agent_tools/mcp/transports/http.js +58 -1
- package/dist/hoi4_agent_tools/mcp/transports/modern-task-routing.js +109 -0
- package/dist/hoi4_agent_tools/mcp/transports/negotiated-stdio.js +121 -0
- package/dist/hoi4_agent_tools/probability/job-operations.js +58 -0
- package/dist/hoi4_agent_tools/probability/operations.js +183 -0
- package/dist/hoi4_agent_tools/probability/service.js +24 -3
- package/dist/hoi4_agent_tools/schemas/analysis.js +228 -0
- package/dist/hoi4_agent_tools/schemas/common.js +2 -0
- package/dist/hoi4_agent_tools/schemas/event.js +72 -1
- package/dist/hoi4_agent_tools/schemas/focus-requests.js +189 -0
- package/dist/hoi4_agent_tools/schemas/gui-requests.js +193 -0
- package/dist/hoi4_agent_tools/schemas/helper-expansion.js +58 -0
- package/dist/hoi4_agent_tools/schemas/map-requests.js +85 -0
- package/dist/hoi4_agent_tools/schemas/probability-requests.js +168 -0
- package/dist/hoi4_agent_tools/schemas/technology.js +90 -1
- package/dist/hoi4_agent_tools/schemas/transaction.js +1 -0
- package/dist/hoi4_agent_tools/technology/graph.js +83 -78
- package/dist/hoi4_agent_tools/technology/helper-expansion.js +33 -0
- package/dist/hoi4_agent_tools/technology/job-operations.js +60 -0
- package/dist/hoi4_agent_tools/technology/operations.js +125 -0
- package/dist/hoi4_agent_tools/technology/service.js +89 -15
- package/dist/hoi4_agent_tools/technology/source-analysis.js +2 -1
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +3 -0
- package/docs/analysis.md +100 -0
- package/docs/development.md +2 -0
- package/docs/events.md +7 -2
- package/docs/helper-expansion.md +78 -0
- package/docs/http.md +2 -0
- package/docs/jobs.md +86 -0
- package/docs/technology.md +6 -2
- package/package.json +15 -6
- 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 +290 -0
- 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 +44 -0
- package/schemas/helper-expansion-summary.schema.json +70 -0
- package/schemas/impact-inspect-input.schema.json +203 -0
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/operation-result.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/server.json +3 -3
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { canonicalJson } from './canonical.js';
|
|
2
|
+
import { boundedSourceHashEvidence, publicArtifactLink } from './artifacts.js';
|
|
3
|
+
import { ServiceError } from './result.js';
|
|
4
|
+
import { PACKAGE_VERSION } from '../version.js';
|
|
5
|
+
import { postValidateTransaction } from './domain-validation.js';
|
|
6
|
+
import { MAX_INLINE_ARTIFACT_LINKS } from './result.js';
|
|
7
|
+
export class AutonomousRewriteError extends ServiceError {
|
|
8
|
+
manifest;
|
|
9
|
+
artifacts;
|
|
10
|
+
constructor(cause, manifest, artifacts = []) {
|
|
11
|
+
const restored = manifest.state === 'rolled_back' && manifest.rollbackStatus === 'applied';
|
|
12
|
+
const code = cause.code.startsWith('TRANSACTION_')
|
|
13
|
+
? `REWRITE_${cause.code.slice('TRANSACTION_'.length)}`
|
|
14
|
+
: cause.code;
|
|
15
|
+
super(code, restored
|
|
16
|
+
? 'Rewrite verification failed and the original files were restored'
|
|
17
|
+
: 'Rewrite failed and automatic recovery could not be completed', {
|
|
18
|
+
execution: 'failed',
|
|
19
|
+
automaticRecovery: restored ? 'restored' : 'incomplete',
|
|
20
|
+
});
|
|
21
|
+
this.manifest = manifest;
|
|
22
|
+
this.artifacts = artifacts;
|
|
23
|
+
this.name = 'AutonomousRewriteError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function storeAutonomousValidationEvidence(engine, manifest, postValidation, principal, signal) {
|
|
27
|
+
const diagnostics = [...manifest.diagnostics, ...postValidation.diagnostics];
|
|
28
|
+
const checks = [...manifest.validation.checks, ...postValidation.checks];
|
|
29
|
+
const passed = manifest.validation.passed &&
|
|
30
|
+
checks.every((check) => check.passed) &&
|
|
31
|
+
!diagnostics.some(({ severity }) => severity === 'error' || severity === 'blocker');
|
|
32
|
+
const sourceEvidence = boundedSourceHashEvidence(Object.fromEntries(manifest.files.flatMap(({ relativePath, afterSha256 }) => afterSha256 === null ? [] : [[relativePath, afterSha256]])));
|
|
33
|
+
const artifact = await engine.artifacts.putChunked(engine.resolver.get(manifest.workspaceId, principal), `${manifest.operationKind}.execution-validation.json`, 'application/json', `${canonicalJson({
|
|
34
|
+
schemaVersion: 1,
|
|
35
|
+
execution: passed ? 'post-validation-passed' : 'post-validation-failed',
|
|
36
|
+
operationKind: manifest.operationKind,
|
|
37
|
+
files: manifest.files.map(({ relativePath, afterSha256, afterSize, mediaType }) => ({
|
|
38
|
+
relativePath,
|
|
39
|
+
afterSha256,
|
|
40
|
+
afterSize,
|
|
41
|
+
mediaType,
|
|
42
|
+
})),
|
|
43
|
+
validation: { passed, checks },
|
|
44
|
+
diagnostics,
|
|
45
|
+
resources: postValidation.artifacts,
|
|
46
|
+
})}\n`, {
|
|
47
|
+
kind: 'autonomous-rewrite-execution-validation',
|
|
48
|
+
toolVersion: PACKAGE_VERSION,
|
|
49
|
+
schemaVersion: 'autonomous-rewrite-execution-validation.v1',
|
|
50
|
+
sourceHashes: sourceEvidence.sourceHashes,
|
|
51
|
+
metadata: {
|
|
52
|
+
operationKind: manifest.operationKind,
|
|
53
|
+
fileCount: manifest.files.length,
|
|
54
|
+
checkCount: checks.length,
|
|
55
|
+
diagnosticCount: diagnostics.length,
|
|
56
|
+
sourceHashInventory: sourceEvidence.inventory,
|
|
57
|
+
},
|
|
58
|
+
}, 'Complete rewrite and post-write validation evidence', signal);
|
|
59
|
+
return publicArtifactLink(artifact);
|
|
60
|
+
}
|
|
61
|
+
function uniqueArtifacts(artifacts) {
|
|
62
|
+
const unique = new Map();
|
|
63
|
+
for (const artifact of artifacts)
|
|
64
|
+
unique.set(artifact.uri, artifact);
|
|
65
|
+
return [...unique.values()].slice(0, MAX_INLINE_ARTIFACT_LINKS);
|
|
66
|
+
}
|
|
67
|
+
export function autonomousResultArtifacts(execution) {
|
|
68
|
+
return uniqueArtifacts([
|
|
69
|
+
...execution.artifacts,
|
|
70
|
+
...(execution.transaction.executionArtifacts ?? []),
|
|
71
|
+
...execution.transaction.artifacts,
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Complete a validated domain rewrite inside one MCP tool call. Hash checks, durable recovery,
|
|
76
|
+
* post-write validation, and automatic failure recovery remain internal guarantees.
|
|
77
|
+
*/
|
|
78
|
+
export async function executePlannedTransaction(engine, transaction, principal, signal, hooks = {}) {
|
|
79
|
+
if (!transaction.validation.passed) {
|
|
80
|
+
return { transaction, outcome: 'blocked', artifacts: [] };
|
|
81
|
+
}
|
|
82
|
+
if (transaction.files.length === 0)
|
|
83
|
+
return { transaction, outcome: 'unchanged', artifacts: [] };
|
|
84
|
+
signal?.throwIfAborted();
|
|
85
|
+
await hooks.beforeApply?.(transaction);
|
|
86
|
+
signal?.throwIfAborted();
|
|
87
|
+
let executionArtifacts = [];
|
|
88
|
+
try {
|
|
89
|
+
const applied = await engine.transactions.apply(transaction.workspaceId, transaction.transactionId, transaction.planHash, {
|
|
90
|
+
...(principal === undefined ? {} : { principal }),
|
|
91
|
+
...(signal === undefined ? {} : { signal }),
|
|
92
|
+
postValidate: async (manifest, validationSignal) => {
|
|
93
|
+
const validation = await postValidateTransaction(engine, manifest, principal, validationSignal);
|
|
94
|
+
executionArtifacts = [...validation.artifacts];
|
|
95
|
+
executionArtifacts.unshift(await storeAutonomousValidationEvidence(engine, manifest, validation, principal, validationSignal));
|
|
96
|
+
return { ...validation, artifacts: executionArtifacts };
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
// Post-validation already invalidates and rebuilds the shared index before apply can succeed.
|
|
100
|
+
return { transaction: applied, outcome: 'applied', artifacts: executionArtifacts };
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
engine.invalidate(transaction.workspaceId);
|
|
104
|
+
try {
|
|
105
|
+
const manifest = await engine.transactions.status(transaction.workspaceId, transaction.transactionId, principal);
|
|
106
|
+
const cause = error instanceof ServiceError
|
|
107
|
+
? error
|
|
108
|
+
: new ServiceError('AUTONOMOUS_REWRITE_FAILED', 'Autonomous rewrite failed');
|
|
109
|
+
throw new AutonomousRewriteError(cause, manifest, executionArtifacts);
|
|
110
|
+
}
|
|
111
|
+
catch (statusError) {
|
|
112
|
+
if (statusError instanceof AutonomousRewriteError)
|
|
113
|
+
throw statusError;
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export function autonomousFailureContext(error) {
|
|
119
|
+
if (!(error instanceof AutonomousRewriteError))
|
|
120
|
+
return undefined;
|
|
121
|
+
const manifest = error.manifest;
|
|
122
|
+
return {
|
|
123
|
+
proposedFiles: manifest.files.slice(0, 100).map(({ relativePath }) => relativePath),
|
|
124
|
+
changedFiles: manifest.rollbackStatus === 'failed'
|
|
125
|
+
? manifest.files.slice(0, 100).map(({ relativePath }) => relativePath)
|
|
126
|
+
: [],
|
|
127
|
+
artifacts: uniqueArtifacts([
|
|
128
|
+
...error.artifacts,
|
|
129
|
+
...(manifest.executionArtifacts ?? []),
|
|
130
|
+
...manifest.artifacts,
|
|
131
|
+
]),
|
|
132
|
+
validation: manifest.validation,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -312,6 +312,7 @@ export class TransactionManager {
|
|
|
312
312
|
diagnostics,
|
|
313
313
|
validation: { passed, checks: validation.checks },
|
|
314
314
|
artifacts,
|
|
315
|
+
executionArtifacts: [],
|
|
315
316
|
appliedFiles: [],
|
|
316
317
|
rollbackStatus: 'available',
|
|
317
318
|
};
|
|
@@ -398,11 +399,17 @@ export class TransactionManager {
|
|
|
398
399
|
}
|
|
399
400
|
if (manifest.diagnostics.length + result.diagnostics.length > TRANSACTION_MAX_DIAGNOSTICS ||
|
|
400
401
|
manifest.validation.checks.length + result.checks.length >
|
|
401
|
-
TRANSACTION_MAX_VALIDATION_CHECKS
|
|
402
|
-
|
|
402
|
+
TRANSACTION_MAX_VALIDATION_CHECKS ||
|
|
403
|
+
(manifest.executionArtifacts?.length ?? 0) + (result.artifacts?.length ?? 0) >
|
|
404
|
+
TRANSACTION_MAX_ARTIFACTS) {
|
|
405
|
+
throw new ServiceError('TRANSACTION_STRUCTURE_LIMIT', 'Post-write validation exceeds the supported transaction review or artifact limits');
|
|
403
406
|
}
|
|
404
407
|
manifest.diagnostics.push(...result.diagnostics);
|
|
405
408
|
manifest.validation.checks.push(...result.checks);
|
|
409
|
+
manifest.executionArtifacts ??= [];
|
|
410
|
+
for (const artifact of result.artifacts ?? [])
|
|
411
|
+
if (!manifest.executionArtifacts.some(({ uri }) => uri === artifact.uri))
|
|
412
|
+
manifest.executionArtifacts.push(artifact);
|
|
406
413
|
manifest.validation.passed =
|
|
407
414
|
manifest.validation.passed &&
|
|
408
415
|
result.checks.every(({ passed }) => passed) &&
|
|
@@ -513,6 +520,61 @@ export class TransactionManager {
|
|
|
513
520
|
totalSize: cached.bytes.length,
|
|
514
521
|
};
|
|
515
522
|
}
|
|
523
|
+
/** Reconcile one interrupted job's journal without sweeping unrelated transactions. */
|
|
524
|
+
async recoverTransaction(workspaceId, transactionId, principal, signal) {
|
|
525
|
+
signal?.throwIfAborted();
|
|
526
|
+
const workspace = this.resolver.get(workspaceId, principal);
|
|
527
|
+
// Authenticate the selected journal and authorize its owner before changing protected state.
|
|
528
|
+
const initial = await this.load(workspace, transactionId, {
|
|
529
|
+
headMode: 'none',
|
|
530
|
+
...(signal === undefined ? {} : { signal }),
|
|
531
|
+
});
|
|
532
|
+
this.assertRecoveryPrincipal(initial, principal);
|
|
533
|
+
return this.withWorkspaceLock(workspace, transactionId, async () => {
|
|
534
|
+
signal?.throwIfAborted();
|
|
535
|
+
// Never restore a pre-lock snapshot: a writer may have committed since the first read.
|
|
536
|
+
const current = await this.load(workspace, transactionId, {
|
|
537
|
+
headMode: 'none',
|
|
538
|
+
...(signal === undefined ? {} : { signal }),
|
|
539
|
+
});
|
|
540
|
+
this.assertRecoveryPrincipal(current, principal);
|
|
541
|
+
const manifest = await this.load(workspace, transactionId, {
|
|
542
|
+
headMode: 'reconcile',
|
|
543
|
+
...(signal === undefined ? {} : { signal }),
|
|
544
|
+
});
|
|
545
|
+
signal?.throwIfAborted();
|
|
546
|
+
if (manifest.state === 'applying' || manifest.state === 'rolling_back') {
|
|
547
|
+
await this.restoreInterruptedTransaction(workspace, manifest, principal);
|
|
548
|
+
}
|
|
549
|
+
return manifest;
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
assertRecoveryPrincipal(manifest, principal) {
|
|
553
|
+
if (principal !== undefined && (manifest.principal ?? null) !== principal) {
|
|
554
|
+
throw new ServiceError('TRANSACTION_PRINCIPAL_MISMATCH', 'Transaction recovery belongs to another principal');
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
async restoreInterruptedTransaction(workspace, manifest, principal) {
|
|
558
|
+
// Restoration and authenticated journal advancement are a non-cancellable critical phase.
|
|
559
|
+
try {
|
|
560
|
+
await this.restoreBeforeState(workspace, manifest, principal);
|
|
561
|
+
manifest.state = 'rolled_back';
|
|
562
|
+
manifest.failure = {
|
|
563
|
+
code: 'TRANSACTION_RECOVERED',
|
|
564
|
+
message: 'Incomplete transaction was rolled back during recovery',
|
|
565
|
+
};
|
|
566
|
+
await this.writeManifest(workspace, manifest);
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
manifest.rollbackStatus = 'failed';
|
|
570
|
+
manifest.state = 'failed';
|
|
571
|
+
manifest.failure = this.safeFailure(error, 'TRANSACTION_RECOVERY_FAILED', 'Interrupted transaction could not be recovered safely');
|
|
572
|
+
await this.writeManifest(workspace, manifest);
|
|
573
|
+
if (error instanceof ServiceError)
|
|
574
|
+
throw error;
|
|
575
|
+
throw new ServiceError('TRANSACTION_RECOVERY_FAILED', 'Interrupted transaction could not be recovered safely');
|
|
576
|
+
}
|
|
577
|
+
}
|
|
516
578
|
async recover(workspaceId, principal, signal) {
|
|
517
579
|
signal?.throwIfAborted();
|
|
518
580
|
const workspace = this.resolver.get(workspaceId, principal);
|
|
@@ -536,38 +598,29 @@ export class TransactionManager {
|
|
|
536
598
|
if (!transactionIdPattern.test(entry.name))
|
|
537
599
|
continue;
|
|
538
600
|
const manifest = await this.load(workspace, entry.name, {
|
|
539
|
-
headMode: '
|
|
601
|
+
headMode: 'none',
|
|
540
602
|
...(signal === undefined ? {} : { signal }),
|
|
541
603
|
});
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
if (manifest.state !== 'applying' && manifest.state !== 'rolling_back')
|
|
546
|
-
continue;
|
|
547
|
-
await this.withWorkspaceLock(workspace, manifest.transactionId, async () => {
|
|
548
|
-
signal?.throwIfAborted();
|
|
549
|
-
// Once source restoration starts, recovery and authenticated journal
|
|
550
|
-
// advancement form a non-cancellable critical phase.
|
|
604
|
+
this.assertRecoveryPrincipal(manifest, principal);
|
|
605
|
+
const interrupted = manifest.state === 'applying' || manifest.state === 'rolling_back';
|
|
606
|
+
if (!interrupted) {
|
|
551
607
|
try {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
};
|
|
558
|
-
await this.writeManifest(workspace, manifest);
|
|
608
|
+
// Ordinary startup must not contend with a live writer just to read terminal journals.
|
|
609
|
+
await this.load(workspace, entry.name, {
|
|
610
|
+
...(signal === undefined ? {} : { signal }),
|
|
611
|
+
});
|
|
612
|
+
continue;
|
|
559
613
|
}
|
|
560
614
|
catch (error) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
manifest.failure = this.safeFailure(error, 'TRANSACTION_RECOVERY_FAILED', 'Interrupted transaction could not be recovered safely');
|
|
564
|
-
await this.writeManifest(workspace, manifest);
|
|
565
|
-
if (error instanceof ServiceError)
|
|
615
|
+
if (!(error instanceof ServiceError) ||
|
|
616
|
+
error.code !== 'TRANSACTION_HEAD_RECONCILIATION_REQUIRED')
|
|
566
617
|
throw error;
|
|
567
|
-
throw new ServiceError('TRANSACTION_RECOVERY_FAILED', 'Interrupted transaction could not be recovered safely');
|
|
568
618
|
}
|
|
569
|
-
}
|
|
570
|
-
|
|
619
|
+
}
|
|
620
|
+
const reconciled = await this.recoverTransaction(workspaceId, entry.name, principal, signal);
|
|
621
|
+
if (interrupted) {
|
|
622
|
+
recovered.push(reconciled);
|
|
623
|
+
}
|
|
571
624
|
}
|
|
572
625
|
return recovered;
|
|
573
626
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { ProbabilityAnalyzer } from '../probability/service.js';
|
|
2
|
+
import { decisionSourceInventory } from './source-inventory.js';
|
|
3
|
+
function selected(snapshot, id) {
|
|
4
|
+
return decisionSourceInventory(snapshot).decisions.find(({ id: candidate }) => candidate === id);
|
|
5
|
+
}
|
|
6
|
+
function summary(result) {
|
|
7
|
+
return {
|
|
8
|
+
analysisId: result.analysisId,
|
|
9
|
+
status: result.status,
|
|
10
|
+
adapter: result.adapter.id,
|
|
11
|
+
sourceHash: result.metadata.sourceHash,
|
|
12
|
+
scenarios: result.scenarios.map((scenario) => ({
|
|
13
|
+
id: scenario.id,
|
|
14
|
+
candidates: scenario.candidates.map((candidate) => ({
|
|
15
|
+
id: candidate.id,
|
|
16
|
+
eligibility: candidate.eligibility,
|
|
17
|
+
supportLevel: candidate.supportLevel,
|
|
18
|
+
...(candidate.rawValue === undefined ? {} : { rawValue: candidate.rawValue }),
|
|
19
|
+
unresolved: candidate.unresolved,
|
|
20
|
+
})),
|
|
21
|
+
unresolved: scenario.unresolved,
|
|
22
|
+
})),
|
|
23
|
+
unresolved: result.unresolved,
|
|
24
|
+
...(result.comparison === undefined ? {} : { comparison: result.comparison }),
|
|
25
|
+
resources: result.resources,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Uses the existing score-only probability adapters; never invents decision selection odds. */
|
|
29
|
+
export async function inspectDecisionAi(engine, before, id, scenarios, overlay, principal, signal) {
|
|
30
|
+
const source = selected(before, id);
|
|
31
|
+
const proposed = overlay === undefined ? undefined : selected(overlay.snapshot, id);
|
|
32
|
+
const baselineWeighted = (source?.fields.ai_will_do.length ?? 0) > 0;
|
|
33
|
+
const proposedWeighted = (proposed?.fields.ai_will_do.length ?? 0) > 0;
|
|
34
|
+
if (!baselineWeighted && !proposedWeighted)
|
|
35
|
+
return undefined;
|
|
36
|
+
const subject = source ?? proposed;
|
|
37
|
+
const adapter = subject.kind === 'mission' ? 'mission_ai_will_do' : 'decision_ai_will_do';
|
|
38
|
+
const scenarioSet = {
|
|
39
|
+
schemaVersion: '1.0',
|
|
40
|
+
id: `decision-${id}`,
|
|
41
|
+
scenarios: scenarios.map(({ closedFlags: _closedFlags, ...scenario }) => scenario),
|
|
42
|
+
};
|
|
43
|
+
const analyzer = new ProbabilityAnalyzer(engine);
|
|
44
|
+
const workspaceId = before.workspaceId;
|
|
45
|
+
const context = {
|
|
46
|
+
workspaceId,
|
|
47
|
+
adapter,
|
|
48
|
+
scenarioSet,
|
|
49
|
+
outputs: ['json'],
|
|
50
|
+
...(principal === undefined ? {} : { principal }),
|
|
51
|
+
...(signal === undefined ? {} : { signal }),
|
|
52
|
+
};
|
|
53
|
+
const sourceFile = source === undefined
|
|
54
|
+
? undefined
|
|
55
|
+
: before.files.find(({ displayPath }) => displayPath === source.path);
|
|
56
|
+
const baselineSelector = sourceFile === undefined
|
|
57
|
+
? undefined
|
|
58
|
+
: { path: sourceFile.relativePath, identifier: id, expectedSourceHash: sourceFile.sha256 };
|
|
59
|
+
const proposedFile = proposed === undefined
|
|
60
|
+
? undefined
|
|
61
|
+
: overlay?.snapshot.files.find(({ displayPath }) => displayPath === proposed.path);
|
|
62
|
+
const proposedSelector = proposedFile === undefined
|
|
63
|
+
? undefined
|
|
64
|
+
: { virtualPatch: proposedFile.bytes.toString('utf8'), identifier: id };
|
|
65
|
+
const isolatedProposal = overlay?.changes.length === 1 &&
|
|
66
|
+
proposedFile !== undefined &&
|
|
67
|
+
overlay.changes[0]?.path === proposedFile.displayPath;
|
|
68
|
+
if (baselineWeighted &&
|
|
69
|
+
proposedWeighted &&
|
|
70
|
+
isolatedProposal &&
|
|
71
|
+
baselineSelector &&
|
|
72
|
+
proposedSelector) {
|
|
73
|
+
const compared = await analyzer.compare({
|
|
74
|
+
...context,
|
|
75
|
+
before: baselineSelector,
|
|
76
|
+
after: proposedSelector,
|
|
77
|
+
});
|
|
78
|
+
return { mode: 'compare', ...summary(compared) };
|
|
79
|
+
}
|
|
80
|
+
if (baselineWeighted && baselineSelector) {
|
|
81
|
+
const evaluated = await analyzer.evaluate({ ...context, source: baselineSelector });
|
|
82
|
+
return {
|
|
83
|
+
mode: 'baseline',
|
|
84
|
+
...summary(evaluated),
|
|
85
|
+
...(overlay === undefined
|
|
86
|
+
? {}
|
|
87
|
+
: {
|
|
88
|
+
proposedUnresolved: 'The proposal changes multiple sources or adds/removes the weighted decision; use probability_compare with explicit source selectors',
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (proposedWeighted && proposedSelector) {
|
|
93
|
+
const evaluated = await analyzer.evaluate({ ...context, source: proposedSelector });
|
|
94
|
+
return {
|
|
95
|
+
mode: 'proposed',
|
|
96
|
+
...summary(evaluated),
|
|
97
|
+
baselineUnresolved: 'The baseline has no matching weighted decision',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
mode: 'unresolved',
|
|
102
|
+
reason: 'Weighted decision source could not be selected for probability analysis',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { hashCanonical } from '../core/canonical.js';
|
|
2
|
+
import { inspectDecisionCost } from './cost.js';
|
|
3
|
+
import { evaluateDecisionGates } from './evaluation.js';
|
|
4
|
+
import { inspectDecisionLifecycle } from './lifecycle.js';
|
|
5
|
+
import { decisionSourceInventory, decisionTargetCatalog, } from './source-inventory.js';
|
|
6
|
+
export function inspectDecisionScenario(snapshot, id, scenario) {
|
|
7
|
+
const source = decisionSourceInventory(snapshot).decisions.find((candidate) => candidate.id === id);
|
|
8
|
+
if (source === undefined)
|
|
9
|
+
return null;
|
|
10
|
+
return {
|
|
11
|
+
targeting: decisionTargetCatalog(source),
|
|
12
|
+
gates: evaluateDecisionGates(snapshot, id, scenario),
|
|
13
|
+
cost: inspectDecisionCost(snapshot, source, scenario),
|
|
14
|
+
lifecycle: inspectDecisionLifecycle(snapshot, source, scenario),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function differences(before, after) {
|
|
18
|
+
if (before === null || after === null)
|
|
19
|
+
return before === after ? [] : ['definition'];
|
|
20
|
+
const fields = [
|
|
21
|
+
['targeting', before.targeting, after.targeting],
|
|
22
|
+
['category_allowed', before.gates.categoryAllowed.state, after.gates.categoryAllowed.state],
|
|
23
|
+
['decision_allowed', before.gates.decisionAllowed.state, after.gates.decisionAllowed.state],
|
|
24
|
+
['category_visible', before.gates.categoryVisible.state, after.gates.categoryVisible.state],
|
|
25
|
+
[
|
|
26
|
+
'decision_visible',
|
|
27
|
+
before.gates.decisionVisible?.state ?? null,
|
|
28
|
+
after.gates.decisionVisible?.state ?? null,
|
|
29
|
+
],
|
|
30
|
+
[
|
|
31
|
+
'category_available',
|
|
32
|
+
before.gates.categoryAvailable.state,
|
|
33
|
+
after.gates.categoryAvailable.state,
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
'decision_available',
|
|
37
|
+
before.gates.decisionAvailable.state,
|
|
38
|
+
after.gates.decisionAvailable.state,
|
|
39
|
+
],
|
|
40
|
+
['eligible', before.gates.eligible, after.gates.eligible],
|
|
41
|
+
['visible', before.gates.visible, after.gates.visible],
|
|
42
|
+
['available', before.gates.available, after.gates.available],
|
|
43
|
+
['target_selection', before.gates.targetSelection.state, after.gates.targetSelection.state],
|
|
44
|
+
['target_trigger', before.gates.targetTrigger.state, after.gates.targetTrigger.state],
|
|
45
|
+
['cost_kind', before.cost.kind, after.cost.kind],
|
|
46
|
+
['cost_amount', before.cost.amount, after.cost.amount],
|
|
47
|
+
['affordable', before.cost.affordable, after.cost.affordable],
|
|
48
|
+
[
|
|
49
|
+
'payment_effects',
|
|
50
|
+
before.cost.paymentEffects.map(({ resource, amount, operation, scope }) => ({
|
|
51
|
+
resource,
|
|
52
|
+
amount,
|
|
53
|
+
operation,
|
|
54
|
+
scope,
|
|
55
|
+
})),
|
|
56
|
+
after.cost.paymentEffects.map(({ resource, amount, operation, scope }) => ({
|
|
57
|
+
resource,
|
|
58
|
+
amount,
|
|
59
|
+
operation,
|
|
60
|
+
scope,
|
|
61
|
+
})),
|
|
62
|
+
],
|
|
63
|
+
['lifecycle', before.lifecycle, after.lifecycle],
|
|
64
|
+
];
|
|
65
|
+
return fields
|
|
66
|
+
.filter(([, left, right]) => hashCanonical(left) !== hashCanonical(right))
|
|
67
|
+
.map(([field]) => field);
|
|
68
|
+
}
|
|
69
|
+
/** Compare declared scenarios against two revision-pinned source snapshots. */
|
|
70
|
+
export function compareDecisionScenarios(before, after, id, scenarios) {
|
|
71
|
+
if (before.workspaceId !== after.workspaceId)
|
|
72
|
+
throw new Error('Decision comparison crosses workspace boundaries');
|
|
73
|
+
if (scenarios.length < 1 || scenarios.length > 64)
|
|
74
|
+
throw new RangeError('Decision comparison requires 1-64 scenarios');
|
|
75
|
+
if (id.length === 0 || id.length > 512)
|
|
76
|
+
throw new RangeError('Decision identifier is invalid');
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
const cases = scenarios.map((scenario) => {
|
|
79
|
+
if (seen.has(scenario.id))
|
|
80
|
+
throw new Error(`Duplicate decision scenario: ${scenario.id}`);
|
|
81
|
+
seen.add(scenario.id);
|
|
82
|
+
const baseline = inspectDecisionScenario(before, id, scenario);
|
|
83
|
+
const proposed = inspectDecisionScenario(after, id, scenario);
|
|
84
|
+
return {
|
|
85
|
+
scenarioId: scenario.id,
|
|
86
|
+
before: baseline,
|
|
87
|
+
after: proposed,
|
|
88
|
+
changed: differences(baseline, proposed),
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
id,
|
|
93
|
+
beforeRevision: before.revision,
|
|
94
|
+
afterRevision: after.revision,
|
|
95
|
+
complete: before.complete &&
|
|
96
|
+
after.complete &&
|
|
97
|
+
cases.every(({ before: baseline, after: proposed }) => (baseline !== null || proposed !== null) &&
|
|
98
|
+
(baseline?.gates.complete ?? true) &&
|
|
99
|
+
(proposed?.gates.complete ?? true) &&
|
|
100
|
+
(baseline?.cost.unresolved.length ?? 0) === 0 &&
|
|
101
|
+
(proposed?.cost.unresolved.length ?? 0) === 0 &&
|
|
102
|
+
(baseline?.lifecycle.unresolved.length ?? 0) === 0 &&
|
|
103
|
+
(proposed?.lifecycle.unresolved.length ?? 0) === 0),
|
|
104
|
+
cases,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.js';
|
|
2
|
+
import { evaluateTriggerBlock, resolveConditionNumber } from '../core/condition-evaluator.js';
|
|
3
|
+
import { rootScopeContext } from '../core/scenario-state.js';
|
|
4
|
+
import { assignments, nodeLocation } from '../core/source/index.js';
|
|
5
|
+
function scalar(assignment) {
|
|
6
|
+
return assignment?.value.type === 'scalar' ? assignment.value.value : undefined;
|
|
7
|
+
}
|
|
8
|
+
function unresolved(source, message) {
|
|
9
|
+
return {
|
|
10
|
+
code: 'DECISION_COST_UNRESOLVED',
|
|
11
|
+
message,
|
|
12
|
+
candidateId: source.id,
|
|
13
|
+
provenance: {
|
|
14
|
+
path: source.path,
|
|
15
|
+
rootKind: source.rootKind,
|
|
16
|
+
loadOrder: source.loadOrder,
|
|
17
|
+
sourceHash: source.sourceHash,
|
|
18
|
+
...(source.location === undefined ? {} : { location: source.location }),
|
|
19
|
+
symbol: source.id,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function directPayments(snapshot, source, scenario, definitions) {
|
|
24
|
+
const paymentEffects = [];
|
|
25
|
+
const conditionalOrHelperEffects = [];
|
|
26
|
+
for (const effect of source.fields.complete_effect) {
|
|
27
|
+
if (effect.value.type !== 'block') {
|
|
28
|
+
conditionalOrHelperEffects.push('malformed complete_effect');
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
for (const entry of assignments(effect.value)) {
|
|
32
|
+
const key = entry.key.value;
|
|
33
|
+
let resource;
|
|
34
|
+
let operation;
|
|
35
|
+
let expression;
|
|
36
|
+
if (key === 'add_political_power' || key === 'add_command_power') {
|
|
37
|
+
resource = key.slice(4);
|
|
38
|
+
operation = 'add';
|
|
39
|
+
expression = scalar(entry);
|
|
40
|
+
}
|
|
41
|
+
else if (key === 'add_to_variable' || key === 'subtract_from_variable') {
|
|
42
|
+
if (entry.value.type === 'block') {
|
|
43
|
+
const values = assignments(entry.value).filter(({ key: field }) => field.value !== 'tooltip');
|
|
44
|
+
if (values.length === 1) {
|
|
45
|
+
resource = `variable.${values[0].key.value}`;
|
|
46
|
+
operation = key === 'add_to_variable' ? 'add' : 'subtract';
|
|
47
|
+
expression = scalar(values[0]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (resource !== undefined && operation !== undefined) {
|
|
52
|
+
const amount = expression === undefined
|
|
53
|
+
? undefined
|
|
54
|
+
: resolveConditionNumber(expression, scenario, rootScopeContext(scenario), definitions, source.path);
|
|
55
|
+
const normalizedOperation = amount !== undefined && amount < 0
|
|
56
|
+
? operation === 'add'
|
|
57
|
+
? 'subtract'
|
|
58
|
+
: 'add'
|
|
59
|
+
: operation;
|
|
60
|
+
paymentEffects.push({
|
|
61
|
+
resource,
|
|
62
|
+
amount: amount === undefined ? null : Math.abs(amount),
|
|
63
|
+
operation: normalizedOperation,
|
|
64
|
+
scope: 'ROOT',
|
|
65
|
+
location: nodeLocation(source.document, entry, key),
|
|
66
|
+
});
|
|
67
|
+
if (amount === undefined)
|
|
68
|
+
conditionalOrHelperEffects.push(`dynamic payment: ${key}`);
|
|
69
|
+
}
|
|
70
|
+
else if (key === 'add_political_power' ||
|
|
71
|
+
key === 'add_command_power' ||
|
|
72
|
+
key === 'add_to_variable' ||
|
|
73
|
+
key === 'subtract_from_variable') {
|
|
74
|
+
conditionalOrHelperEffects.push(`unparsed payment: ${key}`);
|
|
75
|
+
}
|
|
76
|
+
else if (snapshot.index.find('scripted_effect', key) !== undefined ||
|
|
77
|
+
['if', 'else_if', 'else', 'random', 'random_list', 'ROOT', 'FROM', 'meta_effect'].includes(key)) {
|
|
78
|
+
conditionalOrHelperEffects.push(key);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { paymentEffects, conditionalOrHelperEffects };
|
|
83
|
+
}
|
|
84
|
+
/** Cost and payment evidence is source-bounded; a custom cost is never treated as automatic payment. */
|
|
85
|
+
export function inspectDecisionCost(snapshot, source, scenario) {
|
|
86
|
+
const definitions = ClausewitzEvaluationDefinitions.build(snapshot);
|
|
87
|
+
const base = directPayments(snapshot, source, scenario, definitions);
|
|
88
|
+
const cost = source.fields.cost;
|
|
89
|
+
const custom = source.fields.custom_cost_trigger;
|
|
90
|
+
const findings = [];
|
|
91
|
+
const deductions = base.paymentEffects.filter(({ operation }) => operation === 'subtract');
|
|
92
|
+
const seenDeductions = new Set();
|
|
93
|
+
for (const deduction of deductions) {
|
|
94
|
+
if (seenDeductions.has(deduction.resource))
|
|
95
|
+
findings.push(unresolved(source, `Multiple direct payments deduct ${deduction.resource}`));
|
|
96
|
+
seenDeductions.add(deduction.resource);
|
|
97
|
+
}
|
|
98
|
+
if (cost.length > 1 || custom.length > 1 || (cost.length > 0 && custom.length > 0)) {
|
|
99
|
+
findings.push(unresolved(source, 'Conflicting or repeated decision cost declarations'));
|
|
100
|
+
return {
|
|
101
|
+
kind: 'invalid',
|
|
102
|
+
expression: null,
|
|
103
|
+
amount: null,
|
|
104
|
+
affordable: 'unresolved',
|
|
105
|
+
...base,
|
|
106
|
+
unresolved: findings,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (cost.length === 1) {
|
|
110
|
+
if (deductions.some(({ resource }) => resource === 'political_power'))
|
|
111
|
+
findings.push(unresolved(source, 'Engine political-power cost also has an explicit political-power deduction'));
|
|
112
|
+
const expression = scalar(cost[0]);
|
|
113
|
+
const amount = expression === undefined
|
|
114
|
+
? undefined
|
|
115
|
+
: resolveConditionNumber(expression, scenario, rootScopeContext(scenario), definitions, source.path);
|
|
116
|
+
const politicalPower = resolveConditionNumber('political_power', scenario, rootScopeContext(scenario), definitions, source.path);
|
|
117
|
+
if (amount === undefined || amount < 0)
|
|
118
|
+
findings.push(unresolved(source, 'Engine political-power cost is not a resolved nonnegative number'));
|
|
119
|
+
if (politicalPower === undefined)
|
|
120
|
+
findings.push(unresolved(source, 'Scenario does not declare current political power'));
|
|
121
|
+
return {
|
|
122
|
+
kind: 'engine_political_power',
|
|
123
|
+
expression: expression ?? null,
|
|
124
|
+
amount: amount ?? null,
|
|
125
|
+
affordable: amount === undefined || amount < 0 || politicalPower === undefined
|
|
126
|
+
? 'unresolved'
|
|
127
|
+
: politicalPower >= amount
|
|
128
|
+
? 'true'
|
|
129
|
+
: 'false',
|
|
130
|
+
...base,
|
|
131
|
+
unresolved: findings,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (custom.length === 1) {
|
|
135
|
+
const trigger = custom[0].value;
|
|
136
|
+
const evaluated = trigger.type === 'block'
|
|
137
|
+
? evaluateTriggerBlock(trigger, { ...scenario, closedFlags: scenario.closedFlags ?? false }, {
|
|
138
|
+
id: source.id,
|
|
139
|
+
document: source.document,
|
|
140
|
+
provenance: [
|
|
141
|
+
{
|
|
142
|
+
path: source.path,
|
|
143
|
+
rootKind: source.rootKind,
|
|
144
|
+
loadOrder: source.loadOrder,
|
|
145
|
+
sourceHash: source.sourceHash,
|
|
146
|
+
...(source.location === undefined ? {} : { location: source.location }),
|
|
147
|
+
symbol: source.id,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
}, definitions)
|
|
151
|
+
: {
|
|
152
|
+
state: 'unresolved',
|
|
153
|
+
unresolved: [unresolved(source, 'custom_cost_trigger is not a block')],
|
|
154
|
+
};
|
|
155
|
+
findings.push(...evaluated.unresolved);
|
|
156
|
+
if (base.conditionalOrHelperEffects.length > 0)
|
|
157
|
+
findings.push(unresolved(source, 'Custom payment crosses a conditional, helper, dynamic, or unparsed effect path'));
|
|
158
|
+
if (base.paymentEffects.length === 0 && base.conditionalOrHelperEffects.length === 0)
|
|
159
|
+
findings.push(unresolved(source, 'Custom cost has no recognized direct payment effect; other effect semantics remain unverified'));
|
|
160
|
+
return {
|
|
161
|
+
kind: 'custom',
|
|
162
|
+
expression: scalar(source.fields.custom_cost_text[0]) ?? null,
|
|
163
|
+
amount: null,
|
|
164
|
+
affordable: evaluated.state,
|
|
165
|
+
...base,
|
|
166
|
+
unresolved: findings,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
kind: 'none',
|
|
171
|
+
expression: null,
|
|
172
|
+
amount: 0,
|
|
173
|
+
affordable: 'true',
|
|
174
|
+
...base,
|
|
175
|
+
unresolved: findings,
|
|
176
|
+
};
|
|
177
|
+
}
|