@jungjaehoon/mama-core 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,286 +0,0 @@
1
- /**
2
- * MAMA (Memory-Augmented MCP Architecture) - Relevance Scorer
3
- *
4
- * Relevance scoring formula for decision ranking and top-N selection
5
- * Tasks: 1.1-1.4, 2.1-2.7 (Relevance scoring and top-N selection)
6
- * AC #1, #4, #5: Decision relevance, failure priority boost, top-N selection
7
- *
8
- * @module relevance-scorer
9
- * @version 1.0
10
- * @date 2025-11-14
11
- */
12
-
13
- const { cosineSimilarity } = require('./embeddings');
14
-
15
- /**
16
- * Calculate relevance score for a single decision
17
- *
18
- * Task 1.2: Implement calculateRelevance(decision, queryContext) function
19
- * AC #1, #4: Relevance scoring with failure priority boost
20
- *
21
- * Formula:
22
- * Relevance = (Recency × 0.2) + (Importance × 0.5) + (Semantic × 0.3)
23
- *
24
- * Where:
25
- * - Recency: exp(-days_since / 30) [30-day half-life]
26
- * - Importance: OUTCOME_WEIGHTS[outcome]
27
- * - FAILED: 1.0 (highest - failures are most valuable)
28
- * - PARTIAL: 0.7
29
- * - SUCCESS: 0.5
30
- * - null: 0.3 (ongoing, lowest)
31
- * - Semantic: cosineSimilarity(decision.embedding, query.embedding)
32
- *
33
- * @param {Object} decision - Decision object
34
- * @param {number} decision.created_at - Created timestamp
35
- * @param {string} decision.outcome - Outcome type
36
- * @param {Float32Array} decision.embedding - Decision embedding (384-dim)
37
- * @param {Object} queryContext - Query context
38
- * @param {Float32Array} queryContext.embedding - Query embedding (384-dim)
39
- * @returns {number} Relevance score (0.0-1.0)
40
- */
41
- function calculateRelevance(decision, queryContext) {
42
- // ═══════════════════════════════════════════════════════════
43
- // Recency Score (20%)
44
- // ═══════════════════════════════════════════════════════════
45
- // Exponential decay with 30-day half-life
46
- const daysSince = (Date.now() - decision.created_at) / (1000 * 60 * 60 * 24);
47
- const recencyScore = Math.exp(-daysSince / 30);
48
-
49
- // Decay curve:
50
- // 0 days = 1.0
51
- // 30 days = 0.5
52
- // 60 days = 0.25
53
- // 90 days = 0.125
54
-
55
- // ═══════════════════════════════════════════════════════════
56
- // Importance Score (50%) - AC #4: Failure Priority Boost
57
- // ═══════════════════════════════════════════════════════════
58
- const OUTCOME_WEIGHTS = {
59
- FAILED: 1.0, // Highest - failures are most valuable (AC #4)
60
- PARTIAL: 0.7,
61
- SUCCESS: 0.5,
62
- pending: 0.3, // Ongoing/pending, lowest
63
- };
64
-
65
- // Use explicit null check to avoid confusion with object key access
66
- const outcomeKey = decision.outcome ?? 'pending';
67
- const importanceScore = OUTCOME_WEIGHTS[outcomeKey] ?? OUTCOME_WEIGHTS.pending;
68
-
69
- // ═══════════════════════════════════════════════════════════
70
- // Semantic Score (30%)
71
- // ═══════════════════════════════════════════════════════════
72
- let semanticScore = 0;
73
-
74
- if (decision.embedding && queryContext.embedding) {
75
- // Task 1.3: Use cosine similarity function
76
- semanticScore = cosineSimilarity(decision.embedding, queryContext.embedding);
77
- } else {
78
- // Fallback: no semantic match if embeddings missing
79
- semanticScore = 0;
80
- }
81
-
82
- // ═══════════════════════════════════════════════════════════
83
- // Weighted Sum (Total: 100%)
84
- // ═══════════════════════════════════════════════════════════
85
- const relevance = recencyScore * 0.2 + importanceScore * 0.5 + semanticScore * 0.3;
86
-
87
- return relevance;
88
- }
89
-
90
- /**
91
- * Select top N most relevant decisions
92
- *
93
- * Task 2.1: Add selectTopDecisions(decisions, queryContext, n=3) function
94
- * AC #1, #5: Top-N selection with threshold filtering
95
- *
96
- * @param {Array<Object>} decisions - Array of decision objects
97
- * @param {Object} queryContext - Query context with embedding
98
- * @param {number} n - Number of top decisions to return (default: 3)
99
- * @returns {Array<Object>} Top N decisions with relevance scores
100
- */
101
- function selectTopDecisions(decisions, queryContext, n = 3) {
102
- if (!Array.isArray(decisions) || decisions.length === 0) {
103
- return [];
104
- }
105
-
106
- // Task 2.3: Score all results by relevance
107
- const decisionsWithScores = decisions.map((decision) => ({
108
- ...decision,
109
- relevanceScore: calculateRelevance(decision, queryContext),
110
- }));
111
-
112
- // Task 2.4: Sort descending (highest relevance first)
113
- decisionsWithScores.sort((a, b) => b.relevanceScore - a.relevanceScore);
114
-
115
- // Task 2.6: Filter out < 0.5 relevance (AC #1)
116
- const filtered = decisionsWithScores.filter((d) => d.relevanceScore >= 0.5);
117
-
118
- // Task 2.5: Return top 3 (or top N)
119
- const topN = filtered.slice(0, n);
120
-
121
- return topN;
122
- }
123
-
124
- /**
125
- * Cosine similarity helper (re-exported from embeddings.js)
126
- *
127
- * Task 1.3: Implement cosine similarity function
128
- * AC #1: Semantic similarity calculation
129
- *
130
- * Note: This is re-exported from embeddings.js for convenience
131
- *
132
- * @param {Float32Array} vec1 - First embedding vector
133
- * @param {Float32Array} vec2 - Second embedding vector
134
- * @returns {number} Cosine similarity (0.0-1.0)
135
- */
136
- // Already available from embeddings.js - no need to reimplement
137
-
138
- /**
139
- * Format decisions with top-N selection and summary
140
- *
141
- * Task 8.2-8.3: Format top 3 in full detail, rest as summary
142
- * AC #5: Top-N selection with summary
143
- *
144
- * @param {Array<Object>} decisions - All decisions (sorted by relevance)
145
- * @param {number} topN - Number of decisions to show in full detail (default: 3)
146
- * @returns {Object} Formatted context {full: Array, summary: Object}
147
- */
148
- function formatTopNContext(decisions, topN = 3) {
149
- if (!Array.isArray(decisions) || decisions.length === 0) {
150
- return { full: [], summary: null };
151
- }
152
-
153
- // Split into top N and rest
154
- const fullDetailDecisions = decisions.slice(0, topN);
155
- const summaryDecisions = decisions.slice(topN);
156
-
157
- // Full detail for top N
158
- const full = fullDetailDecisions.map((d) => ({
159
- decision_id: d.id,
160
- topic: d.topic,
161
- decision: d.decision,
162
- reasoning: d.reasoning,
163
- outcome: d.outcome,
164
- failure_reason: d.failure_reason,
165
- user_involvement: d.user_involvement,
166
- confidence: d.confidence,
167
- relevanceScore: d.relevanceScore,
168
- created_at: d.created_at,
169
- }));
170
-
171
- // Summary for rest (count, duration, key failures only)
172
- let summary = null;
173
-
174
- if (summaryDecisions.length > 0) {
175
- // Calculate duration (oldest to newest)
176
- const oldestTimestamp = Math.min(...summaryDecisions.map((d) => d.created_at));
177
- const newestTimestamp = Math.max(...summaryDecisions.map((d) => d.created_at));
178
- const durationDays = Math.floor((newestTimestamp - oldestTimestamp) / (1000 * 60 * 60 * 24));
179
-
180
- // Extract key failures
181
- const failures = summaryDecisions
182
- .filter((d) => d.outcome === 'FAILED')
183
- .map((d) => ({ decision: d.decision, reason: d.failure_reason }));
184
-
185
- summary = {
186
- count: summaryDecisions.length,
187
- duration_days: durationDays,
188
- failures: failures.slice(0, 3), // Show max 3 failures
189
- };
190
- }
191
-
192
- return { full, summary };
193
- }
194
-
195
- /**
196
- * Test relevance scoring with sample decisions
197
- *
198
- * Task 1.4: Test relevance scoring with sample decisions
199
- * AC #1, #4: Verify scoring formula and failure priority
200
- *
201
- * @returns {Object} Test results
202
- */
203
- function testRelevanceScoring() {
204
- const now = Date.now();
205
-
206
- // Mock embeddings (dummy for testing)
207
- const queryEmbedding = new Float32Array(384).fill(0.5);
208
- const decisionEmbedding1 = new Float32Array(384).fill(0.5); // Identical (similarity = 1.0)
209
- // eslint-disable-next-line no-unused-vars
210
- const decisionEmbedding2 = new Float32Array(384).fill(0.3); // Different (similarity < 1.0)
211
-
212
- const scenarios = [
213
- // Scenario 1: Recent FAILED decision (should have highest relevance)
214
- {
215
- name: 'Recent FAILED decision',
216
- decision: {
217
- created_at: now - 5 * 24 * 60 * 60 * 1000, // 5 days ago
218
- outcome: 'FAILED',
219
- embedding: decisionEmbedding1,
220
- },
221
- queryContext: { embedding: queryEmbedding },
222
- expected: {
223
- recency: 0.85, // exp(-5/30) ≈ 0.85
224
- importance: 1.0, // FAILED = 1.0 (AC #4)
225
- semantic: 1.0, // Identical embeddings
226
- relevance: 0.87, // (0.85×0.2) + (1.0×0.5) + (1.0×0.3)
227
- },
228
- },
229
-
230
- // Scenario 2: Recent SUCCESS decision (lower importance)
231
- {
232
- name: 'Recent SUCCESS decision',
233
- decision: {
234
- created_at: now - 5 * 24 * 60 * 60 * 1000, // 5 days ago
235
- outcome: 'SUCCESS',
236
- embedding: decisionEmbedding1,
237
- },
238
- queryContext: { embedding: queryEmbedding },
239
- expected: {
240
- recency: 0.85,
241
- importance: 0.5, // SUCCESS = 0.5
242
- semantic: 1.0,
243
- relevance: 0.62, // (0.85×0.2) + (0.5×0.5) + (1.0×0.3)
244
- },
245
- },
246
-
247
- // Scenario 3: Old FAILED decision (recency decay)
248
- {
249
- name: 'Old FAILED decision',
250
- decision: {
251
- created_at: now - 60 * 24 * 60 * 60 * 1000, // 60 days ago
252
- outcome: 'FAILED',
253
- embedding: decisionEmbedding1,
254
- },
255
- queryContext: { embedding: queryEmbedding },
256
- expected: {
257
- recency: 0.25, // exp(-60/30) ≈ 0.25
258
- importance: 1.0,
259
- semantic: 1.0,
260
- relevance: 0.85, // (0.25×0.2) + (1.0×0.5) + (1.0×0.3)
261
- },
262
- },
263
- ];
264
-
265
- const results = scenarios.map((scenario) => {
266
- const calculated = calculateRelevance(scenario.decision, scenario.queryContext);
267
- const pass = Math.abs(calculated - scenario.expected.relevance) < 0.05;
268
-
269
- return {
270
- name: scenario.name,
271
- expected: scenario.expected.relevance.toFixed(2),
272
- calculated: calculated.toFixed(2),
273
- pass,
274
- };
275
- });
276
-
277
- return results;
278
- }
279
-
280
- // Export API
281
- module.exports = {
282
- calculateRelevance,
283
- selectTopDecisions,
284
- formatTopNContext,
285
- testRelevanceScoring,
286
- };
@@ -1,269 +0,0 @@
1
- /**
2
- * MAMA Tier Validator
3
- *
4
- * Centralized tier validation module for MAMA.
5
- * Validates system requirements and determines tier status (1 or 2).
6
- *
7
- * Tier 1: Full features (Node.js 18+, SQLite, Embeddings, Database)
8
- * Tier 2: Degraded mode (missing one or more requirements)
9
- *
10
- * @module tier-validator
11
- * @version 1.0
12
- * @date 2026-01-30
13
- */
14
-
15
- const fs = require('fs');
16
- const path = require('path');
17
- const os = require('os');
18
- const { info: _info, warn: _warn, error: _logError } = require('./debug-logger');
19
-
20
- /**
21
- * Validates Node.js version requirement
22
- *
23
- * @returns {Object} Check result { status: 'pass'|'fail', details: string }
24
- */
25
- function checkNodeVersion() {
26
- try {
27
- const nodeVersion = process.versions.node;
28
- const majorVersion = parseInt(nodeVersion.split('.')[0], 10);
29
-
30
- if (majorVersion >= 18) {
31
- return {
32
- status: 'pass',
33
- details: `v${nodeVersion}`,
34
- };
35
- }
36
-
37
- return {
38
- status: 'fail',
39
- details: `v${nodeVersion} (requires 18+)`,
40
- };
41
- } catch (error) {
42
- return {
43
- status: 'fail',
44
- details: `Error checking version: ${error.message}`,
45
- };
46
- }
47
- }
48
-
49
- /**
50
- * Validates SQLite (better-sqlite3) availability
51
- *
52
- * Reuses logic from packages/mcp-server/scripts/postinstall.js
53
- *
54
- * @returns {Object} Check result { status: 'pass'|'fail', details: string }
55
- */
56
- function checkSQLite() {
57
- try {
58
- // Try to require better-sqlite3
59
- const Database = require('better-sqlite3');
60
-
61
- // Test instantiation with in-memory database
62
- const testDb = new Database(':memory:');
63
- testDb.close();
64
-
65
- return {
66
- status: 'pass',
67
- details: 'better-sqlite3 native module ready',
68
- };
69
- } catch (error) {
70
- return {
71
- status: 'fail',
72
- details: `better-sqlite3 not available: ${error.message}`,
73
- };
74
- }
75
- }
76
-
77
- /**
78
- * Validates embedding model availability
79
- *
80
- * Checks if embedding model has been downloaded to cache directory.
81
- * Reuses logic from packages/mama-core/src/embeddings.js
82
- *
83
- * @returns {Object} Check result { status: 'pass'|'fail', details: string }
84
- */
85
- function checkEmbeddings() {
86
- try {
87
- const { getModelName } = require('./embeddings');
88
- const modelName = getModelName();
89
-
90
- // Check if model is cached
91
- const cacheDir =
92
- process.env.HF_HOME ||
93
- process.env.TRANSFORMERS_CACHE ||
94
- path.join(os.homedir(), '.cache', 'huggingface', 'transformers');
95
-
96
- // Model cache structure: cache_dir/models--org--model/snapshots/hash/
97
- const modelPath = path.join(cacheDir, `models--${modelName.replace('/', '--')}`);
98
-
99
- if (fs.existsSync(modelPath)) {
100
- return {
101
- status: 'pass',
102
- details: `${modelName} (cached)`,
103
- };
104
- }
105
-
106
- return {
107
- status: 'fail',
108
- details: `${modelName} not cached (will download on first use)`,
109
- };
110
- } catch (error) {
111
- return {
112
- status: 'fail',
113
- details: `Error checking embeddings: ${error.message}`,
114
- };
115
- }
116
- }
117
-
118
- /**
119
- * Validates database file accessibility
120
- *
121
- * Tests write access to database location (~/.claude/mama-memory.db)
122
- *
123
- * @returns {Object} Check result { status: 'pass'|'fail', details: string }
124
- */
125
- function checkDatabase() {
126
- try {
127
- const dbPath = process.env.MAMA_DB_PATH || path.join(os.homedir(), '.claude', 'mama-memory.db');
128
-
129
- const dbDir = path.dirname(dbPath);
130
-
131
- // Check if directory exists or can be created
132
- if (!fs.existsSync(dbDir)) {
133
- try {
134
- fs.mkdirSync(dbDir, { recursive: true });
135
- } catch (mkdirErr) {
136
- return {
137
- status: 'fail',
138
- details: `Cannot create database directory: ${mkdirErr.message}`,
139
- };
140
- }
141
- }
142
-
143
- // Check write access
144
- try {
145
- fs.accessSync(dbDir, fs.constants.W_OK);
146
- } catch (accessErr) {
147
- return {
148
- status: 'fail',
149
- details: `No write access to ${dbDir}`,
150
- };
151
- }
152
-
153
- return {
154
- status: 'pass',
155
- details: dbPath,
156
- };
157
- } catch (error) {
158
- return {
159
- status: 'fail',
160
- details: `Error checking database: ${error.message}`,
161
- };
162
- }
163
- }
164
-
165
- /**
166
- * Validates MAMA tier status
167
- *
168
- * Performs all system checks and determines tier:
169
- * - Tier 1: All checks pass (full features)
170
- * - Tier 2: One or more checks fail (degraded mode)
171
- *
172
- * @returns {Promise<Object>} Validation result
173
- * @returns {number} result.tier - 1 (full) or 2 (degraded)
174
- * @returns {Array} result.checks - Array of check results
175
- * @example
176
- * const { tier, checks } = await validateTier();
177
- * console.log(`MAMA Tier: ${tier}`);
178
- * checks.forEach(check => {
179
- * console.log(`${check.name}: ${check.status} (${check.details})`);
180
- * });
181
- */
182
- async function validateTier() {
183
- const checks = [
184
- {
185
- name: 'Node.js',
186
- ...checkNodeVersion(),
187
- },
188
- {
189
- name: 'SQLite',
190
- ...checkSQLite(),
191
- },
192
- {
193
- name: 'Embeddings',
194
- ...checkEmbeddings(),
195
- },
196
- {
197
- name: 'Database',
198
- ...checkDatabase(),
199
- },
200
- ];
201
-
202
- // Determine tier: all pass = tier 1, any fail = tier 2
203
- const tier = checks.every((c) => c.status === 'pass') ? 1 : 2;
204
-
205
- return {
206
- tier,
207
- checks,
208
- };
209
- }
210
-
211
- /**
212
- * Get user-friendly tier description
213
- *
214
- * @param {number} tier - Tier number (1 or 2)
215
- * @returns {string} Human-readable tier description
216
- * @example
217
- * const desc = getTierDescription(1);
218
- * console.log(desc); // "Full Features"
219
- */
220
- function getTierDescription(tier) {
221
- const descriptions = {
222
- 1: 'Full Features - All systems operational',
223
- 2: 'Degraded Mode - Some features unavailable',
224
- };
225
-
226
- return descriptions[tier] || 'Unknown Tier';
227
- }
228
-
229
- /**
230
- * Get tier status banner
231
- *
232
- * Returns formatted banner showing tier and failed checks
233
- *
234
- * @param {Object} validation - Result from validateTier()
235
- * @returns {string} Formatted banner text
236
- */
237
- function getTierBanner(validation) {
238
- const { tier, checks } = validation;
239
- const failedChecks = checks.filter((c) => c.status === 'fail');
240
-
241
- let banner = `\n┌─────────────────────────────────────────┐\n`;
242
- banner += `│ MAMA Tier ${tier}: ${getTierDescription(tier).split(' - ')[0]}\n`;
243
-
244
- if (failedChecks.length > 0) {
245
- banner += `│\n`;
246
- banner += `│ ⚠️ Issues detected:\n`;
247
- failedChecks.forEach((check) => {
248
- banner += `│ • ${check.name}: ${check.details}\n`;
249
- });
250
- } else {
251
- banner += `│ ✅ All systems operational\n`;
252
- }
253
-
254
- banner += `└─────────────────────────────────────────┘\n`;
255
-
256
- return banner;
257
- }
258
-
259
- // Export API
260
- module.exports = {
261
- validateTier,
262
- getTierDescription,
263
- getTierBanner,
264
- // Internal checks (for testing)
265
- checkNodeVersion,
266
- checkSQLite,
267
- checkEmbeddings,
268
- checkDatabase,
269
- };
@@ -1,98 +0,0 @@
1
- /**
2
- * Time Formatter - Human-Readable Time Formatting
3
- *
4
- * Converts Unix timestamps to human-readable relative time format
5
- * Examples: "2d ago", "3h ago", "just now"
6
- *
7
- * Used by list_decisions and recall_decision tools
8
- *
9
- * @module time-formatter
10
- * @date 2025-11-20
11
- */
12
-
13
- const { warn } = require('./debug-logger');
14
-
15
- /**
16
- * Format Unix timestamp (milliseconds) to human-readable relative time
17
- *
18
- * AC #2: Format created_at as human-readable ("2d ago", "3h ago", etc.)
19
- *
20
- * @param {number|string} timestamp - Unix timestamp in milliseconds OR ISO 8601 string
21
- * @returns {string} Human-readable time string
22
- *
23
- * @example
24
- * formatTimeAgo(Date.now() - 3600000) // "1h ago"
25
- * formatTimeAgo(Date.now() - 172800000) // "2d ago"
26
- * formatTimeAgo("2025-11-20T10:30:00Z") // "2d ago" (if today is 2025-11-22)
27
- */
28
- function formatTimeAgo(timestamp) {
29
- try {
30
- // Handle null/undefined (but allow 0 as valid Unix epoch)
31
- if (timestamp === null || timestamp === undefined) {
32
- warn('[time-formatter] Timestamp is null or undefined, returning "unknown"');
33
- return 'unknown';
34
- }
35
-
36
- // Parse ISO 8601 string to timestamp (if string provided)
37
- let timestampMs;
38
- if (typeof timestamp === 'string') {
39
- timestampMs = new Date(timestamp).getTime();
40
- if (isNaN(timestampMs)) {
41
- warn(`[time-formatter] Invalid ISO 8601 string: ${timestamp}`);
42
- return 'unknown';
43
- }
44
- } else {
45
- timestampMs = timestamp;
46
- }
47
-
48
- const now = Date.now();
49
- const diff = now - timestampMs;
50
-
51
- // Handle future timestamps (shouldn't happen, but be defensive)
52
- if (diff < 0) {
53
- warn(`[time-formatter] Future timestamp detected: ${timestamp}`);
54
- return 'just now';
55
- }
56
-
57
- // Calculate time units
58
- const seconds = Math.floor(diff / 1000);
59
- const minutes = Math.floor(seconds / 60);
60
- const hours = Math.floor(minutes / 60);
61
- const days = Math.floor(hours / 24);
62
- const weeks = Math.floor(days / 7);
63
- const months = Math.floor(days / 30);
64
- const years = Math.floor(days / 365);
65
-
66
- // Return human-readable format
67
- if (seconds < 60) {
68
- return 'just now';
69
- }
70
- if (minutes < 60) {
71
- return `${minutes}m ago`;
72
- }
73
- if (hours < 24) {
74
- return `${hours}h ago`;
75
- }
76
- if (days < 7) {
77
- return `${days}d ago`;
78
- }
79
- if (weeks < 4) {
80
- return `${weeks}w ago`;
81
- }
82
- // Guard against "0mo ago" when days are 28-29 (months = Math.floor(days/30) = 0)
83
- if (months < 1) {
84
- return `${weeks}w ago`;
85
- }
86
- if (months < 12) {
87
- return `${months}mo ago`;
88
- }
89
- return `${years}y ago`;
90
- } catch (error) {
91
- warn(`[time-formatter] Error formatting timestamp ${timestamp}: ${error.message}`);
92
- return 'unknown';
93
- }
94
- }
95
-
96
- module.exports = {
97
- formatTimeAgo,
98
- };