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,38 @@
|
|
|
1
|
+
/** Token record for a single unit execution */
|
|
2
|
+
export interface UnitTokenRecord {
|
|
3
|
+
deliberationId: string;
|
|
4
|
+
unit: string;
|
|
5
|
+
phase: string;
|
|
6
|
+
estimatedInputTokens: number;
|
|
7
|
+
estimatedOutputTokens: number;
|
|
8
|
+
timestamp: Date;
|
|
9
|
+
}
|
|
10
|
+
/** Summary of token usage for a deliberation */
|
|
11
|
+
export interface DeliberationTokenSummary {
|
|
12
|
+
deliberationId: string;
|
|
13
|
+
totalEstimatedTokens: number;
|
|
14
|
+
byUnit: Record<string, {
|
|
15
|
+
estimatedTokens: number;
|
|
16
|
+
}>;
|
|
17
|
+
byPhase: Record<string, {
|
|
18
|
+
estimatedTokens: number;
|
|
19
|
+
}>;
|
|
20
|
+
recordCount: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Estimate token count from text.
|
|
24
|
+
* Uses a heuristic that accounts for Japanese/CJK text having fewer chars per token.
|
|
25
|
+
*/
|
|
26
|
+
export declare function estimateTokens(text: string): number;
|
|
27
|
+
export declare class TokenTracker {
|
|
28
|
+
private records;
|
|
29
|
+
private readonly persistDir;
|
|
30
|
+
constructor(workspaceDir?: string);
|
|
31
|
+
addRecord(record: UnitTokenRecord): void;
|
|
32
|
+
summarize(deliberationId?: string): DeliberationTokenSummary;
|
|
33
|
+
persist(deliberationId: string): Promise<void>;
|
|
34
|
+
load(deliberationId: string): Promise<DeliberationTokenSummary | null>;
|
|
35
|
+
loadAll(): Promise<DeliberationTokenSummary[]>;
|
|
36
|
+
getRecords(): readonly UnitTokenRecord[];
|
|
37
|
+
clear(): void;
|
|
38
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { UUID_V4_RE } from '../types/core.js';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import { safeMkdir, safeWriteFile } from '../utils/safe-fs.js';
|
|
6
|
+
import { safeJsonParse } from '../utils/safe-json-parse.js';
|
|
7
|
+
/**
|
|
8
|
+
* Estimate token count from text.
|
|
9
|
+
* Uses a heuristic that accounts for Japanese/CJK text having fewer chars per token.
|
|
10
|
+
*/
|
|
11
|
+
export function estimateTokens(text) {
|
|
12
|
+
if (!text)
|
|
13
|
+
return 0;
|
|
14
|
+
// Count CJK characters
|
|
15
|
+
const cjkChars = (text.match(/[\u3000-\u9FFF\uF900-\uFAFF]/g) || []).length;
|
|
16
|
+
const nonCjkChars = text.length - cjkChars;
|
|
17
|
+
// CJK: ~1.5 chars per token, non-CJK: ~4 chars per token
|
|
18
|
+
return Math.ceil(cjkChars / 1.5 + nonCjkChars / 4);
|
|
19
|
+
}
|
|
20
|
+
export class TokenTracker {
|
|
21
|
+
records = [];
|
|
22
|
+
persistDir;
|
|
23
|
+
constructor(workspaceDir = '.magi') {
|
|
24
|
+
this.persistDir = join(workspaceDir, 'tokens');
|
|
25
|
+
}
|
|
26
|
+
addRecord(record) {
|
|
27
|
+
this.records.push(record);
|
|
28
|
+
}
|
|
29
|
+
summarize(deliberationId) {
|
|
30
|
+
const filtered = deliberationId
|
|
31
|
+
? this.records.filter(r => r.deliberationId === deliberationId)
|
|
32
|
+
: this.records;
|
|
33
|
+
const byUnit = {};
|
|
34
|
+
const byPhase = {};
|
|
35
|
+
let totalEstimatedTokens = 0;
|
|
36
|
+
for (const r of filtered) {
|
|
37
|
+
const tokens = r.estimatedInputTokens + r.estimatedOutputTokens;
|
|
38
|
+
totalEstimatedTokens += tokens;
|
|
39
|
+
if (!byUnit[r.unit])
|
|
40
|
+
byUnit[r.unit] = { estimatedTokens: 0 };
|
|
41
|
+
byUnit[r.unit].estimatedTokens += tokens;
|
|
42
|
+
if (!byPhase[r.phase])
|
|
43
|
+
byPhase[r.phase] = { estimatedTokens: 0 };
|
|
44
|
+
byPhase[r.phase].estimatedTokens += tokens;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
deliberationId: deliberationId ?? 'all',
|
|
48
|
+
totalEstimatedTokens,
|
|
49
|
+
byUnit,
|
|
50
|
+
byPhase,
|
|
51
|
+
recordCount: filtered.length,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async persist(deliberationId) {
|
|
55
|
+
if (!UUID_V4_RE.test(deliberationId)) {
|
|
56
|
+
throw new Error('Invalid deliberation ID: must be UUID v4 format');
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
await safeMkdir(this.persistDir);
|
|
60
|
+
const filePath = join(this.persistDir, `${deliberationId}.json`);
|
|
61
|
+
const summary = this.summarize(deliberationId);
|
|
62
|
+
await safeWriteFile(filePath, JSON.stringify(summary, null, 2));
|
|
63
|
+
logger.debug('Token data persisted', { deliberationId, file: filePath });
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
logger.warn('Failed to persist token data', { deliberationId, error: String(err) });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async load(deliberationId) {
|
|
70
|
+
if (!UUID_V4_RE.test(deliberationId)) {
|
|
71
|
+
throw new Error('Invalid deliberation ID: must be UUID v4 format');
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const filePath = join(this.persistDir, `${deliberationId}.json`);
|
|
75
|
+
const content = await readFile(filePath, 'utf-8');
|
|
76
|
+
return safeJsonParse(content);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
logger.debug('TokenTracker: token data file not found', { deliberationId, error: String(err) });
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async loadAll() {
|
|
84
|
+
try {
|
|
85
|
+
await safeMkdir(this.persistDir);
|
|
86
|
+
const files = await readdir(this.persistDir);
|
|
87
|
+
const summaries = [];
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
if (!file.endsWith('.json'))
|
|
90
|
+
continue;
|
|
91
|
+
try {
|
|
92
|
+
const content = await readFile(join(this.persistDir, file), 'utf-8');
|
|
93
|
+
summaries.push(safeJsonParse(content));
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
logger.debug('TokenTracker: corrupt token file skipped', { file, error: String(err) });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return summaries;
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
logger.debug('TokenTracker: failed to load token summaries', { error: String(err) });
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
getRecords() {
|
|
107
|
+
return this.records;
|
|
108
|
+
}
|
|
109
|
+
clear() {
|
|
110
|
+
this.records = [];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type ExtractionStrategy = 'strict-json' | 'envelope-unwrap' | 'markdown-block' | 'object-scan' | 'none';
|
|
2
|
+
export interface ExtractionResult {
|
|
3
|
+
data: Record<string, unknown> | null;
|
|
4
|
+
raw: string;
|
|
5
|
+
effectiveRaw: string;
|
|
6
|
+
success: boolean;
|
|
7
|
+
strategy: ExtractionStrategy;
|
|
8
|
+
}
|
|
9
|
+
export declare function extractJson(raw: string): ExtractionResult;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAGI JSON Extractor - Unified extraction module
|
|
3
|
+
*
|
|
4
|
+
* Extracts JSON objects from raw CLI output using 4 strategies:
|
|
5
|
+
* 1. strict-json — Full text is a JSON object (with envelope guard)
|
|
6
|
+
* 2. envelope-unwrap — { type:"result", result:"..." } unwrapping
|
|
7
|
+
* 3. markdown-block — ```json ... ``` code block extraction
|
|
8
|
+
* 4. object-scan — Balanced brace scanning for embedded JSON
|
|
9
|
+
*
|
|
10
|
+
* Responsibility: string → Record<string, unknown> | null
|
|
11
|
+
* Downstream: opinion-schema validates the extracted object.
|
|
12
|
+
*/
|
|
13
|
+
import { isLikelyOpinion } from './opinion-schema.js';
|
|
14
|
+
import { logger } from '../utils/logger.js';
|
|
15
|
+
const ENVELOPE_KEYS = ['result', 'response', 'text', 'content', 'output'];
|
|
16
|
+
function looksLikeEnvelope(obj) {
|
|
17
|
+
// Claude CLI pattern: { type: "result", result: "..." }
|
|
18
|
+
if (obj['type'] === 'result' && 'result' in obj)
|
|
19
|
+
return true;
|
|
20
|
+
// Direct opinion — not an envelope
|
|
21
|
+
if (isLikelyOpinion(obj))
|
|
22
|
+
return false;
|
|
23
|
+
// Generic envelope: known wrapper key with string-containing-JSON or nested object
|
|
24
|
+
for (const key of ENVELOPE_KEYS) {
|
|
25
|
+
const val = obj[key];
|
|
26
|
+
if (typeof val === 'string' && val.trimStart().startsWith('{'))
|
|
27
|
+
return true;
|
|
28
|
+
if (isPlainObject(val))
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
function isPlainObject(value) {
|
|
34
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
// Strategy 1: strict-json (envelope guard)
|
|
37
|
+
function tryStrictJson(raw) {
|
|
38
|
+
const trimmed = raw.trim();
|
|
39
|
+
if (!trimmed.startsWith('{'))
|
|
40
|
+
return null;
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(trimmed);
|
|
43
|
+
if (!isPlainObject(parsed))
|
|
44
|
+
return null;
|
|
45
|
+
if (looksLikeEnvelope(parsed))
|
|
46
|
+
return null;
|
|
47
|
+
return { data: parsed, effectiveRaw: raw };
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
logger.debug('JSON extraction: strict-json parse failed', { error: String(err) });
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Strategy 2: envelope-unwrap
|
|
55
|
+
function tryEnvelopeUnwrap(raw) {
|
|
56
|
+
const trimmed = raw.trim();
|
|
57
|
+
if (!trimmed.startsWith('{'))
|
|
58
|
+
return null;
|
|
59
|
+
let envelope;
|
|
60
|
+
try {
|
|
61
|
+
envelope = JSON.parse(trimmed);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
logger.debug('JSON extraction: envelope-unwrap outer parse failed', { error: String(err) });
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
if (!isPlainObject(envelope))
|
|
68
|
+
return null;
|
|
69
|
+
if (!looksLikeEnvelope(envelope))
|
|
70
|
+
return null;
|
|
71
|
+
for (const key of ENVELOPE_KEYS) {
|
|
72
|
+
const value = envelope[key];
|
|
73
|
+
if (typeof value === 'string') {
|
|
74
|
+
try {
|
|
75
|
+
const inner = JSON.parse(value);
|
|
76
|
+
if (isPlainObject(inner)) {
|
|
77
|
+
return { data: inner, effectiveRaw: value };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
logger.debug('JSON extraction: envelope inner value not JSON', { error: String(err) });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (isPlainObject(value)) {
|
|
85
|
+
return { data: value, effectiveRaw: JSON.stringify(value) };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Strip envelope wrapper and return the inner text content.
|
|
92
|
+
* Used when all extraction strategies fail but the raw text is an envelope.
|
|
93
|
+
* This prevents downstream parsers from seeing the envelope JSON as reasoning text.
|
|
94
|
+
*/
|
|
95
|
+
function stripEnvelopeText(raw) {
|
|
96
|
+
const trimmed = raw.trim();
|
|
97
|
+
if (!trimmed.startsWith('{'))
|
|
98
|
+
return null;
|
|
99
|
+
try {
|
|
100
|
+
const obj = JSON.parse(trimmed);
|
|
101
|
+
if (!isPlainObject(obj) || !looksLikeEnvelope(obj))
|
|
102
|
+
return null;
|
|
103
|
+
for (const key of ENVELOPE_KEYS) {
|
|
104
|
+
const value = obj[key];
|
|
105
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// not JSON
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
// Strategy 3: markdown-block
|
|
116
|
+
function tryMarkdownBlock(raw) {
|
|
117
|
+
const regex = /```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/g;
|
|
118
|
+
let match;
|
|
119
|
+
while ((match = regex.exec(raw)) !== null) {
|
|
120
|
+
const content = match[1].trim();
|
|
121
|
+
if (!content.startsWith('{'))
|
|
122
|
+
continue;
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(content);
|
|
125
|
+
if (isPlainObject(parsed)) {
|
|
126
|
+
return { data: parsed, effectiveRaw: raw };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
logger.debug('JSON extraction: markdown block parse failed', { error: String(err) });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
// Strategy 4: object-scan (balanced brace scanning)
|
|
136
|
+
function findMatchingBrace(str, start) {
|
|
137
|
+
let depth = 0;
|
|
138
|
+
let inString = false;
|
|
139
|
+
let escape = false;
|
|
140
|
+
for (let i = start; i < str.length; i++) {
|
|
141
|
+
const ch = str[i];
|
|
142
|
+
if (escape) {
|
|
143
|
+
escape = false;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (ch === '\\' && inString) {
|
|
147
|
+
escape = true;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (ch === '"') {
|
|
151
|
+
inString = !inString;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (inString)
|
|
155
|
+
continue;
|
|
156
|
+
if (ch === '{')
|
|
157
|
+
depth++;
|
|
158
|
+
if (ch === '}') {
|
|
159
|
+
depth--;
|
|
160
|
+
if (depth === 0)
|
|
161
|
+
return i;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return -1;
|
|
165
|
+
}
|
|
166
|
+
function extractBalancedObjects(raw) {
|
|
167
|
+
const results = [];
|
|
168
|
+
let i = 0;
|
|
169
|
+
while (i < raw.length) {
|
|
170
|
+
if (raw[i] === '{') {
|
|
171
|
+
const end = findMatchingBrace(raw, i);
|
|
172
|
+
if (end !== -1) {
|
|
173
|
+
try {
|
|
174
|
+
const parsed = JSON.parse(raw.slice(i, end + 1));
|
|
175
|
+
if (isPlainObject(parsed)) {
|
|
176
|
+
results.push(parsed);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
logger.debug('JSON extraction: object-scan balanced braces but invalid JSON', { error: String(err) });
|
|
181
|
+
}
|
|
182
|
+
i = end + 1;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
i++;
|
|
187
|
+
}
|
|
188
|
+
return results;
|
|
189
|
+
}
|
|
190
|
+
function tryObjectScan(raw) {
|
|
191
|
+
const candidates = extractBalancedObjects(raw);
|
|
192
|
+
if (candidates.length === 0)
|
|
193
|
+
return null;
|
|
194
|
+
// Filter out envelope objects — they should have been handled by envelope-unwrap.
|
|
195
|
+
// If they weren't (e.g. inner content wasn't valid JSON), returning the envelope
|
|
196
|
+
// itself would leak raw JSON into downstream reasoning fields.
|
|
197
|
+
const filtered = candidates.filter(c => !looksLikeEnvelope(c));
|
|
198
|
+
if (filtered.length === 0)
|
|
199
|
+
return null;
|
|
200
|
+
// Rank: isLikelyOpinion candidates first
|
|
201
|
+
const ranked = filtered.sort((a, b) => {
|
|
202
|
+
const aLikely = isLikelyOpinion(a) ? 0 : 1;
|
|
203
|
+
const bLikely = isLikelyOpinion(b) ? 0 : 1;
|
|
204
|
+
return aLikely - bLikely;
|
|
205
|
+
});
|
|
206
|
+
return { data: ranked[0], effectiveRaw: raw };
|
|
207
|
+
}
|
|
208
|
+
// Main entry point
|
|
209
|
+
const strategies = [
|
|
210
|
+
{ name: 'strict-json', fn: tryStrictJson },
|
|
211
|
+
{ name: 'envelope-unwrap', fn: tryEnvelopeUnwrap },
|
|
212
|
+
{ name: 'markdown-block', fn: tryMarkdownBlock },
|
|
213
|
+
{ name: 'object-scan', fn: tryObjectScan },
|
|
214
|
+
];
|
|
215
|
+
export function extractJson(raw) {
|
|
216
|
+
for (const { name, fn } of strategies) {
|
|
217
|
+
const result = fn(raw);
|
|
218
|
+
if (result) {
|
|
219
|
+
return {
|
|
220
|
+
data: result.data,
|
|
221
|
+
raw,
|
|
222
|
+
effectiveRaw: result.effectiveRaw,
|
|
223
|
+
success: true,
|
|
224
|
+
strategy: name,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// All strategies failed. If the raw text is an envelope (e.g. Claude's
|
|
229
|
+
// {"type":"result","result":"plain text"}), strip it so downstream
|
|
230
|
+
// parsers see the inner text instead of the envelope JSON.
|
|
231
|
+
const stripped = stripEnvelopeText(raw);
|
|
232
|
+
return {
|
|
233
|
+
data: null,
|
|
234
|
+
raw,
|
|
235
|
+
effectiveRaw: stripped ?? raw,
|
|
236
|
+
success: false,
|
|
237
|
+
strategy: 'none',
|
|
238
|
+
};
|
|
239
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAGI Opinion Zod Schema - Single Source of Truth
|
|
3
|
+
*
|
|
4
|
+
* All validation, normalization, and JSON Schema generation for opinion payloads.
|
|
5
|
+
* Replaces duplicated isValidOpinion()/isValidOpinionJson()/parseUnstructured()
|
|
6
|
+
* across adapters and pipelines.
|
|
7
|
+
*
|
|
8
|
+
* VoteSchema / ConfidenceSchema are canonical in core.ts and re-exported here
|
|
9
|
+
* for backward compatibility.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from 'zod/v4';
|
|
12
|
+
import type { MagiOpinion, MagiUnit } from '../types/core.js';
|
|
13
|
+
import { VoteSchema, ConfidenceSchema } from '../types/core.js';
|
|
14
|
+
export { VoteSchema, ConfidenceSchema };
|
|
15
|
+
/** Full opinion payload schema for runtime validation */
|
|
16
|
+
export declare const OpinionPayloadSchema: z.ZodObject<{
|
|
17
|
+
vote: z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>, z.ZodEnum<{
|
|
18
|
+
APPROVE: "APPROVE";
|
|
19
|
+
REJECT: "REJECT";
|
|
20
|
+
ABSTAIN: "ABSTAIN";
|
|
21
|
+
}>>;
|
|
22
|
+
confidence: z.ZodPipe<z.ZodNumber, z.ZodTransform<number, number>>;
|
|
23
|
+
reasoning: z.ZodString;
|
|
24
|
+
keyPoints: z.ZodArray<z.ZodString>;
|
|
25
|
+
suggestions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
26
|
+
}, z.core.$strip>;
|
|
27
|
+
/** Inferred type from the validated payload */
|
|
28
|
+
export type OpinionPayload = z.infer<typeof OpinionPayloadSchema>;
|
|
29
|
+
/** JSON Schema for CLI structured output (sent to Claude --json-schema) */
|
|
30
|
+
export declare const OPINION_JSON_SCHEMA: Record<string, unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* Full validation + normalization for pipeline use.
|
|
33
|
+
* Returns normalized payload or null on failure.
|
|
34
|
+
*/
|
|
35
|
+
export declare function validateOpinion(obj: Record<string, unknown>): OpinionPayload | null;
|
|
36
|
+
/**
|
|
37
|
+
* Lightweight check for adapter use.
|
|
38
|
+
* Only verifies vote + reasoning exist (intentionally loose).
|
|
39
|
+
* Adapters use this to identify which JSON blob is likely an opinion.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isLikelyOpinion(obj: Record<string, unknown>): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* SafeOpinionSchema — derived from OpinionPayloadSchema with security constraints.
|
|
44
|
+
*
|
|
45
|
+
* Used at the orchestrator's trust boundary (where CLI output enters the system).
|
|
46
|
+
* The base OpinionPayloadSchema remains unchanged to preserve adapter semantics
|
|
47
|
+
* and the 61 existing tests.
|
|
48
|
+
*
|
|
49
|
+
* Additional constraints:
|
|
50
|
+
* - reasoning: max 10000 chars, control characters stripped
|
|
51
|
+
* - keyPoints: max 20 items, each max 1000 chars
|
|
52
|
+
* - suggestions: max 20 items, each max 1000 chars
|
|
53
|
+
* - confidence: must be finite (rejects NaN, Infinity)
|
|
54
|
+
*/
|
|
55
|
+
export declare const SafeOpinionSchema: z.ZodObject<{
|
|
56
|
+
vote: z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>, z.ZodEnum<{
|
|
57
|
+
APPROVE: "APPROVE";
|
|
58
|
+
REJECT: "REJECT";
|
|
59
|
+
ABSTAIN: "ABSTAIN";
|
|
60
|
+
}>>;
|
|
61
|
+
confidence: z.ZodPipe<z.ZodPipe<z.ZodNumber, z.ZodTransform<number, number>>, z.ZodNumber>;
|
|
62
|
+
reasoning: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
63
|
+
keyPoints: z.ZodArray<z.ZodString>;
|
|
64
|
+
suggestions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
65
|
+
}, z.core.$strip>;
|
|
66
|
+
export type SafeOpinionPayload = z.infer<typeof SafeOpinionSchema>;
|
|
67
|
+
/**
|
|
68
|
+
* Validate opinion with safe constraints.
|
|
69
|
+
* Returns validated payload or null on failure.
|
|
70
|
+
*/
|
|
71
|
+
export declare function validateSafeOpinion(obj: Record<string, unknown>): SafeOpinionPayload | null;
|
|
72
|
+
/**
|
|
73
|
+
* Extract opinion from unstructured text via keyword matching.
|
|
74
|
+
* Used when structured parsing fails entirely.
|
|
75
|
+
*/
|
|
76
|
+
export declare function parseUnstructuredOpinion(raw: string, unit: MagiUnit): Omit<MagiOpinion, 'rawOutput' | 'meta'>;
|
|
77
|
+
/**
|
|
78
|
+
* Pipeline-common parse logic. Tries structured validation first,
|
|
79
|
+
* falls back to unstructured keyword extraction.
|
|
80
|
+
*/
|
|
81
|
+
export declare function parseOpinionFromResponse(raw: string, structured: Record<string, unknown> | null, unit: MagiUnit): Omit<MagiOpinion, 'rawOutput' | 'meta'>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAGI Opinion Zod Schema - Single Source of Truth
|
|
3
|
+
*
|
|
4
|
+
* All validation, normalization, and JSON Schema generation for opinion payloads.
|
|
5
|
+
* Replaces duplicated isValidOpinion()/isValidOpinionJson()/parseUnstructured()
|
|
6
|
+
* across adapters and pipelines.
|
|
7
|
+
*
|
|
8
|
+
* VoteSchema / ConfidenceSchema are canonical in core.ts and re-exported here
|
|
9
|
+
* for backward compatibility.
|
|
10
|
+
*/
|
|
11
|
+
import { z, toJSONSchema } from 'zod/v4';
|
|
12
|
+
import { VoteSchema, ConfidenceSchema } from '../types/core.js';
|
|
13
|
+
import { scoreText } from './unstructured-parser.js';
|
|
14
|
+
// Re-export so existing consumers of VoteSchema/ConfidenceSchema from this module still work
|
|
15
|
+
export { VoteSchema, ConfidenceSchema };
|
|
16
|
+
// ============================================================
|
|
17
|
+
// 1. Zod Schemas (runtime validation with transforms)
|
|
18
|
+
// ============================================================
|
|
19
|
+
/** Full opinion payload schema for runtime validation */
|
|
20
|
+
export const OpinionPayloadSchema = z.object({
|
|
21
|
+
vote: VoteSchema,
|
|
22
|
+
confidence: ConfidenceSchema,
|
|
23
|
+
reasoning: z.string().min(1),
|
|
24
|
+
keyPoints: z.array(z.string()).min(1),
|
|
25
|
+
suggestions: z.array(z.string()).optional(),
|
|
26
|
+
});
|
|
27
|
+
// ============================================================
|
|
28
|
+
// 2. JSON Schema generation (transform-free for clean output)
|
|
29
|
+
// ============================================================
|
|
30
|
+
const OpinionRawSchema = z.object({
|
|
31
|
+
vote: z.enum(['APPROVE', 'REJECT', 'ABSTAIN'])
|
|
32
|
+
.describe('Your vote on this matter'),
|
|
33
|
+
confidence: z.number().min(0).max(1)
|
|
34
|
+
.describe('Confidence in your vote (0.0 to 1.0)'),
|
|
35
|
+
reasoning: z.string().min(1)
|
|
36
|
+
.describe('Detailed reasoning for your vote'),
|
|
37
|
+
keyPoints: z.array(z.string())
|
|
38
|
+
.describe('Key points supporting your position'),
|
|
39
|
+
suggestions: z.array(z.string()).optional()
|
|
40
|
+
.describe('Optional suggestions for improvement'),
|
|
41
|
+
});
|
|
42
|
+
/** JSON Schema for CLI structured output (sent to Claude --json-schema) */
|
|
43
|
+
export const OPINION_JSON_SCHEMA = toJSONSchema(OpinionRawSchema);
|
|
44
|
+
// ============================================================
|
|
45
|
+
// 3. Validation functions
|
|
46
|
+
// ============================================================
|
|
47
|
+
/**
|
|
48
|
+
* Full validation + normalization for pipeline use.
|
|
49
|
+
* Returns normalized payload or null on failure.
|
|
50
|
+
*/
|
|
51
|
+
export function validateOpinion(obj) {
|
|
52
|
+
const result = OpinionPayloadSchema.safeParse(obj);
|
|
53
|
+
return result.success ? result.data : null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Lightweight check for adapter use.
|
|
57
|
+
* Only verifies vote + reasoning exist (intentionally loose).
|
|
58
|
+
* Adapters use this to identify which JSON blob is likely an opinion.
|
|
59
|
+
*/
|
|
60
|
+
export function isLikelyOpinion(obj) {
|
|
61
|
+
return (typeof obj['vote'] === 'string' &&
|
|
62
|
+
['APPROVE', 'REJECT', 'ABSTAIN'].includes(obj['vote'].trim().toUpperCase()) &&
|
|
63
|
+
typeof obj['reasoning'] === 'string');
|
|
64
|
+
}
|
|
65
|
+
// ============================================================
|
|
66
|
+
// 3b. Safe opinion schema for orchestrator trust boundary
|
|
67
|
+
// ============================================================
|
|
68
|
+
/** Control character regex (matches \x00-\x08, \x0B, \x0C, \x0E-\x1F, \x7F) */
|
|
69
|
+
const CONTROL_CHARS_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
|
|
70
|
+
/**
|
|
71
|
+
* SafeOpinionSchema — derived from OpinionPayloadSchema with security constraints.
|
|
72
|
+
*
|
|
73
|
+
* Used at the orchestrator's trust boundary (where CLI output enters the system).
|
|
74
|
+
* The base OpinionPayloadSchema remains unchanged to preserve adapter semantics
|
|
75
|
+
* and the 61 existing tests.
|
|
76
|
+
*
|
|
77
|
+
* Additional constraints:
|
|
78
|
+
* - reasoning: max 10000 chars, control characters stripped
|
|
79
|
+
* - keyPoints: max 20 items, each max 1000 chars
|
|
80
|
+
* - suggestions: max 20 items, each max 1000 chars
|
|
81
|
+
* - confidence: must be finite (rejects NaN, Infinity)
|
|
82
|
+
*/
|
|
83
|
+
export const SafeOpinionSchema = z.object({
|
|
84
|
+
vote: VoteSchema,
|
|
85
|
+
confidence: ConfidenceSchema.pipe(z.number().refine((n) => Number.isFinite(n), 'Confidence must be finite')),
|
|
86
|
+
reasoning: z
|
|
87
|
+
.string()
|
|
88
|
+
.min(1)
|
|
89
|
+
.max(10_000)
|
|
90
|
+
.transform((s) => s.replace(CONTROL_CHARS_RE, '')),
|
|
91
|
+
keyPoints: z
|
|
92
|
+
.array(z.string().max(1_000))
|
|
93
|
+
.min(1)
|
|
94
|
+
.max(20),
|
|
95
|
+
suggestions: z
|
|
96
|
+
.array(z.string().max(1_000))
|
|
97
|
+
.max(20)
|
|
98
|
+
.optional(),
|
|
99
|
+
});
|
|
100
|
+
/**
|
|
101
|
+
* Validate opinion with safe constraints.
|
|
102
|
+
* Returns validated payload or null on failure.
|
|
103
|
+
*/
|
|
104
|
+
export function validateSafeOpinion(obj) {
|
|
105
|
+
const result = SafeOpinionSchema.safeParse(obj);
|
|
106
|
+
return result.success ? result.data : null;
|
|
107
|
+
}
|
|
108
|
+
// ============================================================
|
|
109
|
+
// 4. Unstructured text fallback parser
|
|
110
|
+
// ============================================================
|
|
111
|
+
/**
|
|
112
|
+
* Extract opinion from unstructured text via keyword matching.
|
|
113
|
+
* Used when structured parsing fails entirely.
|
|
114
|
+
*/
|
|
115
|
+
export function parseUnstructuredOpinion(raw, unit) {
|
|
116
|
+
const result = scoreText(raw);
|
|
117
|
+
return {
|
|
118
|
+
unit,
|
|
119
|
+
vote: result.vote,
|
|
120
|
+
confidence: result.confidence,
|
|
121
|
+
reasoning: raw.slice(0, 2000),
|
|
122
|
+
keyPoints: ['(Parsed from unstructured output)'],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// ============================================================
|
|
126
|
+
// 5. Unified parse: structured → validate → fallback
|
|
127
|
+
// ============================================================
|
|
128
|
+
/**
|
|
129
|
+
* Pipeline-common parse logic. Tries structured validation first,
|
|
130
|
+
* falls back to unstructured keyword extraction.
|
|
131
|
+
*/
|
|
132
|
+
export function parseOpinionFromResponse(raw, structured, unit) {
|
|
133
|
+
if (structured) {
|
|
134
|
+
const validated = validateOpinion(structured);
|
|
135
|
+
if (validated) {
|
|
136
|
+
return {
|
|
137
|
+
unit,
|
|
138
|
+
vote: validated.vote,
|
|
139
|
+
confidence: validated.confidence,
|
|
140
|
+
reasoning: validated.reasoning,
|
|
141
|
+
keyPoints: validated.keyPoints,
|
|
142
|
+
suggestions: validated.suggestions,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return parseUnstructuredOpinion(raw, unit);
|
|
147
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weighted scoring engine for unstructured text → vote extraction.
|
|
3
|
+
*
|
|
4
|
+
* Fixes the false-positive bug where "I don't approve" was classified as APPROVE.
|
|
5
|
+
* Uses 22 patterns with negation-aware matching, weighted scoring,
|
|
6
|
+
* and dynamic confidence extraction.
|
|
7
|
+
*/
|
|
8
|
+
import type { Vote } from '../types/core.js';
|
|
9
|
+
export interface ScoreResult {
|
|
10
|
+
vote: Vote;
|
|
11
|
+
confidence: number;
|
|
12
|
+
_debug: {
|
|
13
|
+
approveScore: number;
|
|
14
|
+
rejectScore: number;
|
|
15
|
+
abstainScore: number;
|
|
16
|
+
extractedConfidence: number | null;
|
|
17
|
+
matchedPatterns: string[];
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export declare function scoreText(raw: string): ScoreResult;
|