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
package/src/provider.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TSUNAMI memory provider types
|
|
3
|
+
*
|
|
4
|
+
* Defines the memory provider interface and associated option types
|
|
5
|
+
* used by the memory fabric layer.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface MemoryAddOptions {
|
|
9
|
+
wing?: string;
|
|
10
|
+
room?: string;
|
|
11
|
+
importance?: number;
|
|
12
|
+
source?: string;
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
projectDir?: string;
|
|
15
|
+
fingerprint?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MemorySearchOptions {
|
|
19
|
+
query: string;
|
|
20
|
+
wing?: string;
|
|
21
|
+
room?: string;
|
|
22
|
+
limit?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface MemoryRecallOptions {
|
|
26
|
+
wing?: string;
|
|
27
|
+
room?: string;
|
|
28
|
+
limit?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MemoryWakeOptions {
|
|
32
|
+
wing?: string;
|
|
33
|
+
limit?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface MemorySyncOptions {
|
|
37
|
+
projectDir?: string;
|
|
38
|
+
refreshGraph?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface MemoryCompactOptions {
|
|
42
|
+
targetWing?: string;
|
|
43
|
+
targetRoom?: string;
|
|
44
|
+
maxEntries?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface MemoryPrefetchOptions {
|
|
48
|
+
query?: string;
|
|
49
|
+
wing?: string;
|
|
50
|
+
limit?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface MemoryQueryIntent {
|
|
54
|
+
query: string;
|
|
55
|
+
primaryWing?: string;
|
|
56
|
+
primaryRoom?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface MemoryProvider {
|
|
60
|
+
add(entry: MemoryAddOptions): string;
|
|
61
|
+
search(opts: MemorySearchOptions): any[];
|
|
62
|
+
recall(opts: MemoryRecallOptions): any[];
|
|
63
|
+
wake(opts: MemoryWakeOptions): any[];
|
|
64
|
+
status(): Record<string, unknown>;
|
|
65
|
+
listWings(): Record<string, number>;
|
|
66
|
+
listRooms(wing?: string): Record<string, number>;
|
|
67
|
+
timeline(limit?: number): any[];
|
|
68
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TSUNAMI durable recovery stub
|
|
3
|
+
*
|
|
4
|
+
* Provides the durable recovery store interface for cross-session recovery.
|
|
5
|
+
* Returns null by default; the storm center handles missing recovery gracefully.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface DurableRecoveryRecord {
|
|
9
|
+
recoveryId: string;
|
|
10
|
+
source: string;
|
|
11
|
+
note?: string;
|
|
12
|
+
lineageDepth?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DurableRecoveryLatestOpts {
|
|
16
|
+
sessionId?: string;
|
|
17
|
+
projectDir?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const durableRecoveryStore = {
|
|
21
|
+
latest(_opts?: DurableRecoveryLatestOpts): DurableRecoveryRecord | null {
|
|
22
|
+
return null;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TSUNAMI state path utility
|
|
3
|
+
*
|
|
4
|
+
* Resolves paths under the TSUNAMI_HOME directory for state storage.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const TSUNAMI_HOME = process.env.TSUNAMI_HOME || '.tsunami';
|
|
8
|
+
|
|
9
|
+
export function statePath(...parts: string[]): string {
|
|
10
|
+
return [TSUNAMI_HOME, ...parts].join('/');
|
|
11
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Storm center — supporting basin analysis
|
|
2
|
+
import { classifyTsunamiText } from '../tsunami_classifier';
|
|
3
|
+
import type { TsunamiStormCenter } from './types';
|
|
4
|
+
|
|
5
|
+
export function buildSupportingBasins(input: {
|
|
6
|
+
flow: TsunamiStormCenter['flow'];
|
|
7
|
+
anchors: TsunamiStormCenter['anchors'];
|
|
8
|
+
recovery: DurableRecoveryRecord | null;
|
|
9
|
+
evidence: TsunamiStormCenter['evidence'];
|
|
10
|
+
issues: TsunamiStormCenter['issues'];
|
|
11
|
+
repairSuggestions: TsunamiStormCenter['repairSuggestions'];
|
|
12
|
+
}) {
|
|
13
|
+
const basinMap = new Map<string, { energy: number; drivers: Set<string> }>();
|
|
14
|
+
const add = (basin: string, energy: number, driver: string) => {
|
|
15
|
+
const key = String(basin || '').trim();
|
|
16
|
+
if (!key) return;
|
|
17
|
+
const entry = basinMap.get(key) ?? { energy: 0, drivers: new Set<string>() };
|
|
18
|
+
entry.energy += Math.max(0, energy);
|
|
19
|
+
if (driver) entry.drivers.add(driver);
|
|
20
|
+
basinMap.set(key, entry);
|
|
21
|
+
};
|
|
22
|
+
const classifyInto = (text: string, baseEnergy: number, driver: string) => {
|
|
23
|
+
const normalized = String(text || '').trim();
|
|
24
|
+
if (!normalized) return;
|
|
25
|
+
const classification = classifyTsunamiText(normalized);
|
|
26
|
+
add(classification.basin, baseEnergy * Math.max(0.35, classification.confidence), `${driver}:${classification.current}`);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
add(input.flow.basin, 0.7 + input.flow.confidence * 0.3, `focus:${input.flow.current}`);
|
|
30
|
+
|
|
31
|
+
for (const anchor of input.anchors.slice(0, 2)) {
|
|
32
|
+
classifyInto(`${anchor.title}\n${anchor.summary}`, 0.42 + Math.min(0.18, anchor.confidence * 0.15), 'anchor');
|
|
33
|
+
}
|
|
34
|
+
if (input.recovery) {
|
|
35
|
+
classifyInto(`${input.recovery.note || ''}\n${input.recovery.source || ''}`, 0.24, 'recovery');
|
|
36
|
+
}
|
|
37
|
+
for (const snippet of input.evidence.slice(0, 2)) {
|
|
38
|
+
classifyInto(`${snippet.title || ''}\n${snippet.quote || ''}`, 0.28, 'evidence');
|
|
39
|
+
}
|
|
40
|
+
for (const suggestion of input.repairSuggestions.slice(0, 2)) {
|
|
41
|
+
const weight = suggestion.priority === 'P0' ? 0.46 : suggestion.priority === 'P1' ? 0.36 : 0.28;
|
|
42
|
+
classifyInto(`${suggestion.title}\n${suggestion.detail || ''}`, weight, 'repair');
|
|
43
|
+
}
|
|
44
|
+
for (const issue of input.issues.slice(0, 2)) {
|
|
45
|
+
const weight = issue.severity === 'high' ? 0.42 : issue.severity === 'medium' ? 0.32 : 0.24;
|
|
46
|
+
classifyInto(`${issue.code}\n${issue.detail || ''}`, weight, 'issue');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return Array.from(basinMap.entries())
|
|
50
|
+
.map(([basin, entry]) => ({
|
|
51
|
+
basin,
|
|
52
|
+
energy: Number(entry.energy.toFixed(2)),
|
|
53
|
+
drivers: Array.from(entry.drivers).slice(0, 3),
|
|
54
|
+
}))
|
|
55
|
+
.sort((a, b) => b.energy - a.energy || a.basin.localeCompare(b.basin))
|
|
56
|
+
.slice(0, 3);
|
|
57
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { TsunamiStormPressure, TsunamiStormReadiness, TsunamiStormCenterStormMode, TsunamiStormBoundary } from './types';
|
|
2
|
+
|
|
3
|
+
export function buildStormBoundary(input: {
|
|
4
|
+
stormPressure?: TsunamiStormPressure;
|
|
5
|
+
stormReadiness?: TsunamiStormReadiness;
|
|
6
|
+
stormMode?: TsunamiStormCenterStormMode;
|
|
7
|
+
topRepair?: TsunamiStormCenter['topRepair'];
|
|
8
|
+
topIssue?: TsunamiStormCenter['topIssue'];
|
|
9
|
+
supportingBasins: Array<{ basin: string; energy: number; drivers: string[] }>;
|
|
10
|
+
}): TsunamiStormBoundary | undefined {
|
|
11
|
+
const pressure = input.stormPressure;
|
|
12
|
+
const readiness = input.stormReadiness;
|
|
13
|
+
const mode = input.stormMode;
|
|
14
|
+
if (!pressure && !readiness && !mode) return undefined;
|
|
15
|
+
|
|
16
|
+
const seaMixCount = input.supportingBasins.length;
|
|
17
|
+
const weakSupport = Boolean(readiness && (readiness.level === 'weak' || readiness.level === 'partial'));
|
|
18
|
+
const strongSupport = Boolean(readiness && (readiness.level === 'ready' || readiness.level === 'fortified'));
|
|
19
|
+
const severePressure = pressure?.level === 'critical' || pressure?.level === 'rising';
|
|
20
|
+
const mixed = Boolean(mode?.mixed);
|
|
21
|
+
const hasRepair = Boolean(input.topRepair);
|
|
22
|
+
const hasIssue = Boolean(input.topIssue);
|
|
23
|
+
|
|
24
|
+
if (pressure?.level === 'critical' || ((hasRepair || hasIssue) && severePressure && mixed)) {
|
|
25
|
+
return {
|
|
26
|
+
mode: 'spilling',
|
|
27
|
+
expand: false,
|
|
28
|
+
reason: `pressure ${pressure?.level || 'high'} with active drift/repair energy requires containment first`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (weakSupport && seaMixCount <= 1) {
|
|
32
|
+
return {
|
|
33
|
+
mode: 'permeable',
|
|
34
|
+
expand: true,
|
|
35
|
+
reason: `support is ${readiness?.level || 'thin'} and sea mix is narrow, so widen anchors/evidence before pushing forward`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (strongSupport && (pressure?.level === 'calm' || pressure?.level === 'steady') && !mixed) {
|
|
39
|
+
return {
|
|
40
|
+
mode: 'sealed',
|
|
41
|
+
expand: false,
|
|
42
|
+
reason: `support is ${readiness?.level || 'stable'} and pressure is ${pressure?.level || 'calm'}, so keep the storm tightly scoped`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
mode: 'guarded',
|
|
47
|
+
expand: false,
|
|
48
|
+
reason: mixed
|
|
49
|
+
? 'mixed sea-state suggests holding the current lane until the dominant current is clearer'
|
|
50
|
+
: 'boundary is mostly stable, but keep scope guarded while pressure and support finish converging',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { TsunamiStormGate, TsunamiStormHorizon, TsunamiStormReadiness, TsunamiStormConfidence, TsunamiStormBudget } from './types';
|
|
2
|
+
|
|
3
|
+
export function buildStormBudget(input: {
|
|
4
|
+
stormGate?: TsunamiStormGate;
|
|
5
|
+
stormHorizon?: TsunamiStormHorizon;
|
|
6
|
+
stormReadiness?: TsunamiStormReadiness;
|
|
7
|
+
stormConfidence?: TsunamiStormConfidence;
|
|
8
|
+
}): TsunamiStormBudget | undefined {
|
|
9
|
+
const gate = input.stormGate;
|
|
10
|
+
const horizon = input.stormHorizon;
|
|
11
|
+
const readiness = input.stormReadiness;
|
|
12
|
+
const confidence = input.stormConfidence;
|
|
13
|
+
if (!gate && !horizon && !readiness && !confidence) return undefined;
|
|
14
|
+
|
|
15
|
+
const horizonSteps = Math.max(0, Number(horizon?.steps || 0));
|
|
16
|
+
if (gate?.verdict === 'hold') {
|
|
17
|
+
return {
|
|
18
|
+
mode: 'frozen',
|
|
19
|
+
steps: 0,
|
|
20
|
+
reason: gate.reason,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (gate?.verdict === 'guarded' || readiness?.level === 'weak' || readiness?.level === 'partial' || confidence?.level === 'guarded' || confidence?.level === 'low') {
|
|
24
|
+
return {
|
|
25
|
+
mode: 'minimal',
|
|
26
|
+
steps: 1,
|
|
27
|
+
reason: 'advance only the immediate correction move while support and confidence continue converging',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (gate?.verdict === 'expand' && (confidence?.level === 'high' || confidence?.level === 'confident')) {
|
|
31
|
+
return {
|
|
32
|
+
mode: 'open',
|
|
33
|
+
steps: Math.max(2, Math.min(3, horizonSteps || 3)),
|
|
34
|
+
reason: 'guidance is strong enough to approve a broader forward window',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
mode: 'guided',
|
|
39
|
+
steps: Math.max(1, Math.min(2, horizonSteps || 2)),
|
|
40
|
+
reason: 'advance with a bounded budget while keeping the mainline under active watch',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
// Storm center — orchestrator
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { basename, join, resolve } from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
getProjectLatestHandoff, getProjectStateStatus, listProjectWikiPages,
|
|
6
|
+
queryProjectWiki, resolveProjectTaskThread,
|
|
7
|
+
type ProjectHandoffRecord, type ProjectTaskThread,
|
|
8
|
+
} from '../core/project_state';
|
|
9
|
+
import { durableRecoveryStore, type DurableRecoveryRecord } from '../runtime/checkpoints/durable_recovery';
|
|
10
|
+
import { auditMemoryFabric } from '../memory_audit';
|
|
11
|
+
import { classifyTsunamiText } from '../tsunami_classifier';
|
|
12
|
+
import { tsunamiGraphQueryEntity } from '../tsunami_graph_runtime';
|
|
13
|
+
import { syncProjectRuntimeGraph, type TsunamiRuntimeGraphSyncSummary } from '../tsunami_runtime_graph_sync';
|
|
14
|
+
import {
|
|
15
|
+
buildProjectNode, buildTaskThreadNode, buildHandoffNode,
|
|
16
|
+
buildAnchorNode, buildRecoveryNode, clampEnergy, safeTrim,
|
|
17
|
+
pickFocusQuery, buildFallbackDocStormSupport, readAnchorCandidates,
|
|
18
|
+
withTsunamiStormRetry,
|
|
19
|
+
} from './helpers';
|
|
20
|
+
import { buildCurrents, buildCurrentMix } from './signals';
|
|
21
|
+
import { buildSupportingBasins } from './basins';
|
|
22
|
+
import { buildStormMode } from './mode';
|
|
23
|
+
import { buildStormPressure } from './pressure';
|
|
24
|
+
import { buildStormDirective, buildStormAction } from './directive';
|
|
25
|
+
import { buildStormReadiness } from './readiness';
|
|
26
|
+
import { buildStormBoundary } from './boundary';
|
|
27
|
+
import { buildStormHorizon } from './horizon';
|
|
28
|
+
import { buildStormConfidence } from './confidence';
|
|
29
|
+
import { buildStormGate } from './gate';
|
|
30
|
+
import { buildStormBudget } from './budget';
|
|
31
|
+
import { buildStormSelection } from './selection';
|
|
32
|
+
import { buildStormCoverage } from './coverage';
|
|
33
|
+
import { buildStormSaturation } from './saturation';
|
|
34
|
+
import { buildStormIntake } from './intake';
|
|
35
|
+
import type { TsunamiStormCenter, TsunamiStormCenterCurrent, BuildStormCenterOpts } from './types';
|
|
36
|
+
|
|
37
|
+
export { isTsunamiStormRetryableError, withTsunamiStormRetry } from './helpers';
|
|
38
|
+
|
|
39
|
+
export function buildTsunamiStormCenterOnce(opts: BuildStormCenterOpts = {}): TsunamiStormCenter {
|
|
40
|
+
const projectDir = resolve(opts.projectDir || process.cwd());
|
|
41
|
+
const status = getProjectStateStatus(projectDir);
|
|
42
|
+
const query = String(opts.query ?? '').trim();
|
|
43
|
+
const threadMatch = resolveProjectTaskThread(projectDir, query, 3);
|
|
44
|
+
const thread = threadMatch.thread;
|
|
45
|
+
const handoff = getProjectLatestHandoff(projectDir, {
|
|
46
|
+
sessionId: opts.sessionId,
|
|
47
|
+
featureId: thread?.featureId,
|
|
48
|
+
}) ?? getProjectLatestHandoff(projectDir);
|
|
49
|
+
const focusQuery = pickFocusQuery({
|
|
50
|
+
query,
|
|
51
|
+
thread,
|
|
52
|
+
handoff,
|
|
53
|
+
activeFeatureTitle: status.activeFeature?.title,
|
|
54
|
+
});
|
|
55
|
+
const docSupport = buildFallbackDocStormSupport(projectDir, focusQuery, Math.max(2, Math.min(8, opts.evidenceLimit ?? 4)));
|
|
56
|
+
const wikiAnchors = readAnchorCandidates(projectDir, thread?.featureId, 3).map((page) => ({
|
|
57
|
+
pageId: page.pageId,
|
|
58
|
+
title: page.title,
|
|
59
|
+
summary: page.summary,
|
|
60
|
+
confidence: page.confidence,
|
|
61
|
+
tags: page.tags,
|
|
62
|
+
}));
|
|
63
|
+
const anchors = wikiAnchors.length > 0 ? wikiAnchors : docSupport.anchors;
|
|
64
|
+
const evidenceResult = queryProjectWiki(projectDir, focusQuery, Math.max(2, Math.min(8, opts.evidenceLimit ?? 4)));
|
|
65
|
+
const evidence = evidenceResult.evidence.length > 0 ? evidenceResult.evidence : docSupport.evidence;
|
|
66
|
+
const audit = auditMemoryFabric(projectDir);
|
|
67
|
+
const recovery = durableRecoveryStore.latest({
|
|
68
|
+
sessionId: opts.sessionId,
|
|
69
|
+
projectDir,
|
|
70
|
+
}) ?? durableRecoveryStore.latest({ projectDir });
|
|
71
|
+
const refreshGraph = opts.refreshGraph !== false;
|
|
72
|
+
const syncSummary = refreshGraph ? syncProjectRuntimeGraph(projectDir) : null;
|
|
73
|
+
const requestedRelationLimit = Math.max(4, Math.min(24, opts.relationLimit ?? 10));
|
|
74
|
+
const graph = {
|
|
75
|
+
project: tsunamiGraphQueryEntity(buildProjectNode(projectDir), undefined, 'both').slice(0, requestedRelationLimit),
|
|
76
|
+
thread: thread ? tsunamiGraphQueryEntity(buildTaskThreadNode(thread.id), undefined, 'both').slice(0, requestedRelationLimit) : [],
|
|
77
|
+
handoff: handoff ? tsunamiGraphQueryEntity(buildHandoffNode(handoff.id), undefined, 'both').slice(0, requestedRelationLimit) : [],
|
|
78
|
+
anchor: anchors[0] ? tsunamiGraphQueryEntity(buildAnchorNode(anchors[0].pageId), undefined, 'both').slice(0, requestedRelationLimit) : [],
|
|
79
|
+
recovery: recovery ? tsunamiGraphQueryEntity(buildRecoveryNode(recovery.recoveryId), undefined, 'both').slice(0, requestedRelationLimit) : [],
|
|
80
|
+
};
|
|
81
|
+
const graphEdges = graph.project.length + graph.thread.length + graph.handoff.length + graph.anchor.length + graph.recovery.length;
|
|
82
|
+
const requestedSignalLimit = Math.max(2, Math.min(6, opts.signalLimit ?? 4));
|
|
83
|
+
const currents = buildCurrents({
|
|
84
|
+
thread,
|
|
85
|
+
handoff,
|
|
86
|
+
anchors,
|
|
87
|
+
recovery,
|
|
88
|
+
evidence,
|
|
89
|
+
issues: audit.issues,
|
|
90
|
+
repairSuggestions: audit.repairSuggestions,
|
|
91
|
+
signalLimit: requestedSignalLimit,
|
|
92
|
+
fallbackMainlineTitle: !thread && !handoff && !status.activeFeature ? docSupport.mainlineTitle : undefined,
|
|
93
|
+
fallbackMainlineSummary: !thread && !handoff && !status.activeFeature ? docSupport.mainlineSummary : undefined,
|
|
94
|
+
});
|
|
95
|
+
const currentMix = buildCurrentMix(currents);
|
|
96
|
+
const stormMode = buildStormMode(currentMix);
|
|
97
|
+
const stormPressure = buildStormPressure({
|
|
98
|
+
currentMix,
|
|
99
|
+
stormMode,
|
|
100
|
+
issues: audit.issues,
|
|
101
|
+
repairSuggestions: audit.repairSuggestions,
|
|
102
|
+
anchors,
|
|
103
|
+
evidence,
|
|
104
|
+
recovery,
|
|
105
|
+
});
|
|
106
|
+
const flow = classifyTsunamiText([
|
|
107
|
+
thread?.title || '',
|
|
108
|
+
thread?.summary || '',
|
|
109
|
+
handoff?.task || '',
|
|
110
|
+
handoff?.summary || '',
|
|
111
|
+
handoff?.nextStep || '',
|
|
112
|
+
status.activeFeature?.title || '',
|
|
113
|
+
status.activeFeature?.detail || '',
|
|
114
|
+
].filter(Boolean).join('\n'));
|
|
115
|
+
const topRepair = audit.repairSuggestions[0]
|
|
116
|
+
? {
|
|
117
|
+
title: audit.repairSuggestions[0].title,
|
|
118
|
+
priority: audit.repairSuggestions[0].priority,
|
|
119
|
+
detail: safeTrim(audit.repairSuggestions[0].detail, 140),
|
|
120
|
+
}
|
|
121
|
+
: undefined;
|
|
122
|
+
const topIssue = audit.issues[0]
|
|
123
|
+
? {
|
|
124
|
+
code: audit.issues[0].code,
|
|
125
|
+
severity: audit.issues[0].severity,
|
|
126
|
+
detail: safeTrim(audit.issues[0].detail, 140),
|
|
127
|
+
}
|
|
128
|
+
: undefined;
|
|
129
|
+
const focus = {
|
|
130
|
+
title: thread?.title || handoff?.task || status.activeFeature?.title || docSupport.mainlineTitle || basename(projectDir),
|
|
131
|
+
status: thread?.status || status.activeFeature?.status,
|
|
132
|
+
summary: thread?.summary || handoff?.summary || status.activeFeature?.detail || docSupport.mainlineSummary,
|
|
133
|
+
nextStep: thread?.nextStep || handoff?.nextStep,
|
|
134
|
+
featureId: thread?.featureId || handoff?.progressFeatureId || status.activeFeature?.id,
|
|
135
|
+
};
|
|
136
|
+
const stormDirective = buildStormDirective({
|
|
137
|
+
stormPressure,
|
|
138
|
+
stormMode,
|
|
139
|
+
topRepair,
|
|
140
|
+
topIssue,
|
|
141
|
+
focus,
|
|
142
|
+
});
|
|
143
|
+
const stormAction = buildStormAction({
|
|
144
|
+
stormDirective,
|
|
145
|
+
topRepair,
|
|
146
|
+
topIssue,
|
|
147
|
+
focus,
|
|
148
|
+
});
|
|
149
|
+
const stormReadiness = buildStormReadiness({
|
|
150
|
+
hasMainline: Boolean(thread || handoff || status.activeFeature || docSupport.mainlineTitle),
|
|
151
|
+
anchors,
|
|
152
|
+
evidence,
|
|
153
|
+
recovery,
|
|
154
|
+
graphEdges,
|
|
155
|
+
});
|
|
156
|
+
const supportingBasins = buildSupportingBasins({
|
|
157
|
+
flow: {
|
|
158
|
+
basin: flow.basin,
|
|
159
|
+
current: flow.current,
|
|
160
|
+
confidence: Number(flow.confidence.toFixed(2)),
|
|
161
|
+
},
|
|
162
|
+
anchors,
|
|
163
|
+
recovery,
|
|
164
|
+
evidence,
|
|
165
|
+
issues: audit.issues,
|
|
166
|
+
repairSuggestions: audit.repairSuggestions,
|
|
167
|
+
});
|
|
168
|
+
const stormBoundary = buildStormBoundary({
|
|
169
|
+
stormPressure,
|
|
170
|
+
stormReadiness,
|
|
171
|
+
stormMode,
|
|
172
|
+
topRepair,
|
|
173
|
+
topIssue,
|
|
174
|
+
supportingBasins,
|
|
175
|
+
});
|
|
176
|
+
const stormHorizon = buildStormHorizon({
|
|
177
|
+
stormPressure,
|
|
178
|
+
stormReadiness,
|
|
179
|
+
stormBoundary,
|
|
180
|
+
stormMode,
|
|
181
|
+
topRepair,
|
|
182
|
+
topIssue,
|
|
183
|
+
});
|
|
184
|
+
const stormConfidence = buildStormConfidence({
|
|
185
|
+
stormPressure,
|
|
186
|
+
stormReadiness,
|
|
187
|
+
stormBoundary,
|
|
188
|
+
stormHorizon,
|
|
189
|
+
stormMode,
|
|
190
|
+
});
|
|
191
|
+
const stormGate = buildStormGate({
|
|
192
|
+
stormPressure,
|
|
193
|
+
stormReadiness,
|
|
194
|
+
stormConfidence,
|
|
195
|
+
stormBoundary,
|
|
196
|
+
stormHorizon,
|
|
197
|
+
});
|
|
198
|
+
const stormBudget = buildStormBudget({
|
|
199
|
+
stormGate,
|
|
200
|
+
stormHorizon,
|
|
201
|
+
stormReadiness,
|
|
202
|
+
stormConfidence,
|
|
203
|
+
});
|
|
204
|
+
const stormSelection = buildStormSelection({
|
|
205
|
+
stormBudget,
|
|
206
|
+
stormBoundary,
|
|
207
|
+
stormGate,
|
|
208
|
+
stormHorizon,
|
|
209
|
+
});
|
|
210
|
+
const totalSignalCount = currents.length;
|
|
211
|
+
const totalEvidenceCount = evidence.length;
|
|
212
|
+
const totalRelationCount = graph.project.length + graph.thread.length + graph.handoff.length + graph.anchor.length + graph.recovery.length;
|
|
213
|
+
const selectedCurrents = currents.slice(0, Math.max(1, stormSelection?.signalLimit ?? requestedSignalLimit));
|
|
214
|
+
const selectedEvidence = evidence.slice(0, Math.max(1, stormSelection?.evidenceLimit ?? evidence.length));
|
|
215
|
+
const appliedRelationLimit = Math.max(1, stormSelection?.relationLimit ?? requestedRelationLimit);
|
|
216
|
+
const selectedGraph = {
|
|
217
|
+
project: graph.project.slice(0, appliedRelationLimit) as unknown as Record<string, unknown>[],
|
|
218
|
+
thread: graph.thread.slice(0, appliedRelationLimit) as unknown as Record<string, unknown>[],
|
|
219
|
+
handoff: graph.handoff.slice(0, appliedRelationLimit) as unknown as Record<string, unknown>[],
|
|
220
|
+
anchor: graph.anchor.slice(0, appliedRelationLimit) as unknown as Record<string, unknown>[],
|
|
221
|
+
recovery: graph.recovery.slice(0, appliedRelationLimit) as unknown as Record<string, unknown>[],
|
|
222
|
+
};
|
|
223
|
+
const selectedCurrentMix = buildCurrentMix(selectedCurrents);
|
|
224
|
+
const selectedRelationCount = selectedGraph.project.length + selectedGraph.thread.length + selectedGraph.handoff.length + selectedGraph.anchor.length + selectedGraph.recovery.length;
|
|
225
|
+
const stormCoverage = buildStormCoverage({
|
|
226
|
+
stormSelection,
|
|
227
|
+
selectedSignals: selectedCurrents.length,
|
|
228
|
+
totalSignals: totalSignalCount,
|
|
229
|
+
selectedEvidence: selectedEvidence.length,
|
|
230
|
+
totalEvidence: totalEvidenceCount,
|
|
231
|
+
selectedRelations: selectedRelationCount,
|
|
232
|
+
totalRelations: totalRelationCount,
|
|
233
|
+
});
|
|
234
|
+
const stormSaturation = buildStormSaturation({
|
|
235
|
+
stormSelection,
|
|
236
|
+
selectedSignals: selectedCurrents.length,
|
|
237
|
+
totalSignals: totalSignalCount,
|
|
238
|
+
selectedEvidence: selectedEvidence.length,
|
|
239
|
+
totalEvidence: totalEvidenceCount,
|
|
240
|
+
selectedRelations: selectedRelationCount,
|
|
241
|
+
totalRelations: totalRelationCount,
|
|
242
|
+
});
|
|
243
|
+
const stormIntake = buildStormIntake({
|
|
244
|
+
stormSelection,
|
|
245
|
+
stormCoverage,
|
|
246
|
+
stormSaturation,
|
|
247
|
+
stormBudget,
|
|
248
|
+
stormGate,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
projectDir,
|
|
253
|
+
projectNode: buildProjectNode(projectDir),
|
|
254
|
+
query,
|
|
255
|
+
focusQuery,
|
|
256
|
+
refreshedGraph: refreshGraph,
|
|
257
|
+
syncSummary,
|
|
258
|
+
focus,
|
|
259
|
+
flow: {
|
|
260
|
+
basin: flow.basin,
|
|
261
|
+
current: flow.current,
|
|
262
|
+
confidence: Number(flow.confidence.toFixed(2)),
|
|
263
|
+
},
|
|
264
|
+
supportingBasins,
|
|
265
|
+
thread,
|
|
266
|
+
handoff,
|
|
267
|
+
recovery,
|
|
268
|
+
anchors,
|
|
269
|
+
evidence: selectedEvidence,
|
|
270
|
+
graph: selectedGraph,
|
|
271
|
+
issues: audit.issues,
|
|
272
|
+
repairSuggestions: audit.repairSuggestions,
|
|
273
|
+
currents: selectedCurrents,
|
|
274
|
+
currentMix: selectedCurrentMix,
|
|
275
|
+
stormMode,
|
|
276
|
+
stormPressure,
|
|
277
|
+
stormDirective,
|
|
278
|
+
stormAction,
|
|
279
|
+
stormReadiness,
|
|
280
|
+
stormBoundary,
|
|
281
|
+
stormHorizon,
|
|
282
|
+
stormConfidence,
|
|
283
|
+
stormGate,
|
|
284
|
+
stormBudget,
|
|
285
|
+
stormSelection,
|
|
286
|
+
stormCoverage,
|
|
287
|
+
stormSaturation,
|
|
288
|
+
stormIntake,
|
|
289
|
+
topRepair,
|
|
290
|
+
topIssue,
|
|
291
|
+
metrics: {
|
|
292
|
+
issueCount: audit.issueCount,
|
|
293
|
+
repairCount: audit.repairSuggestions.length,
|
|
294
|
+
evidenceCount: evidence.length,
|
|
295
|
+
anchorCount: anchors.length,
|
|
296
|
+
graphEdges,
|
|
297
|
+
recoveryDepth: Number(recovery?.lineageDepth ?? 0),
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
export function buildTsunamiStormCenter(opts: BuildStormCenterOpts = {}): TsunamiStormCenter {
|
|
302
|
+
return withTsunamiStormRetry(() => buildTsunamiStormCenterOnce(opts));
|
|
303
|
+
}
|
|
304
|
+
export function formatTsunamiStormCenterText(center: TsunamiStormCenter): string {
|
|
305
|
+
const lines = [
|
|
306
|
+
'TSUNAMI Storm Center',
|
|
307
|
+
`Project: ${center.projectDir}`,
|
|
308
|
+
`Focus: ${center.focus.title}${center.focus.status ? ` [${center.focus.status}]` : ''}`,
|
|
309
|
+
`Focus Query: ${center.focusQuery}`,
|
|
310
|
+
];
|
|
311
|
+
if (center.focus.nextStep) lines.push(`Next Step: ${center.focus.nextStep}`);
|
|
312
|
+
if (center.focus.summary) lines.push(`Summary: ${safeTrim(center.focus.summary, 180)}`);
|
|
313
|
+
lines.push(`Flow: ${center.flow.basin}/${center.flow.current} (confidence=${center.flow.confidence.toFixed(2)})`);
|
|
314
|
+
if (center.stormAction) {
|
|
315
|
+
lines.push(`Storm Action: ${center.stormAction.label}${center.stormAction.target ? ` -> ${center.stormAction.target}` : ''} · ${safeTrim(center.stormAction.reason, 88)}`);
|
|
316
|
+
}
|
|
317
|
+
if (center.stormReadiness) {
|
|
318
|
+
lines.push(`Storm Readiness: ${center.stormReadiness.level} (${center.stormReadiness.score.toFixed(2)})${center.stormReadiness.gaps.length ? ` · gaps=${center.stormReadiness.gaps.join('|')}` : ''}`);
|
|
319
|
+
}
|
|
320
|
+
if (center.stormConfidence) {
|
|
321
|
+
lines.push(`Storm Confidence: ${center.stormConfidence.level} (${center.stormConfidence.score.toFixed(2)}) · ${safeTrim(center.stormConfidence.reason, 88)}`);
|
|
322
|
+
}
|
|
323
|
+
if (center.stormGate) {
|
|
324
|
+
lines.push(`Storm Gate: ${center.stormGate.verdict}${center.stormGate.allowForward ? ' · forward-ok' : ' · hold'} · ${safeTrim(center.stormGate.reason, 88)}`);
|
|
325
|
+
}
|
|
326
|
+
if (center.stormBudget) {
|
|
327
|
+
lines.push(`Storm Budget: ${center.stormBudget.mode} (${center.stormBudget.steps} step${center.stormBudget.steps !== 1 ? 's' : ''}) · ${safeTrim(center.stormBudget.reason, 88)}`);
|
|
328
|
+
}
|
|
329
|
+
if (center.stormSelection) {
|
|
330
|
+
lines.push(`Storm Selection: ${center.stormSelection.profile} · signals=${center.stormSelection.signalLimit} evidence=${center.stormSelection.evidenceLimit} relations=${center.stormSelection.relationLimit} · ${safeTrim(center.stormSelection.reason, 88)}`);
|
|
331
|
+
}
|
|
332
|
+
if (center.stormCoverage) {
|
|
333
|
+
lines.push(`Storm Coverage: ${center.stormCoverage.mode} (${center.stormCoverage.score.toFixed(2)}) · signals=${center.stormCoverage.selectedSignals}/${center.stormCoverage.totalSignals} evidence=${center.stormCoverage.selectedEvidence}/${center.stormCoverage.totalEvidence} relations=${center.stormCoverage.selectedRelations}/${center.stormCoverage.totalRelations} · ${safeTrim(center.stormCoverage.reason, 88)}`);
|
|
334
|
+
}
|
|
335
|
+
if (center.stormSaturation) {
|
|
336
|
+
lines.push(`Storm Saturation: ${center.stormSaturation.level} · ${center.stormSaturation.hitLanes.length ? center.stormSaturation.hitLanes.join('/') : 'clear'} · ${safeTrim(center.stormSaturation.reason, 88)}`);
|
|
337
|
+
}
|
|
338
|
+
if (center.stormIntake) {
|
|
339
|
+
lines.push(`Storm Intake: ${center.stormIntake.mode} -> ${center.stormIntake.target} · next=${center.stormIntake.nextSignalLimit}/${center.stormIntake.nextEvidenceLimit}/${center.stormIntake.nextRelationLimit} · ${safeTrim(center.stormIntake.reason, 88)}`);
|
|
340
|
+
}
|
|
341
|
+
if (center.stormBoundary) {
|
|
342
|
+
lines.push(`Storm Boundary: ${center.stormBoundary.mode}${center.stormBoundary.expand ? ' · expand-support' : ' · keep-tight'} · ${safeTrim(center.stormBoundary.reason, 88)}`);
|
|
343
|
+
}
|
|
344
|
+
if (center.stormHorizon) {
|
|
345
|
+
lines.push(`Storm Horizon: ${center.stormHorizon.label} (${center.stormHorizon.steps} step${center.stormHorizon.steps > 1 ? 's' : ''}) · ${safeTrim(center.stormHorizon.reason, 88)}`);
|
|
346
|
+
}
|
|
347
|
+
if (center.supportingBasins.length > 0) {
|
|
348
|
+
lines.push(`Sea Mix: ${center.supportingBasins.map((item) => `${item.basin}(${item.energy.toFixed(2)})`).join(' · ')}`);
|
|
349
|
+
lines.push(`Sea Drivers: ${center.supportingBasins.slice(0, 2).map((item) => `${item.basin}<=${item.drivers.slice(0, 2).join(', ')}`).join(' | ')}`);
|
|
350
|
+
}
|
|
351
|
+
if (center.currentMix.length > 0) {
|
|
352
|
+
lines.push(`Current Mix: ${center.currentMix.map((item) => `${item.kind}(${item.energy.toFixed(2)}/${item.count})`).join(' · ')}`);
|
|
353
|
+
}
|
|
354
|
+
if (center.stormMode) {
|
|
355
|
+
lines.push(`Storm Mode: ${center.stormMode.label} (dominant=${center.stormMode.dominantKind} @ ${center.stormMode.dominance.toFixed(2)})`);
|
|
356
|
+
}
|
|
357
|
+
if (center.stormPressure) {
|
|
358
|
+
lines.push(`Storm Pressure: ${center.stormPressure.level} (${center.stormPressure.score.toFixed(2)})${center.stormPressure.reasons.length ? ` · ${center.stormPressure.reasons.join(' | ')}` : ''}`);
|
|
359
|
+
}
|
|
360
|
+
if (center.stormDirective) {
|
|
361
|
+
lines.push(`Storm Directive: ${center.stormDirective.label} (${center.stormDirective.lane}) · ${safeTrim(center.stormDirective.reason, 88)}`);
|
|
362
|
+
}
|
|
363
|
+
lines.push(
|
|
364
|
+
`Metrics: anchors=${center.metrics.anchorCount} | evidence=${center.metrics.evidenceCount} | issues=${center.metrics.issueCount} | repairs=${center.metrics.repairCount} | graph_edges=${center.metrics.graphEdges} | recovery_depth=${center.metrics.recoveryDepth}`,
|
|
365
|
+
);
|
|
366
|
+
if (center.currents.length > 0) {
|
|
367
|
+
lines.push('', 'Currents:');
|
|
368
|
+
for (const current of center.currents.slice(0, 8)) {
|
|
369
|
+
lines.push(`- [${current.kind}] energy=${current.energy.toFixed(2)} ${current.label}`);
|
|
370
|
+
if (current.detail) lines.push(` ${current.detail}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (center.repairSuggestions.length > 0) {
|
|
374
|
+
lines.push('', 'Repair Energy:');
|
|
375
|
+
for (const suggestion of center.repairSuggestions.slice(0, 3)) {
|
|
376
|
+
lines.push(`- [${suggestion.priority}] ${suggestion.title}: ${safeTrim(suggestion.detail, 160)}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (center.topIssue) {
|
|
380
|
+
lines.push('', 'Top Issue:');
|
|
381
|
+
lines.push(`- [${center.topIssue.severity ?? 'low'}] ${center.topIssue.code}: ${center.topIssue.detail ?? ''}`.trim());
|
|
382
|
+
}
|
|
383
|
+
if (center.evidence.length > 0) {
|
|
384
|
+
lines.push('', 'Evidence:');
|
|
385
|
+
for (const snippet of center.evidence.slice(0, 3)) {
|
|
386
|
+
lines.push(`- ${safeTrim(snippet.quote, 160)}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (center.anchors.length > 0) {
|
|
390
|
+
lines.push('', 'Anchors:');
|
|
391
|
+
for (const anchor of center.anchors.slice(0, 2)) {
|
|
392
|
+
lines.push(`- ${anchor.title} (confidence=${anchor.confidence.toFixed(2)})`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return lines.join('\n');
|
|
396
|
+
}
|