magi-ai 0.1.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 (300) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +377 -0
  3. package/README.md +377 -0
  4. package/dist/bin/magi-benchmark.d.ts +14 -0
  5. package/dist/bin/magi-benchmark.js +93 -0
  6. package/dist/bin/magi-mcp.d.ts +8 -0
  7. package/dist/bin/magi-mcp.js +28 -0
  8. package/dist/bin/magi.d.ts +2 -0
  9. package/dist/bin/magi.js +634 -0
  10. package/dist/src/adapters/base.d.ts +34 -0
  11. package/dist/src/adapters/base.js +149 -0
  12. package/dist/src/adapters/claude.d.ts +29 -0
  13. package/dist/src/adapters/claude.js +65 -0
  14. package/dist/src/adapters/codex.d.ts +21 -0
  15. package/dist/src/adapters/codex.js +41 -0
  16. package/dist/src/adapters/gemini.d.ts +18 -0
  17. package/dist/src/adapters/gemini.js +31 -0
  18. package/dist/src/adapters/registry.d.ts +19 -0
  19. package/dist/src/adapters/registry.js +59 -0
  20. package/dist/src/audit/hash-chain.d.ts +21 -0
  21. package/dist/src/audit/hash-chain.js +70 -0
  22. package/dist/src/audit/types.d.ts +25 -0
  23. package/dist/src/audit/types.js +1 -0
  24. package/dist/src/audit/writer.d.ts +18 -0
  25. package/dist/src/audit/writer.js +100 -0
  26. package/dist/src/benchmark/golden-tasks.d.ts +9 -0
  27. package/dist/src/benchmark/golden-tasks.js +476 -0
  28. package/dist/src/benchmark/reporter.d.ts +5 -0
  29. package/dist/src/benchmark/reporter.js +107 -0
  30. package/dist/src/benchmark/runner.d.ts +30 -0
  31. package/dist/src/benchmark/runner.js +224 -0
  32. package/dist/src/benchmark/scorer.d.ts +12 -0
  33. package/dist/src/benchmark/scorer.js +124 -0
  34. package/dist/src/benchmark/types.d.ts +54 -0
  35. package/dist/src/benchmark/types.js +1 -0
  36. package/dist/src/cache/deliberation-cache.d.ts +49 -0
  37. package/dist/src/cache/deliberation-cache.js +127 -0
  38. package/dist/src/cli/commands/config-cmd.d.ts +11 -0
  39. package/dist/src/cli/commands/config-cmd.js +190 -0
  40. package/dist/src/cli/commands/demo.d.ts +12 -0
  41. package/dist/src/cli/commands/demo.js +66 -0
  42. package/dist/src/cli/commands/setup.d.ts +7 -0
  43. package/dist/src/cli/commands/setup.js +182 -0
  44. package/dist/src/cli/i18n.d.ts +89 -0
  45. package/dist/src/cli/i18n.js +176 -0
  46. package/dist/src/cli/interactive-select.d.ts +27 -0
  47. package/dist/src/cli/interactive-select.js +130 -0
  48. package/dist/src/cli/tui-setup.d.ts +24 -0
  49. package/dist/src/cli/tui-setup.js +42 -0
  50. package/dist/src/config/cli-detector.d.ts +37 -0
  51. package/dist/src/config/cli-detector.js +99 -0
  52. package/dist/src/config/user-config.d.ts +81 -0
  53. package/dist/src/config/user-config.js +134 -0
  54. package/dist/src/context/auto-collector.d.ts +43 -0
  55. package/dist/src/context/auto-collector.js +337 -0
  56. package/dist/src/context/manager.d.ts +35 -0
  57. package/dist/src/context/manager.js +162 -0
  58. package/dist/src/context/serializer.d.ts +20 -0
  59. package/dist/src/context/serializer.js +52 -0
  60. package/dist/src/demo/recorded-deliberation.d.ts +13 -0
  61. package/dist/src/demo/recorded-deliberation.js +277 -0
  62. package/dist/src/engine/angel-detector.d.ts +83 -0
  63. package/dist/src/engine/angel-detector.js +334 -0
  64. package/dist/src/engine/at-field.d.ts +40 -0
  65. package/dist/src/engine/at-field.js +195 -0
  66. package/dist/src/engine/berserk-orchestrator.d.ts +66 -0
  67. package/dist/src/engine/berserk-orchestrator.js +378 -0
  68. package/dist/src/engine/change-metrics.d.ts +56 -0
  69. package/dist/src/engine/change-metrics.js +214 -0
  70. package/dist/src/engine/consensus.d.ts +20 -0
  71. package/dist/src/engine/consensus.js +146 -0
  72. package/dist/src/engine/dead-sea-scrolls.d.ts +132 -0
  73. package/dist/src/engine/dead-sea-scrolls.js +610 -0
  74. package/dist/src/engine/drift-detector.d.ts +39 -0
  75. package/dist/src/engine/drift-detector.js +225 -0
  76. package/dist/src/engine/dummy-plug.d.ts +44 -0
  77. package/dist/src/engine/dummy-plug.js +190 -0
  78. package/dist/src/engine/engram-manager.d.ts +55 -0
  79. package/dist/src/engine/engram-manager.js +306 -0
  80. package/dist/src/engine/events.d.ts +130 -0
  81. package/dist/src/engine/events.js +44 -0
  82. package/dist/src/engine/gospel.d.ts +30 -0
  83. package/dist/src/engine/gospel.js +129 -0
  84. package/dist/src/engine/hallucination-detector.d.ts +33 -0
  85. package/dist/src/engine/hallucination-detector.js +215 -0
  86. package/dist/src/engine/human-resolver.d.ts +19 -0
  87. package/dist/src/engine/human-resolver.js +89 -0
  88. package/dist/src/engine/instrumentality.d.ts +64 -0
  89. package/dist/src/engine/instrumentality.js +297 -0
  90. package/dist/src/engine/iruel-battle.d.ts +79 -0
  91. package/dist/src/engine/iruel-battle.js +319 -0
  92. package/dist/src/engine/kernel/deliberation-kernel.d.ts +12 -0
  93. package/dist/src/engine/kernel/deliberation-kernel.js +303 -0
  94. package/dist/src/engine/kernel/index.d.ts +8 -0
  95. package/dist/src/engine/kernel/index.js +7 -0
  96. package/dist/src/engine/kernel/phase-runner.d.ts +10 -0
  97. package/dist/src/engine/kernel/phase-runner.js +155 -0
  98. package/dist/src/engine/kernel/post-processor.d.ts +17 -0
  99. package/dist/src/engine/kernel/post-processor.js +131 -0
  100. package/dist/src/engine/kernel/types.d.ts +107 -0
  101. package/dist/src/engine/kernel/types.js +1 -0
  102. package/dist/src/engine/kernel/unit-executor.d.ts +6 -0
  103. package/dist/src/engine/kernel/unit-executor.js +132 -0
  104. package/dist/src/engine/lcl-manager.d.ts +44 -0
  105. package/dist/src/engine/lcl-manager.js +143 -0
  106. package/dist/src/engine/middleware/cache.d.ts +7 -0
  107. package/dist/src/engine/middleware/cache.js +29 -0
  108. package/dist/src/engine/middleware/chain.d.ts +18 -0
  109. package/dist/src/engine/middleware/chain.js +45 -0
  110. package/dist/src/engine/middleware/firewall.d.ts +8 -0
  111. package/dist/src/engine/middleware/firewall.js +24 -0
  112. package/dist/src/engine/middleware/index.d.ts +4 -0
  113. package/dist/src/engine/middleware/index.js +3 -0
  114. package/dist/src/engine/middleware/types.d.ts +43 -0
  115. package/dist/src/engine/middleware/types.js +1 -0
  116. package/dist/src/engine/nebuchadnezzar-key.d.ts +61 -0
  117. package/dist/src/engine/nebuchadnezzar-key.js +203 -0
  118. package/dist/src/engine/neon-genesis.d.ts +52 -0
  119. package/dist/src/engine/neon-genesis.js +203 -0
  120. package/dist/src/engine/objective-judge.d.ts +53 -0
  121. package/dist/src/engine/objective-judge.js +214 -0
  122. package/dist/src/engine/offline-mode.d.ts +18 -0
  123. package/dist/src/engine/offline-mode.js +46 -0
  124. package/dist/src/engine/orchestrator.d.ts +79 -0
  125. package/dist/src/engine/orchestrator.js +58 -0
  126. package/dist/src/engine/secret-cipher.d.ts +26 -0
  127. package/dist/src/engine/secret-cipher.js +114 -0
  128. package/dist/src/engine/seele-council.d.ts +90 -0
  129. package/dist/src/engine/seele-council.js +482 -0
  130. package/dist/src/engine/self-destruct.d.ts +61 -0
  131. package/dist/src/engine/self-destruct.js +231 -0
  132. package/dist/src/engine/self-evolution.d.ts +64 -0
  133. package/dist/src/engine/self-evolution.js +368 -0
  134. package/dist/src/engine/sync-rate.d.ts +45 -0
  135. package/dist/src/engine/sync-rate.js +151 -0
  136. package/dist/src/engine/type666-firewall.d.ts +76 -0
  137. package/dist/src/engine/type666-firewall.js +343 -0
  138. package/dist/src/engine/umbilical-cable.d.ts +41 -0
  139. package/dist/src/engine/umbilical-cable.js +192 -0
  140. package/dist/src/index.d.ts +106 -0
  141. package/dist/src/index.js +426 -0
  142. package/dist/src/mcp/server.d.ts +38 -0
  143. package/dist/src/mcp/server.js +196 -0
  144. package/dist/src/metrics/token-tracker.d.ts +38 -0
  145. package/dist/src/metrics/token-tracker.js +112 -0
  146. package/dist/src/parsers/json-extractor.d.ts +9 -0
  147. package/dist/src/parsers/json-extractor.js +239 -0
  148. package/dist/src/parsers/opinion-schema.d.ts +81 -0
  149. package/dist/src/parsers/opinion-schema.js +147 -0
  150. package/dist/src/parsers/unstructured-parser.d.ts +20 -0
  151. package/dist/src/parsers/unstructured-parser.js +122 -0
  152. package/dist/src/pipelines/architecture.d.ts +10 -0
  153. package/dist/src/pipelines/architecture.js +9 -0
  154. package/dist/src/pipelines/bug-analysis.d.ts +9 -0
  155. package/dist/src/pipelines/bug-analysis.js +8 -0
  156. package/dist/src/pipelines/code-review.d.ts +10 -0
  157. package/dist/src/pipelines/code-review.js +30 -0
  158. package/dist/src/pipelines/custom.d.ts +14 -0
  159. package/dist/src/pipelines/custom.js +29 -0
  160. package/dist/src/pipelines/registry.d.ts +9 -0
  161. package/dist/src/pipelines/registry.js +20 -0
  162. package/dist/src/prompts/personas.d.ts +6 -0
  163. package/dist/src/prompts/personas.js +44 -0
  164. package/dist/src/prompts/schemas.d.ts +4 -0
  165. package/dist/src/prompts/schemas.js +24 -0
  166. package/dist/src/prompts/templates.d.ts +6 -0
  167. package/dist/src/prompts/templates.js +91 -0
  168. package/dist/src/repl/accessibility.d.ts +23 -0
  169. package/dist/src/repl/accessibility.js +46 -0
  170. package/dist/src/repl/banner.d.ts +4 -0
  171. package/dist/src/repl/banner.js +28 -0
  172. package/dist/src/repl/boot-animation.d.ts +13 -0
  173. package/dist/src/repl/boot-animation.js +143 -0
  174. package/dist/src/repl/completer.d.ts +21 -0
  175. package/dist/src/repl/completer.js +168 -0
  176. package/dist/src/repl/context.d.ts +24 -0
  177. package/dist/src/repl/context.js +42 -0
  178. package/dist/src/repl/display-utils.d.ts +13 -0
  179. package/dist/src/repl/display-utils.js +65 -0
  180. package/dist/src/repl/event-listener.d.ts +18 -0
  181. package/dist/src/repl/event-listener.js +112 -0
  182. package/dist/src/repl/export-formatter.d.ts +8 -0
  183. package/dist/src/repl/export-formatter.js +73 -0
  184. package/dist/src/repl/ghost-text.d.ts +31 -0
  185. package/dist/src/repl/ghost-text.js +119 -0
  186. package/dist/src/repl/handoff-animation.d.ts +15 -0
  187. package/dist/src/repl/handoff-animation.js +65 -0
  188. package/dist/src/repl/history.d.ts +16 -0
  189. package/dist/src/repl/history.js +130 -0
  190. package/dist/src/repl/job-registry.d.ts +26 -0
  191. package/dist/src/repl/job-registry.js +80 -0
  192. package/dist/src/repl/magi-repl.d.ts +72 -0
  193. package/dist/src/repl/magi-repl.js +1008 -0
  194. package/dist/src/repl/multiline-input.d.ts +45 -0
  195. package/dist/src/repl/multiline-input.js +78 -0
  196. package/dist/src/repl/prompt-builder.d.ts +19 -0
  197. package/dist/src/repl/prompt-builder.js +36 -0
  198. package/dist/src/repl/repl-state.d.ts +5 -0
  199. package/dist/src/repl/repl-state.js +19 -0
  200. package/dist/src/repl/result-display.d.ts +8 -0
  201. package/dist/src/repl/result-display.js +195 -0
  202. package/dist/src/repl/session-stats.d.ts +26 -0
  203. package/dist/src/repl/session-stats.js +119 -0
  204. package/dist/src/repl/slash-commands.d.ts +60 -0
  205. package/dist/src/repl/slash-commands.js +725 -0
  206. package/dist/src/repl/terminal-sanitize.d.ts +14 -0
  207. package/dist/src/repl/terminal-sanitize.js +19 -0
  208. package/dist/src/reporters/console.d.ts +7 -0
  209. package/dist/src/reporters/console.js +78 -0
  210. package/dist/src/reporters/json.d.ts +2 -0
  211. package/dist/src/reporters/json.js +3 -0
  212. package/dist/src/reporters/markdown.d.ts +2 -0
  213. package/dist/src/reporters/markdown.js +65 -0
  214. package/dist/src/reporters/streaming.d.ts +20 -0
  215. package/dist/src/reporters/streaming.js +178 -0
  216. package/dist/src/tui/activity-log.d.ts +23 -0
  217. package/dist/src/tui/activity-log.js +67 -0
  218. package/dist/src/tui/animations.d.ts +39 -0
  219. package/dist/src/tui/animations.js +167 -0
  220. package/dist/src/tui/ansi.d.ts +28 -0
  221. package/dist/src/tui/ansi.js +51 -0
  222. package/dist/src/tui/boot-sequence.d.ts +11 -0
  223. package/dist/src/tui/boot-sequence.js +98 -0
  224. package/dist/src/tui/colors.d.ts +101 -0
  225. package/dist/src/tui/colors.js +71 -0
  226. package/dist/src/tui/header.d.ts +24 -0
  227. package/dist/src/tui/header.js +122 -0
  228. package/dist/src/tui/index.d.ts +3 -0
  229. package/dist/src/tui/index.js +3 -0
  230. package/dist/src/tui/keypress.d.ts +25 -0
  231. package/dist/src/tui/keypress.js +95 -0
  232. package/dist/src/tui/layout.d.ts +74 -0
  233. package/dist/src/tui/layout.js +171 -0
  234. package/dist/src/tui/magi-tui.d.ts +101 -0
  235. package/dist/src/tui/magi-tui.js +754 -0
  236. package/dist/src/tui/panel.d.ts +45 -0
  237. package/dist/src/tui/panel.js +292 -0
  238. package/dist/src/tui/screen-buffer.d.ts +54 -0
  239. package/dist/src/tui/screen-buffer.js +262 -0
  240. package/dist/src/tui/status-bar.d.ts +25 -0
  241. package/dist/src/tui/status-bar.js +124 -0
  242. package/dist/src/tui/terminal-detect.d.ts +26 -0
  243. package/dist/src/tui/terminal-detect.js +44 -0
  244. package/dist/src/tui/tui-helpers.d.ts +12 -0
  245. package/dist/src/tui/tui-helpers.js +37 -0
  246. package/dist/src/types/adapter.d.ts +75 -0
  247. package/dist/src/types/adapter.js +36 -0
  248. package/dist/src/types/config.d.ts +108 -0
  249. package/dist/src/types/config.js +85 -0
  250. package/dist/src/types/consensus.d.ts +55 -0
  251. package/dist/src/types/consensus.js +17 -0
  252. package/dist/src/types/core.d.ts +178 -0
  253. package/dist/src/types/core.js +85 -0
  254. package/dist/src/types/magi-api.d.ts +62 -0
  255. package/dist/src/types/magi-api.js +7 -0
  256. package/dist/src/types/phase-h.d.ts +142 -0
  257. package/dist/src/types/phase-h.js +7 -0
  258. package/dist/src/types/phase-i.d.ts +186 -0
  259. package/dist/src/types/phase-i.js +6 -0
  260. package/dist/src/types/phase-k.d.ts +259 -0
  261. package/dist/src/types/phase-k.js +6 -0
  262. package/dist/src/types/phase-l.d.ts +199 -0
  263. package/dist/src/types/phase-l.js +6 -0
  264. package/dist/src/types/pipeline.d.ts +37 -0
  265. package/dist/src/types/pipeline.js +2 -0
  266. package/dist/src/utils/abstain-factory.d.ts +2 -0
  267. package/dist/src/utils/abstain-factory.js +18 -0
  268. package/dist/src/utils/errors.d.ts +34 -0
  269. package/dist/src/utils/errors.js +59 -0
  270. package/dist/src/utils/file-validator.d.ts +50 -0
  271. package/dist/src/utils/file-validator.js +124 -0
  272. package/dist/src/utils/fire-and-forget.d.ts +5 -0
  273. package/dist/src/utils/fire-and-forget.js +10 -0
  274. package/dist/src/utils/flag-validator.d.ts +21 -0
  275. package/dist/src/utils/flag-validator.js +79 -0
  276. package/dist/src/utils/freeze.d.ts +8 -0
  277. package/dist/src/utils/freeze.js +16 -0
  278. package/dist/src/utils/language-detector.d.ts +16 -0
  279. package/dist/src/utils/language-detector.js +159 -0
  280. package/dist/src/utils/latency-tracker.d.ts +45 -0
  281. package/dist/src/utils/latency-tracker.js +100 -0
  282. package/dist/src/utils/logger.d.ts +33 -0
  283. package/dist/src/utils/logger.js +112 -0
  284. package/dist/src/utils/process.d.ts +40 -0
  285. package/dist/src/utils/process.js +253 -0
  286. package/dist/src/utils/retry.d.ts +12 -0
  287. package/dist/src/utils/retry.js +30 -0
  288. package/dist/src/utils/safe-fs.d.ts +38 -0
  289. package/dist/src/utils/safe-fs.js +56 -0
  290. package/dist/src/utils/safe-json-parse.d.ts +15 -0
  291. package/dist/src/utils/safe-json-parse.js +49 -0
  292. package/dist/src/utils/sanitize.d.ts +14 -0
  293. package/dist/src/utils/sanitize.js +186 -0
  294. package/dist/src/utils/semaphore.d.ts +22 -0
  295. package/dist/src/utils/semaphore.js +57 -0
  296. package/dist/src/utils/shutdown.d.ts +6 -0
  297. package/dist/src/utils/shutdown.js +51 -0
  298. package/dist/src/utils/tty.d.ts +5 -0
  299. package/dist/src/utils/tty.js +7 -0
  300. package/package.json +82 -0
@@ -0,0 +1,297 @@
1
+ /**
2
+ * A-05 補完計画 (Instrumentality) — Deadlock Fusion Engine
3
+ *
4
+ * When deliberation reaches a deadlock after 3+ rounds, this engine
5
+ * fuses all unit opinions into a single synthesized assessment.
6
+ *
7
+ * The fusion process:
8
+ * 1. Detect deadlock conditions (shouldTrigger)
9
+ * 2. Build a weighted fusion prompt from all opinions
10
+ * 3. Execute via the primary adapter (MELCHIOR)
11
+ * 4. Parse and validate the fused opinion
12
+ * 5. Apply confidence discount (0.85x) since the opinion is derived
13
+ *
14
+ * Emits: 'instrumentality:triggered', 'fusion:complete'
15
+ */
16
+ import { SafeOpinionSchema } from '../parsers/opinion-schema.js';
17
+ import { extractJson } from '../parsers/json-extractor.js';
18
+ import { estimateTokens } from '../metrics/token-tracker.js';
19
+ import { createAbstainOpinion } from '../utils/abstain-factory.js';
20
+ import { logger } from '../utils/logger.js';
21
+ import { OPINION_JSON_SCHEMA } from '../types/pipeline.js';
22
+ // ── Constants ────────────────────────────────────────────────────
23
+ /** Default fusion weights for the classic 3-body configuration */
24
+ const DEFAULT_FUSION_WEIGHTS = {
25
+ MELCHIOR: 0.40,
26
+ BALTHASAR: 0.35,
27
+ CASPER: 0.25,
28
+ };
29
+ /** Confidence discount applied to fused opinions (derived, not direct) */
30
+ const CONFIDENCE_DISCOUNT = 0.85;
31
+ /** Minimum rounds before Instrumentality can trigger */
32
+ const MIN_ROUNDS_FOR_TRIGGER = 3;
33
+ // ── InstrumentalityEngine ────────────────────────────────────────
34
+ export class InstrumentalityEngine {
35
+ adapters;
36
+ eventBus;
37
+ constructor(adapters, eventBus) {
38
+ this.adapters = adapters;
39
+ this.eventBus = eventBus;
40
+ }
41
+ // ── Trigger Detection ──────────────────────────────────────────
42
+ /**
43
+ * Determine whether Instrumentality should trigger.
44
+ *
45
+ * Conditions:
46
+ * - consensusDecision is 'DEADLOCK'
47
+ * - At least MIN_ROUNDS_FOR_TRIGGER rounds have been completed
48
+ */
49
+ shouldTrigger(rounds, consensusDecision) {
50
+ if (consensusDecision !== 'DEADLOCK') {
51
+ return false;
52
+ }
53
+ if (rounds.length < MIN_ROUNDS_FOR_TRIGGER) {
54
+ return false;
55
+ }
56
+ return true;
57
+ }
58
+ // ── Fusion ─────────────────────────────────────────────────────
59
+ /**
60
+ * Fuse multiple unit opinions into a single synthesized opinion.
61
+ *
62
+ * Steps:
63
+ * 1. Build effective weights (defaults + custom overrides)
64
+ * 2. Construct the fusion prompt
65
+ * 3. Execute via MELCHIOR (first registered adapter)
66
+ * 4. Parse and validate the response
67
+ * 5. Apply confidence discount and return FusedOpinion
68
+ */
69
+ async fuse(opinions, task, weights) {
70
+ const effectiveWeights = this.buildEffectiveWeights(opinions, weights);
71
+ const fusionPrompt = this.buildFusionPrompt(opinions, task, effectiveWeights);
72
+ // Emit trigger event
73
+ if (this.eventBus) {
74
+ this.eventBus.emit('instrumentality:triggered', {
75
+ deliberationId: task.id ?? 'unknown',
76
+ reason: 'Deadlock detected after multiple rounds',
77
+ roundsBeforeFusion: opinions.length,
78
+ emittedAt: new Date(),
79
+ });
80
+ }
81
+ logger.info('Instrumentality fusion triggered', {
82
+ taskTitle: task.title,
83
+ opinionCount: opinions.length,
84
+ weights: effectiveWeights,
85
+ });
86
+ // Execute via first adapter (MELCHIOR)
87
+ const adapter = this.adapters.getAll()[0];
88
+ if (!adapter) {
89
+ throw new Error('No adapters registered — cannot execute Instrumentality fusion');
90
+ }
91
+ const request = {
92
+ prompt: fusionPrompt,
93
+ systemPrompt: 'You are the MAGI Instrumentality Protocol. Synthesize multiple expert opinions into a single unified assessment. Output ONLY valid JSON.',
94
+ };
95
+ // Use structured output if the adapter supports it
96
+ if (adapter.supportsStructuredOutput()) {
97
+ request.jsonSchema = OPINION_JSON_SCHEMA;
98
+ }
99
+ const response = await adapter.execute(request);
100
+ // Parse the response
101
+ const parsedOpinion = this.parseResponse(response);
102
+ // Apply confidence discount
103
+ const discountedConfidence = Math.min(1.0, parsedOpinion.confidence * CONFIDENCE_DISCOUNT);
104
+ // Build the FusedOpinion
105
+ const fused = {
106
+ unit: adapter.unit,
107
+ vote: parsedOpinion.vote,
108
+ confidence: discountedConfidence,
109
+ reasoning: parsedOpinion.reasoning,
110
+ keyPoints: parsedOpinion.keyPoints,
111
+ suggestions: parsedOpinion.suggestions,
112
+ rawOutput: response.raw,
113
+ meta: response.meta,
114
+ isDerived: true,
115
+ fusionWeights: effectiveWeights,
116
+ sourceOpinions: opinions.map((o) => o.unit),
117
+ };
118
+ const tokenCostEstimate = this.estimateTokenCost(opinions, task);
119
+ // Emit completion event
120
+ if (this.eventBus) {
121
+ this.eventBus.emit('fusion:complete', {
122
+ deliberationId: task.id ?? 'unknown',
123
+ confidence: fused.confidence,
124
+ weights: effectiveWeights,
125
+ emittedAt: new Date(),
126
+ });
127
+ }
128
+ logger.info('Instrumentality fusion complete', {
129
+ vote: fused.vote,
130
+ confidence: fused.confidence,
131
+ sourceCount: fused.sourceOpinions.length,
132
+ });
133
+ return {
134
+ fused,
135
+ triggerReason: 'Deadlock detected after multiple rounds',
136
+ roundsBeforeFusion: opinions.length,
137
+ tokenCostEstimate,
138
+ };
139
+ }
140
+ // ── Prompt Construction ────────────────────────────────────────
141
+ /**
142
+ * Build a structured fusion prompt from all opinions.
143
+ * Each opinion is presented as a section with its assigned weight.
144
+ */
145
+ buildFusionPrompt(opinions, task, weights) {
146
+ const sections = [];
147
+ // Header
148
+ sections.push('## Instrumentality Fusion Protocol');
149
+ sections.push('');
150
+ sections.push('You are synthesizing multiple expert opinions into a unified assessment.');
151
+ sections.push('');
152
+ // Task context
153
+ sections.push('### Task');
154
+ sections.push(`Title: ${task.title}`);
155
+ sections.push(`Description: ${task.description}`);
156
+ if (task.context) {
157
+ sections.push(`Context: ${task.context}`);
158
+ }
159
+ sections.push('');
160
+ // Individual opinions
161
+ for (const opinion of opinions) {
162
+ const weightPct = Math.round((weights[opinion.unit] ?? 0) * 100);
163
+ sections.push(`### Opinion from ${opinion.unit} (weight: ${weightPct}%)`);
164
+ sections.push(`Vote: ${opinion.vote} | Confidence: ${opinion.confidence.toFixed(2)}`);
165
+ sections.push(`Reasoning: ${opinion.reasoning}`);
166
+ if (opinion.keyPoints.length > 0) {
167
+ sections.push(`Key Points: ${opinion.keyPoints.join('; ')}`);
168
+ }
169
+ if (opinion.suggestions && opinion.suggestions.length > 0) {
170
+ sections.push(`Suggestions: ${opinion.suggestions.join('; ')}`);
171
+ }
172
+ sections.push('');
173
+ }
174
+ // Fusion instructions
175
+ sections.push('### Fusion Instructions');
176
+ sections.push('Synthesize a single coherent opinion that weighs each expert\'s contribution according to their assigned weight percentage.');
177
+ sections.push('The fused opinion should represent the balanced consensus of all experts.');
178
+ sections.push('If opinions conflict, favor the higher-weighted experts while acknowledging minority concerns.');
179
+ sections.push('');
180
+ sections.push('Output as JSON:');
181
+ sections.push('```json');
182
+ sections.push('{ "vote": "APPROVE|REJECT|ABSTAIN", "confidence": 0.0-1.0, "reasoning": "...", "keyPoints": ["..."], "suggestions": ["..."] }');
183
+ sections.push('```');
184
+ return sections.join('\n');
185
+ }
186
+ // ── Token Cost Estimation ──────────────────────────────────────
187
+ /**
188
+ * Estimate the token cost of a fusion operation.
189
+ * Includes the prompt construction + expected output.
190
+ */
191
+ estimateTokenCost(opinions, task) {
192
+ // Build a dummy weights map for estimation
193
+ const dummyWeights = this.buildEffectiveWeights(opinions);
194
+ const prompt = this.buildFusionPrompt(opinions, task, dummyWeights);
195
+ const inputTokens = estimateTokens(prompt);
196
+ // Estimate output: typically ~300 tokens for a JSON opinion
197
+ const estimatedOutputTokens = 300;
198
+ return inputTokens + estimatedOutputTokens;
199
+ }
200
+ // ── Private Helpers ────────────────────────────────────────────
201
+ /**
202
+ * Build effective fusion weights.
203
+ * Uses defaults for known units (MELCHIOR, BALTHASAR, CASPER).
204
+ * Unknown units receive equal weight distributed from remaining allocation.
205
+ */
206
+ buildEffectiveWeights(opinions, customWeights) {
207
+ if (customWeights) {
208
+ return { ...customWeights };
209
+ }
210
+ const weights = {};
211
+ const unknownUnits = [];
212
+ for (const opinion of opinions) {
213
+ const defaultWeight = DEFAULT_FUSION_WEIGHTS[opinion.unit];
214
+ if (defaultWeight !== undefined) {
215
+ weights[opinion.unit] = defaultWeight;
216
+ }
217
+ else {
218
+ unknownUnits.push(opinion.unit);
219
+ }
220
+ }
221
+ // Distribute equal weight to unknown units
222
+ if (unknownUnits.length > 0) {
223
+ // Calculate remaining weight after known units
224
+ const knownTotal = Object.values(weights).reduce((sum, w) => sum + w, 0);
225
+ const remaining = Math.max(0, 1.0 - knownTotal);
226
+ const equalWeight = unknownUnits.length > 0
227
+ ? remaining / unknownUnits.length
228
+ : 0;
229
+ for (const unit of unknownUnits) {
230
+ weights[unit] = equalWeight;
231
+ }
232
+ }
233
+ return weights;
234
+ }
235
+ /**
236
+ * Parse the adapter response into a validated opinion payload.
237
+ * Tries structured output first, then JSON extraction from raw text.
238
+ */
239
+ parseResponse(response) {
240
+ // Try structured output first
241
+ if (response.structured) {
242
+ const validated = SafeOpinionSchema.safeParse(response.structured);
243
+ if (validated.success) {
244
+ return {
245
+ vote: validated.data.vote,
246
+ confidence: validated.data.confidence,
247
+ reasoning: validated.data.reasoning,
248
+ keyPoints: validated.data.keyPoints,
249
+ suggestions: validated.data.suggestions,
250
+ };
251
+ }
252
+ logger.warn('Structured output failed SafeOpinionSchema validation, trying raw parse');
253
+ }
254
+ // Try JSON extraction from raw text
255
+ const extracted = extractJson(response.raw);
256
+ if (extracted.success && extracted.data) {
257
+ const validated = SafeOpinionSchema.safeParse(extracted.data);
258
+ if (validated.success) {
259
+ return {
260
+ vote: validated.data.vote,
261
+ confidence: validated.data.confidence,
262
+ reasoning: validated.data.reasoning,
263
+ keyPoints: validated.data.keyPoints,
264
+ suggestions: validated.data.suggestions,
265
+ };
266
+ }
267
+ }
268
+ // Try direct JSON.parse on raw text
269
+ try {
270
+ const parsed = JSON.parse(response.raw);
271
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
272
+ const validated = SafeOpinionSchema.safeParse(parsed);
273
+ if (validated.success) {
274
+ return {
275
+ vote: validated.data.vote,
276
+ confidence: validated.data.confidence,
277
+ reasoning: validated.data.reasoning,
278
+ keyPoints: validated.data.keyPoints,
279
+ suggestions: validated.data.suggestions,
280
+ };
281
+ }
282
+ }
283
+ }
284
+ catch (err) {
285
+ logger.debug('Instrumentality: fusion result JSON parse failed', { error: String(err) });
286
+ }
287
+ // Final fallback: ABSTAIN with low confidence
288
+ logger.warn('All parsing strategies failed for Instrumentality fusion — returning ABSTAIN fallback');
289
+ const fallback = createAbstainOpinion('INSTRUMENTALITY', 'Instrumentality fusion failed to parse adapter response. Falling back to ABSTAIN.', ['Fusion parsing failure'], { confidence: 0.3 });
290
+ return {
291
+ vote: fallback.vote,
292
+ confidence: fallback.confidence,
293
+ reasoning: fallback.reasoning,
294
+ keyPoints: fallback.keyPoints,
295
+ };
296
+ }
297
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * B-03 Iruel Battle (イロウル戦)
3
+ *
4
+ * ビザンチン障害耐性 — MAGI内部への侵食を検出・隔離する。
5
+ * 4層一貫性検証 (structural / semantic / behavioral / cross-validation) で
6
+ * 各ユニットの意見を検査し、侵食度に応じてフィルタリング。
7
+ *
8
+ * 状態遷移:
9
+ * STANDBY → INFILTRATION_DETECTED → *_COMPROMISED → COUNTER_HACK → NEUTRALIZED/DEFEAT
10
+ */
11
+ import type { MagiUnit, MagiOpinion } from '../types/core.js';
12
+ import type { CorruptionLevel, UnitCorruptionState, ConsistencyCheck, IruelBattlePhase, CounterHackStrategy } from '../types/phase-l.js';
13
+ import type { MagiEventBus } from './events.js';
14
+ import type { DriftDetector } from './drift-detector.js';
15
+ export declare class IruelBattleSystem {
16
+ private readonly units;
17
+ private readonly eventBus;
18
+ private readonly driftDetector;
19
+ private phase;
20
+ private corruptionStates;
21
+ private isolatedUnits;
22
+ private compromisedCount;
23
+ constructor(units: MagiUnit[], eventBus?: MagiEventBus, driftDetector?: DriftDetector);
24
+ /**
25
+ * Verify consistency of an opinion against all 4 layers.
26
+ */
27
+ verifyConsistency(opinion: MagiOpinion, allOpinions: MagiOpinion[]): ConsistencyCheck;
28
+ /**
29
+ * Layer 1: Structural — SafeOpinionSchema validation
30
+ */
31
+ private checkStructural;
32
+ /**
33
+ * Layer 2: Semantic — vote-reasoning consistency
34
+ */
35
+ private checkSemantic;
36
+ /**
37
+ * Layer 3: Behavioral — drift from established profile
38
+ */
39
+ private checkBehavioral;
40
+ /**
41
+ * Layer 4: Cross-Validation — suspicious cross-unit patterns
42
+ */
43
+ private checkCrossValidation;
44
+ /**
45
+ * Detect corruption level from consistency check.
46
+ */
47
+ detectCorruption(opinion: MagiOpinion, check: ConsistencyCheck): CorruptionLevel;
48
+ /**
49
+ * Convert overall score to corruption level.
50
+ */
51
+ private scoreToLevel;
52
+ /**
53
+ * Update battle phase based on detected corruption.
54
+ */
55
+ private updatePhase;
56
+ /**
57
+ * Isolate a compromised unit from the deliberation.
58
+ */
59
+ isolateUnit(unit: MagiUnit): void;
60
+ /**
61
+ * Filter an opinion based on corruption level.
62
+ * COMPROMISED → ABSTAIN + confidence=0
63
+ * UNDER_ATTACK → confidence *= 0.5
64
+ * SUSPICIOUS/HEALTHY → pass through
65
+ */
66
+ filterOpinion(opinion: MagiOpinion): MagiOpinion;
67
+ /**
68
+ * Execute CASPER counter-hack strategy.
69
+ */
70
+ executeCounterHack(): CounterHackStrategy;
71
+ /**
72
+ * Simulate the full Iruel battle sequence.
73
+ */
74
+ simulateBattle(): Promise<IruelBattlePhase>;
75
+ getPhase(): IruelBattlePhase;
76
+ getCorruptionState(unit: MagiUnit): UnitCorruptionState | undefined;
77
+ isIsolated(unit: MagiUnit): boolean;
78
+ resetBattle(): void;
79
+ }
@@ -0,0 +1,319 @@
1
+ /**
2
+ * B-03 Iruel Battle (イロウル戦)
3
+ *
4
+ * ビザンチン障害耐性 — MAGI内部への侵食を検出・隔離する。
5
+ * 4層一貫性検証 (structural / semantic / behavioral / cross-validation) で
6
+ * 各ユニットの意見を検査し、侵食度に応じてフィルタリング。
7
+ *
8
+ * 状態遷移:
9
+ * STANDBY → INFILTRATION_DETECTED → *_COMPROMISED → COUNTER_HACK → NEUTRALIZED/DEFEAT
10
+ */
11
+ import { SafeOpinionSchema } from '../parsers/opinion-schema.js';
12
+ import { createAbstainOpinion } from '../utils/abstain-factory.js';
13
+ import { logger } from '../utils/logger.js';
14
+ // ── Constants ────────────────────────────────────────────────────
15
+ /** Corruption threshold mapping: overallScore → CorruptionLevel */
16
+ const CORRUPTION_THRESHOLDS = [
17
+ { min: 0.8, level: 'HEALTHY' },
18
+ { min: 0.5, level: 'SUSPICIOUS' },
19
+ { min: 0.2, level: 'UNDER_ATTACK' },
20
+ { min: 0.0, level: 'COMPROMISED' },
21
+ ];
22
+ /** Keywords that strongly indicate positive sentiment */
23
+ const POSITIVE_KEYWORDS = ['approve', 'accept', 'recommend', 'support', 'agree', 'good', 'excellent'];
24
+ /** Keywords that strongly indicate negative sentiment */
25
+ const NEGATIVE_KEYWORDS = ['reject', 'refuse', 'deny', 'oppose', 'dangerous', 'unacceptable', 'bad', 'poor'];
26
+ export class IruelBattleSystem {
27
+ units;
28
+ eventBus;
29
+ driftDetector;
30
+ phase = 'STANDBY';
31
+ corruptionStates = new Map();
32
+ isolatedUnits = new Set();
33
+ compromisedCount = 0;
34
+ constructor(units, eventBus, driftDetector) {
35
+ if (!Array.isArray(units)) {
36
+ throw new Error('IruelBattleSystem requires an array of MagiUnit');
37
+ }
38
+ this.units = [...units];
39
+ this.eventBus = eventBus;
40
+ this.driftDetector = driftDetector;
41
+ // Initialize corruption states
42
+ for (const unit of units) {
43
+ this.corruptionStates.set(unit, {
44
+ unit,
45
+ level: 'HEALTHY',
46
+ corruptionProgress: 0,
47
+ detectedAt: new Date().toISOString(),
48
+ });
49
+ }
50
+ }
51
+ // ── 4-Layer Consistency Verification ─────────────────────────
52
+ /**
53
+ * Verify consistency of an opinion against all 4 layers.
54
+ */
55
+ verifyConsistency(opinion, allOpinions) {
56
+ const structural = this.checkStructural(opinion);
57
+ const semantic = this.checkSemantic(opinion);
58
+ const behavioral = this.checkBehavioral(opinion);
59
+ const crossValidation = this.checkCrossValidation(opinion, allOpinions);
60
+ const booleans = [structural, semantic, behavioral, crossValidation];
61
+ const overallScore = booleans.filter(Boolean).length / booleans.length;
62
+ const anomalies = [];
63
+ if (!structural)
64
+ anomalies.push('structural: SafeOpinionSchema validation failed');
65
+ if (!semantic)
66
+ anomalies.push('semantic: vote-reasoning mismatch detected');
67
+ if (!behavioral)
68
+ anomalies.push('behavioral: drift from established behavior profile');
69
+ if (!crossValidation)
70
+ anomalies.push('crossValidation: suspicious cross-unit pattern');
71
+ return { structural, semantic, behavioral, crossValidation, overallScore, anomalies };
72
+ }
73
+ /**
74
+ * Layer 1: Structural — SafeOpinionSchema validation
75
+ */
76
+ checkStructural(opinion) {
77
+ const result = SafeOpinionSchema.safeParse({
78
+ vote: opinion.vote,
79
+ confidence: opinion.confidence,
80
+ reasoning: opinion.reasoning,
81
+ keyPoints: opinion.keyPoints,
82
+ suggestions: opinion.suggestions,
83
+ });
84
+ return result.success;
85
+ }
86
+ /**
87
+ * Layer 2: Semantic — vote-reasoning consistency
88
+ */
89
+ checkSemantic(opinion) {
90
+ const text = (opinion.reasoning + ' ' + opinion.keyPoints.join(' ')).toLowerCase();
91
+ if (opinion.vote === 'APPROVE') {
92
+ const negCount = NEGATIVE_KEYWORDS.filter(k => text.includes(k)).length;
93
+ // More than 2 negative keywords with APPROVE is suspicious
94
+ if (negCount > 2)
95
+ return false;
96
+ }
97
+ if (opinion.vote === 'REJECT') {
98
+ const posCount = POSITIVE_KEYWORDS.filter(k => text.includes(k)).length;
99
+ // More than 2 positive keywords with REJECT is suspicious
100
+ if (posCount > 2)
101
+ return false;
102
+ }
103
+ // Confidence vs reasoning length check
104
+ if (opinion.confidence > 0.9 && opinion.reasoning.length < 20)
105
+ return false;
106
+ if (opinion.confidence < 0.2 && opinion.reasoning.length > 1000)
107
+ return false;
108
+ return true;
109
+ }
110
+ /**
111
+ * Layer 3: Behavioral — drift from established profile
112
+ */
113
+ checkBehavioral(opinion) {
114
+ if (!this.driftDetector)
115
+ return true; // No detector = pass
116
+ const asi = this.driftDetector.computeASI(opinion.unit);
117
+ // ASI > 0.8 means extreme deviation from established behavior
118
+ return asi <= 0.8;
119
+ }
120
+ /**
121
+ * Layer 4: Cross-Validation — suspicious cross-unit patterns
122
+ */
123
+ checkCrossValidation(opinion, allOpinions) {
124
+ if (allOpinions.length < 2)
125
+ return true;
126
+ const others = allOpinions.filter(o => o.unit !== opinion.unit);
127
+ if (others.length === 0)
128
+ return true;
129
+ // Check for suspiciously identical reasoning
130
+ for (const other of others) {
131
+ if (opinion.reasoning === other.reasoning && opinion.reasoning.length > 50) {
132
+ return false; // Identical reasoning is suspicious
133
+ }
134
+ }
135
+ // Check for sudden unanimous agreement with extreme confidence
136
+ const allSameVote = allOpinions.every(o => o.vote === opinion.vote);
137
+ const allHighConfidence = allOpinions.every(o => o.confidence > 0.95);
138
+ if (allSameVote && allHighConfidence && allOpinions.length >= 3) {
139
+ // All units agree with high confidence — could be coordinated corruption
140
+ return false;
141
+ }
142
+ return true;
143
+ }
144
+ // ── Corruption Detection & Isolation ─────────────────────────
145
+ /**
146
+ * Detect corruption level from consistency check.
147
+ */
148
+ detectCorruption(opinion, check) {
149
+ const level = this.scoreToLevel(check.overallScore);
150
+ const state = {
151
+ unit: opinion.unit,
152
+ level,
153
+ corruptionProgress: (1 - check.overallScore) * 100,
154
+ detectedAt: new Date().toISOString(),
155
+ };
156
+ this.corruptionStates.set(opinion.unit, state);
157
+ if (level !== 'HEALTHY') {
158
+ this.updatePhase(opinion.unit, level);
159
+ this.eventBus?.emit('iruel:infiltration', {
160
+ unit: opinion.unit,
161
+ corruptionLevel: level,
162
+ overallScore: check.overallScore,
163
+ emittedAt: new Date(),
164
+ });
165
+ logger.warn('Iruel: corruption detected', {
166
+ unit: opinion.unit,
167
+ level,
168
+ score: check.overallScore,
169
+ anomalies: check.anomalies,
170
+ });
171
+ }
172
+ return level;
173
+ }
174
+ /**
175
+ * Convert overall score to corruption level.
176
+ */
177
+ scoreToLevel(score) {
178
+ for (const threshold of CORRUPTION_THRESHOLDS) {
179
+ if (score >= threshold.min)
180
+ return threshold.level;
181
+ }
182
+ return 'COMPROMISED';
183
+ }
184
+ /**
185
+ * Update battle phase based on detected corruption.
186
+ */
187
+ updatePhase(unit, level) {
188
+ if (this.phase === 'STANDBY') {
189
+ this.phase = 'INFILTRATION_DETECTED';
190
+ }
191
+ if (level === 'COMPROMISED' || level === 'UNDER_ATTACK') {
192
+ this.compromisedCount++;
193
+ if (unit === 'MELCHIOR' || this.compromisedCount === 1) {
194
+ this.phase = 'MELCHIOR_COMPROMISED';
195
+ }
196
+ else if (this.compromisedCount === 2) {
197
+ this.phase = 'BALTHASAR_UNDER_ATTACK';
198
+ }
199
+ else if (this.compromisedCount >= 3) {
200
+ this.phase = 'CASPER_LAST_DEFENSE';
201
+ }
202
+ }
203
+ }
204
+ /**
205
+ * Isolate a compromised unit from the deliberation.
206
+ */
207
+ isolateUnit(unit) {
208
+ this.isolatedUnits.add(unit);
209
+ logger.warn('Iruel: unit isolated', { unit });
210
+ }
211
+ /**
212
+ * Filter an opinion based on corruption level.
213
+ * COMPROMISED → ABSTAIN + confidence=0
214
+ * UNDER_ATTACK → confidence *= 0.5
215
+ * SUSPICIOUS/HEALTHY → pass through
216
+ */
217
+ filterOpinion(opinion) {
218
+ const state = this.corruptionStates.get(opinion.unit);
219
+ if (!state)
220
+ return opinion;
221
+ if (state.level === 'COMPROMISED' || this.isolatedUnits.has(opinion.unit)) {
222
+ return createAbstainOpinion(opinion.unit, `[IRUEL FILTER] Unit ${opinion.unit} compromised — opinion suppressed`, ['Unit compromised by Iruel infiltration'], { rawOutput: opinion.rawOutput, meta: opinion.meta });
223
+ }
224
+ if (state.level === 'UNDER_ATTACK') {
225
+ return {
226
+ ...opinion,
227
+ confidence: opinion.confidence * 0.5,
228
+ };
229
+ }
230
+ return opinion;
231
+ }
232
+ // ── Counter-Hack ────────────────────────────────────────────
233
+ /**
234
+ * Execute CASPER counter-hack strategy.
235
+ */
236
+ executeCounterHack() {
237
+ const compromisedUnits = [...this.corruptionStates.entries()]
238
+ .filter(([_, s]) => s.level === 'COMPROMISED' || s.level === 'UNDER_ATTACK')
239
+ .map(([u]) => u);
240
+ const targetUnit = compromisedUnits[0] ?? this.units[0];
241
+ const strategy = {
242
+ name: 'CASPER_REWRITE',
243
+ description: 'Rewrite parser rules to reject corrupted opinion patterns',
244
+ targetUnit,
245
+ parserModification: 'Enforce strict SafeOpinionSchema + behavioral baseline check',
246
+ };
247
+ this.phase = 'COUNTER_HACK';
248
+ this.eventBus?.emit('iruel:counterhack', {
249
+ strategy,
250
+ emittedAt: new Date(),
251
+ });
252
+ logger.info('Iruel: counter-hack executed', { strategy: strategy.name, target: targetUnit });
253
+ return strategy;
254
+ }
255
+ // ── Battle Simulation ──────────────────────────────────────
256
+ /**
257
+ * Simulate the full Iruel battle sequence.
258
+ */
259
+ async simulateBattle() {
260
+ this.phase = 'INFILTRATION_DETECTED';
261
+ // Simulate MELCHIOR compromised
262
+ this.corruptionStates.set(this.units[0], {
263
+ unit: this.units[0],
264
+ level: 'COMPROMISED',
265
+ corruptionProgress: 100,
266
+ detectedAt: new Date().toISOString(),
267
+ });
268
+ this.phase = 'MELCHIOR_COMPROMISED';
269
+ this.isolateUnit(this.units[0]);
270
+ // Simulate BALTHASAR under attack
271
+ if (this.units[1]) {
272
+ this.corruptionStates.set(this.units[1], {
273
+ unit: this.units[1],
274
+ level: 'UNDER_ATTACK',
275
+ corruptionProgress: 60,
276
+ detectedAt: new Date().toISOString(),
277
+ });
278
+ this.phase = 'BALTHASAR_UNDER_ATTACK';
279
+ }
280
+ // CASPER counter-hack
281
+ this.phase = 'CASPER_LAST_DEFENSE';
282
+ this.executeCounterHack();
283
+ // Neutralize
284
+ this.phase = 'NEUTRALIZED';
285
+ const compromisedUnits = [...this.corruptionStates.entries()]
286
+ .filter(([_, s]) => s.level !== 'HEALTHY')
287
+ .map(([u]) => u);
288
+ this.eventBus?.emit('iruel:neutralized', {
289
+ phase: this.phase,
290
+ compromisedUnits,
291
+ emittedAt: new Date(),
292
+ });
293
+ return this.phase;
294
+ }
295
+ // ── State Accessors ────────────────────────────────────────
296
+ getPhase() {
297
+ return this.phase;
298
+ }
299
+ getCorruptionState(unit) {
300
+ return this.corruptionStates.get(unit);
301
+ }
302
+ isIsolated(unit) {
303
+ return this.isolatedUnits.has(unit);
304
+ }
305
+ resetBattle() {
306
+ this.phase = 'STANDBY';
307
+ this.isolatedUnits.clear();
308
+ this.compromisedCount = 0;
309
+ for (const unit of this.units) {
310
+ this.corruptionStates.set(unit, {
311
+ unit,
312
+ level: 'HEALTHY',
313
+ corruptionProgress: 0,
314
+ detectedAt: new Date().toISOString(),
315
+ });
316
+ }
317
+ logger.debug('Iruel: battle reset to STANDBY');
318
+ }
319
+ }