tsunami-memory 1.0.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.md +501 -0
- package/README.zh-CN.md +485 -0
- package/package.json +46 -0
- package/server/api.ts +125 -0
- package/server/mcp.ts +221 -0
- package/src/bun_memory_store.ts +340 -0
- package/src/classifier_keywords.ts +115 -0
- package/src/core/project_state.ts +88 -0
- package/src/index.ts +54 -0
- package/src/legacy_compat/tsunami_compat.ts +22 -0
- package/src/legacy_compat/tsunami_legacy_identity.ts +13 -0
- package/src/legacy_compat/tsunami_legacy_taxonomy.ts +197 -0
- package/src/memory_audit.ts +32 -0
- package/src/memory_conflict_resolver.ts +14 -0
- package/src/memory_fabric.ts +31 -0
- package/src/memory_manager.ts +10 -0
- package/src/memory_promotion.ts +22 -0
- package/src/memory_recovery.ts +14 -0
- package/src/memory_runtime.ts +7 -0
- package/src/migration.ts +163 -0
- package/src/provider.ts +68 -0
- package/src/runtime/checkpoints/durable_recovery.ts +24 -0
- package/src/runtime/paths.ts +11 -0
- package/src/storm/basins.ts +57 -0
- package/src/storm/boundary.ts +52 -0
- package/src/storm/budget.ts +42 -0
- package/src/storm/center.ts +396 -0
- package/src/storm/confidence.ts +88 -0
- package/src/storm/coverage.ts +44 -0
- package/src/storm/directive.ts +94 -0
- package/src/storm/gate.ts +43 -0
- package/src/storm/helpers.ts +172 -0
- package/src/storm/horizon.ts +52 -0
- package/src/storm/intake.ts +80 -0
- package/src/storm/mode.ts +21 -0
- package/src/storm/pressure.ts +56 -0
- package/src/storm/readiness.ts +29 -0
- package/src/storm/saturation.ts +45 -0
- package/src/storm/selection.ts +49 -0
- package/src/storm/signals.ts +105 -0
- package/src/storm/types.ts +216 -0
- package/src/tsunami_bun_backend.ts +705 -0
- package/src/tsunami_chinese_dialect.ts +19 -0
- package/src/tsunami_classifier.ts +137 -0
- package/src/tsunami_client.ts +710 -0
- package/src/tsunami_execution_gate.ts +232 -0
- package/src/tsunami_graph_runtime.ts +359 -0
- package/src/tsunami_identity.ts +35 -0
- package/src/tsunami_routing.ts +169 -0
- package/src/tsunami_runtime_graph_sync.ts +17 -0
- package/src/tsunami_schema.ts +403 -0
- package/src/tsunami_storage_paths.ts +8 -0
- package/src/tsunami_storm_center.ts +53 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { TsunamiStormPressure, TsunamiStormReadiness, TsunamiStormBoundary, TsunamiStormHorizon, TsunamiStormCenterStormMode, TsunamiStormConfidence } from './types';
|
|
2
|
+
|
|
3
|
+
export function buildStormConfidence(input: {
|
|
4
|
+
stormPressure?: TsunamiStormPressure;
|
|
5
|
+
stormReadiness?: TsunamiStormReadiness;
|
|
6
|
+
stormBoundary?: TsunamiStormBoundary;
|
|
7
|
+
stormHorizon?: TsunamiStormHorizon;
|
|
8
|
+
stormMode?: TsunamiStormCenterStormMode;
|
|
9
|
+
}): TsunamiStormConfidence | undefined {
|
|
10
|
+
const pressure = input.stormPressure;
|
|
11
|
+
const readiness = input.stormReadiness;
|
|
12
|
+
const boundary = input.stormBoundary;
|
|
13
|
+
const horizon = input.stormHorizon;
|
|
14
|
+
const mode = input.stormMode;
|
|
15
|
+
if (!pressure && !readiness && !boundary && !horizon && !mode) return undefined;
|
|
16
|
+
|
|
17
|
+
let score = 0.18;
|
|
18
|
+
const reasons: string[] = [];
|
|
19
|
+
|
|
20
|
+
if (readiness?.level === 'fortified') {
|
|
21
|
+
score += 0.32;
|
|
22
|
+
reasons.push('fortified support');
|
|
23
|
+
} else if (readiness?.level === 'ready') {
|
|
24
|
+
score += 0.22;
|
|
25
|
+
reasons.push('ready support');
|
|
26
|
+
} else if (readiness?.level === 'partial') {
|
|
27
|
+
score += 0.08;
|
|
28
|
+
reasons.push('partial support');
|
|
29
|
+
} else if (readiness?.level === 'weak') {
|
|
30
|
+
score -= 0.08;
|
|
31
|
+
reasons.push('weak support');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (boundary?.mode === 'sealed') {
|
|
35
|
+
score += 0.16;
|
|
36
|
+
reasons.push('sealed boundary');
|
|
37
|
+
} else if (boundary?.mode === 'guarded') {
|
|
38
|
+
score += 0.08;
|
|
39
|
+
reasons.push('guarded boundary');
|
|
40
|
+
} else if (boundary?.mode === 'permeable') {
|
|
41
|
+
score -= 0.06;
|
|
42
|
+
reasons.push('permeable boundary');
|
|
43
|
+
} else if (boundary?.mode === 'spilling') {
|
|
44
|
+
score -= 0.18;
|
|
45
|
+
reasons.push('spilling boundary');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (pressure?.level === 'calm') {
|
|
49
|
+
score += 0.12;
|
|
50
|
+
reasons.push('calm pressure');
|
|
51
|
+
} else if (pressure?.level === 'steady') {
|
|
52
|
+
score += 0.06;
|
|
53
|
+
reasons.push('steady pressure');
|
|
54
|
+
} else if (pressure?.level === 'rising') {
|
|
55
|
+
score -= 0.08;
|
|
56
|
+
reasons.push('rising pressure');
|
|
57
|
+
} else if (pressure?.level === 'critical') {
|
|
58
|
+
score -= 0.18;
|
|
59
|
+
reasons.push('critical pressure');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (mode?.mixed) {
|
|
63
|
+
score -= 0.06;
|
|
64
|
+
reasons.push('mixed sea-state');
|
|
65
|
+
} else if (mode) {
|
|
66
|
+
score += 0.04;
|
|
67
|
+
reasons.push('coherent sea-state');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (horizon?.label === 'multi_step') {
|
|
71
|
+
score += 0.1;
|
|
72
|
+
reasons.push('multi-step horizon');
|
|
73
|
+
} else if (horizon?.label === 'two_step') {
|
|
74
|
+
score += 0.05;
|
|
75
|
+
reasons.push('two-step horizon');
|
|
76
|
+
} else if (horizon?.label === 'single_step') {
|
|
77
|
+
score -= 0.04;
|
|
78
|
+
reasons.push('single-step horizon');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const normalized = Math.max(0, Math.min(1, Number(score.toFixed(2))));
|
|
82
|
+
const level = normalized >= 0.74 ? 'high' : normalized >= 0.54 ? 'confident' : normalized >= 0.32 ? 'guarded' : 'low';
|
|
83
|
+
return {
|
|
84
|
+
level,
|
|
85
|
+
score: normalized,
|
|
86
|
+
reason: reasons.slice(0, 3).join(' · ') || 'confidence still settling',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { TsunamiStormSelection, TsunamiStormCoverage } from './types';
|
|
2
|
+
|
|
3
|
+
export function buildStormCoverage(input: {
|
|
4
|
+
stormSelection?: TsunamiStormSelection;
|
|
5
|
+
selectedSignals: number;
|
|
6
|
+
totalSignals: number;
|
|
7
|
+
selectedEvidence: number;
|
|
8
|
+
totalEvidence: number;
|
|
9
|
+
selectedRelations: number;
|
|
10
|
+
totalRelations: number;
|
|
11
|
+
}): TsunamiStormCoverage | undefined {
|
|
12
|
+
const totals = [
|
|
13
|
+
input.totalSignals,
|
|
14
|
+
input.totalEvidence,
|
|
15
|
+
input.totalRelations,
|
|
16
|
+
];
|
|
17
|
+
if (!totals.some((value) => value > 0)) return undefined;
|
|
18
|
+
|
|
19
|
+
const signalRatio = input.totalSignals > 0 ? input.selectedSignals / input.totalSignals : 1;
|
|
20
|
+
const evidenceRatio = input.totalEvidence > 0 ? input.selectedEvidence / input.totalEvidence : 1;
|
|
21
|
+
const relationRatio = input.totalRelations > 0 ? input.selectedRelations / input.totalRelations : 1;
|
|
22
|
+
const score = Number(((signalRatio + evidenceRatio + relationRatio) / 3).toFixed(2));
|
|
23
|
+
const mode = score >= 0.95 ? 'full' : score >= 0.72 ? 'broad' : score >= 0.45 ? 'focused' : 'narrow';
|
|
24
|
+
|
|
25
|
+
const weakest =
|
|
26
|
+
signalRatio <= evidenceRatio && signalRatio <= relationRatio ? 'signal lane'
|
|
27
|
+
: evidenceRatio <= relationRatio ? 'evidence lane'
|
|
28
|
+
: 'relation lane';
|
|
29
|
+
const reason = input.stormSelection
|
|
30
|
+
? `${input.stormSelection.profile} selection currently covers ${weakest} most narrowly`
|
|
31
|
+
: `current storm coverage is most limited by the ${weakest}`;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
mode,
|
|
35
|
+
score,
|
|
36
|
+
selectedSignals: input.selectedSignals,
|
|
37
|
+
totalSignals: input.totalSignals,
|
|
38
|
+
selectedEvidence: input.selectedEvidence,
|
|
39
|
+
totalEvidence: input.totalEvidence,
|
|
40
|
+
selectedRelations: input.selectedRelations,
|
|
41
|
+
totalRelations: input.totalRelations,
|
|
42
|
+
reason,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { TsunamiStormPressure, TsunamiStormCenterStormMode, TsunamiStormDirective, TsunamiStormAction } from './types';
|
|
2
|
+
|
|
3
|
+
export function buildStormDirective(input: {
|
|
4
|
+
stormPressure?: TsunamiStormPressure;
|
|
5
|
+
stormMode?: TsunamiStormCenterStormMode;
|
|
6
|
+
topRepair?: TsunamiStormCenter['topRepair'];
|
|
7
|
+
topIssue?: TsunamiStormCenter['topIssue'];
|
|
8
|
+
focus: TsunamiStormCenter['focus'];
|
|
9
|
+
}): TsunamiStormDirective | undefined {
|
|
10
|
+
const pressure = input.stormPressure;
|
|
11
|
+
const mode = input.stormMode;
|
|
12
|
+
if ((pressure?.level === 'critical' || pressure?.level === 'rising') && input.topRepair) {
|
|
13
|
+
return {
|
|
14
|
+
label: pressure.level === 'critical' ? 'stabilize-now' : 'stabilize-repair',
|
|
15
|
+
lane: pressure.level === 'critical' ? 'stabilize' : 'repair',
|
|
16
|
+
reason: `${pressure.level} pressure + ${input.topRepair.priority || 'repair'} ${input.topRepair.title}`.trim(),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if ((pressure?.level === 'rising' || pressure?.level === 'critical') && input.topIssue && !input.topRepair) {
|
|
20
|
+
return {
|
|
21
|
+
label: 'diagnose-drift',
|
|
22
|
+
lane: 'diagnose',
|
|
23
|
+
reason: `${pressure.level} pressure + issue ${input.topIssue.code}`.trim(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (mode?.dominantKind === 'anchor' || mode?.dominantKind === 'evidence') {
|
|
27
|
+
return {
|
|
28
|
+
label: 'consolidate-context',
|
|
29
|
+
lane: 'consolidate',
|
|
30
|
+
reason: `${mode.label} requires clearer anchors and evidence`.trim(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (mode?.dominantKind === 'issue') {
|
|
34
|
+
return {
|
|
35
|
+
label: 'diagnose-thread',
|
|
36
|
+
lane: 'diagnose',
|
|
37
|
+
reason: `${mode.label} suggests drift diagnosis before forward motion`.trim(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (mode?.dominantKind === 'repair') {
|
|
41
|
+
return {
|
|
42
|
+
label: 'apply-repair',
|
|
43
|
+
lane: 'repair',
|
|
44
|
+
reason: `${mode.label} suggests closing the top repair path first`.trim(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
label: 'advance-mainline',
|
|
49
|
+
lane: 'advance',
|
|
50
|
+
reason: input.focus.nextStep || input.focus.summary || 'mainline is ready to advance',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function buildStormAction(input: {
|
|
54
|
+
stormDirective?: TsunamiStormDirective;
|
|
55
|
+
topRepair?: TsunamiStormCenter['topRepair'];
|
|
56
|
+
topIssue?: TsunamiStormCenter['topIssue'];
|
|
57
|
+
focus: TsunamiStormCenter['focus'];
|
|
58
|
+
}): TsunamiStormAction | undefined {
|
|
59
|
+
const directive = input.stormDirective;
|
|
60
|
+
if (!directive) return undefined;
|
|
61
|
+
if (directive.lane === 'stabilize' && input.topRepair) {
|
|
62
|
+
return {
|
|
63
|
+
label: 'close top repair before expanding scope',
|
|
64
|
+
target: input.topRepair.title,
|
|
65
|
+
reason: directive.reason,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (directive.lane === 'repair' && input.topRepair) {
|
|
69
|
+
return {
|
|
70
|
+
label: 'apply the priority repair path',
|
|
71
|
+
target: input.topRepair.title,
|
|
72
|
+
reason: directive.reason,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (directive.lane === 'diagnose' && input.topIssue) {
|
|
76
|
+
return {
|
|
77
|
+
label: 'diagnose the dominant drift before coding forward',
|
|
78
|
+
target: input.topIssue.detail || input.topIssue.code,
|
|
79
|
+
reason: directive.reason,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (directive.lane === 'consolidate') {
|
|
83
|
+
return {
|
|
84
|
+
label: 'refresh anchors and evidence around the mainline',
|
|
85
|
+
target: input.focus.title,
|
|
86
|
+
reason: directive.reason,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
label: input.focus.nextStep || 'advance the mainline carefully',
|
|
91
|
+
target: input.focus.title,
|
|
92
|
+
reason: directive.reason,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { TsunamiStormPressure, TsunamiStormReadiness, TsunamiStormConfidence, TsunamiStormBoundary, TsunamiStormHorizon, TsunamiStormGate } from './types';
|
|
2
|
+
|
|
3
|
+
export function buildStormGate(input: {
|
|
4
|
+
stormPressure?: TsunamiStormPressure;
|
|
5
|
+
stormReadiness?: TsunamiStormReadiness;
|
|
6
|
+
stormConfidence?: TsunamiStormConfidence;
|
|
7
|
+
stormBoundary?: TsunamiStormBoundary;
|
|
8
|
+
stormHorizon?: TsunamiStormHorizon;
|
|
9
|
+
}): TsunamiStormGate | undefined {
|
|
10
|
+
const pressure = input.stormPressure;
|
|
11
|
+
const readiness = input.stormReadiness;
|
|
12
|
+
const confidence = input.stormConfidence;
|
|
13
|
+
const boundary = input.stormBoundary;
|
|
14
|
+
const horizon = input.stormHorizon;
|
|
15
|
+
if (!pressure && !readiness && !confidence && !boundary && !horizon) return undefined;
|
|
16
|
+
|
|
17
|
+
if (pressure?.level === 'critical' || boundary?.mode === 'spilling' || confidence?.level === 'low') {
|
|
18
|
+
return {
|
|
19
|
+
verdict: 'hold',
|
|
20
|
+
allowForward: false,
|
|
21
|
+
reason: `hold the line until pressure/boundary/confidence stabilizes (${pressure?.level || boundary?.mode || confidence?.level || 'unknown'})`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (boundary?.expand || readiness?.level === 'partial' || readiness?.level === 'weak') {
|
|
25
|
+
return {
|
|
26
|
+
verdict: 'guarded',
|
|
27
|
+
allowForward: false,
|
|
28
|
+
reason: `keep execution guarded while support is still ${readiness?.level || 'thin'} and the storm may need wider support`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if ((confidence?.level === 'high' || confidence?.level === 'confident') && boundary?.mode === 'sealed' && (horizon?.steps || 0) >= 3) {
|
|
32
|
+
return {
|
|
33
|
+
verdict: 'expand',
|
|
34
|
+
allowForward: true,
|
|
35
|
+
reason: `guidance is ${confidence?.level || 'strong'} with a sealed boundary and long horizon, so we can advance confidently`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
verdict: 'proceed',
|
|
40
|
+
allowForward: true,
|
|
41
|
+
reason: `guidance is stable enough to move forward, but keep the mainline under watch`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Storm center — utility functions
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
import { listProjectWikiPages } from '../core/project_state';
|
|
5
|
+
import type { ProjectHandoffRecord, ProjectTaskThread } from '../core/project_state';
|
|
6
|
+
import type { DurableRecoveryRecord } from '../runtime/checkpoints/durable_recovery';
|
|
7
|
+
import type { TsunamiStormCenterCurrent } from './types';
|
|
8
|
+
|
|
9
|
+
const TSUNAMI_STORM_RETRY_DELAYS_MS = [8, 18, 36] as const;
|
|
10
|
+
|
|
11
|
+
export function buildProjectNode(projectDir: string): string {
|
|
12
|
+
return `project:${basename(projectDir) || projectDir}`;
|
|
13
|
+
}
|
|
14
|
+
export function buildTaskThreadNode(threadId: string): string {
|
|
15
|
+
return `task_thread:${threadId}`;
|
|
16
|
+
}
|
|
17
|
+
export function buildHandoffNode(handoffId: string): string {
|
|
18
|
+
return `handoff:${handoffId}`;
|
|
19
|
+
}
|
|
20
|
+
export function buildAnchorNode(pageId: string): string {
|
|
21
|
+
return `recovery_anchor:${pageId}`;
|
|
22
|
+
}
|
|
23
|
+
export function buildRecoveryNode(recoveryId: string): string {
|
|
24
|
+
return `recovery_record:${recoveryId}`;
|
|
25
|
+
}
|
|
26
|
+
export function clampEnergy(value: number): number {
|
|
27
|
+
if (!Number.isFinite(value)) return 0;
|
|
28
|
+
return Math.max(0, Math.min(1, Number(value.toFixed(2))));
|
|
29
|
+
}
|
|
30
|
+
export function safeTrim(value: string | undefined, max = 160): string {
|
|
31
|
+
const text = String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
32
|
+
if (!text) return '';
|
|
33
|
+
return text.length > max ? `${text.slice(0, max)}...` : text;
|
|
34
|
+
}
|
|
35
|
+
export function blockForRetry(ms: number) {
|
|
36
|
+
const delay = Math.max(0, Math.floor(ms));
|
|
37
|
+
if (!delay) return;
|
|
38
|
+
try {
|
|
39
|
+
const shared = new SharedArrayBuffer(4);
|
|
40
|
+
Atomics.wait(new Int32Array(shared), 0, 0, delay);
|
|
41
|
+
} catch {
|
|
42
|
+
const end = Date.now() + delay;
|
|
43
|
+
while (Date.now() < end) {
|
|
44
|
+
// Busy wait fallback for runtimes where Atomics.wait is unavailable.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function isTsunamiStormRetryableError(error: unknown): boolean {
|
|
49
|
+
const message = String((error as any)?.message || error || '').toLowerCase();
|
|
50
|
+
if (!message) return false;
|
|
51
|
+
return message.includes('database is locked')
|
|
52
|
+
|| message.includes('sqlite_busy')
|
|
53
|
+
|| message.includes('sql_busy')
|
|
54
|
+
|| message.includes('busy timeout');
|
|
55
|
+
}
|
|
56
|
+
export function withTsunamiStormRetry<T>(runner: () => T): T {
|
|
57
|
+
let lastError: unknown;
|
|
58
|
+
for (let attempt = 0; attempt <= TSUNAMI_STORM_RETRY_DELAYS_MS.length; attempt += 1) {
|
|
59
|
+
try {
|
|
60
|
+
return runner();
|
|
61
|
+
} catch (error) {
|
|
62
|
+
lastError = error;
|
|
63
|
+
if (!isTsunamiStormRetryableError(error) || attempt >= TSUNAMI_STORM_RETRY_DELAYS_MS.length) {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
blockForRetry(TSUNAMI_STORM_RETRY_DELAYS_MS[attempt]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError || 'unknown storm retry failure'));
|
|
70
|
+
}
|
|
71
|
+
export function pickFocusQuery(input: {
|
|
72
|
+
query?: string;
|
|
73
|
+
thread: ProjectTaskThread | null;
|
|
74
|
+
handoff: ProjectHandoffRecord | null;
|
|
75
|
+
activeFeatureTitle?: string;
|
|
76
|
+
}): string {
|
|
77
|
+
const direct = String(input.query ?? '').trim();
|
|
78
|
+
if (direct) return direct;
|
|
79
|
+
return (
|
|
80
|
+
input.thread?.title
|
|
81
|
+
|| input.handoff?.task
|
|
82
|
+
|| input.activeFeatureTitle
|
|
83
|
+
|| 'current mainline'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
export function tokenizeStormSupportQuery(query: string): string[] {
|
|
87
|
+
return String(query || '')
|
|
88
|
+
.toLowerCase()
|
|
89
|
+
.split(/[\s,,。;;::|/\\()\[\]{}"']+/)
|
|
90
|
+
.map((item) => item.trim())
|
|
91
|
+
.filter((item) => item.length >= 2);
|
|
92
|
+
}
|
|
93
|
+
export function pickFallbackDocSnippet(content: string, query: string, max = 180): string {
|
|
94
|
+
const lines = String(content || '')
|
|
95
|
+
.split('\n')
|
|
96
|
+
.map((line) => line.replace(/\s+/g, ' ').trim())
|
|
97
|
+
.filter(Boolean)
|
|
98
|
+
.filter((line) => !/^#+\s*/.test(line));
|
|
99
|
+
if (!lines.length) return '';
|
|
100
|
+
const tokens = tokenizeStormSupportQuery(query);
|
|
101
|
+
if (tokens.length > 0) {
|
|
102
|
+
const matched = lines.find((line) => tokens.some((token) => line.toLowerCase().includes(token)));
|
|
103
|
+
if (matched) return safeTrim(matched, max);
|
|
104
|
+
}
|
|
105
|
+
return safeTrim(lines.find((line) => line.length >= 20) || lines[0], max);
|
|
106
|
+
}
|
|
107
|
+
export function buildFallbackDocStormSupport(projectDir: string, focusQuery: string, limit = 2): TsunamiFallbackDocSupport {
|
|
108
|
+
const candidates = [
|
|
109
|
+
join(projectDir, 'README.md'),
|
|
110
|
+
join(projectDir, 'CHANGELOG.md'),
|
|
111
|
+
join(projectDir, 'README.md'),
|
|
112
|
+
];
|
|
113
|
+
const anchors: TsunamiStormCenter['anchors'] = [];
|
|
114
|
+
const evidence: TsunamiStormCenter['evidence'] = [];
|
|
115
|
+
let mainlineTitle: string | undefined;
|
|
116
|
+
let mainlineSummary: string | undefined;
|
|
117
|
+
|
|
118
|
+
for (const filePath of candidates) {
|
|
119
|
+
if (!existsSync(filePath)) continue;
|
|
120
|
+
let content = '';
|
|
121
|
+
try {
|
|
122
|
+
content = readFileSync(filePath, 'utf8');
|
|
123
|
+
} catch (error: any) {
|
|
124
|
+
console.warn(`[TSUNAMI] failed to read fallback storm support doc ${filePath}:`, error?.message ?? error);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const snippet = pickFallbackDocSnippet(content, focusQuery);
|
|
128
|
+
if (!snippet) continue;
|
|
129
|
+
const label = basename(filePath);
|
|
130
|
+
if (!mainlineTitle) {
|
|
131
|
+
mainlineTitle = `Project Mainline / ${label}`;
|
|
132
|
+
mainlineSummary = snippet;
|
|
133
|
+
}
|
|
134
|
+
if (anchors.length < 1 && label === 'README.md') {
|
|
135
|
+
anchors.push({
|
|
136
|
+
pageId: `storm-fallback-anchor:${label}`,
|
|
137
|
+
title: `Fallback Anchor / ${label}`,
|
|
138
|
+
summary: snippet,
|
|
139
|
+
confidence: 0.54,
|
|
140
|
+
tags: ['storm-fallback', 'project-doc'],
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (evidence.length < limit) {
|
|
144
|
+
evidence.push({
|
|
145
|
+
snippetId: `storm-fallback-evidence:${label}:${evidence.length + 1}`,
|
|
146
|
+
pageId: `storm-fallback-doc:${label}`,
|
|
147
|
+
title: `Fallback Evidence / ${label}`,
|
|
148
|
+
sourcePath: filePath,
|
|
149
|
+
sourceRef: `file:${label}`,
|
|
150
|
+
quote: snippet,
|
|
151
|
+
tags: ['storm-fallback', 'project-doc'],
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
anchors,
|
|
158
|
+
evidence,
|
|
159
|
+
mainlineTitle,
|
|
160
|
+
mainlineSummary,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
export function readAnchorCandidates(projectDir: string, featureId?: string, limit = 3) {
|
|
164
|
+
const pages = listProjectWikiPages(projectDir, 12)
|
|
165
|
+
.filter((page) => page.title.startsWith('Recovery Anchor /') || page.tags.includes('recovery-anchor'))
|
|
166
|
+
.filter((page) => {
|
|
167
|
+
if (!featureId) return true;
|
|
168
|
+
return page.sourceRefs.some((ref) => ref.toLowerCase() === `feature_id:${featureId.toLowerCase()}`);
|
|
169
|
+
})
|
|
170
|
+
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
171
|
+
return pages.slice(0, limit);
|
|
172
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { TsunamiStormPressure, TsunamiStormReadiness, TsunamiStormBoundary, TsunamiStormCenterStormMode, TsunamiStormHorizon } from './types';
|
|
2
|
+
|
|
3
|
+
export function buildStormHorizon(input: {
|
|
4
|
+
stormPressure?: TsunamiStormPressure;
|
|
5
|
+
stormReadiness?: TsunamiStormReadiness;
|
|
6
|
+
stormBoundary?: TsunamiStormBoundary;
|
|
7
|
+
stormMode?: TsunamiStormCenterStormMode;
|
|
8
|
+
topRepair?: TsunamiStormCenter['topRepair'];
|
|
9
|
+
topIssue?: TsunamiStormCenter['topIssue'];
|
|
10
|
+
}): TsunamiStormHorizon | undefined {
|
|
11
|
+
const pressure = input.stormPressure;
|
|
12
|
+
const readiness = input.stormReadiness;
|
|
13
|
+
const boundary = input.stormBoundary;
|
|
14
|
+
const mode = input.stormMode;
|
|
15
|
+
if (!pressure && !readiness && !boundary && !mode) return undefined;
|
|
16
|
+
|
|
17
|
+
if (pressure?.level === 'critical' || boundary?.mode === 'spilling') {
|
|
18
|
+
return {
|
|
19
|
+
label: 'single_step',
|
|
20
|
+
steps: 1,
|
|
21
|
+
reason: `pressure ${pressure?.level || 'critical'} and boundary ${boundary?.mode || 'open'} mean we should only plan the next safe move`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (boundary?.expand || readiness?.level === 'weak' || readiness?.level === 'partial') {
|
|
25
|
+
return {
|
|
26
|
+
label: 'short_run',
|
|
27
|
+
steps: 1,
|
|
28
|
+
reason: `support is still ${readiness?.level || 'thin'}, so gather anchors/evidence before planning beyond the immediate step`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (readiness?.level === 'fortified' && boundary?.mode === 'sealed' && !mode?.mixed && (pressure?.level === 'calm' || pressure?.level === 'steady')) {
|
|
32
|
+
return {
|
|
33
|
+
label: 'multi_step',
|
|
34
|
+
steps: 3,
|
|
35
|
+
reason: `support is fortified, boundary is sealed, and pressure is ${pressure?.level || 'calm'}, so the storm can safely see several steps ahead`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (readiness?.level === 'ready' || boundary?.mode === 'guarded') {
|
|
39
|
+
return {
|
|
40
|
+
label: 'two_step',
|
|
41
|
+
steps: 2,
|
|
42
|
+
reason: `support is stable enough for the next two moves, but we should keep the mainline under watch`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
label: 'short_run',
|
|
47
|
+
steps: 1,
|
|
48
|
+
reason: input.topRepair || input.topIssue
|
|
49
|
+
? 'active repair/drift pressure suggests holding planning to the immediate correction window'
|
|
50
|
+
: 'storm support is still settling, so keep the planning window short',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { TsunamiStormSelection, TsunamiStormCoverage, TsunamiStormSaturation, TsunamiStormBudget, TsunamiStormGate, TsunamiStormIntake } from './types';
|
|
2
|
+
|
|
3
|
+
export function buildStormIntake(input: {
|
|
4
|
+
stormSelection?: TsunamiStormSelection;
|
|
5
|
+
stormCoverage?: TsunamiStormCoverage;
|
|
6
|
+
stormSaturation?: TsunamiStormSaturation;
|
|
7
|
+
stormBudget?: TsunamiStormBudget;
|
|
8
|
+
stormGate?: TsunamiStormGate;
|
|
9
|
+
}): TsunamiStormIntake | undefined {
|
|
10
|
+
const selection = input.stormSelection;
|
|
11
|
+
const coverage = input.stormCoverage;
|
|
12
|
+
const saturation = input.stormSaturation;
|
|
13
|
+
const budget = input.stormBudget;
|
|
14
|
+
const gate = input.stormGate;
|
|
15
|
+
if (!selection && !coverage && !saturation && !budget && !gate) return undefined;
|
|
16
|
+
|
|
17
|
+
const nextSignalLimit = Math.max(1, selection?.signalLimit ?? 3);
|
|
18
|
+
const nextEvidenceLimit = Math.max(1, selection?.evidenceLimit ?? 2);
|
|
19
|
+
const nextRelationLimit = Math.max(1, selection?.relationLimit ?? 6);
|
|
20
|
+
|
|
21
|
+
if (gate?.verdict === 'hold' || budget?.mode === 'frozen') {
|
|
22
|
+
return {
|
|
23
|
+
mode: 'hold',
|
|
24
|
+
target: 'balanced',
|
|
25
|
+
nextSignalLimit,
|
|
26
|
+
nextEvidenceLimit,
|
|
27
|
+
nextRelationLimit,
|
|
28
|
+
reason: budget?.reason || gate?.reason || 'hold the intake steady until the storm is safe to widen again',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (saturation && saturation.hitLanes.length > 0) {
|
|
33
|
+
const target: TsunamiStormIntake['target'] =
|
|
34
|
+
saturation.hitLanes.length > 1 ? 'balanced' : saturation.hitLanes[0];
|
|
35
|
+
const bump = saturation.level === 'saturated' ? 2 : 1;
|
|
36
|
+
return {
|
|
37
|
+
mode: saturation.level === 'saturated' ? 'widen' : 'rebalance',
|
|
38
|
+
target,
|
|
39
|
+
nextSignalLimit: target === 'signals' || target === 'balanced' ? nextSignalLimit + bump : nextSignalLimit,
|
|
40
|
+
nextEvidenceLimit: target === 'evidence' || target === 'balanced' ? nextEvidenceLimit + bump : nextEvidenceLimit,
|
|
41
|
+
nextRelationLimit:
|
|
42
|
+
target === 'relations'
|
|
43
|
+
? nextRelationLimit + bump * 2
|
|
44
|
+
: target === 'balanced'
|
|
45
|
+
? nextRelationLimit + bump
|
|
46
|
+
: nextRelationLimit,
|
|
47
|
+
reason: saturation.reason,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const signalRatio = coverage && coverage.totalSignals > 0 ? coverage.selectedSignals / coverage.totalSignals : 1;
|
|
52
|
+
const evidenceRatio = coverage && coverage.totalEvidence > 0 ? coverage.selectedEvidence / coverage.totalEvidence : 1;
|
|
53
|
+
const relationRatio = coverage && coverage.totalRelations > 0 ? coverage.selectedRelations / coverage.totalRelations : 1;
|
|
54
|
+
|
|
55
|
+
let target: TsunamiStormIntake['target'] = 'balanced';
|
|
56
|
+
if (signalRatio <= evidenceRatio && signalRatio <= relationRatio) target = 'signals';
|
|
57
|
+
else if (evidenceRatio <= relationRatio) target = 'evidence';
|
|
58
|
+
else target = 'relations';
|
|
59
|
+
|
|
60
|
+
if ((coverage?.score ?? 1) >= 0.9) {
|
|
61
|
+
return {
|
|
62
|
+
mode: 'steady',
|
|
63
|
+
target: 'balanced',
|
|
64
|
+
nextSignalLimit,
|
|
65
|
+
nextEvidenceLimit,
|
|
66
|
+
nextRelationLimit,
|
|
67
|
+
reason: 'coverage is already strong enough, so keep the intake steady around the current storm surface',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const bump = coverage && coverage.score < 0.45 ? 2 : 1;
|
|
72
|
+
return {
|
|
73
|
+
mode: coverage && coverage.score < 0.6 ? 'widen' : 'rebalance',
|
|
74
|
+
target,
|
|
75
|
+
nextSignalLimit: target === 'signals' ? nextSignalLimit + bump : nextSignalLimit,
|
|
76
|
+
nextEvidenceLimit: target === 'evidence' ? nextEvidenceLimit + bump : nextEvidenceLimit,
|
|
77
|
+
nextRelationLimit: target === 'relations' ? nextRelationLimit + bump * 2 : nextRelationLimit,
|
|
78
|
+
reason: coverage?.reason || `${selection?.profile || 'current'} intake should rebalance toward ${target}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { TsunamiStormCenterCurrentMix, TsunamiStormCenterStormMode } from './types';
|
|
2
|
+
|
|
3
|
+
export function buildStormMode(currentMix: TsunamiStormCenterCurrentMix[]): TsunamiStormCenterStormMode | undefined {
|
|
4
|
+
if (!currentMix.length) return undefined;
|
|
5
|
+
const dominant = currentMix[0];
|
|
6
|
+
const totalEnergy = currentMix.reduce((sum, item) => sum + item.energy, 0);
|
|
7
|
+
const dominance = totalEnergy > 0 ? Number((dominant.energy / totalEnergy).toFixed(2)) : 0;
|
|
8
|
+
const mixed = dominance < 0.56;
|
|
9
|
+
const base = dominant.kind === 'primary_thread' ? 'thread' : dominant.kind;
|
|
10
|
+
const label = mixed
|
|
11
|
+
? `mixed-${base}`
|
|
12
|
+
: (dominant.kind === 'repair' || dominant.kind === 'issue' || dominant.kind === 'evidence'
|
|
13
|
+
? `${base}-heavy`
|
|
14
|
+
: `${base}-led`);
|
|
15
|
+
return {
|
|
16
|
+
label,
|
|
17
|
+
dominantKind: dominant.kind,
|
|
18
|
+
dominance,
|
|
19
|
+
mixed,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { TsunamiStormCenterCurrentMix, TsunamiStormCenterStormMode, TsunamiStormPressure } from './types';
|
|
2
|
+
|
|
3
|
+
export function buildStormPressure(input: {
|
|
4
|
+
currentMix: TsunamiStormCenterCurrentMix[];
|
|
5
|
+
stormMode?: TsunamiStormCenterStormMode;
|
|
6
|
+
issues: TsunamiStormCenter['issues'];
|
|
7
|
+
repairSuggestions: TsunamiStormCenter['repairSuggestions'];
|
|
8
|
+
anchors: TsunamiStormCenter['anchors'];
|
|
9
|
+
evidence: TsunamiStormCenter['evidence'];
|
|
10
|
+
recovery: DurableRecoveryRecord | null;
|
|
11
|
+
}): TsunamiStormPressure | undefined {
|
|
12
|
+
if (!input.currentMix.length) return undefined;
|
|
13
|
+
const reasons: string[] = [];
|
|
14
|
+
let score = 0.12;
|
|
15
|
+
const dominant = input.currentMix[0];
|
|
16
|
+
score += Math.min(0.24, dominant.energy * 0.16);
|
|
17
|
+
reasons.push(`dominant:${dominant.kind}`);
|
|
18
|
+
const highIssues = input.issues.filter((issue) => issue.severity === 'high').length;
|
|
19
|
+
const mediumIssues = input.issues.filter((issue) => issue.severity === 'medium').length;
|
|
20
|
+
if (highIssues > 0) {
|
|
21
|
+
score += Math.min(0.24, highIssues * 0.12);
|
|
22
|
+
reasons.push(`high_issue:${highIssues}`);
|
|
23
|
+
} else if (mediumIssues > 0) {
|
|
24
|
+
score += Math.min(0.16, mediumIssues * 0.08);
|
|
25
|
+
reasons.push(`medium_issue:${mediumIssues}`);
|
|
26
|
+
}
|
|
27
|
+
const p0Repairs = input.repairSuggestions.filter((item) => item.priority === 'P0').length;
|
|
28
|
+
const p1Repairs = input.repairSuggestions.filter((item) => item.priority === 'P1').length;
|
|
29
|
+
if (p0Repairs > 0) {
|
|
30
|
+
score += Math.min(0.22, p0Repairs * 0.11);
|
|
31
|
+
reasons.push(`p0_repair:${p0Repairs}`);
|
|
32
|
+
} else if (p1Repairs > 0) {
|
|
33
|
+
score += Math.min(0.14, p1Repairs * 0.07);
|
|
34
|
+
reasons.push(`p1_repair:${p1Repairs}`);
|
|
35
|
+
}
|
|
36
|
+
if (input.stormMode?.mixed) {
|
|
37
|
+
score += 0.08;
|
|
38
|
+
reasons.push('mixed');
|
|
39
|
+
}
|
|
40
|
+
if (input.recovery && Number(input.recovery.lineageDepth ?? 0) > 0) {
|
|
41
|
+
score += Math.min(0.12, Number(input.recovery.lineageDepth ?? 0) * 0.03);
|
|
42
|
+
reasons.push(`recovery_depth:${Number(input.recovery.lineageDepth ?? 0)}`);
|
|
43
|
+
}
|
|
44
|
+
const stabilizer = Math.min(0.12, input.anchors.length * 0.03 + input.evidence.length * 0.015);
|
|
45
|
+
if (stabilizer > 0) {
|
|
46
|
+
score -= stabilizer;
|
|
47
|
+
reasons.push(`stabilizer:${stabilizer.toFixed(2)}`);
|
|
48
|
+
}
|
|
49
|
+
const normalized = Math.max(0, Math.min(1, Number(score.toFixed(2))));
|
|
50
|
+
const level = normalized >= 0.74 ? 'critical' : normalized >= 0.52 ? 'rising' : normalized >= 0.28 ? 'steady' : 'calm';
|
|
51
|
+
return {
|
|
52
|
+
level,
|
|
53
|
+
score: normalized,
|
|
54
|
+
reasons: reasons.slice(0, 5),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { TsunamiStormReadiness } from './types';
|
|
2
|
+
|
|
3
|
+
export function buildStormReadiness(input: {
|
|
4
|
+
hasMainline: boolean;
|
|
5
|
+
anchors: TsunamiStormCenter['anchors'];
|
|
6
|
+
evidence: TsunamiStormCenter['evidence'];
|
|
7
|
+
recovery: DurableRecoveryRecord | null;
|
|
8
|
+
graphEdges: number;
|
|
9
|
+
}): TsunamiStormReadiness {
|
|
10
|
+
let score = 0;
|
|
11
|
+
const gaps: string[] = [];
|
|
12
|
+
if (input.hasMainline) score += 0.24;
|
|
13
|
+
else gaps.push('mainline');
|
|
14
|
+
if (input.anchors.length > 0) score += Math.min(0.24, 0.14 + input.anchors.length * 0.05);
|
|
15
|
+
else gaps.push('anchor');
|
|
16
|
+
if (input.evidence.length > 0) score += Math.min(0.18, 0.1 + input.evidence.length * 0.03);
|
|
17
|
+
else gaps.push('evidence');
|
|
18
|
+
if (input.recovery) score += 0.12;
|
|
19
|
+
else gaps.push('recovery');
|
|
20
|
+
if (input.graphEdges >= 4) score += 0.14;
|
|
21
|
+
else gaps.push('graph');
|
|
22
|
+
const normalized = Math.max(0, Math.min(1, Number(score.toFixed(2))));
|
|
23
|
+
const level = normalized >= 0.82 ? 'fortified' : normalized >= 0.58 ? 'ready' : normalized >= 0.32 ? 'partial' : 'weak';
|
|
24
|
+
return {
|
|
25
|
+
level,
|
|
26
|
+
score: normalized,
|
|
27
|
+
gaps: gaps.slice(0, 4),
|
|
28
|
+
};
|
|
29
|
+
}
|