@lumenflow/memory 2.2.2 → 2.3.1

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.
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Memory Profile Core (WU-1237)
3
+ *
4
+ * Generates project knowledge profiles for agent context injection.
5
+ *
6
+ * Features:
7
+ * - Filters to lifecycle=project nodes only
8
+ * - Configurable limit (default N=20)
9
+ * - Tag-based filtering
10
+ * - Output format compatible with mem:context
11
+ * - Deterministic ordering by recency
12
+ *
13
+ * @see {@link packages/@lumenflow/cli/src/mem-profile.ts} - CLI wrapper
14
+ * @see {@link packages/@lumenflow/memory/__tests__/mem-profile-core.test.ts} - Tests
15
+ */
16
+ import path from 'node:path';
17
+ import { loadMemory } from './memory-store.js';
18
+ import { LUMENFLOW_MEMORY_PATHS } from './paths.js';
19
+ /**
20
+ * Default maximum number of nodes to include in profile
21
+ */
22
+ export const DEFAULT_PROFILE_LIMIT = 20;
23
+ /**
24
+ * Section header for profile output
25
+ */
26
+ const PROFILE_SECTION_HEADER = '## Project Profile';
27
+ /**
28
+ * Comparator for sorting nodes by recency (most recent first).
29
+ * Uses created_at as primary sort, id as secondary for stability.
30
+ *
31
+ * @param a - First node
32
+ * @param b - Second node
33
+ * @returns Comparison result
34
+ */
35
+ function compareByRecency(a, b) {
36
+ const aTime = new Date(a.created_at).getTime();
37
+ const bTime = new Date(b.created_at).getTime();
38
+ // Most recent first
39
+ if (aTime !== bTime) {
40
+ return bTime - aTime;
41
+ }
42
+ // Stable sort by ID for identical timestamps
43
+ return a.id.localeCompare(b.id);
44
+ }
45
+ /**
46
+ * Filters nodes by lifecycle=project
47
+ *
48
+ * @param nodes - All nodes
49
+ * @returns Only project-level nodes
50
+ */
51
+ function filterProjectNodes(nodes) {
52
+ return nodes.filter((node) => node.lifecycle === 'project');
53
+ }
54
+ /**
55
+ * Filters nodes by tag
56
+ *
57
+ * @param nodes - Nodes to filter
58
+ * @param tag - Tag to filter by
59
+ * @returns Nodes with the specified tag
60
+ */
61
+ function filterByTag(nodes, tag) {
62
+ return nodes.filter((node) => node.tags?.includes(tag));
63
+ }
64
+ /**
65
+ * Formats a single node for profile output
66
+ *
67
+ * @param node - Node to format
68
+ * @returns Formatted line
69
+ */
70
+ function formatNode(node) {
71
+ const timestamp = new Date(node.created_at).toISOString().split('T')[0];
72
+ return `- [${node.id}] (${timestamp}): ${node.content}`;
73
+ }
74
+ /**
75
+ * Formats all nodes into a profile block
76
+ *
77
+ * @param nodes - Nodes to format
78
+ * @returns Formatted profile block
79
+ */
80
+ function formatProfileBlock(nodes) {
81
+ if (nodes.length === 0) {
82
+ return '';
83
+ }
84
+ const lines = [PROFILE_SECTION_HEADER, ''];
85
+ for (const node of nodes) {
86
+ lines.push(formatNode(node));
87
+ }
88
+ lines.push('');
89
+ return lines.join('\n');
90
+ }
91
+ /**
92
+ * Calculates tag statistics for nodes
93
+ *
94
+ * @param nodes - Nodes to analyze
95
+ * @returns Tag counts
96
+ */
97
+ function calculateTagStats(nodes) {
98
+ const tagCounts = {};
99
+ for (const node of nodes) {
100
+ if (node.tags) {
101
+ for (const tag of node.tags) {
102
+ tagCounts[tag] = (tagCounts[tag] || 0) + 1;
103
+ }
104
+ }
105
+ }
106
+ return tagCounts;
107
+ }
108
+ /**
109
+ * Generates a project knowledge profile for context injection.
110
+ *
111
+ * Filters to project-level nodes, applies tag filter if specified,
112
+ * sorts by recency, and limits to the configured maximum.
113
+ *
114
+ * Output is formatted for integration with mem:context.
115
+ *
116
+ * @param baseDir - Base directory containing .lumenflow/memory
117
+ * @param options - Profile generation options
118
+ * @returns Result with nodes and formatted profile block
119
+ *
120
+ * @example
121
+ * // Get top 20 project memories
122
+ * const result = await generateProfile('/path/to/project');
123
+ * console.log(result.profileBlock);
124
+ *
125
+ * @example
126
+ * // Get top 10 decisions
127
+ * const result = await generateProfile('/path/to/project', {
128
+ * limit: 10,
129
+ * tag: 'decision',
130
+ * });
131
+ */
132
+ export async function generateProfile(baseDir, options = {}) {
133
+ const { limit = DEFAULT_PROFILE_LIMIT, tag } = options;
134
+ const memoryDir = path.join(baseDir, LUMENFLOW_MEMORY_PATHS.MEMORY_DIR);
135
+ // Load all memory nodes
136
+ let allNodes = [];
137
+ try {
138
+ const memory = await loadMemory(memoryDir);
139
+ allNodes = memory.nodes;
140
+ }
141
+ catch (error) {
142
+ // If memory directory doesn't exist or is empty, return empty result
143
+ const err = error;
144
+ if (err.code === 'ENOENT') {
145
+ return {
146
+ success: true,
147
+ nodes: [],
148
+ profileBlock: '',
149
+ stats: {
150
+ totalProjectNodes: 0,
151
+ includedNodes: 0,
152
+ byTag: {},
153
+ },
154
+ };
155
+ }
156
+ throw error;
157
+ }
158
+ // Filter to project lifecycle only
159
+ let projectNodes = filterProjectNodes(allNodes);
160
+ // Store total before any additional filtering
161
+ const totalProjectNodes = projectNodes.length;
162
+ // Apply tag filter if specified
163
+ if (tag) {
164
+ projectNodes = filterByTag(projectNodes, tag);
165
+ }
166
+ // Sort by recency (most recent first)
167
+ const sortedNodes = [...projectNodes].sort(compareByRecency);
168
+ // Apply limit
169
+ const limitedNodes = sortedNodes.slice(0, limit);
170
+ // Calculate statistics
171
+ const stats = {
172
+ totalProjectNodes,
173
+ includedNodes: limitedNodes.length,
174
+ byTag: calculateTagStats(limitedNodes),
175
+ };
176
+ // Format the profile block
177
+ const profileBlock = formatProfileBlock(limitedNodes);
178
+ return {
179
+ success: true,
180
+ nodes: limitedNodes,
181
+ profileBlock,
182
+ stats,
183
+ };
184
+ }
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Memory Promote Core (WU-1237)
3
+ *
4
+ * Promotes session/WU learnings into project-level knowledge nodes.
5
+ *
6
+ * Features:
7
+ * - Promote individual nodes to project lifecycle
8
+ * - Promote all summaries from a WU
9
+ * - Enforced taxonomy tags (decision, convention, pattern, etc.)
10
+ * - Creates discovered_from relationships for provenance
11
+ * - Dry-run mode for preview without writes
12
+ *
13
+ * @see {@link packages/@lumenflow/cli/src/mem-promote.ts} - CLI wrapper
14
+ * @see {@link packages/@lumenflow/memory/__tests__/mem-promote-core.test.ts} - Tests
15
+ */
16
+ import fs from 'node:fs/promises';
17
+ import path from 'node:path';
18
+ import { loadMemory, appendNode } from './memory-store.js';
19
+ import { MEMORY_PATTERNS } from './memory-schema.js';
20
+ import { generateMemId } from './mem-id.js';
21
+ import { LUMENFLOW_MEMORY_PATHS } from './paths.js';
22
+ /**
23
+ * Relationships file name
24
+ */
25
+ const RELATIONSHIPS_FILE_NAME = 'relationships.jsonl';
26
+ /**
27
+ * Allowed tags for promoted nodes.
28
+ * These form the project knowledge taxonomy.
29
+ */
30
+ export const ALLOWED_PROMOTION_TAGS = [
31
+ 'decision',
32
+ 'convention',
33
+ 'pattern',
34
+ 'pitfall',
35
+ 'interface',
36
+ 'invariant',
37
+ 'faq',
38
+ ];
39
+ /**
40
+ * Error messages for validation
41
+ */
42
+ const ERROR_MESSAGES = {
43
+ NODE_NOT_FOUND: 'Node not found',
44
+ ALREADY_PROJECT: 'Node is already at project lifecycle',
45
+ INVALID_TAG: `Tag must be one of: ${ALLOWED_PROMOTION_TAGS.join(', ')}`,
46
+ INVALID_WU_ID: 'Invalid WU ID format. Expected pattern: WU-XXX (e.g., WU-1234)',
47
+ };
48
+ /**
49
+ * Validates the promotion tag against allowed taxonomy
50
+ *
51
+ * @param tag - Tag to validate
52
+ * @throws If tag is not in ALLOWED_PROMOTION_TAGS
53
+ */
54
+ function validateTag(tag) {
55
+ if (!ALLOWED_PROMOTION_TAGS.includes(tag)) {
56
+ throw new Error(ERROR_MESSAGES.INVALID_TAG);
57
+ }
58
+ }
59
+ /**
60
+ * Validates WU ID format
61
+ *
62
+ * @param wuId - WU ID to validate
63
+ * @throws If WU ID is invalid
64
+ */
65
+ function validateWuId(wuId) {
66
+ if (!MEMORY_PATTERNS.WU_ID.test(wuId)) {
67
+ throw new Error(ERROR_MESSAGES.INVALID_WU_ID);
68
+ }
69
+ }
70
+ /**
71
+ * Appends a relationship to the relationships.jsonl file
72
+ *
73
+ * @param memoryDir - Memory directory path
74
+ * @param relationship - Relationship to append
75
+ */
76
+ async function appendRelationship(memoryDir, relationship) {
77
+ const filePath = path.join(memoryDir, RELATIONSHIPS_FILE_NAME);
78
+ const line = JSON.stringify(relationship) + '\n';
79
+ await fs.appendFile(filePath, line, { encoding: 'utf-8' });
80
+ }
81
+ /**
82
+ * Creates a promoted node from a source node
83
+ *
84
+ * @param sourceNode - Source node to promote
85
+ * @param tag - Taxonomy tag to apply
86
+ * @returns New node with project lifecycle
87
+ */
88
+ function createPromotedNode(sourceNode, tag) {
89
+ const now = new Date().toISOString();
90
+ // Generate new ID based on content + timestamp for uniqueness
91
+ const newId = generateMemId(`${sourceNode.content}${now}`);
92
+ // Build tags array, ensuring the promotion tag is included
93
+ const existingTags = sourceNode.tags ?? [];
94
+ const tags = existingTags.includes(tag) ? existingTags : [...existingTags, tag];
95
+ return {
96
+ id: newId,
97
+ type: sourceNode.type,
98
+ lifecycle: 'project',
99
+ content: sourceNode.content,
100
+ created_at: now,
101
+ wu_id: sourceNode.wu_id,
102
+ session_id: sourceNode.session_id,
103
+ metadata: {
104
+ ...sourceNode.metadata,
105
+ promoted_from: sourceNode.id,
106
+ promoted_at: now,
107
+ },
108
+ tags,
109
+ };
110
+ }
111
+ /**
112
+ * Creates a discovered_from relationship between promoted and source nodes
113
+ *
114
+ * @param promotedNodeId - ID of the promoted node
115
+ * @param sourceNodeId - ID of the source node
116
+ * @returns Relationship object
117
+ */
118
+ function createRelationship(promotedNodeId, sourceNodeId) {
119
+ return {
120
+ from_id: promotedNodeId,
121
+ to_id: sourceNodeId,
122
+ type: 'discovered_from',
123
+ created_at: new Date().toISOString(),
124
+ metadata: {},
125
+ };
126
+ }
127
+ /**
128
+ * Promotes a single node to project lifecycle.
129
+ *
130
+ * Creates a new project-level node with the same content and a
131
+ * discovered_from relationship back to the source node.
132
+ *
133
+ * @param baseDir - Base directory containing .lumenflow/memory
134
+ * @param options - Promotion options
135
+ * @returns Result with the promoted node
136
+ * @throws If node not found, already project level, or invalid tag
137
+ *
138
+ * @example
139
+ * const result = await promoteNode('/path/to/project', {
140
+ * nodeId: 'mem-abc1',
141
+ * tag: 'pattern',
142
+ * });
143
+ * console.log(`Promoted to ${result.promotedNode.id}`);
144
+ */
145
+ export async function promoteNode(baseDir, options) {
146
+ const { nodeId, tag, dryRun = false } = options;
147
+ // Validate tag
148
+ validateTag(tag);
149
+ const memoryDir = path.join(baseDir, LUMENFLOW_MEMORY_PATHS.MEMORY_DIR);
150
+ // Load memory and find the source node
151
+ const memory = await loadMemory(memoryDir);
152
+ const sourceNode = memory.byId.get(nodeId);
153
+ if (!sourceNode) {
154
+ throw new Error(`${ERROR_MESSAGES.NODE_NOT_FOUND}: ${nodeId}`);
155
+ }
156
+ if (sourceNode.lifecycle === 'project') {
157
+ throw new Error(`${ERROR_MESSAGES.ALREADY_PROJECT}: ${nodeId}`);
158
+ }
159
+ // Create the promoted node
160
+ const promotedNode = createPromotedNode(sourceNode, tag);
161
+ // Create the relationship
162
+ const relationship = createRelationship(promotedNode.id, sourceNode.id);
163
+ // Write if not dry run
164
+ if (!dryRun) {
165
+ await appendNode(memoryDir, promotedNode);
166
+ await appendRelationship(memoryDir, relationship);
167
+ }
168
+ return {
169
+ success: true,
170
+ promotedNode,
171
+ relationship,
172
+ dryRun,
173
+ };
174
+ }
175
+ /**
176
+ * Promotes all summaries from a WU to project lifecycle.
177
+ *
178
+ * Finds all summary-type nodes linked to the WU and promotes each one
179
+ * to project level with discovered_from relationships.
180
+ *
181
+ * @param baseDir - Base directory containing .lumenflow/memory
182
+ * @param options - Promotion options
183
+ * @returns Result with all promoted nodes
184
+ * @throws If WU ID is invalid or tag is invalid
185
+ *
186
+ * @example
187
+ * const result = await promoteFromWu('/path/to/project', {
188
+ * wuId: 'WU-1234',
189
+ * tag: 'decision',
190
+ * });
191
+ * console.log(`Promoted ${result.promotedNodes.length} summaries`);
192
+ */
193
+ export async function promoteFromWu(baseDir, options) {
194
+ const { wuId, tag, dryRun = false } = options;
195
+ // Validate inputs
196
+ validateWuId(wuId);
197
+ validateTag(tag);
198
+ const memoryDir = path.join(baseDir, LUMENFLOW_MEMORY_PATHS.MEMORY_DIR);
199
+ // Load memory
200
+ const memory = await loadMemory(memoryDir);
201
+ // Find all summaries for this WU that aren't already project level
202
+ const wuNodes = memory.byWu.get(wuId) ?? [];
203
+ const summaries = wuNodes.filter((node) => node.type === 'summary' && node.lifecycle !== 'project');
204
+ // If no summaries, return empty result
205
+ if (summaries.length === 0) {
206
+ return {
207
+ success: true,
208
+ promotedNodes: [],
209
+ relationships: [],
210
+ dryRun,
211
+ };
212
+ }
213
+ const promotedNodes = [];
214
+ const relationships = [];
215
+ // Promote each summary
216
+ for (const summary of summaries) {
217
+ const promotedNode = createPromotedNode(summary, tag);
218
+ const relationship = createRelationship(promotedNode.id, summary.id);
219
+ promotedNodes.push(promotedNode);
220
+ relationships.push(relationship);
221
+ // Write if not dry run
222
+ if (!dryRun) {
223
+ await appendNode(memoryDir, promotedNode);
224
+ await appendRelationship(memoryDir, relationship);
225
+ }
226
+ }
227
+ return {
228
+ success: true,
229
+ promotedNodes,
230
+ relationships,
231
+ dryRun,
232
+ };
233
+ }
@@ -16,8 +16,8 @@
16
16
  * - Lifecycle is not `ephemeral`
17
17
  * - Status is not `closed` (metadata.status !== 'closed')
18
18
  *
19
- * @see {@link tools/mem-ready.mjs} - CLI implementation
20
- * @see {@link tools/__tests__/mem-ready.test.mjs} - Tests
19
+ * @see {@link packages/@lumenflow/cli/src/mem-ready.ts} - CLI implementation
20
+ * @see {@link packages/@lumenflow/cli/src/__tests__/mem-ready.test.ts} - Tests
21
21
  */
22
22
  import fs from 'node:fs/promises';
23
23
  import path from 'node:path';
@@ -10,8 +10,8 @@
10
10
  * - Lane-targeted signals for cross-team communication
11
11
  * - Read/unread tracking for mem:inbox integration
12
12
  *
13
- * @see {@link tools/mem-signal.mjs} - CLI wrapper
14
- * @see {@link tools/__tests__/mem-signal.test.mjs} - Tests
13
+ * @see {@link packages/@lumenflow/cli/src/mem-signal.ts} - CLI wrapper
14
+ * @see {@link packages/@lumenflow/cli/src/__tests__/mem-signal.test.ts} - Tests
15
15
  */
16
16
  import { randomBytes } from 'node:crypto';
17
17
  import fs from 'node:fs/promises';
@@ -22,7 +22,7 @@ import { LUMENFLOW_MEMORY_PATHS } from './paths.js';
22
22
  */
23
23
  export const SIGNAL_FILE_NAME = 'signals.jsonl';
24
24
  /**
25
- * WU ID validation pattern (from memory-schema.mjs)
25
+ * WU ID validation pattern (from memory-schema.ts)
26
26
  */
27
27
  const WU_ID_PATTERN = /^WU-\d+$/;
28
28
  /**
@@ -10,9 +10,9 @@
10
10
  * - Idempotent: multiple starts create separate sessions
11
11
  * - Auto-initializes memory layer if not present
12
12
  *
13
- * @see {@link tools/mem-start.mjs} - CLI wrapper
14
- * @see {@link tools/__tests__/mem-start.test.mjs} - Tests
15
- * @see {@link tools/lib/memory-schema.mjs} - Schema definitions
13
+ * @see {@link packages/@lumenflow/cli/src/mem-start.ts} - CLI wrapper
14
+ * @see {@link packages/@lumenflow/cli/src/__tests__/mem-start.test.ts} - Tests
15
+ * @see {@link packages/@lumenflow/cli/src/lib/memory-schema.ts} - Schema definitions
16
16
  */
17
17
  import { randomUUID } from 'node:crypto';
18
18
  import fs from 'node:fs/promises';
@@ -10,8 +10,8 @@
10
10
  * - Respect lifecycle TTL (ephemeral, session, wu, project)
11
11
  * - Support dry-run mode for preview
12
12
  *
13
- * @see {@link tools/mem-summarize.mjs} - CLI wrapper
14
- * @see {@link tools/__tests__/mem-summarize.test.mjs} - Tests
13
+ * @see {@link packages/@lumenflow/cli/src/mem-summarize.ts} - CLI wrapper
14
+ * @see {@link packages/@lumenflow/cli/src/__tests__/mem-summarize.test.ts} - Tests
15
15
  */
16
16
  import { loadMemory, appendNode } from './memory-store.js';
17
17
  import { generateMemId } from './mem-id.js';
@@ -8,18 +8,15 @@
8
8
  * - Promote discovery to WU (integrates with wu:create)
9
9
  * - Archive discovery without promotion
10
10
  *
11
- * @see {@link tools/mem-triage.mjs} - CLI wrapper
12
- * @see {@link tools/__tests__/mem-triage.test.mjs} - Tests
11
+ * @see {@link packages/@lumenflow/cli/src/mem-triage.ts} - CLI wrapper
12
+ * @see {@link packages/@lumenflow/cli/src/__tests__/mem-triage.test.ts} - Tests
13
13
  */
14
14
  import fs from 'node:fs/promises';
15
15
  import path from 'node:path';
16
16
  import { loadMemory, appendNode } from './memory-store.js';
17
17
  import { validateLaneFormat } from '@lumenflow/core/lane-checker';
18
+ import { createWuPaths } from '@lumenflow/core/lib/wu-paths.js';
18
19
  import { LUMENFLOW_MEMORY_PATHS } from './paths.js';
19
- /**
20
- * WU directory path relative to base
21
- */
22
- const WU_DIR = 'docs/04-operations/tasks/wu';
23
20
  /**
24
21
  * Relationships file name
25
22
  */
@@ -244,7 +241,8 @@ export async function archiveDiscovery(baseDir, options) {
244
241
  * @returns Next WU ID (e.g., 'WU-1502')
245
242
  */
246
243
  async function getNextWuId(baseDir) {
247
- const wuDir = path.join(baseDir, WU_DIR);
244
+ const paths = createWuPaths({ projectRoot: baseDir });
245
+ const wuDir = path.join(baseDir, paths.WU_DIR());
248
246
  let maxId = 0;
249
247
  try {
250
248
  const files = await fs.readdir(wuDir);
@@ -9,7 +9,7 @@
9
9
  * - Lifecycles (4 levels: ephemeral, session, wu, project)
10
10
  * - Relationships (4 types: blocks, parent_child, related, discovered_from)
11
11
  *
12
- * @see {@link tools/lib/__tests__/memory-schema.test.mjs} - Tests
12
+ * @see {@link packages/@lumenflow/cli/src/lib/__tests__/memory-schema.test.ts} - Tests
13
13
  */
14
14
  import { z } from 'zod';
15
15
  /**