hoi4-agent-tools 3.0.9 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/README.md +10 -2
  3. package/dist/bin/stdio.js +23 -12
  4. package/dist/hoi4_agent_tools/core/analysis-checkpoints.js +82 -0
  5. package/dist/hoi4_agent_tools/core/analysis-evidence.js +9 -0
  6. package/dist/hoi4_agent_tools/core/analysis-job-operations.js +38 -0
  7. package/dist/hoi4_agent_tools/core/artifact-resource.js +173 -0
  8. package/dist/hoi4_agent_tools/core/artifacts.js +84 -2
  9. package/dist/hoi4_agent_tools/core/condition-evaluator.js +16 -0
  10. package/dist/hoi4_agent_tools/core/dependency-pages.js +216 -0
  11. package/dist/hoi4_agent_tools/core/dependency-walk.js +266 -0
  12. package/dist/hoi4_agent_tools/core/domain-validation.js +188 -0
  13. package/dist/hoi4_agent_tools/core/engine.js +48 -5
  14. package/dist/hoi4_agent_tools/core/helper-expansion.js +186 -0
  15. package/dist/hoi4_agent_tools/core/impact-comparison.js +49 -0
  16. package/dist/hoi4_agent_tools/core/impact-graph.js +301 -0
  17. package/dist/hoi4_agent_tools/core/impact-semantic.js +256 -0
  18. package/dist/hoi4_agent_tools/core/index-segments.js +137 -0
  19. package/dist/hoi4_agent_tools/core/index.js +47 -4
  20. package/dist/hoi4_agent_tools/core/job-controls.js +109 -0
  21. package/dist/hoi4_agent_tools/core/job-executor.js +190 -0
  22. package/dist/hoi4_agent_tools/core/job-service.js +112 -0
  23. package/dist/hoi4_agent_tools/core/job-store.js +641 -0
  24. package/dist/hoi4_agent_tools/core/job-transactions.js +126 -0
  25. package/dist/hoi4_agent_tools/core/job-worker-host.js +202 -0
  26. package/dist/hoi4_agent_tools/core/job-worker.js +64 -0
  27. package/dist/hoi4_agent_tools/core/operation-context.js +31 -0
  28. package/dist/hoi4_agent_tools/core/operation-result.js +412 -0
  29. package/dist/hoi4_agent_tools/core/operation-tasks.js +387 -0
  30. package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +383 -0
  31. package/dist/hoi4_agent_tools/core/result.js +1 -0
  32. package/dist/hoi4_agent_tools/core/scanner.js +15 -75
  33. package/dist/hoi4_agent_tools/core/semantic-dependencies.js +233 -0
  34. package/dist/hoi4_agent_tools/core/shared-request-capacity.js +43 -11
  35. package/dist/hoi4_agent_tools/core/source/cache.js +107 -0
  36. package/dist/hoi4_agent_tools/core/source/parser.js +11 -1
  37. package/dist/hoi4_agent_tools/core/source-overlay.js +125 -0
  38. package/dist/hoi4_agent_tools/core/transaction-execution.js +134 -0
  39. package/dist/hoi4_agent_tools/core/transactions.js +80 -27
  40. package/dist/hoi4_agent_tools/decision/ai.js +104 -0
  41. package/dist/hoi4_agent_tools/decision/comparison.js +106 -0
  42. package/dist/hoi4_agent_tools/decision/cost.js +177 -0
  43. package/dist/hoi4_agent_tools/decision/evaluation.js +256 -0
  44. package/dist/hoi4_agent_tools/decision/lifecycle.js +114 -0
  45. package/dist/hoi4_agent_tools/decision/service.js +172 -0
  46. package/dist/hoi4_agent_tools/decision/source-inventory.js +182 -0
  47. package/dist/hoi4_agent_tools/event/graph.js +214 -290
  48. package/dist/hoi4_agent_tools/event/helper-expansion.js +33 -0
  49. package/dist/hoi4_agent_tools/event/job-operations.js +51 -0
  50. package/dist/hoi4_agent_tools/event/operations.js +179 -0
  51. package/dist/hoi4_agent_tools/event/service.js +67 -15
  52. package/dist/hoi4_agent_tools/event/source-analysis.js +5 -4
  53. package/dist/hoi4_agent_tools/focus/job-operations.js +77 -0
  54. package/dist/hoi4_agent_tools/focus/tool-operations.js +669 -0
  55. package/dist/hoi4_agent_tools/focus/tool-support.js +123 -0
  56. package/dist/hoi4_agent_tools/gui/job-operations.js +81 -0
  57. package/dist/hoi4_agent_tools/gui/tool-operations.js +270 -0
  58. package/dist/hoi4_agent_tools/impact/scenario-suites.js +74 -0
  59. package/dist/hoi4_agent_tools/impact/service.js +127 -0
  60. package/dist/hoi4_agent_tools/map/job-operations.js +76 -0
  61. package/dist/hoi4_agent_tools/map/tool-operations.js +493 -0
  62. package/dist/hoi4_agent_tools/mcp/prompts/probability.js +25 -0
  63. package/dist/hoi4_agent_tools/mcp/prompts/register-modern.js +29 -0
  64. package/dist/hoi4_agent_tools/mcp/resources/register-modern.js +34 -0
  65. package/dist/hoi4_agent_tools/mcp/resources/register.js +4 -150
  66. package/dist/hoi4_agent_tools/mcp/server/base-tools.js +3 -219
  67. package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +188 -0
  68. package/dist/hoi4_agent_tools/mcp/server/create.js +18 -6
  69. package/dist/hoi4_agent_tools/mcp/server/instructions.js +2 -0
  70. package/dist/hoi4_agent_tools/mcp/server/job-task-store.js +67 -0
  71. package/dist/hoi4_agent_tools/mcp/server/legacy-task-tools.js +20 -0
  72. package/dist/hoi4_agent_tools/mcp/server/modern-tasks.js +168 -0
  73. package/dist/hoi4_agent_tools/mcp/server/progress.js +17 -15
  74. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +45 -0
  75. package/dist/hoi4_agent_tools/mcp/server/result.js +2 -412
  76. package/dist/hoi4_agent_tools/mcp/server/task-request-context.js +9 -0
  77. package/dist/hoi4_agent_tools/mcp/server/task-tool-catalog.js +16 -0
  78. package/dist/hoi4_agent_tools/mcp/server/task-tool-definition.js +1 -0
  79. package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js +1 -123
  80. package/dist/hoi4_agent_tools/mcp/tools/analysis.js +65 -0
  81. package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +38 -21
  82. package/dist/hoi4_agent_tools/mcp/tools/event.js +24 -315
  83. package/dist/hoi4_agent_tools/mcp/tools/focus.js +34 -1039
  84. package/dist/hoi4_agent_tools/mcp/tools/gui.js +19 -504
  85. package/dist/hoi4_agent_tools/mcp/tools/job.js +46 -0
  86. package/dist/hoi4_agent_tools/mcp/tools/map.js +21 -600
  87. package/dist/hoi4_agent_tools/mcp/tools/probability.js +39 -463
  88. package/dist/hoi4_agent_tools/mcp/tools/technology.js +40 -304
  89. package/dist/hoi4_agent_tools/mcp/transports/http.js +58 -1
  90. package/dist/hoi4_agent_tools/mcp/transports/modern-task-routing.js +109 -0
  91. package/dist/hoi4_agent_tools/mcp/transports/negotiated-stdio.js +121 -0
  92. package/dist/hoi4_agent_tools/probability/job-operations.js +58 -0
  93. package/dist/hoi4_agent_tools/probability/operations.js +183 -0
  94. package/dist/hoi4_agent_tools/probability/service.js +24 -3
  95. package/dist/hoi4_agent_tools/schemas/analysis.js +228 -0
  96. package/dist/hoi4_agent_tools/schemas/common.js +2 -0
  97. package/dist/hoi4_agent_tools/schemas/event.js +72 -1
  98. package/dist/hoi4_agent_tools/schemas/focus-requests.js +189 -0
  99. package/dist/hoi4_agent_tools/schemas/gui-requests.js +193 -0
  100. package/dist/hoi4_agent_tools/schemas/helper-expansion.js +58 -0
  101. package/dist/hoi4_agent_tools/schemas/map-requests.js +85 -0
  102. package/dist/hoi4_agent_tools/schemas/probability-requests.js +168 -0
  103. package/dist/hoi4_agent_tools/schemas/technology.js +90 -1
  104. package/dist/hoi4_agent_tools/schemas/transaction.js +1 -0
  105. package/dist/hoi4_agent_tools/technology/graph.js +83 -78
  106. package/dist/hoi4_agent_tools/technology/helper-expansion.js +33 -0
  107. package/dist/hoi4_agent_tools/technology/job-operations.js +60 -0
  108. package/dist/hoi4_agent_tools/technology/operations.js +125 -0
  109. package/dist/hoi4_agent_tools/technology/service.js +89 -15
  110. package/dist/hoi4_agent_tools/technology/source-analysis.js +2 -1
  111. package/dist/hoi4_agent_tools/version.js +1 -1
  112. package/docs/README.md +3 -0
  113. package/docs/analysis.md +100 -0
  114. package/docs/development.md +2 -0
  115. package/docs/events.md +7 -2
  116. package/docs/helper-expansion.md +78 -0
  117. package/docs/http.md +2 -0
  118. package/docs/jobs.md +86 -0
  119. package/docs/technology.md +6 -2
  120. package/package.json +15 -6
  121. package/schemas/configuration.schema.json +1 -1
  122. package/schemas/continuous-focus-palette.schema.json +1 -1
  123. package/schemas/custom-weighted-pool.schema.json +1 -1
  124. package/schemas/decision-inspect-input.schema.json +290 -0
  125. package/schemas/event-feature-manifest.schema.json +1 -1
  126. package/schemas/focus-plan.schema.json +1 -1
  127. package/schemas/focus-planning-sidecar.schema.json +1 -1
  128. package/schemas/gui-animation-source.schema.json +1 -1
  129. package/schemas/gui-helper.schema.json +1 -1
  130. package/schemas/gui-scenario.schema.json +1 -1
  131. package/schemas/helper-expansion-request.schema.json +44 -0
  132. package/schemas/helper-expansion-summary.schema.json +70 -0
  133. package/schemas/impact-inspect-input.schema.json +203 -0
  134. package/schemas/map-operation.schema.json +1 -1
  135. package/schemas/operation-result.schema.json +1 -1
  136. package/schemas/probability-analysis-result.schema.json +1 -1
  137. package/schemas/probability-compare-input.schema.json +1 -1
  138. package/schemas/probability-evaluate-input.schema.json +1 -1
  139. package/schemas/probability-inspect-input.schema.json +1 -1
  140. package/schemas/probability-render-input.schema.json +1 -1
  141. package/schemas/probability-scenario-set.schema.json +1 -1
  142. package/schemas/probability-sequence-input.schema.json +1 -1
  143. package/schemas/probability-simulate-input.schema.json +1 -1
  144. package/schemas/probability-sweep-input.schema.json +1 -1
  145. package/server.json +3 -3
@@ -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
- throw new ServiceError('TRANSACTION_STRUCTURE_LIMIT', 'Post-write validation exceeds the supported transaction review structure limits');
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: 'reconcile',
601
+ headMode: 'none',
540
602
  ...(signal === undefined ? {} : { signal }),
541
603
  });
542
- if (principal !== undefined && (manifest.principal ?? null) !== principal) {
543
- throw new ServiceError('TRANSACTION_PRINCIPAL_MISMATCH', 'Transaction recovery belongs to another principal');
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
- await this.restoreBeforeState(workspace, manifest, principal);
553
- manifest.state = 'rolled_back';
554
- manifest.failure = {
555
- code: 'TRANSACTION_RECOVERED',
556
- message: 'Incomplete transaction was rolled back during recovery',
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
- manifest.rollbackStatus = 'failed';
562
- manifest.state = 'failed';
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
- recovered.push(manifest);
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
+ }