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.
- package/LICENSE +21 -0
- package/README.ja.md +377 -0
- package/README.md +377 -0
- package/dist/bin/magi-benchmark.d.ts +14 -0
- package/dist/bin/magi-benchmark.js +93 -0
- package/dist/bin/magi-mcp.d.ts +8 -0
- package/dist/bin/magi-mcp.js +28 -0
- package/dist/bin/magi.d.ts +2 -0
- package/dist/bin/magi.js +634 -0
- package/dist/src/adapters/base.d.ts +34 -0
- package/dist/src/adapters/base.js +149 -0
- package/dist/src/adapters/claude.d.ts +29 -0
- package/dist/src/adapters/claude.js +65 -0
- package/dist/src/adapters/codex.d.ts +21 -0
- package/dist/src/adapters/codex.js +41 -0
- package/dist/src/adapters/gemini.d.ts +18 -0
- package/dist/src/adapters/gemini.js +31 -0
- package/dist/src/adapters/registry.d.ts +19 -0
- package/dist/src/adapters/registry.js +59 -0
- package/dist/src/audit/hash-chain.d.ts +21 -0
- package/dist/src/audit/hash-chain.js +70 -0
- package/dist/src/audit/types.d.ts +25 -0
- package/dist/src/audit/types.js +1 -0
- package/dist/src/audit/writer.d.ts +18 -0
- package/dist/src/audit/writer.js +100 -0
- package/dist/src/benchmark/golden-tasks.d.ts +9 -0
- package/dist/src/benchmark/golden-tasks.js +476 -0
- package/dist/src/benchmark/reporter.d.ts +5 -0
- package/dist/src/benchmark/reporter.js +107 -0
- package/dist/src/benchmark/runner.d.ts +30 -0
- package/dist/src/benchmark/runner.js +224 -0
- package/dist/src/benchmark/scorer.d.ts +12 -0
- package/dist/src/benchmark/scorer.js +124 -0
- package/dist/src/benchmark/types.d.ts +54 -0
- package/dist/src/benchmark/types.js +1 -0
- package/dist/src/cache/deliberation-cache.d.ts +49 -0
- package/dist/src/cache/deliberation-cache.js +127 -0
- package/dist/src/cli/commands/config-cmd.d.ts +11 -0
- package/dist/src/cli/commands/config-cmd.js +190 -0
- package/dist/src/cli/commands/demo.d.ts +12 -0
- package/dist/src/cli/commands/demo.js +66 -0
- package/dist/src/cli/commands/setup.d.ts +7 -0
- package/dist/src/cli/commands/setup.js +182 -0
- package/dist/src/cli/i18n.d.ts +89 -0
- package/dist/src/cli/i18n.js +176 -0
- package/dist/src/cli/interactive-select.d.ts +27 -0
- package/dist/src/cli/interactive-select.js +130 -0
- package/dist/src/cli/tui-setup.d.ts +24 -0
- package/dist/src/cli/tui-setup.js +42 -0
- package/dist/src/config/cli-detector.d.ts +37 -0
- package/dist/src/config/cli-detector.js +99 -0
- package/dist/src/config/user-config.d.ts +81 -0
- package/dist/src/config/user-config.js +134 -0
- package/dist/src/context/auto-collector.d.ts +43 -0
- package/dist/src/context/auto-collector.js +337 -0
- package/dist/src/context/manager.d.ts +35 -0
- package/dist/src/context/manager.js +162 -0
- package/dist/src/context/serializer.d.ts +20 -0
- package/dist/src/context/serializer.js +52 -0
- package/dist/src/demo/recorded-deliberation.d.ts +13 -0
- package/dist/src/demo/recorded-deliberation.js +277 -0
- package/dist/src/engine/angel-detector.d.ts +83 -0
- package/dist/src/engine/angel-detector.js +334 -0
- package/dist/src/engine/at-field.d.ts +40 -0
- package/dist/src/engine/at-field.js +195 -0
- package/dist/src/engine/berserk-orchestrator.d.ts +66 -0
- package/dist/src/engine/berserk-orchestrator.js +378 -0
- package/dist/src/engine/change-metrics.d.ts +56 -0
- package/dist/src/engine/change-metrics.js +214 -0
- package/dist/src/engine/consensus.d.ts +20 -0
- package/dist/src/engine/consensus.js +146 -0
- package/dist/src/engine/dead-sea-scrolls.d.ts +132 -0
- package/dist/src/engine/dead-sea-scrolls.js +610 -0
- package/dist/src/engine/drift-detector.d.ts +39 -0
- package/dist/src/engine/drift-detector.js +225 -0
- package/dist/src/engine/dummy-plug.d.ts +44 -0
- package/dist/src/engine/dummy-plug.js +190 -0
- package/dist/src/engine/engram-manager.d.ts +55 -0
- package/dist/src/engine/engram-manager.js +306 -0
- package/dist/src/engine/events.d.ts +130 -0
- package/dist/src/engine/events.js +44 -0
- package/dist/src/engine/gospel.d.ts +30 -0
- package/dist/src/engine/gospel.js +129 -0
- package/dist/src/engine/hallucination-detector.d.ts +33 -0
- package/dist/src/engine/hallucination-detector.js +215 -0
- package/dist/src/engine/human-resolver.d.ts +19 -0
- package/dist/src/engine/human-resolver.js +89 -0
- package/dist/src/engine/instrumentality.d.ts +64 -0
- package/dist/src/engine/instrumentality.js +297 -0
- package/dist/src/engine/iruel-battle.d.ts +79 -0
- package/dist/src/engine/iruel-battle.js +319 -0
- package/dist/src/engine/kernel/deliberation-kernel.d.ts +12 -0
- package/dist/src/engine/kernel/deliberation-kernel.js +303 -0
- package/dist/src/engine/kernel/index.d.ts +8 -0
- package/dist/src/engine/kernel/index.js +7 -0
- package/dist/src/engine/kernel/phase-runner.d.ts +10 -0
- package/dist/src/engine/kernel/phase-runner.js +155 -0
- package/dist/src/engine/kernel/post-processor.d.ts +17 -0
- package/dist/src/engine/kernel/post-processor.js +131 -0
- package/dist/src/engine/kernel/types.d.ts +107 -0
- package/dist/src/engine/kernel/types.js +1 -0
- package/dist/src/engine/kernel/unit-executor.d.ts +6 -0
- package/dist/src/engine/kernel/unit-executor.js +132 -0
- package/dist/src/engine/lcl-manager.d.ts +44 -0
- package/dist/src/engine/lcl-manager.js +143 -0
- package/dist/src/engine/middleware/cache.d.ts +7 -0
- package/dist/src/engine/middleware/cache.js +29 -0
- package/dist/src/engine/middleware/chain.d.ts +18 -0
- package/dist/src/engine/middleware/chain.js +45 -0
- package/dist/src/engine/middleware/firewall.d.ts +8 -0
- package/dist/src/engine/middleware/firewall.js +24 -0
- package/dist/src/engine/middleware/index.d.ts +4 -0
- package/dist/src/engine/middleware/index.js +3 -0
- package/dist/src/engine/middleware/types.d.ts +43 -0
- package/dist/src/engine/middleware/types.js +1 -0
- package/dist/src/engine/nebuchadnezzar-key.d.ts +61 -0
- package/dist/src/engine/nebuchadnezzar-key.js +203 -0
- package/dist/src/engine/neon-genesis.d.ts +52 -0
- package/dist/src/engine/neon-genesis.js +203 -0
- package/dist/src/engine/objective-judge.d.ts +53 -0
- package/dist/src/engine/objective-judge.js +214 -0
- package/dist/src/engine/offline-mode.d.ts +18 -0
- package/dist/src/engine/offline-mode.js +46 -0
- package/dist/src/engine/orchestrator.d.ts +79 -0
- package/dist/src/engine/orchestrator.js +58 -0
- package/dist/src/engine/secret-cipher.d.ts +26 -0
- package/dist/src/engine/secret-cipher.js +114 -0
- package/dist/src/engine/seele-council.d.ts +90 -0
- package/dist/src/engine/seele-council.js +482 -0
- package/dist/src/engine/self-destruct.d.ts +61 -0
- package/dist/src/engine/self-destruct.js +231 -0
- package/dist/src/engine/self-evolution.d.ts +64 -0
- package/dist/src/engine/self-evolution.js +368 -0
- package/dist/src/engine/sync-rate.d.ts +45 -0
- package/dist/src/engine/sync-rate.js +151 -0
- package/dist/src/engine/type666-firewall.d.ts +76 -0
- package/dist/src/engine/type666-firewall.js +343 -0
- package/dist/src/engine/umbilical-cable.d.ts +41 -0
- package/dist/src/engine/umbilical-cable.js +192 -0
- package/dist/src/index.d.ts +106 -0
- package/dist/src/index.js +426 -0
- package/dist/src/mcp/server.d.ts +38 -0
- package/dist/src/mcp/server.js +196 -0
- package/dist/src/metrics/token-tracker.d.ts +38 -0
- package/dist/src/metrics/token-tracker.js +112 -0
- package/dist/src/parsers/json-extractor.d.ts +9 -0
- package/dist/src/parsers/json-extractor.js +239 -0
- package/dist/src/parsers/opinion-schema.d.ts +81 -0
- package/dist/src/parsers/opinion-schema.js +147 -0
- package/dist/src/parsers/unstructured-parser.d.ts +20 -0
- package/dist/src/parsers/unstructured-parser.js +122 -0
- package/dist/src/pipelines/architecture.d.ts +10 -0
- package/dist/src/pipelines/architecture.js +9 -0
- package/dist/src/pipelines/bug-analysis.d.ts +9 -0
- package/dist/src/pipelines/bug-analysis.js +8 -0
- package/dist/src/pipelines/code-review.d.ts +10 -0
- package/dist/src/pipelines/code-review.js +30 -0
- package/dist/src/pipelines/custom.d.ts +14 -0
- package/dist/src/pipelines/custom.js +29 -0
- package/dist/src/pipelines/registry.d.ts +9 -0
- package/dist/src/pipelines/registry.js +20 -0
- package/dist/src/prompts/personas.d.ts +6 -0
- package/dist/src/prompts/personas.js +44 -0
- package/dist/src/prompts/schemas.d.ts +4 -0
- package/dist/src/prompts/schemas.js +24 -0
- package/dist/src/prompts/templates.d.ts +6 -0
- package/dist/src/prompts/templates.js +91 -0
- package/dist/src/repl/accessibility.d.ts +23 -0
- package/dist/src/repl/accessibility.js +46 -0
- package/dist/src/repl/banner.d.ts +4 -0
- package/dist/src/repl/banner.js +28 -0
- package/dist/src/repl/boot-animation.d.ts +13 -0
- package/dist/src/repl/boot-animation.js +143 -0
- package/dist/src/repl/completer.d.ts +21 -0
- package/dist/src/repl/completer.js +168 -0
- package/dist/src/repl/context.d.ts +24 -0
- package/dist/src/repl/context.js +42 -0
- package/dist/src/repl/display-utils.d.ts +13 -0
- package/dist/src/repl/display-utils.js +65 -0
- package/dist/src/repl/event-listener.d.ts +18 -0
- package/dist/src/repl/event-listener.js +112 -0
- package/dist/src/repl/export-formatter.d.ts +8 -0
- package/dist/src/repl/export-formatter.js +73 -0
- package/dist/src/repl/ghost-text.d.ts +31 -0
- package/dist/src/repl/ghost-text.js +119 -0
- package/dist/src/repl/handoff-animation.d.ts +15 -0
- package/dist/src/repl/handoff-animation.js +65 -0
- package/dist/src/repl/history.d.ts +16 -0
- package/dist/src/repl/history.js +130 -0
- package/dist/src/repl/job-registry.d.ts +26 -0
- package/dist/src/repl/job-registry.js +80 -0
- package/dist/src/repl/magi-repl.d.ts +72 -0
- package/dist/src/repl/magi-repl.js +1008 -0
- package/dist/src/repl/multiline-input.d.ts +45 -0
- package/dist/src/repl/multiline-input.js +78 -0
- package/dist/src/repl/prompt-builder.d.ts +19 -0
- package/dist/src/repl/prompt-builder.js +36 -0
- package/dist/src/repl/repl-state.d.ts +5 -0
- package/dist/src/repl/repl-state.js +19 -0
- package/dist/src/repl/result-display.d.ts +8 -0
- package/dist/src/repl/result-display.js +195 -0
- package/dist/src/repl/session-stats.d.ts +26 -0
- package/dist/src/repl/session-stats.js +119 -0
- package/dist/src/repl/slash-commands.d.ts +60 -0
- package/dist/src/repl/slash-commands.js +725 -0
- package/dist/src/repl/terminal-sanitize.d.ts +14 -0
- package/dist/src/repl/terminal-sanitize.js +19 -0
- package/dist/src/reporters/console.d.ts +7 -0
- package/dist/src/reporters/console.js +78 -0
- package/dist/src/reporters/json.d.ts +2 -0
- package/dist/src/reporters/json.js +3 -0
- package/dist/src/reporters/markdown.d.ts +2 -0
- package/dist/src/reporters/markdown.js +65 -0
- package/dist/src/reporters/streaming.d.ts +20 -0
- package/dist/src/reporters/streaming.js +178 -0
- package/dist/src/tui/activity-log.d.ts +23 -0
- package/dist/src/tui/activity-log.js +67 -0
- package/dist/src/tui/animations.d.ts +39 -0
- package/dist/src/tui/animations.js +167 -0
- package/dist/src/tui/ansi.d.ts +28 -0
- package/dist/src/tui/ansi.js +51 -0
- package/dist/src/tui/boot-sequence.d.ts +11 -0
- package/dist/src/tui/boot-sequence.js +98 -0
- package/dist/src/tui/colors.d.ts +101 -0
- package/dist/src/tui/colors.js +71 -0
- package/dist/src/tui/header.d.ts +24 -0
- package/dist/src/tui/header.js +122 -0
- package/dist/src/tui/index.d.ts +3 -0
- package/dist/src/tui/index.js +3 -0
- package/dist/src/tui/keypress.d.ts +25 -0
- package/dist/src/tui/keypress.js +95 -0
- package/dist/src/tui/layout.d.ts +74 -0
- package/dist/src/tui/layout.js +171 -0
- package/dist/src/tui/magi-tui.d.ts +101 -0
- package/dist/src/tui/magi-tui.js +754 -0
- package/dist/src/tui/panel.d.ts +45 -0
- package/dist/src/tui/panel.js +292 -0
- package/dist/src/tui/screen-buffer.d.ts +54 -0
- package/dist/src/tui/screen-buffer.js +262 -0
- package/dist/src/tui/status-bar.d.ts +25 -0
- package/dist/src/tui/status-bar.js +124 -0
- package/dist/src/tui/terminal-detect.d.ts +26 -0
- package/dist/src/tui/terminal-detect.js +44 -0
- package/dist/src/tui/tui-helpers.d.ts +12 -0
- package/dist/src/tui/tui-helpers.js +37 -0
- package/dist/src/types/adapter.d.ts +75 -0
- package/dist/src/types/adapter.js +36 -0
- package/dist/src/types/config.d.ts +108 -0
- package/dist/src/types/config.js +85 -0
- package/dist/src/types/consensus.d.ts +55 -0
- package/dist/src/types/consensus.js +17 -0
- package/dist/src/types/core.d.ts +178 -0
- package/dist/src/types/core.js +85 -0
- package/dist/src/types/magi-api.d.ts +62 -0
- package/dist/src/types/magi-api.js +7 -0
- package/dist/src/types/phase-h.d.ts +142 -0
- package/dist/src/types/phase-h.js +7 -0
- package/dist/src/types/phase-i.d.ts +186 -0
- package/dist/src/types/phase-i.js +6 -0
- package/dist/src/types/phase-k.d.ts +259 -0
- package/dist/src/types/phase-k.js +6 -0
- package/dist/src/types/phase-l.d.ts +199 -0
- package/dist/src/types/phase-l.js +6 -0
- package/dist/src/types/pipeline.d.ts +37 -0
- package/dist/src/types/pipeline.js +2 -0
- package/dist/src/utils/abstain-factory.d.ts +2 -0
- package/dist/src/utils/abstain-factory.js +18 -0
- package/dist/src/utils/errors.d.ts +34 -0
- package/dist/src/utils/errors.js +59 -0
- package/dist/src/utils/file-validator.d.ts +50 -0
- package/dist/src/utils/file-validator.js +124 -0
- package/dist/src/utils/fire-and-forget.d.ts +5 -0
- package/dist/src/utils/fire-and-forget.js +10 -0
- package/dist/src/utils/flag-validator.d.ts +21 -0
- package/dist/src/utils/flag-validator.js +79 -0
- package/dist/src/utils/freeze.d.ts +8 -0
- package/dist/src/utils/freeze.js +16 -0
- package/dist/src/utils/language-detector.d.ts +16 -0
- package/dist/src/utils/language-detector.js +159 -0
- package/dist/src/utils/latency-tracker.d.ts +45 -0
- package/dist/src/utils/latency-tracker.js +100 -0
- package/dist/src/utils/logger.d.ts +33 -0
- package/dist/src/utils/logger.js +112 -0
- package/dist/src/utils/process.d.ts +40 -0
- package/dist/src/utils/process.js +253 -0
- package/dist/src/utils/retry.d.ts +12 -0
- package/dist/src/utils/retry.js +30 -0
- package/dist/src/utils/safe-fs.d.ts +38 -0
- package/dist/src/utils/safe-fs.js +56 -0
- package/dist/src/utils/safe-json-parse.d.ts +15 -0
- package/dist/src/utils/safe-json-parse.js +49 -0
- package/dist/src/utils/sanitize.d.ts +14 -0
- package/dist/src/utils/sanitize.js +186 -0
- package/dist/src/utils/semaphore.d.ts +22 -0
- package/dist/src/utils/semaphore.js +57 -0
- package/dist/src/utils/shutdown.d.ts +6 -0
- package/dist/src/utils/shutdown.js +51 -0
- package/dist/src/utils/tty.d.ts +5 -0
- package/dist/src/utils/tty.js +7 -0
- package/package.json +82 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A-03 暴走モード (BERSERK) — BerserkOrchestrator
|
|
3
|
+
*
|
|
4
|
+
* Evolutionary deliberation engine that generates 5 strategy variants
|
|
5
|
+
* per unit, scores candidates by fitness, and iterates until UNANIMOUS
|
|
6
|
+
* consensus is reached or maxRounds safety valve triggers.
|
|
7
|
+
*
|
|
8
|
+
* Flow:
|
|
9
|
+
* 1. Generate 5 BerserkStrategy prompts (alpha~epsilon)
|
|
10
|
+
* 2. Execute adapters x strategies in parallel (up to concurrencyLimit)
|
|
11
|
+
* 3. Score candidates by fitness (confidence, reasoning quality, key points)
|
|
12
|
+
* 4. Select top-N survivors
|
|
13
|
+
* 5. Present survivors to ConsensusEngine
|
|
14
|
+
* 6. If UNANIMOUS → done. Else → re-vote with next round.
|
|
15
|
+
*
|
|
16
|
+
* Cache is intentionally disabled — BERSERK demands fresh computation.
|
|
17
|
+
* Timeout overrides are not applied — each adapter uses its own defaults.
|
|
18
|
+
*/
|
|
19
|
+
import { isUnanimous as isUnanimousDecision } from '../types/consensus.js';
|
|
20
|
+
import { ConsensusEngine } from './consensus.js';
|
|
21
|
+
import { UnitExecutor } from './kernel/unit-executor.js';
|
|
22
|
+
import { LatencyTracker } from '../utils/latency-tracker.js';
|
|
23
|
+
import { Semaphore } from '../utils/semaphore.js';
|
|
24
|
+
import { logger } from '../utils/logger.js';
|
|
25
|
+
import { fireAndForget } from '../utils/fire-and-forget.js';
|
|
26
|
+
import { createAbstainOpinion } from '../utils/abstain-factory.js';
|
|
27
|
+
import { randomUUID } from 'node:crypto';
|
|
28
|
+
// ── Default configuration ──────────────────────────────────────
|
|
29
|
+
const DEFAULT_BERSERK_CONFIG = {
|
|
30
|
+
maxRounds: 10,
|
|
31
|
+
concurrencyLimit: 5,
|
|
32
|
+
survivorCount: 5,
|
|
33
|
+
};
|
|
34
|
+
const DEFAULT_CONSENSUS_CONFIG = {
|
|
35
|
+
quorum: 2,
|
|
36
|
+
deadlockStrategy: 'melchior-tiebreak',
|
|
37
|
+
minConfidenceThreshold: 0,
|
|
38
|
+
useWeightedVoting: false,
|
|
39
|
+
};
|
|
40
|
+
// ── Strategy definitions ───────────────────────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* Generate 5 distinct BerserkStrategy definitions.
|
|
43
|
+
* Each provides a unique analytical perspective to maximize opinion diversity.
|
|
44
|
+
*/
|
|
45
|
+
export function generateStrategyPrompts() {
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
label: 'alpha',
|
|
49
|
+
name: '楽観 (Optimistic)',
|
|
50
|
+
promptPrefix: [
|
|
51
|
+
'You are in OPTIMISTIC mode. Focus on the best-case outcomes.',
|
|
52
|
+
'Identify opportunities, strengths, and potential benefits.',
|
|
53
|
+
'Assume favorable conditions and highlight positive aspects.',
|
|
54
|
+
'Look for creative solutions and innovative approaches.',
|
|
55
|
+
'Your perspective emphasizes what could go RIGHT.',
|
|
56
|
+
].join('\n'),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
label: 'beta',
|
|
60
|
+
name: '悲観 (Pessimistic)',
|
|
61
|
+
promptPrefix: [
|
|
62
|
+
'You are in PESSIMISTIC mode. Focus on worst-case scenarios.',
|
|
63
|
+
'Identify risks, weaknesses, and potential failures.',
|
|
64
|
+
'Assume unfavorable conditions and highlight vulnerabilities.',
|
|
65
|
+
'Consider edge cases, resource constraints, and failure modes.',
|
|
66
|
+
'Your perspective emphasizes what could go WRONG.',
|
|
67
|
+
].join('\n'),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
label: 'gamma',
|
|
71
|
+
name: '破壊的創造 (Destructive Creation)',
|
|
72
|
+
promptPrefix: [
|
|
73
|
+
'You are in DESTRUCTIVE CREATION mode. Challenge every assumption.',
|
|
74
|
+
'Tear down the existing approach and rebuild from first principles.',
|
|
75
|
+
'Question fundamental premises. Propose radical alternatives.',
|
|
76
|
+
'Think orthogonally — what if the problem itself is wrong?',
|
|
77
|
+
'Your perspective demolishes conventions to find breakthrough solutions.',
|
|
78
|
+
].join('\n'),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
label: 'delta',
|
|
82
|
+
name: '対抗的 (Adversarial)',
|
|
83
|
+
promptPrefix: [
|
|
84
|
+
'You are in ADVERSARIAL mode. Argue against the majority position.',
|
|
85
|
+
'Find flaws in popular reasoning. Play devil\'s advocate.',
|
|
86
|
+
'Stress-test every argument. Probe for logical fallacies.',
|
|
87
|
+
'Consider how an attacker, saboteur, or malicious actor would exploit this.',
|
|
88
|
+
'Your perspective is deliberately contrarian to strengthen analysis.',
|
|
89
|
+
].join('\n'),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
label: 'epsilon',
|
|
93
|
+
name: '保守的 (Conservative)',
|
|
94
|
+
promptPrefix: [
|
|
95
|
+
'You are in CONSERVATIVE mode. Prioritize stability and safety.',
|
|
96
|
+
'Favor proven approaches over novel ones.',
|
|
97
|
+
'Minimize risk, preserve backward compatibility, and avoid breaking changes.',
|
|
98
|
+
'Consider long-term maintenance burden and operational complexity.',
|
|
99
|
+
'Your perspective values reliability and predictability above all.',
|
|
100
|
+
].join('\n'),
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
// ── Fitness scoring ────────────────────────────────────────────
|
|
105
|
+
/** Score a candidate opinion for evolutionary fitness. */
|
|
106
|
+
function computeFitness(opinion) {
|
|
107
|
+
const confidenceScore = opinion.confidence * 0.5;
|
|
108
|
+
const reasoningQuality = computeReasoningQuality(opinion.reasoning) * 0.3;
|
|
109
|
+
const keyPointScore = computeKeyPointScore(opinion.keyPoints) * 0.2;
|
|
110
|
+
return confidenceScore + reasoningQuality + keyPointScore;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Estimate reasoning quality from text length and structure.
|
|
114
|
+
* Normalized to 0.0 - 1.0.
|
|
115
|
+
*/
|
|
116
|
+
function computeReasoningQuality(reasoning) {
|
|
117
|
+
if (!reasoning)
|
|
118
|
+
return 0;
|
|
119
|
+
const length = reasoning.length;
|
|
120
|
+
// Diminishing returns beyond 2000 chars
|
|
121
|
+
const lengthScore = Math.min(length / 2000, 1.0);
|
|
122
|
+
// Bonus for structured reasoning (sentences, paragraphs)
|
|
123
|
+
const sentenceCount = (reasoning.match(/[.!?。!?]\s/g) ?? []).length + 1;
|
|
124
|
+
const structureBonus = Math.min(sentenceCount / 10, 0.3);
|
|
125
|
+
return Math.min(lengthScore + structureBonus, 1.0);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Score key points by count and average length.
|
|
129
|
+
* Normalized to 0.0 - 1.0.
|
|
130
|
+
*/
|
|
131
|
+
function computeKeyPointScore(keyPoints) {
|
|
132
|
+
if (!keyPoints || keyPoints.length === 0)
|
|
133
|
+
return 0;
|
|
134
|
+
const countScore = Math.min(keyPoints.length / 5, 1.0);
|
|
135
|
+
const avgLength = keyPoints.reduce((sum, kp) => sum + kp.length, 0) / keyPoints.length;
|
|
136
|
+
const qualityScore = Math.min(avgLength / 100, 1.0);
|
|
137
|
+
return (countScore + qualityScore) / 2;
|
|
138
|
+
}
|
|
139
|
+
// ── BerserkOrchestrator ────────────────────────────────────────
|
|
140
|
+
export class BerserkOrchestrator {
|
|
141
|
+
adapters;
|
|
142
|
+
pipelines;
|
|
143
|
+
contextManager;
|
|
144
|
+
eventBus;
|
|
145
|
+
config;
|
|
146
|
+
semaphore;
|
|
147
|
+
consensusEngine;
|
|
148
|
+
strategies;
|
|
149
|
+
type666Firewall;
|
|
150
|
+
unitExecutor;
|
|
151
|
+
fileAccessEnabled;
|
|
152
|
+
constructor(adapters, pipelines, contextManager, eventBus, config, type666Firewall, fileAccessEnabled) {
|
|
153
|
+
this.adapters = adapters;
|
|
154
|
+
this.pipelines = pipelines;
|
|
155
|
+
this.contextManager = contextManager;
|
|
156
|
+
this.eventBus = eventBus;
|
|
157
|
+
this.config = { ...DEFAULT_BERSERK_CONFIG, ...config };
|
|
158
|
+
this.semaphore = new Semaphore(this.config.concurrencyLimit);
|
|
159
|
+
this.consensusEngine = new ConsensusEngine(DEFAULT_CONSENSUS_CONFIG);
|
|
160
|
+
this.strategies = generateStrategyPrompts();
|
|
161
|
+
this.type666Firewall = type666Firewall;
|
|
162
|
+
this.fileAccessEnabled = fileAccessEnabled;
|
|
163
|
+
this.unitExecutor = new UnitExecutor({
|
|
164
|
+
adapters,
|
|
165
|
+
pipelines,
|
|
166
|
+
contextManager,
|
|
167
|
+
latencyTracker: new LatencyTracker(),
|
|
168
|
+
eventBus,
|
|
169
|
+
modules: {},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
// ── Public API ─────────────────────────────────────────────
|
|
173
|
+
/**
|
|
174
|
+
* Execute BERSERK deliberation.
|
|
175
|
+
*
|
|
176
|
+
* Generates strategy-varied opinions in parallel, selects by fitness,
|
|
177
|
+
* and iterates until UNANIMOUS or maxRounds.
|
|
178
|
+
*/
|
|
179
|
+
async deliberate(task, signal) {
|
|
180
|
+
const id = task.id ?? randomUUID();
|
|
181
|
+
const startedAt = new Date();
|
|
182
|
+
const rounds = [];
|
|
183
|
+
const berserkRounds = [];
|
|
184
|
+
const pipeline = this.pipelines.get(task.type);
|
|
185
|
+
// Phase K: Firewall scan before BERSERK execution
|
|
186
|
+
if (this.type666Firewall) {
|
|
187
|
+
const scanResult = this.type666Firewall.scan(task.description);
|
|
188
|
+
if (!scanResult.passed) {
|
|
189
|
+
logger.warn('Type-666 Firewall blocked BERSERK input', {
|
|
190
|
+
threats: scanResult.threats.length, totalScore: scanResult.totalScore,
|
|
191
|
+
});
|
|
192
|
+
logger.audit('firewall.blocked_berserk', id, {
|
|
193
|
+
threats: scanResult.threats.length, totalScore: scanResult.totalScore,
|
|
194
|
+
});
|
|
195
|
+
throw new Error(`Type-666 Firewall blocked BERSERK: ${scanResult.threats.length} threats detected (score: ${scanResult.totalScore.toFixed(2)})`);
|
|
196
|
+
}
|
|
197
|
+
logger.audit('firewall.scan_berserk', id, { passed: true, totalScore: scanResult.totalScore });
|
|
198
|
+
}
|
|
199
|
+
await this.contextManager.initWorkspace(id, task);
|
|
200
|
+
logger.info('BERSERK MODE ACTIVATED', { id, maxRounds: this.config.maxRounds });
|
|
201
|
+
logger.audit('berserk.activated', id, {
|
|
202
|
+
strategies: this.strategies.map(s => s.label),
|
|
203
|
+
maxRounds: this.config.maxRounds,
|
|
204
|
+
});
|
|
205
|
+
this.eventBus?.emit('berserk:activated', {
|
|
206
|
+
deliberationId: id,
|
|
207
|
+
round: 0,
|
|
208
|
+
candidateCount: this.adapters.size * this.strategies.length,
|
|
209
|
+
strategies: this.strategies.map(s => s.label),
|
|
210
|
+
emittedAt: new Date(),
|
|
211
|
+
});
|
|
212
|
+
for (let round = 1; round <= this.config.maxRounds; round++) {
|
|
213
|
+
if (signal?.aborted) {
|
|
214
|
+
throw new Error('BERSERK deliberation cancelled');
|
|
215
|
+
}
|
|
216
|
+
logger.info(`BERSERK round ${round}/${this.config.maxRounds}`, { id });
|
|
217
|
+
// 1. Execute all adapter x strategy combinations in parallel
|
|
218
|
+
const candidates = await this.executeParallel(task, this.strategies, pipeline, id);
|
|
219
|
+
// 2. Score and select survivors
|
|
220
|
+
const survivors = this.fitnessSelection(candidates);
|
|
221
|
+
// 3. Build a DeliberationRound from survivors
|
|
222
|
+
const roundStart = new Date();
|
|
223
|
+
const survivorOpinions = survivors.map(s => s.opinion);
|
|
224
|
+
const deliberationRound = {
|
|
225
|
+
roundNumber: round,
|
|
226
|
+
phase: 'initial-opinion',
|
|
227
|
+
opinions: survivorOpinions,
|
|
228
|
+
startedAt: roundStart,
|
|
229
|
+
completedAt: new Date(),
|
|
230
|
+
};
|
|
231
|
+
rounds.push(deliberationRound);
|
|
232
|
+
// Save round (fire-and-forget)
|
|
233
|
+
fireAndForget(this.contextManager.saveRound(id, deliberationRound), 'berserk.saveRound');
|
|
234
|
+
// 4. Check consensus via ConsensusEngine
|
|
235
|
+
const consensus = this.consensusEngine.calculate(survivorOpinions);
|
|
236
|
+
const isUnanimous = isUnanimousDecision(consensus.decision);
|
|
237
|
+
const berserkRound = {
|
|
238
|
+
roundNumber: round,
|
|
239
|
+
candidates,
|
|
240
|
+
survivors,
|
|
241
|
+
consensusReached: isUnanimous,
|
|
242
|
+
decision: consensus.decision,
|
|
243
|
+
};
|
|
244
|
+
berserkRounds.push(berserkRound);
|
|
245
|
+
logger.info(`BERSERK round ${round} result: ${consensus.decision}`, {
|
|
246
|
+
id,
|
|
247
|
+
candidateCount: candidates.length,
|
|
248
|
+
survivorCount: survivors.length,
|
|
249
|
+
unanimous: isUnanimous,
|
|
250
|
+
});
|
|
251
|
+
if (isUnanimous) {
|
|
252
|
+
logger.info('BERSERK UNANIMOUS reached — deactivating', { id, round });
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
// If we've hit maxRounds without unanimity, use the last consensus
|
|
256
|
+
if (round === this.config.maxRounds) {
|
|
257
|
+
logger.warn('BERSERK maxRounds reached — forcing deactivation', {
|
|
258
|
+
id, maxRounds: this.config.maxRounds, decision: consensus.decision,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Final consensus from the last round's survivors
|
|
263
|
+
const lastRound = rounds[rounds.length - 1];
|
|
264
|
+
const finalOpinions = lastRound?.opinions ?? [];
|
|
265
|
+
const finalConsensus = this.consensusEngine.calculate(finalOpinions);
|
|
266
|
+
const completedAt = new Date();
|
|
267
|
+
const totalDurationMs = completedAt.getTime() - startedAt.getTime();
|
|
268
|
+
logger.info('BERSERK MODE DEACTIVATED', {
|
|
269
|
+
id,
|
|
270
|
+
rounds: rounds.length,
|
|
271
|
+
decision: finalConsensus.decision,
|
|
272
|
+
totalDurationMs,
|
|
273
|
+
});
|
|
274
|
+
logger.audit('berserk.deactivated', id, {
|
|
275
|
+
totalRounds: rounds.length,
|
|
276
|
+
decision: finalConsensus.decision,
|
|
277
|
+
totalDurationMs,
|
|
278
|
+
});
|
|
279
|
+
this.eventBus?.emit('berserk:deactivated', {
|
|
280
|
+
deliberationId: id,
|
|
281
|
+
totalRounds: rounds.length,
|
|
282
|
+
decision: finalConsensus.decision,
|
|
283
|
+
emittedAt: new Date(),
|
|
284
|
+
});
|
|
285
|
+
const deliberation = {
|
|
286
|
+
id,
|
|
287
|
+
task,
|
|
288
|
+
rounds,
|
|
289
|
+
consensus: finalConsensus,
|
|
290
|
+
totalDurationMs,
|
|
291
|
+
startedAt,
|
|
292
|
+
completedAt,
|
|
293
|
+
};
|
|
294
|
+
await this.contextManager.saveDeliberation(deliberation).catch((err) => {
|
|
295
|
+
logger.warn('Failed to save BERSERK deliberation', { error: String(err) });
|
|
296
|
+
});
|
|
297
|
+
return deliberation;
|
|
298
|
+
}
|
|
299
|
+
// ── Parallel execution ────────────────────────────────────
|
|
300
|
+
/**
|
|
301
|
+
* Execute all adapter x strategy combinations in parallel,
|
|
302
|
+
* bounded by the Semaphore concurrency limit.
|
|
303
|
+
*/
|
|
304
|
+
async executeParallel(task, strategies, pipeline, deliberationId) {
|
|
305
|
+
const allAdapters = this.adapters.getAll();
|
|
306
|
+
// Build execution list: adapter x strategy
|
|
307
|
+
const execPairs = [];
|
|
308
|
+
for (const adapter of allAdapters) {
|
|
309
|
+
for (const strategy of strategies) {
|
|
310
|
+
execPairs.push({ adapter, strategy });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// Execute with semaphore-bounded concurrency via shared UnitExecutor
|
|
314
|
+
const results = await Promise.allSettled(execPairs.map(({ adapter, strategy }) => this.semaphore.run(() => this.unitExecutor.execute({
|
|
315
|
+
deliberationId,
|
|
316
|
+
adapter,
|
|
317
|
+
task,
|
|
318
|
+
pipeline,
|
|
319
|
+
phase: 'initial-opinion',
|
|
320
|
+
previousRounds: [],
|
|
321
|
+
config: pipeline.defaultConfig,
|
|
322
|
+
berserkStrategy: { name: strategy.name, promptPrefix: strategy.promptPrefix },
|
|
323
|
+
fileAccessEnabled: this.fileAccessEnabled,
|
|
324
|
+
}))));
|
|
325
|
+
const candidates = [];
|
|
326
|
+
for (let i = 0; i < results.length; i++) {
|
|
327
|
+
const result = results[i];
|
|
328
|
+
const { adapter, strategy } = execPairs[i];
|
|
329
|
+
if (result.status === 'fulfilled') {
|
|
330
|
+
const opinion = result.value;
|
|
331
|
+
const fitnessScore = computeFitness(opinion);
|
|
332
|
+
candidates.push({
|
|
333
|
+
unit: adapter.unit,
|
|
334
|
+
strategy: strategy.label,
|
|
335
|
+
opinion,
|
|
336
|
+
fitnessScore,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
// Adapter failure -> ABSTAIN fallback candidate
|
|
341
|
+
logger.warn(`BERSERK adapter failure: ${adapter.unit}/${strategy.label}`, {
|
|
342
|
+
error: String(result.reason),
|
|
343
|
+
});
|
|
344
|
+
logger.audit('berserk.adapter_failure', deliberationId, {
|
|
345
|
+
unit: adapter.unit,
|
|
346
|
+
strategy: strategy.label,
|
|
347
|
+
error: String(result.reason),
|
|
348
|
+
});
|
|
349
|
+
const fallbackOpinion = createAbstainOpinion(adapter.unit, `${adapter.displayName} failed in BERSERK strategy ${strategy.label}: ${result.reason?.message ?? 'unknown error'}`, ['Adapter execution failed in BERSERK mode']);
|
|
350
|
+
candidates.push({
|
|
351
|
+
unit: adapter.unit,
|
|
352
|
+
strategy: strategy.label,
|
|
353
|
+
opinion: fallbackOpinion,
|
|
354
|
+
fitnessScore: 0,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return candidates;
|
|
359
|
+
}
|
|
360
|
+
// ── Fitness selection ─────────────────────────────────────
|
|
361
|
+
/**
|
|
362
|
+
* Select top-N candidates by fitness score.
|
|
363
|
+
* Returns candidates sorted by fitness descending, capped at survivorCount.
|
|
364
|
+
*/
|
|
365
|
+
fitnessSelection(candidates) {
|
|
366
|
+
const sorted = [...candidates].sort((a, b) => b.fitnessScore - a.fitnessScore);
|
|
367
|
+
return sorted.slice(0, this.config.survivorCount);
|
|
368
|
+
}
|
|
369
|
+
// ── Accessors (for testing) ───────────────────────────────
|
|
370
|
+
/** Get the strategies used by this orchestrator. */
|
|
371
|
+
getStrategies() {
|
|
372
|
+
return [...this.strategies];
|
|
373
|
+
}
|
|
374
|
+
/** Get the current config. */
|
|
375
|
+
getConfig() {
|
|
376
|
+
return { ...this.config };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChangeMetrics — Kamei metrics computation module (Phase K Step 0)
|
|
3
|
+
*
|
|
4
|
+
* Shared by AngelDetector (A-04) and DeadSeaScrolls (A-08).
|
|
5
|
+
* Computes commit-level metrics: NS/ND/NF/entropy/LA/LD/NDEV/AGE
|
|
6
|
+
* plus file-level helpers and co-change graph construction.
|
|
7
|
+
*
|
|
8
|
+
* All git commands use execFile with array arguments (no shell injection)
|
|
9
|
+
* and a 10-second timeout.
|
|
10
|
+
*/
|
|
11
|
+
import type { KameiMetrics, CommitFileChange, CochangeEdge } from '../types/phase-k.js';
|
|
12
|
+
/**
|
|
13
|
+
* Execute a git command with array arguments and 10s timeout.
|
|
14
|
+
* Returns trimmed stdout.
|
|
15
|
+
*/
|
|
16
|
+
export declare function execGit(args: string[], cwd?: string): Promise<string>;
|
|
17
|
+
/**
|
|
18
|
+
* Parse `git diff-tree --no-commit-id -r --numstat` output
|
|
19
|
+
* to extract file changes for a single commit.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getCommitFiles(commitHash: string, cwd?: string): Promise<CommitFileChange[]>;
|
|
22
|
+
/**
|
|
23
|
+
* Get the age of a file in days since its last modification.
|
|
24
|
+
* Uses `git log -1 --format=%ct` to get the last commit timestamp.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getFileAge(filePath: string, cwd?: string): Promise<number>;
|
|
27
|
+
/**
|
|
28
|
+
* Count unique developers (by email) who have touched a file.
|
|
29
|
+
* Uses `git log --format=%ae`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function getFileDevCount(filePath: string, cwd?: string): Promise<number>;
|
|
32
|
+
/**
|
|
33
|
+
* Compute Shannon entropy of changes across files.
|
|
34
|
+
* Uses proportion of (linesAdded + linesDeleted) per file relative to total.
|
|
35
|
+
* Pure function, no I/O.
|
|
36
|
+
*/
|
|
37
|
+
export declare function computeEntropy(changes: CommitFileChange[]): number;
|
|
38
|
+
/**
|
|
39
|
+
* Build a co-change graph from a list of commits.
|
|
40
|
+
* For each commit, every pair of files changed together gets an edge
|
|
41
|
+
* with weight incremented by 1.
|
|
42
|
+
*/
|
|
43
|
+
export declare function getCochangeGraph(commitHashes: string[], cwd?: string): Promise<CochangeEdge[]>;
|
|
44
|
+
/**
|
|
45
|
+
* Compute full Kamei metrics for a single commit.
|
|
46
|
+
*
|
|
47
|
+
* - NS = number of unique top-level directories (subsystems)
|
|
48
|
+
* - ND = number of unique directory paths
|
|
49
|
+
* - NF = number of changed files
|
|
50
|
+
* - entropy = Shannon entropy of line changes
|
|
51
|
+
* - LA = total lines added
|
|
52
|
+
* - LD = total lines deleted
|
|
53
|
+
* - NDEV = max unique developer count across changed files
|
|
54
|
+
* - AGE = average file age in days across changed files
|
|
55
|
+
*/
|
|
56
|
+
export declare function computeKameiMetrics(commitHash: string, cwd?: string): Promise<KameiMetrics>;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChangeMetrics — Kamei metrics computation module (Phase K Step 0)
|
|
3
|
+
*
|
|
4
|
+
* Shared by AngelDetector (A-04) and DeadSeaScrolls (A-08).
|
|
5
|
+
* Computes commit-level metrics: NS/ND/NF/entropy/LA/LD/NDEV/AGE
|
|
6
|
+
* plus file-level helpers and co-change graph construction.
|
|
7
|
+
*
|
|
8
|
+
* All git commands use execFile with array arguments (no shell injection)
|
|
9
|
+
* and a 10-second timeout.
|
|
10
|
+
*/
|
|
11
|
+
import { execFile } from 'node:child_process';
|
|
12
|
+
const GIT_TIMEOUT_MS = 10_000;
|
|
13
|
+
// ── Git helper ──────────────────────────────────────────────
|
|
14
|
+
/**
|
|
15
|
+
* Execute a git command with array arguments and 10s timeout.
|
|
16
|
+
* Returns trimmed stdout.
|
|
17
|
+
*/
|
|
18
|
+
export async function execGit(args, cwd) {
|
|
19
|
+
const options = {
|
|
20
|
+
timeout: GIT_TIMEOUT_MS,
|
|
21
|
+
maxBuffer: 10 * 1024 * 1024, // 10 MB
|
|
22
|
+
};
|
|
23
|
+
if (cwd) {
|
|
24
|
+
options.cwd = cwd;
|
|
25
|
+
}
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
execFile('git', args, options, (error, stdout) => {
|
|
28
|
+
if (error) {
|
|
29
|
+
reject(error);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
resolve(stdout.trim());
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// ── getCommitFiles ──────────────────────────────────────────
|
|
37
|
+
/**
|
|
38
|
+
* Parse `git diff-tree --no-commit-id -r --numstat` output
|
|
39
|
+
* to extract file changes for a single commit.
|
|
40
|
+
*/
|
|
41
|
+
export async function getCommitFiles(commitHash, cwd) {
|
|
42
|
+
const output = await execGit(['diff-tree', '--no-commit-id', '-r', '--numstat', commitHash], cwd);
|
|
43
|
+
if (!output)
|
|
44
|
+
return [];
|
|
45
|
+
const lines = output.split('\n');
|
|
46
|
+
const changes = [];
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
const trimmed = line.trim();
|
|
49
|
+
if (!trimmed)
|
|
50
|
+
continue;
|
|
51
|
+
// numstat format: "added\tdeleted\tfilepath"
|
|
52
|
+
// Binary files show: "-\t-\tfilepath"
|
|
53
|
+
const parts = trimmed.split('\t');
|
|
54
|
+
if (parts.length < 3)
|
|
55
|
+
continue;
|
|
56
|
+
const addedStr = parts[0];
|
|
57
|
+
const deletedStr = parts[1];
|
|
58
|
+
const filePath = parts.slice(2).join('\t'); // handle paths with tabs (unlikely but safe)
|
|
59
|
+
const isBinary = addedStr === '-' && deletedStr === '-';
|
|
60
|
+
const linesAdded = isBinary ? 0 : parseInt(addedStr, 10);
|
|
61
|
+
const linesDeleted = isBinary ? 0 : parseInt(deletedStr, 10);
|
|
62
|
+
changes.push({
|
|
63
|
+
status: 'M', // numstat does not provide status; default to M
|
|
64
|
+
filePath,
|
|
65
|
+
linesAdded: Number.isNaN(linesAdded) ? 0 : linesAdded,
|
|
66
|
+
linesDeleted: Number.isNaN(linesDeleted) ? 0 : linesDeleted,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return changes;
|
|
70
|
+
}
|
|
71
|
+
// ── getFileAge ──────────────────────────────────────────────
|
|
72
|
+
/**
|
|
73
|
+
* Get the age of a file in days since its last modification.
|
|
74
|
+
* Uses `git log -1 --format=%ct` to get the last commit timestamp.
|
|
75
|
+
*/
|
|
76
|
+
export async function getFileAge(filePath, cwd) {
|
|
77
|
+
const output = await execGit(['log', '-1', '--format=%ct', '--', filePath], cwd);
|
|
78
|
+
if (!output)
|
|
79
|
+
return 0;
|
|
80
|
+
const timestamp = parseInt(output, 10);
|
|
81
|
+
if (Number.isNaN(timestamp))
|
|
82
|
+
return 0;
|
|
83
|
+
const now = Date.now() / 1000; // current time in seconds
|
|
84
|
+
const diffSeconds = now - timestamp;
|
|
85
|
+
return Math.max(0, Math.floor(diffSeconds / 86400)); // convert to days
|
|
86
|
+
}
|
|
87
|
+
// ── getFileDevCount ─────────────────────────────────────────
|
|
88
|
+
/**
|
|
89
|
+
* Count unique developers (by email) who have touched a file.
|
|
90
|
+
* Uses `git log --format=%ae`.
|
|
91
|
+
*/
|
|
92
|
+
export async function getFileDevCount(filePath, cwd) {
|
|
93
|
+
const output = await execGit(['log', '--format=%ae', '--', filePath], cwd);
|
|
94
|
+
if (!output)
|
|
95
|
+
return 0;
|
|
96
|
+
const emails = output.split('\n').filter(Boolean);
|
|
97
|
+
const unique = new Set(emails);
|
|
98
|
+
return unique.size;
|
|
99
|
+
}
|
|
100
|
+
// ── computeEntropy ──────────────────────────────────────────
|
|
101
|
+
/**
|
|
102
|
+
* Compute Shannon entropy of changes across files.
|
|
103
|
+
* Uses proportion of (linesAdded + linesDeleted) per file relative to total.
|
|
104
|
+
* Pure function, no I/O.
|
|
105
|
+
*/
|
|
106
|
+
export function computeEntropy(changes) {
|
|
107
|
+
if (changes.length === 0)
|
|
108
|
+
return 0;
|
|
109
|
+
const total = changes.reduce((sum, c) => sum + c.linesAdded + c.linesDeleted, 0);
|
|
110
|
+
if (total === 0)
|
|
111
|
+
return 0;
|
|
112
|
+
let entropy = 0;
|
|
113
|
+
for (const change of changes) {
|
|
114
|
+
const fileChanges = change.linesAdded + change.linesDeleted;
|
|
115
|
+
if (fileChanges === 0)
|
|
116
|
+
continue;
|
|
117
|
+
const p = fileChanges / total;
|
|
118
|
+
entropy -= p * Math.log2(p);
|
|
119
|
+
}
|
|
120
|
+
return entropy;
|
|
121
|
+
}
|
|
122
|
+
// ── getCochangeGraph ────────────────────────────────────────
|
|
123
|
+
/**
|
|
124
|
+
* Build a co-change graph from a list of commits.
|
|
125
|
+
* For each commit, every pair of files changed together gets an edge
|
|
126
|
+
* with weight incremented by 1.
|
|
127
|
+
*/
|
|
128
|
+
export async function getCochangeGraph(commitHashes, cwd) {
|
|
129
|
+
const edgeMap = new Map();
|
|
130
|
+
for (const hash of commitHashes) {
|
|
131
|
+
const files = await getCommitFiles(hash, cwd);
|
|
132
|
+
const filePaths = files.map(f => f.filePath);
|
|
133
|
+
// Generate all pairs (i < j) to avoid duplicates
|
|
134
|
+
for (let i = 0; i < filePaths.length; i++) {
|
|
135
|
+
for (let j = i + 1; j < filePaths.length; j++) {
|
|
136
|
+
const a = filePaths[i];
|
|
137
|
+
const b = filePaths[j];
|
|
138
|
+
// Canonical key: sorted alphabetically
|
|
139
|
+
const key = a < b ? `${a}\0${b}` : `${b}\0${a}`;
|
|
140
|
+
edgeMap.set(key, (edgeMap.get(key) ?? 0) + 1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const edges = [];
|
|
145
|
+
for (const [key, weight] of edgeMap) {
|
|
146
|
+
const [fileA, fileB] = key.split('\0');
|
|
147
|
+
edges.push({ fileA, fileB, weight });
|
|
148
|
+
}
|
|
149
|
+
return edges;
|
|
150
|
+
}
|
|
151
|
+
// ── computeKameiMetrics ─────────────────────────────────────
|
|
152
|
+
/**
|
|
153
|
+
* Compute full Kamei metrics for a single commit.
|
|
154
|
+
*
|
|
155
|
+
* - NS = number of unique top-level directories (subsystems)
|
|
156
|
+
* - ND = number of unique directory paths
|
|
157
|
+
* - NF = number of changed files
|
|
158
|
+
* - entropy = Shannon entropy of line changes
|
|
159
|
+
* - LA = total lines added
|
|
160
|
+
* - LD = total lines deleted
|
|
161
|
+
* - NDEV = max unique developer count across changed files
|
|
162
|
+
* - AGE = average file age in days across changed files
|
|
163
|
+
*/
|
|
164
|
+
export async function computeKameiMetrics(commitHash, cwd) {
|
|
165
|
+
const files = await getCommitFiles(commitHash, cwd);
|
|
166
|
+
const NF = files.length;
|
|
167
|
+
const LA = files.reduce((sum, f) => sum + f.linesAdded, 0);
|
|
168
|
+
const LD = files.reduce((sum, f) => sum + f.linesDeleted, 0);
|
|
169
|
+
const entropy = computeEntropy(files);
|
|
170
|
+
// Directories and subsystems
|
|
171
|
+
const directories = new Set();
|
|
172
|
+
const subsystems = new Set();
|
|
173
|
+
for (const file of files) {
|
|
174
|
+
const parts = file.filePath.split('/');
|
|
175
|
+
if (parts.length > 1) {
|
|
176
|
+
// Top-level directory = subsystem
|
|
177
|
+
subsystems.add(parts[0]);
|
|
178
|
+
// Full directory path (everything except the filename)
|
|
179
|
+
directories.add(parts.slice(0, -1).join('/'));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
// Root-level file: subsystem is '.' (root)
|
|
183
|
+
subsystems.add('.');
|
|
184
|
+
directories.add('.');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const NS = subsystems.size;
|
|
188
|
+
const ND = directories.size;
|
|
189
|
+
// NDEV and AGE require per-file git queries
|
|
190
|
+
let maxDevCount = 0;
|
|
191
|
+
let totalAge = 0;
|
|
192
|
+
if (files.length > 0) {
|
|
193
|
+
const devCounts = await Promise.all(files.map(f => getFileDevCount(f.filePath, cwd)));
|
|
194
|
+
const ages = await Promise.all(files.map(f => getFileAge(f.filePath, cwd)));
|
|
195
|
+
for (const count of devCounts) {
|
|
196
|
+
if (count > maxDevCount)
|
|
197
|
+
maxDevCount = count;
|
|
198
|
+
}
|
|
199
|
+
totalAge = ages.reduce((sum, age) => sum + age, 0);
|
|
200
|
+
}
|
|
201
|
+
const NDEV = maxDevCount;
|
|
202
|
+
const AGE = files.length > 0 ? totalAge / files.length : 0;
|
|
203
|
+
return {
|
|
204
|
+
commitHash,
|
|
205
|
+
NS,
|
|
206
|
+
ND,
|
|
207
|
+
NF,
|
|
208
|
+
entropy,
|
|
209
|
+
LA,
|
|
210
|
+
LD,
|
|
211
|
+
NDEV,
|
|
212
|
+
AGE,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ConsensusResult, ConsensusConfig } from '../types/consensus.js';
|
|
2
|
+
import type { MagiOpinion } from '../types/core.js';
|
|
3
|
+
/**
|
|
4
|
+
* MAGI Consensus Engine
|
|
5
|
+
*
|
|
6
|
+
* Calculates consensus from N MAGI units' opinions (2-7) using:
|
|
7
|
+
* - Unanimous: all agree
|
|
8
|
+
* - Majority: more than half of non-abstain voters
|
|
9
|
+
* - Deadlock resolution: configurable strategy
|
|
10
|
+
*/
|
|
11
|
+
export declare class ConsensusEngine {
|
|
12
|
+
private config;
|
|
13
|
+
constructor(config: ConsensusConfig);
|
|
14
|
+
calculate(opinions: MagiOpinion[]): ConsensusResult;
|
|
15
|
+
private tallyVotes;
|
|
16
|
+
private resolveDeadlock;
|
|
17
|
+
private getUnitWeight;
|
|
18
|
+
private buildResult;
|
|
19
|
+
private buildSummary;
|
|
20
|
+
}
|