@massu/core 0.1.1 → 0.1.2
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/README.md +2 -2
- package/dist/hooks/cost-tracker.js +23 -35
- package/dist/hooks/post-edit-context.js +2 -2
- package/dist/hooks/post-tool-use.js +43 -58
- package/dist/hooks/pre-compact.js +23 -38
- package/dist/hooks/pre-delete-check.js +18 -31
- package/dist/hooks/quality-event.js +23 -35
- package/dist/hooks/session-end.js +62 -78
- package/dist/hooks/session-start.js +33 -42
- package/dist/hooks/user-prompt.js +23 -38
- package/package.json +8 -14
- package/src/adr-generator.ts +9 -2
- package/src/analytics.ts +9 -3
- package/src/audit-trail.ts +10 -3
- package/src/cloud-sync.ts +14 -18
- package/src/commands/init.ts +1 -5
- package/src/cost-tracker.ts +11 -6
- package/src/dependency-scorer.ts +9 -2
- package/src/docs-tools.ts +13 -10
- package/src/hooks/post-edit-context.ts +3 -3
- package/src/hooks/session-end.ts +3 -3
- package/src/hooks/session-start.ts +2 -2
- package/src/memory-db.ts +1351 -23
- package/src/memory-tools.ts +14 -15
- package/src/observability-tools.ts +13 -2
- package/src/prompt-analyzer.ts +9 -2
- package/src/regression-detector.ts +9 -3
- package/src/security-scorer.ts +9 -2
- package/src/sentinel-db.ts +43 -88
- package/src/sentinel-tools.ts +8 -11
- package/src/server.ts +1 -2
- package/src/team-knowledge.ts +9 -2
- package/src/tools.ts +771 -35
- package/src/validate-features-runner.ts +0 -1
- package/src/validation-engine.ts +9 -2
- package/dist/cli.js +0 -7890
- package/dist/server.js +0 -7008
- package/src/__tests__/adr-generator.test.ts +0 -260
- package/src/__tests__/analytics.test.ts +0 -282
- package/src/__tests__/audit-trail.test.ts +0 -382
- package/src/__tests__/backfill-sessions.test.ts +0 -690
- package/src/__tests__/cli.test.ts +0 -290
- package/src/__tests__/cloud-sync.test.ts +0 -261
- package/src/__tests__/config-sections.test.ts +0 -359
- package/src/__tests__/config.test.ts +0 -732
- package/src/__tests__/cost-tracker.test.ts +0 -348
- package/src/__tests__/db.test.ts +0 -177
- package/src/__tests__/dependency-scorer.test.ts +0 -325
- package/src/__tests__/docs-integration.test.ts +0 -178
- package/src/__tests__/docs-tools.test.ts +0 -199
- package/src/__tests__/domains.test.ts +0 -236
- package/src/__tests__/hooks.test.ts +0 -221
- package/src/__tests__/import-resolver.test.ts +0 -95
- package/src/__tests__/integration/path-traversal.test.ts +0 -134
- package/src/__tests__/integration/pricing-consistency.test.ts +0 -88
- package/src/__tests__/integration/tool-registration.test.ts +0 -146
- package/src/__tests__/memory-db.test.ts +0 -404
- package/src/__tests__/memory-enhancements.test.ts +0 -316
- package/src/__tests__/memory-tools.test.ts +0 -199
- package/src/__tests__/middleware-tree.test.ts +0 -177
- package/src/__tests__/observability-tools.test.ts +0 -595
- package/src/__tests__/observability.test.ts +0 -437
- package/src/__tests__/observation-extractor.test.ts +0 -167
- package/src/__tests__/page-deps.test.ts +0 -60
- package/src/__tests__/prompt-analyzer.test.ts +0 -298
- package/src/__tests__/regression-detector.test.ts +0 -295
- package/src/__tests__/rules.test.ts +0 -87
- package/src/__tests__/schema-mapper.test.ts +0 -29
- package/src/__tests__/security-scorer.test.ts +0 -238
- package/src/__tests__/security-utils.test.ts +0 -175
- package/src/__tests__/sentinel-db.test.ts +0 -491
- package/src/__tests__/sentinel-scanner.test.ts +0 -750
- package/src/__tests__/sentinel-tools.test.ts +0 -324
- package/src/__tests__/sentinel-types.test.ts +0 -750
- package/src/__tests__/server.test.ts +0 -452
- package/src/__tests__/session-archiver.test.ts +0 -524
- package/src/__tests__/session-state-generator.test.ts +0 -900
- package/src/__tests__/team-knowledge.test.ts +0 -327
- package/src/__tests__/tools.test.ts +0 -340
- package/src/__tests__/transcript-parser.test.ts +0 -195
- package/src/__tests__/trpc-index.test.ts +0 -25
- package/src/__tests__/validate-features-runner.test.ts +0 -517
- package/src/__tests__/validation-engine.test.ts +0 -300
- package/src/core-tools.ts +0 -685
- package/src/memory-queries.ts +0 -804
- package/src/memory-schema.ts +0 -546
- package/src/tool-helpers.ts +0 -41
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
-
import { readFileSync, existsSync } from 'fs';
|
|
6
|
-
import { resolve } from 'path';
|
|
7
|
-
import { parse as parseYaml } from 'yaml';
|
|
8
|
-
|
|
9
|
-
// Mock config to return real project root
|
|
10
|
-
const PROJECT_ROOT = resolve(__dirname, '../../../../..');
|
|
11
|
-
|
|
12
|
-
vi.mock('../../config.ts', () => ({
|
|
13
|
-
getConfig: () => {
|
|
14
|
-
// Read actual massu.config.yaml for this test
|
|
15
|
-
const configPath = resolve(PROJECT_ROOT, 'massu.config.yaml');
|
|
16
|
-
if (existsSync(configPath)) {
|
|
17
|
-
const raw = readFileSync(configPath, 'utf-8');
|
|
18
|
-
const parsed = parseYaml(raw);
|
|
19
|
-
return {
|
|
20
|
-
toolPrefix: parsed.toolPrefix || 'massu',
|
|
21
|
-
framework: parsed.framework || { type: 'typescript' },
|
|
22
|
-
paths: parsed.paths || { source: 'src' },
|
|
23
|
-
domains: parsed.domains || [],
|
|
24
|
-
analytics: parsed.analytics || {},
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
return {
|
|
28
|
-
toolPrefix: 'massu',
|
|
29
|
-
framework: { type: 'typescript' },
|
|
30
|
-
paths: { source: 'src' },
|
|
31
|
-
domains: [],
|
|
32
|
-
analytics: {},
|
|
33
|
-
};
|
|
34
|
-
},
|
|
35
|
-
getProjectRoot: () => PROJECT_ROOT,
|
|
36
|
-
getResolvedPaths: () => ({
|
|
37
|
-
codegraphDbPath: resolve(PROJECT_ROOT, 'codegraph.db'),
|
|
38
|
-
dataDbPath: resolve(PROJECT_ROOT, 'data.db'),
|
|
39
|
-
}),
|
|
40
|
-
}));
|
|
41
|
-
|
|
42
|
-
describe('Integration: Pricing Consistency', () => {
|
|
43
|
-
it('massu.config.yaml exists and parses without errors', () => {
|
|
44
|
-
const configPath = resolve(PROJECT_ROOT, 'massu.config.yaml');
|
|
45
|
-
expect(existsSync(configPath)).toBe(true);
|
|
46
|
-
|
|
47
|
-
const raw = readFileSync(configPath, 'utf-8');
|
|
48
|
-
const parsed = parseYaml(raw);
|
|
49
|
-
expect(parsed).toBeTruthy();
|
|
50
|
-
expect(parsed.toolPrefix).toBeTruthy();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('toolPrefix in config matches expected format', () => {
|
|
54
|
-
const configPath = resolve(PROJECT_ROOT, 'massu.config.yaml');
|
|
55
|
-
const raw = readFileSync(configPath, 'utf-8');
|
|
56
|
-
const parsed = parseYaml(raw);
|
|
57
|
-
|
|
58
|
-
// toolPrefix should be a string without spaces or special chars
|
|
59
|
-
expect(typeof parsed.toolPrefix).toBe('string');
|
|
60
|
-
expect(parsed.toolPrefix).toMatch(/^[a-z][a-z0-9_]*$/);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('DEFAULT_MODEL_PRICING in cost-tracker has required models', () => {
|
|
64
|
-
// Read cost-tracker.ts to extract pricing data
|
|
65
|
-
const costTrackerPath = resolve(PROJECT_ROOT, 'packages/core/src/cost-tracker.ts');
|
|
66
|
-
expect(existsSync(costTrackerPath)).toBe(true);
|
|
67
|
-
|
|
68
|
-
const content = readFileSync(costTrackerPath, 'utf-8');
|
|
69
|
-
|
|
70
|
-
// Must have a default model
|
|
71
|
-
expect(content).toContain("'default'");
|
|
72
|
-
|
|
73
|
-
// Must have pricing structure with input_per_million and output_per_million
|
|
74
|
-
expect(content).toContain('input_per_million');
|
|
75
|
-
expect(content).toContain('output_per_million');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('config file has required top-level sections', () => {
|
|
79
|
-
const configPath = resolve(PROJECT_ROOT, 'massu.config.yaml');
|
|
80
|
-
const raw = readFileSync(configPath, 'utf-8');
|
|
81
|
-
const parsed = parseYaml(raw);
|
|
82
|
-
|
|
83
|
-
// Required sections
|
|
84
|
-
expect(parsed.toolPrefix).toBeTruthy();
|
|
85
|
-
expect(parsed.framework).toBeTruthy();
|
|
86
|
-
expect(parsed.paths).toBeTruthy();
|
|
87
|
-
});
|
|
88
|
-
});
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
-
import Database from 'better-sqlite3';
|
|
6
|
-
import { getToolDefinitions, handleToolCall } from '../../tools.ts';
|
|
7
|
-
|
|
8
|
-
// Mock all external dependencies
|
|
9
|
-
vi.mock('../../config.ts', () => ({
|
|
10
|
-
getConfig: () => ({
|
|
11
|
-
toolPrefix: 'massu',
|
|
12
|
-
framework: { type: 'typescript', router: 'trpc', orm: 'prisma' },
|
|
13
|
-
paths: {
|
|
14
|
-
source: 'src',
|
|
15
|
-
routers: 'src/server/api/routers',
|
|
16
|
-
middleware: 'src/middleware.ts',
|
|
17
|
-
},
|
|
18
|
-
domains: [],
|
|
19
|
-
}),
|
|
20
|
-
getProjectRoot: () => '/test/project',
|
|
21
|
-
getResolvedPaths: () => ({
|
|
22
|
-
codegraphDbPath: '/test/codegraph.db',
|
|
23
|
-
dataDbPath: '/test/data.db',
|
|
24
|
-
}),
|
|
25
|
-
}));
|
|
26
|
-
|
|
27
|
-
function createMockDb(): Database.Database {
|
|
28
|
-
const db = new Database(':memory:');
|
|
29
|
-
// Create memory DB tables
|
|
30
|
-
db.exec(`
|
|
31
|
-
CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, created_at TEXT, updated_at TEXT, context TEXT);
|
|
32
|
-
CREATE TABLE IF NOT EXISTS observations (id INTEGER PRIMARY KEY, session_id TEXT, content TEXT, category TEXT, confidence REAL, created_at TEXT);
|
|
33
|
-
CREATE TABLE IF NOT EXISTS analytics_events (id INTEGER PRIMARY KEY, session_id TEXT, event_type TEXT, data TEXT, created_at TEXT);
|
|
34
|
-
CREATE TABLE IF NOT EXISTS cost_records (id INTEGER PRIMARY KEY, session_id TEXT, model TEXT, input_tokens INTEGER, output_tokens INTEGER, cache_read_tokens INTEGER DEFAULT 0, cache_write_tokens INTEGER DEFAULT 0, cost REAL, created_at TEXT);
|
|
35
|
-
CREATE TABLE IF NOT EXISTS prompt_analyses (id INTEGER PRIMARY KEY, session_id TEXT, tool_name TEXT, prompt_text TEXT, analysis TEXT, created_at TEXT);
|
|
36
|
-
CREATE TABLE IF NOT EXISTS audit_trail (id INTEGER PRIMARY KEY, session_id TEXT, action TEXT, details TEXT, created_at TEXT);
|
|
37
|
-
CREATE TABLE IF NOT EXISTS validation_results (id INTEGER PRIMARY KEY, session_id TEXT, rule_id TEXT, result TEXT, details TEXT, created_at TEXT);
|
|
38
|
-
CREATE TABLE IF NOT EXISTS adrs (id INTEGER PRIMARY KEY, session_id TEXT, title TEXT, status TEXT, context TEXT, decision TEXT, consequences TEXT, created_at TEXT);
|
|
39
|
-
CREATE TABLE IF NOT EXISTS security_scores (id INTEGER PRIMARY KEY, session_id TEXT, file_path TEXT, score REAL, findings TEXT, created_at TEXT);
|
|
40
|
-
CREATE TABLE IF NOT EXISTS dependency_scores (id INTEGER PRIMARY KEY, session_id TEXT, package_name TEXT, score REAL, details TEXT, created_at TEXT);
|
|
41
|
-
CREATE TABLE IF NOT EXISTS team_knowledge (id INTEGER PRIMARY KEY, session_id TEXT, topic TEXT, content TEXT, author TEXT, created_at TEXT);
|
|
42
|
-
CREATE TABLE IF NOT EXISTS regression_baselines (id INTEGER PRIMARY KEY, session_id TEXT, metric TEXT, value REAL, created_at TEXT);
|
|
43
|
-
CREATE TABLE IF NOT EXISTS observability_spans (id INTEGER PRIMARY KEY, session_id TEXT, trace_id TEXT, span_id TEXT, parent_span_id TEXT, name TEXT, start_time TEXT, end_time TEXT, duration_ms REAL, status TEXT, attributes TEXT);
|
|
44
|
-
CREATE TABLE IF NOT EXISTS observability_metrics (id INTEGER PRIMARY KEY, session_id TEXT, name TEXT, value REAL, unit TEXT, dimensions TEXT, timestamp TEXT);
|
|
45
|
-
CREATE TABLE IF NOT EXISTS observability_logs (id INTEGER PRIMARY KEY, session_id TEXT, level TEXT, message TEXT, context TEXT, timestamp TEXT);
|
|
46
|
-
`);
|
|
47
|
-
return db;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
vi.mock('../../memory-db.ts', () => ({
|
|
51
|
-
getMemoryDb: () => createMockDb(),
|
|
52
|
-
}));
|
|
53
|
-
|
|
54
|
-
vi.mock('../../db.ts', async (importOriginal) => {
|
|
55
|
-
const actual = await importOriginal() as Record<string, unknown>;
|
|
56
|
-
return {
|
|
57
|
-
...actual,
|
|
58
|
-
isDataStale: () => false,
|
|
59
|
-
updateBuildTimestamp: vi.fn(),
|
|
60
|
-
};
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
vi.mock('../../import-resolver.ts', () => ({
|
|
64
|
-
buildImportIndex: () => 0,
|
|
65
|
-
}));
|
|
66
|
-
|
|
67
|
-
vi.mock('../../trpc-index.ts', () => ({
|
|
68
|
-
buildTrpcIndex: () => ({ totalProcedures: 0, withCallers: 0, withoutCallers: 0 }),
|
|
69
|
-
}));
|
|
70
|
-
|
|
71
|
-
vi.mock('../../page-deps.ts', () => ({
|
|
72
|
-
buildPageDeps: () => 0,
|
|
73
|
-
findAffectedPages: () => [],
|
|
74
|
-
}));
|
|
75
|
-
|
|
76
|
-
vi.mock('../../middleware-tree.ts', () => ({
|
|
77
|
-
buildMiddlewareTree: () => 0,
|
|
78
|
-
isInMiddlewareTree: () => false,
|
|
79
|
-
getMiddlewareTree: () => [],
|
|
80
|
-
}));
|
|
81
|
-
|
|
82
|
-
vi.mock('../../domains.ts', () => ({
|
|
83
|
-
classifyFile: () => ({ domain: 'unknown', layer: 'unknown' }),
|
|
84
|
-
classifyRouter: () => 'unknown',
|
|
85
|
-
findCrossDomainImports: () => [],
|
|
86
|
-
getFilesInDomain: () => [],
|
|
87
|
-
}));
|
|
88
|
-
|
|
89
|
-
vi.mock('../../schema-mapper.ts', () => ({
|
|
90
|
-
parsePrismaSchema: () => [],
|
|
91
|
-
detectMismatches: () => [],
|
|
92
|
-
findColumnUsageInRouters: () => [],
|
|
93
|
-
}));
|
|
94
|
-
|
|
95
|
-
vi.mock('../../sentinel-scanner.ts', () => ({
|
|
96
|
-
runFeatureScan: () => ({ newFeatures: 0, updatedFeatures: 0 }),
|
|
97
|
-
}));
|
|
98
|
-
|
|
99
|
-
describe('Integration: Tool Registration Completeness', () => {
|
|
100
|
-
it('every tool in getToolDefinitions() has a matching handler', () => {
|
|
101
|
-
const definitions = getToolDefinitions();
|
|
102
|
-
|
|
103
|
-
// Every tool should have a name and inputSchema
|
|
104
|
-
for (const tool of definitions) {
|
|
105
|
-
expect(tool.name).toBeTruthy();
|
|
106
|
-
expect(tool.inputSchema).toBeTruthy();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// There should be a meaningful number of tools
|
|
110
|
-
expect(definitions.length).toBeGreaterThan(10);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('no tool returns "Unknown tool" when called with valid name', () => {
|
|
114
|
-
const definitions = getToolDefinitions();
|
|
115
|
-
const unknownResponses: string[] = [];
|
|
116
|
-
|
|
117
|
-
for (const tool of definitions) {
|
|
118
|
-
try {
|
|
119
|
-
const result = handleToolCall(tool.name, {});
|
|
120
|
-
const text = result.content[0]?.text || '';
|
|
121
|
-
if (text.includes('Unknown tool')) {
|
|
122
|
-
unknownResponses.push(tool.name);
|
|
123
|
-
}
|
|
124
|
-
} catch {
|
|
125
|
-
// Some tools may throw on missing args - that's fine,
|
|
126
|
-
// it means the handler IS registered and tried to execute
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
expect(unknownResponses).toEqual([]);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('tool names all start with configured prefix', () => {
|
|
134
|
-
const definitions = getToolDefinitions();
|
|
135
|
-
for (const tool of definitions) {
|
|
136
|
-
expect(tool.name).toMatch(/^massu_/);
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('no duplicate tool names exist', () => {
|
|
141
|
-
const definitions = getToolDefinitions();
|
|
142
|
-
const names = definitions.map(d => d.name);
|
|
143
|
-
const uniqueNames = new Set(names);
|
|
144
|
-
expect(uniqueNames.size).toBe(names.length);
|
|
145
|
-
});
|
|
146
|
-
});
|
|
@@ -1,404 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
5
|
-
import Database from 'better-sqlite3';
|
|
6
|
-
import {
|
|
7
|
-
getMemoryDb,
|
|
8
|
-
createSession,
|
|
9
|
-
endSession,
|
|
10
|
-
addObservation,
|
|
11
|
-
addSummary,
|
|
12
|
-
addUserPrompt,
|
|
13
|
-
searchObservations,
|
|
14
|
-
getRecentObservations,
|
|
15
|
-
getSessionSummaries,
|
|
16
|
-
getSessionTimeline,
|
|
17
|
-
getFailedAttempts,
|
|
18
|
-
getDecisionsAbout,
|
|
19
|
-
pruneOldObservations,
|
|
20
|
-
deduplicateFailedAttempt,
|
|
21
|
-
getSessionsByTask,
|
|
22
|
-
getCrossTaskProgress,
|
|
23
|
-
assignImportance,
|
|
24
|
-
linkSessionToTask,
|
|
25
|
-
autoDetectTaskId,
|
|
26
|
-
} from '../memory-db.ts';
|
|
27
|
-
import { resolve } from 'path';
|
|
28
|
-
import { unlinkSync, existsSync } from 'fs';
|
|
29
|
-
|
|
30
|
-
// P7-001: Memory Database Tests
|
|
31
|
-
|
|
32
|
-
const TEST_DB_PATH = resolve(__dirname, '../test-memory.db');
|
|
33
|
-
|
|
34
|
-
function createTestDb(): Database.Database {
|
|
35
|
-
// Remove existing test DB
|
|
36
|
-
if (existsSync(TEST_DB_PATH)) {
|
|
37
|
-
unlinkSync(TEST_DB_PATH);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Temporarily override getMemoryDb behavior by directly creating a db
|
|
41
|
-
const db = new Database(TEST_DB_PATH);
|
|
42
|
-
db.pragma('journal_mode = WAL');
|
|
43
|
-
db.pragma('foreign_keys = ON');
|
|
44
|
-
|
|
45
|
-
// Init schema manually (same as getMemoryDb)
|
|
46
|
-
db.exec(`
|
|
47
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
48
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
49
|
-
session_id TEXT UNIQUE NOT NULL,
|
|
50
|
-
project TEXT NOT NULL DEFAULT 'my-project',
|
|
51
|
-
git_branch TEXT,
|
|
52
|
-
started_at TEXT NOT NULL,
|
|
53
|
-
started_at_epoch INTEGER NOT NULL,
|
|
54
|
-
ended_at TEXT,
|
|
55
|
-
ended_at_epoch INTEGER,
|
|
56
|
-
status TEXT CHECK(status IN ('active', 'completed', 'abandoned')) NOT NULL DEFAULT 'active',
|
|
57
|
-
plan_file TEXT,
|
|
58
|
-
plan_phase TEXT,
|
|
59
|
-
task_id TEXT
|
|
60
|
-
);
|
|
61
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_session_id ON sessions(session_id);
|
|
62
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at_epoch DESC);
|
|
63
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_task_id ON sessions(task_id);
|
|
64
|
-
|
|
65
|
-
CREATE TABLE IF NOT EXISTS observations (
|
|
66
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
67
|
-
session_id TEXT NOT NULL,
|
|
68
|
-
type TEXT NOT NULL CHECK(type IN (
|
|
69
|
-
'decision', 'bugfix', 'feature', 'refactor', 'discovery',
|
|
70
|
-
'cr_violation', 'vr_check', 'pattern_compliance', 'failed_attempt',
|
|
71
|
-
'file_change', 'incident_near_miss'
|
|
72
|
-
)),
|
|
73
|
-
title TEXT NOT NULL,
|
|
74
|
-
detail TEXT,
|
|
75
|
-
files_involved TEXT DEFAULT '[]',
|
|
76
|
-
plan_item TEXT,
|
|
77
|
-
cr_rule TEXT,
|
|
78
|
-
vr_type TEXT,
|
|
79
|
-
evidence TEXT,
|
|
80
|
-
importance INTEGER NOT NULL DEFAULT 3 CHECK(importance BETWEEN 1 AND 5),
|
|
81
|
-
recurrence_count INTEGER NOT NULL DEFAULT 1,
|
|
82
|
-
original_tokens INTEGER DEFAULT 0,
|
|
83
|
-
created_at TEXT NOT NULL,
|
|
84
|
-
created_at_epoch INTEGER NOT NULL,
|
|
85
|
-
FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
86
|
-
);
|
|
87
|
-
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
88
|
-
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
89
|
-
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
|
|
90
|
-
CREATE INDEX IF NOT EXISTS idx_observations_importance ON observations(importance DESC);
|
|
91
|
-
|
|
92
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
|
|
93
|
-
title, detail, evidence,
|
|
94
|
-
content='observations',
|
|
95
|
-
content_rowid='id'
|
|
96
|
-
);
|
|
97
|
-
CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
|
|
98
|
-
INSERT INTO observations_fts(rowid, title, detail, evidence)
|
|
99
|
-
VALUES (new.id, new.title, new.detail, new.evidence);
|
|
100
|
-
END;
|
|
101
|
-
CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
|
|
102
|
-
INSERT INTO observations_fts(observations_fts, rowid, title, detail, evidence)
|
|
103
|
-
VALUES ('delete', old.id, old.title, old.detail, old.evidence);
|
|
104
|
-
END;
|
|
105
|
-
CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
|
|
106
|
-
INSERT INTO observations_fts(observations_fts, rowid, title, detail, evidence)
|
|
107
|
-
VALUES ('delete', old.id, old.title, old.detail, old.evidence);
|
|
108
|
-
INSERT INTO observations_fts(rowid, title, detail, evidence)
|
|
109
|
-
VALUES (new.id, new.title, new.detail, new.evidence);
|
|
110
|
-
END;
|
|
111
|
-
|
|
112
|
-
CREATE TABLE IF NOT EXISTS session_summaries (
|
|
113
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
114
|
-
session_id TEXT NOT NULL,
|
|
115
|
-
request TEXT,
|
|
116
|
-
investigated TEXT,
|
|
117
|
-
decisions TEXT,
|
|
118
|
-
completed TEXT,
|
|
119
|
-
failed_attempts TEXT,
|
|
120
|
-
next_steps TEXT,
|
|
121
|
-
files_created TEXT DEFAULT '[]',
|
|
122
|
-
files_modified TEXT DEFAULT '[]',
|
|
123
|
-
verification_results TEXT DEFAULT '{}',
|
|
124
|
-
plan_progress TEXT DEFAULT '{}',
|
|
125
|
-
created_at TEXT NOT NULL,
|
|
126
|
-
created_at_epoch INTEGER NOT NULL,
|
|
127
|
-
FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
128
|
-
);
|
|
129
|
-
CREATE INDEX IF NOT EXISTS idx_summaries_session ON session_summaries(session_id);
|
|
130
|
-
|
|
131
|
-
CREATE TABLE IF NOT EXISTS user_prompts (
|
|
132
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
133
|
-
session_id TEXT NOT NULL,
|
|
134
|
-
prompt_text TEXT NOT NULL,
|
|
135
|
-
prompt_number INTEGER NOT NULL DEFAULT 1,
|
|
136
|
-
created_at TEXT NOT NULL,
|
|
137
|
-
created_at_epoch INTEGER NOT NULL,
|
|
138
|
-
FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS user_prompts_fts USING fts5(
|
|
142
|
-
prompt_text,
|
|
143
|
-
content='user_prompts',
|
|
144
|
-
content_rowid='id'
|
|
145
|
-
);
|
|
146
|
-
CREATE TRIGGER IF NOT EXISTS prompts_ai AFTER INSERT ON user_prompts BEGIN
|
|
147
|
-
INSERT INTO user_prompts_fts(rowid, prompt_text) VALUES (new.id, new.prompt_text);
|
|
148
|
-
END;
|
|
149
|
-
|
|
150
|
-
CREATE TABLE IF NOT EXISTS memory_meta (
|
|
151
|
-
key TEXT PRIMARY KEY,
|
|
152
|
-
value TEXT NOT NULL
|
|
153
|
-
);
|
|
154
|
-
`);
|
|
155
|
-
|
|
156
|
-
return db;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
describe('Memory Database', () => {
|
|
160
|
-
let db: Database.Database;
|
|
161
|
-
|
|
162
|
-
beforeEach(() => {
|
|
163
|
-
db = createTestDb();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
afterEach(() => {
|
|
167
|
-
db.close();
|
|
168
|
-
if (existsSync(TEST_DB_PATH)) {
|
|
169
|
-
unlinkSync(TEST_DB_PATH);
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
describe('Schema', () => {
|
|
174
|
-
it('creates all tables', () => {
|
|
175
|
-
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all() as { name: string }[];
|
|
176
|
-
const tableNames = tables.map(t => t.name);
|
|
177
|
-
expect(tableNames).toContain('sessions');
|
|
178
|
-
expect(tableNames).toContain('observations');
|
|
179
|
-
expect(tableNames).toContain('session_summaries');
|
|
180
|
-
expect(tableNames).toContain('user_prompts');
|
|
181
|
-
expect(tableNames).toContain('memory_meta');
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('creates FTS5 tables', () => {
|
|
185
|
-
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%fts%'").all() as { name: string }[];
|
|
186
|
-
const tableNames = tables.map(t => t.name);
|
|
187
|
-
expect(tableNames.some(n => n.includes('observations_fts'))).toBe(true);
|
|
188
|
-
expect(tableNames.some(n => n.includes('user_prompts_fts'))).toBe(true);
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
describe('Session CRUD', () => {
|
|
193
|
-
it('creates a session with INSERT OR IGNORE', () => {
|
|
194
|
-
createSession(db, 'test-session-1', { branch: 'main' });
|
|
195
|
-
const session = db.prepare('SELECT * FROM sessions WHERE session_id = ?').get('test-session-1') as Record<string, unknown>;
|
|
196
|
-
expect(session).toBeTruthy();
|
|
197
|
-
expect(session.git_branch).toBe('main');
|
|
198
|
-
expect(session.status).toBe('active');
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it('is idempotent (INSERT OR IGNORE)', () => {
|
|
202
|
-
createSession(db, 'test-session-1', { branch: 'main' });
|
|
203
|
-
createSession(db, 'test-session-1', { branch: 'feature' }); // Should not throw or update
|
|
204
|
-
const count = db.prepare('SELECT COUNT(*) as c FROM sessions WHERE session_id = ?').get('test-session-1') as { c: number };
|
|
205
|
-
expect(count.c).toBe(1);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('ends a session', () => {
|
|
209
|
-
createSession(db, 'test-session-1');
|
|
210
|
-
endSession(db, 'test-session-1', 'completed');
|
|
211
|
-
const session = db.prepare('SELECT * FROM sessions WHERE session_id = ?').get('test-session-1') as Record<string, unknown>;
|
|
212
|
-
expect(session.status).toBe('completed');
|
|
213
|
-
expect(session.ended_at).toBeTruthy();
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
describe('Observations', () => {
|
|
218
|
-
beforeEach(() => {
|
|
219
|
-
createSession(db, 'test-session-1');
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('adds an observation', () => {
|
|
223
|
-
const id = addObservation(db, 'test-session-1', 'decision', 'Use FTS5 for search', 'Full-text search is faster');
|
|
224
|
-
expect(id).toBeGreaterThan(0);
|
|
225
|
-
const obs = db.prepare('SELECT * FROM observations WHERE id = ?').get(id) as Record<string, unknown>;
|
|
226
|
-
expect(obs.title).toBe('Use FTS5 for search');
|
|
227
|
-
expect(obs.type).toBe('decision');
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('auto-assigns importance', () => {
|
|
231
|
-
const id1 = addObservation(db, 'test-session-1', 'decision', 'Important decision', null);
|
|
232
|
-
const id2 = addObservation(db, 'test-session-1', 'file_change', 'Changed a file', null);
|
|
233
|
-
const obs1 = db.prepare('SELECT importance FROM observations WHERE id = ?').get(id1) as { importance: number };
|
|
234
|
-
const obs2 = db.prepare('SELECT importance FROM observations WHERE id = ?').get(id2) as { importance: number };
|
|
235
|
-
expect(obs1.importance).toBe(5); // decision
|
|
236
|
-
expect(obs2.importance).toBe(1); // file_change
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('searches observations with FTS5', () => {
|
|
240
|
-
addObservation(db, 'test-session-1', 'decision', 'Use FTS5 for search', 'Better performance');
|
|
241
|
-
addObservation(db, 'test-session-1', 'feature', 'Add login page', 'New auth flow');
|
|
242
|
-
const results = searchObservations(db, 'FTS5');
|
|
243
|
-
expect(results.length).toBe(1);
|
|
244
|
-
expect(results[0].title).toContain('FTS5');
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('gets recent observations', () => {
|
|
248
|
-
addObservation(db, 'test-session-1', 'decision', 'Decision 1', null);
|
|
249
|
-
addObservation(db, 'test-session-1', 'feature', 'Feature 1', null);
|
|
250
|
-
const recent = getRecentObservations(db, 10, 'test-session-1');
|
|
251
|
-
expect(recent.length).toBe(2);
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
describe('Session Summaries', () => {
|
|
256
|
-
beforeEach(() => {
|
|
257
|
-
createSession(db, 'test-session-1');
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('adds and retrieves a summary', () => {
|
|
261
|
-
addSummary(db, 'test-session-1', {
|
|
262
|
-
request: 'Fix the login bug',
|
|
263
|
-
completed: 'Fixed auth flow',
|
|
264
|
-
planProgress: { 'P1-001': 'complete' },
|
|
265
|
-
});
|
|
266
|
-
const summaries = getSessionSummaries(db, 5);
|
|
267
|
-
expect(summaries.length).toBe(1);
|
|
268
|
-
expect(summaries[0].request).toBe('Fix the login bug');
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
describe('Session Timeline', () => {
|
|
273
|
-
it('returns full timeline', () => {
|
|
274
|
-
createSession(db, 'test-session-1');
|
|
275
|
-
addObservation(db, 'test-session-1', 'decision', 'Decision 1', null);
|
|
276
|
-
addUserPrompt(db, 'test-session-1', 'Fix the bug', 1);
|
|
277
|
-
addSummary(db, 'test-session-1', { request: 'Fix the bug' });
|
|
278
|
-
|
|
279
|
-
const timeline = getSessionTimeline(db, 'test-session-1');
|
|
280
|
-
expect(timeline.session).toBeTruthy();
|
|
281
|
-
expect(timeline.observations.length).toBe(1);
|
|
282
|
-
expect(timeline.summary).toBeTruthy();
|
|
283
|
-
expect(timeline.prompts.length).toBe(1);
|
|
284
|
-
});
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
describe('Failed Attempts', () => {
|
|
288
|
-
beforeEach(() => {
|
|
289
|
-
createSession(db, 'test-session-1');
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it('retrieves failed attempts', () => {
|
|
293
|
-
addObservation(db, 'test-session-1', 'failed_attempt', 'Regex parser fails on nested braces', 'Stopped at first }');
|
|
294
|
-
const failures = getFailedAttempts(db);
|
|
295
|
-
expect(failures.length).toBe(1);
|
|
296
|
-
expect(failures[0].title).toContain('Regex parser');
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it('searches failed attempts with FTS5', () => {
|
|
300
|
-
addObservation(db, 'test-session-1', 'failed_attempt', 'Regex parser fails on nested braces', 'Stopped at first }');
|
|
301
|
-
addObservation(db, 'test-session-1', 'failed_attempt', 'process.cwd() wrong in tests', 'Returns test runner dir');
|
|
302
|
-
const results = getFailedAttempts(db, 'regex');
|
|
303
|
-
expect(results.length).toBe(1);
|
|
304
|
-
expect(results[0].title).toContain('Regex');
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
describe('Decisions', () => {
|
|
309
|
-
it('searches decisions with FTS5', () => {
|
|
310
|
-
createSession(db, 'test-session-1');
|
|
311
|
-
addObservation(db, 'test-session-1', 'decision', 'Use esbuild instead of tsc', 'Faster bundling');
|
|
312
|
-
addObservation(db, 'test-session-1', 'decision', 'Use FTS5 for search', 'Better performance');
|
|
313
|
-
const results = getDecisionsAbout(db, 'esbuild');
|
|
314
|
-
expect(results.length).toBe(1);
|
|
315
|
-
expect(results[0].title).toContain('esbuild');
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
describe('Deduplication', () => {
|
|
320
|
-
it('increments recurrence_count for duplicate failed attempts', () => {
|
|
321
|
-
createSession(db, 'test-session-1');
|
|
322
|
-
createSession(db, 'test-session-2');
|
|
323
|
-
deduplicateFailedAttempt(db, 'test-session-1', 'process.cwd() wrong in tests', 'Returns runner dir');
|
|
324
|
-
deduplicateFailedAttempt(db, 'test-session-2', 'process.cwd() wrong in tests', 'Same issue again');
|
|
325
|
-
|
|
326
|
-
const failures = getFailedAttempts(db);
|
|
327
|
-
expect(failures.length).toBe(1);
|
|
328
|
-
expect(failures[0].recurrence_count).toBe(2);
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
describe('Task Linking', () => {
|
|
333
|
-
it('links sessions to tasks and gets cross-task progress', () => {
|
|
334
|
-
createSession(db, 'session-1', { planFile: '/path/2026-01-30-memory-system.md' });
|
|
335
|
-
createSession(db, 'session-2', { planFile: '/path/2026-01-30-memory-system.md' });
|
|
336
|
-
|
|
337
|
-
// Verify auto-detected task_id
|
|
338
|
-
const s1 = db.prepare('SELECT task_id FROM sessions WHERE session_id = ?').get('session-1') as { task_id: string };
|
|
339
|
-
expect(s1.task_id).toBe('2026-01-30-memory-system');
|
|
340
|
-
|
|
341
|
-
// Add summaries with plan progress
|
|
342
|
-
addSummary(db, 'session-1', { planProgress: { 'P1-001': 'complete', 'P1-002': 'in_progress' } });
|
|
343
|
-
addSummary(db, 'session-2', { planProgress: { 'P1-002': 'complete', 'P2-001': 'complete' } });
|
|
344
|
-
|
|
345
|
-
const progress = getCrossTaskProgress(db, '2026-01-30-memory-system');
|
|
346
|
-
expect(progress['P1-001']).toBe('complete');
|
|
347
|
-
expect(progress['P1-002']).toBe('complete'); // Later status wins
|
|
348
|
-
expect(progress['P2-001']).toBe('complete');
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it('gets sessions by task', () => {
|
|
352
|
-
createSession(db, 'session-1');
|
|
353
|
-
createSession(db, 'session-2');
|
|
354
|
-
linkSessionToTask(db, 'session-1', 'task-1');
|
|
355
|
-
linkSessionToTask(db, 'session-2', 'task-1');
|
|
356
|
-
|
|
357
|
-
const sessions = getSessionsByTask(db, 'task-1');
|
|
358
|
-
expect(sessions.length).toBe(2);
|
|
359
|
-
});
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
describe('Pruning', () => {
|
|
363
|
-
it('prunes old observations', () => {
|
|
364
|
-
createSession(db, 'test-session-1');
|
|
365
|
-
// Insert observation with old epoch
|
|
366
|
-
const oldEpoch = Math.floor(Date.now() / 1000) - (100 * 86400); // 100 days ago
|
|
367
|
-
db.prepare(`
|
|
368
|
-
INSERT INTO observations (session_id, type, title, importance, created_at, created_at_epoch)
|
|
369
|
-
VALUES (?, 'discovery', 'Old observation', 1, ?, ?)
|
|
370
|
-
`).run('test-session-1', new Date(oldEpoch * 1000).toISOString(), oldEpoch);
|
|
371
|
-
|
|
372
|
-
addObservation(db, 'test-session-1', 'decision', 'Recent decision', null);
|
|
373
|
-
|
|
374
|
-
const pruned = pruneOldObservations(db, 90);
|
|
375
|
-
expect(pruned).toBe(1);
|
|
376
|
-
|
|
377
|
-
const remaining = db.prepare('SELECT COUNT(*) as c FROM observations').get() as { c: number };
|
|
378
|
-
expect(remaining.c).toBe(1);
|
|
379
|
-
});
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
describe('Importance', () => {
|
|
383
|
-
it('assigns correct importance by type', () => {
|
|
384
|
-
expect(assignImportance('decision')).toBe(5);
|
|
385
|
-
expect(assignImportance('failed_attempt')).toBe(5);
|
|
386
|
-
expect(assignImportance('cr_violation')).toBe(4);
|
|
387
|
-
expect(assignImportance('vr_check', 'FAIL')).toBe(4);
|
|
388
|
-
expect(assignImportance('vr_check', 'PASS')).toBe(2);
|
|
389
|
-
expect(assignImportance('feature')).toBe(3);
|
|
390
|
-
expect(assignImportance('bugfix')).toBe(3);
|
|
391
|
-
expect(assignImportance('refactor')).toBe(2);
|
|
392
|
-
expect(assignImportance('file_change')).toBe(1);
|
|
393
|
-
expect(assignImportance('discovery')).toBe(1);
|
|
394
|
-
});
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
describe('autoDetectTaskId', () => {
|
|
398
|
-
it('derives task_id from plan file path', () => {
|
|
399
|
-
expect(autoDetectTaskId('/path/to/2026-01-30-massu-memory.md')).toBe('2026-01-30-massu-memory');
|
|
400
|
-
expect(autoDetectTaskId(null)).toBeNull();
|
|
401
|
-
expect(autoDetectTaskId(undefined)).toBeNull();
|
|
402
|
-
});
|
|
403
|
-
});
|
|
404
|
-
});
|