clearctx 3.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.
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Pattern Registry — Layer 0: Session Continuity Engine
3
+ *
4
+ * Stores and retrieves coding patterns learned during sessions.
5
+ * Patterns are project-scoped and persist across sessions.
6
+ *
7
+ * Example pattern:
8
+ * {
9
+ * id: 'p_1707856123456_a1b2c3d4',
10
+ * rule: 'Always normalize phone numbers to 10 digits',
11
+ * context: 'auth module',
12
+ * example: 'phone.replace(/\D/g, "").slice(-10)',
13
+ * tags: ['validation', 'auth'],
14
+ * createdAt: '2024-02-13T12:34:56.789Z'
15
+ * }
16
+ */
17
+
18
+ const crypto = require('crypto');
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const os = require('os');
22
+
23
+ // These are helper modules already in the codebase
24
+ const { atomicWriteJson, readJsonSafe } = require('./atomic-io');
25
+ const { acquireLock, releaseLock } = require('./file-lock');
26
+
27
+ /**
28
+ * PatternRegistry class for managing coding patterns.
29
+ *
30
+ * This class stores patterns (rules, examples, context) that help maintain
31
+ * consistency across sessions. Each pattern has a unique ID and metadata.
32
+ */
33
+ class PatternRegistry {
34
+ /**
35
+ * Creates a new PatternRegistry instance.
36
+ *
37
+ * @param {string} projectPath - Absolute path to the project directory
38
+ *
39
+ * The registry creates a project-specific storage directory using a hash
40
+ * of the project path, so different projects have separate pattern stores.
41
+ */
42
+ constructor(projectPath) {
43
+ // Create a short hash of the project path to identify this project
44
+ const projectHash = crypto
45
+ .createHash('sha256')
46
+ .update(projectPath)
47
+ .digest('hex')
48
+ .slice(0, 16);
49
+
50
+ // Storage directory for this project's patterns
51
+ this.storageDir = path.join(
52
+ os.homedir(),
53
+ '.clearctx',
54
+ 'continuity',
55
+ projectHash
56
+ );
57
+
58
+ // File that holds all patterns as JSON
59
+ this.patternsPath = path.join(this.storageDir, 'patterns.json');
60
+
61
+ // Directory for lock files (prevents race conditions)
62
+ this.locksDir = path.join(this.storageDir, 'locks');
63
+
64
+ // Create directories if they don't exist yet
65
+ fs.mkdirSync(this.storageDir, { recursive: true });
66
+ fs.mkdirSync(this.locksDir, { recursive: true });
67
+ }
68
+
69
+ /**
70
+ * Adds a new pattern to the registry.
71
+ *
72
+ * @param {Object} options - Pattern details
73
+ * @param {string} options.rule - The pattern rule (required)
74
+ * @param {string} [options.context] - Where this pattern applies
75
+ * @param {string} [options.example] - Code example or explanation
76
+ * @param {string[]} [options.tags] - Tags for categorization
77
+ * @returns {Object} The created pattern entry
78
+ *
79
+ * Example:
80
+ * registry.add({
81
+ * rule: 'Always validate email format',
82
+ * context: 'user registration',
83
+ * example: '/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)',
84
+ * tags: ['validation', 'user']
85
+ * });
86
+ */
87
+ add({ rule, context, example, tags }) {
88
+ // Generate a unique ID for this pattern
89
+ // Format: 'p_' + timestamp + '_' + random hex
90
+ const id = 'p_' + Date.now() + '_' + crypto.randomBytes(4).toString('hex');
91
+
92
+ try {
93
+ // Lock the patterns file to prevent conflicts from other sessions
94
+ acquireLock(this.locksDir, 'patterns');
95
+
96
+ // Read existing patterns (returns {} if file doesn't exist)
97
+ const patterns = readJsonSafe(this.patternsPath, {});
98
+
99
+ // Build the new pattern entry
100
+ const entry = {
101
+ id,
102
+ rule,
103
+ context: context || '',
104
+ example: example || '',
105
+ tags: tags || [],
106
+ createdAt: new Date().toISOString()
107
+ };
108
+
109
+ // Add the pattern to the collection
110
+ patterns[id] = entry;
111
+
112
+ // Write back to disk atomically (safe even if interrupted)
113
+ atomicWriteJson(this.patternsPath, patterns);
114
+
115
+ return entry;
116
+ } finally {
117
+ // Always release the lock, even if an error occurred
118
+ releaseLock(this.locksDir, 'patterns');
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Lists all patterns, optionally filtered by tag or context.
124
+ *
125
+ * @param {Object} [options] - Filter options
126
+ * @param {string} [options.tag] - Filter by tag (exact match)
127
+ * @param {string} [options.context] - Filter by context (substring match, case-insensitive)
128
+ * @returns {Array} Array of pattern entries, sorted by creation time (newest first)
129
+ *
130
+ * Example:
131
+ * registry.list({ tag: 'validation' })
132
+ * registry.list({ context: 'auth' })
133
+ * registry.list() // all patterns
134
+ */
135
+ list({ tag, context } = {}) {
136
+ // Read all patterns from disk
137
+ const patterns = readJsonSafe(this.patternsPath, {});
138
+
139
+ // Convert from object to array
140
+ let entries = Object.values(patterns);
141
+
142
+ // Apply tag filter if provided
143
+ if (tag) {
144
+ entries = entries.filter(entry => entry.tags.includes(tag));
145
+ }
146
+
147
+ // Apply context filter if provided (case-insensitive substring match)
148
+ if (context) {
149
+ const contextLower = context.toLowerCase();
150
+ entries = entries.filter(entry =>
151
+ entry.context.toLowerCase().includes(contextLower)
152
+ );
153
+ }
154
+
155
+ // Sort by creation time, newest first
156
+ entries.sort((a, b) => {
157
+ return new Date(b.createdAt) - new Date(a.createdAt);
158
+ });
159
+
160
+ return entries;
161
+ }
162
+
163
+ /**
164
+ * Removes a pattern from the registry.
165
+ *
166
+ * @param {string} patternId - The ID of the pattern to remove
167
+ * @returns {Object} Confirmation object with removed ID
168
+ * @throws {Error} If pattern ID doesn't exist
169
+ *
170
+ * Example:
171
+ * registry.remove('p_1707856123456_a1b2c3d4')
172
+ */
173
+ remove(patternId) {
174
+ try {
175
+ // Lock the patterns file
176
+ acquireLock(this.locksDir, 'patterns');
177
+
178
+ // Read current patterns
179
+ const patterns = readJsonSafe(this.patternsPath, {});
180
+
181
+ // Check if the pattern exists
182
+ if (!patterns[patternId]) {
183
+ throw new Error(`Pattern not found: ${patternId}`);
184
+ }
185
+
186
+ // Delete the pattern
187
+ delete patterns[patternId];
188
+
189
+ // Write back to disk
190
+ atomicWriteJson(this.patternsPath, patterns);
191
+
192
+ return { removed: patternId };
193
+ } finally {
194
+ // Always release the lock
195
+ releaseLock(this.locksDir, 'patterns');
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Gets a specific pattern by ID.
201
+ *
202
+ * @param {string} patternId - The ID of the pattern to retrieve
203
+ * @returns {Object|null} The pattern entry, or null if not found
204
+ *
205
+ * Example:
206
+ * const pattern = registry.get('p_1707856123456_a1b2c3d4');
207
+ * if (pattern) {
208
+ * console.log(pattern.rule);
209
+ * }
210
+ */
211
+ get(patternId) {
212
+ // Read patterns (no lock needed for simple reads)
213
+ const patterns = readJsonSafe(this.patternsPath, {});
214
+
215
+ // Return the pattern or null if not found
216
+ return patterns[patternId] || null;
217
+ }
218
+ }
219
+
220
+ // Export the class so other modules can use it
221
+ module.exports = PatternRegistry;