@fermindi/pwn-cli 0.1.1 → 0.3.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 -21
- package/README.md +265 -251
- package/cli/batch.js +333 -333
- package/cli/codespaces.js +303 -303
- package/cli/index.js +112 -91
- package/cli/inject.js +90 -67
- package/cli/knowledge.js +531 -531
- package/cli/migrate.js +466 -0
- package/cli/notify.js +135 -135
- package/cli/patterns.js +665 -665
- package/cli/save.js +206 -0
- package/cli/status.js +91 -91
- package/cli/update.js +189 -0
- package/cli/validate.js +61 -61
- package/package.json +70 -70
- package/src/core/inject.js +300 -204
- package/src/core/state.js +91 -91
- package/src/core/validate.js +202 -202
- package/src/core/workspace.js +176 -176
- package/src/index.js +20 -20
- package/src/knowledge/gc.js +308 -308
- package/src/knowledge/lifecycle.js +401 -401
- package/src/knowledge/promote.js +364 -364
- package/src/knowledge/references.js +342 -342
- package/src/patterns/matcher.js +218 -218
- package/src/patterns/registry.js +375 -375
- package/src/patterns/triggers.js +423 -423
- package/src/services/batch-service.js +849 -849
- package/src/services/notification-service.js +342 -342
- package/templates/codespaces/devcontainer.json +52 -52
- package/templates/codespaces/setup.sh +70 -70
- package/templates/workspace/.ai/README.md +164 -164
- package/templates/workspace/.ai/agents/README.md +204 -204
- package/templates/workspace/.ai/agents/claude.md +625 -625
- package/templates/workspace/.ai/config/README.md +79 -79
- package/templates/workspace/.ai/config/notifications.template.json +20 -20
- package/templates/workspace/.ai/memory/deadends.md +79 -79
- package/templates/workspace/.ai/memory/decisions.md +58 -58
- package/templates/workspace/.ai/memory/patterns.md +65 -65
- package/templates/workspace/.ai/patterns/backend/README.md +126 -126
- package/templates/workspace/.ai/patterns/frontend/README.md +103 -103
- package/templates/workspace/.ai/patterns/index.md +256 -256
- package/templates/workspace/.ai/patterns/triggers.json +1087 -1087
- package/templates/workspace/.ai/patterns/universal/README.md +141 -141
- package/templates/workspace/.ai/state.template.json +8 -8
- package/templates/workspace/.ai/tasks/active.md +77 -77
- package/templates/workspace/.ai/tasks/backlog.md +95 -95
- package/templates/workspace/.ai/workflows/batch-task.md +356 -356
|
@@ -1,342 +1,342 @@
|
|
|
1
|
-
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
2
|
-
import { join, relative } from 'path';
|
|
3
|
-
import {
|
|
4
|
-
getKnowledgeRegistry,
|
|
5
|
-
saveKnowledgeRegistry,
|
|
6
|
-
updateKnowledgeItem
|
|
7
|
-
} from './lifecycle.js';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Scan a file for knowledge references (DEC-XXX, DE-XXX)
|
|
11
|
-
* @param {string} filePath - Path to file
|
|
12
|
-
* @returns {string[]} Array of referenced IDs
|
|
13
|
-
*/
|
|
14
|
-
export function scanFileForReferences(filePath) {
|
|
15
|
-
if (!existsSync(filePath)) {
|
|
16
|
-
return [];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
const content = readFileSync(filePath, 'utf8');
|
|
21
|
-
const references = new Set();
|
|
22
|
-
|
|
23
|
-
// Match DEC-XXX and DE-XXX patterns
|
|
24
|
-
const decisionRegex = /DEC-\d+/g;
|
|
25
|
-
const deadendRegex = /DE-\d+/g;
|
|
26
|
-
|
|
27
|
-
let match;
|
|
28
|
-
while ((match = decisionRegex.exec(content)) !== null) {
|
|
29
|
-
references.add(match[0]);
|
|
30
|
-
}
|
|
31
|
-
while ((match = deadendRegex.exec(content)) !== null) {
|
|
32
|
-
references.add(match[0]);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return Array.from(references);
|
|
36
|
-
} catch {
|
|
37
|
-
return [];
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Recursively scan directory for knowledge references
|
|
43
|
-
* @param {string} dir - Directory to scan
|
|
44
|
-
* @param {Object} options - Options
|
|
45
|
-
* @param {string[]} options.exclude - Patterns to exclude
|
|
46
|
-
* @returns {Map<string, string[]>} Map of file path to referenced IDs
|
|
47
|
-
*/
|
|
48
|
-
export function scanDirectoryForReferences(dir, options = {}) {
|
|
49
|
-
const exclude = options.exclude || [
|
|
50
|
-
'node_modules', '.git', '.ai', 'dist', 'build', 'coverage',
|
|
51
|
-
'.next', '.nuxt', '.output', 'vendor', '__pycache__'
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
const results = new Map();
|
|
55
|
-
|
|
56
|
-
function walk(currentDir) {
|
|
57
|
-
try {
|
|
58
|
-
const entries = readdirSync(currentDir);
|
|
59
|
-
|
|
60
|
-
for (const entry of entries) {
|
|
61
|
-
const fullPath = join(currentDir, entry);
|
|
62
|
-
|
|
63
|
-
// Skip excluded directories
|
|
64
|
-
if (exclude.some(pattern => entry === pattern || fullPath.includes(pattern))) {
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const stat = statSync(fullPath);
|
|
70
|
-
|
|
71
|
-
if (stat.isDirectory()) {
|
|
72
|
-
walk(fullPath);
|
|
73
|
-
} else if (stat.isFile()) {
|
|
74
|
-
// Skip binary and non-text files
|
|
75
|
-
if (isScannable(entry)) {
|
|
76
|
-
const refs = scanFileForReferences(fullPath);
|
|
77
|
-
if (refs.length > 0) {
|
|
78
|
-
results.set(fullPath, refs);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
} catch {
|
|
83
|
-
// Skip files we can't access
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
} catch {
|
|
87
|
-
// Skip directories we can't access
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
walk(dir);
|
|
92
|
-
return results;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Check if a file should be scanned for references
|
|
97
|
-
* @param {string} filename - File name
|
|
98
|
-
* @returns {boolean}
|
|
99
|
-
*/
|
|
100
|
-
function isScannable(filename) {
|
|
101
|
-
const scannableExtensions = [
|
|
102
|
-
'.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
|
|
103
|
-
'.py', '.rb', '.go', '.rs', '.java', '.kt', '.scala',
|
|
104
|
-
'.c', '.cpp', '.h', '.hpp', '.cs',
|
|
105
|
-
'.php', '.swift', '.m',
|
|
106
|
-
'.md', '.mdx', '.txt', '.rst',
|
|
107
|
-
'.yml', '.yaml', '.json', '.toml',
|
|
108
|
-
'.html', '.css', '.scss', '.less',
|
|
109
|
-
'.sql', '.graphql', '.prisma',
|
|
110
|
-
'.sh', '.bash', '.zsh', '.fish',
|
|
111
|
-
'.vue', '.svelte', '.astro'
|
|
112
|
-
];
|
|
113
|
-
|
|
114
|
-
const extension = filename.substring(filename.lastIndexOf('.'));
|
|
115
|
-
return scannableExtensions.includes(extension);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Build a reference map: which files reference which knowledge items
|
|
120
|
-
* @param {string} cwd - Working directory
|
|
121
|
-
* @returns {Object} Reference map
|
|
122
|
-
*/
|
|
123
|
-
export function buildReferenceMap(cwd = process.cwd()) {
|
|
124
|
-
const fileRefs = scanDirectoryForReferences(cwd);
|
|
125
|
-
|
|
126
|
-
// Build inverse map: knowledge ID -> files
|
|
127
|
-
const knowledgeToFiles = new Map();
|
|
128
|
-
|
|
129
|
-
for (const [file, refs] of fileRefs) {
|
|
130
|
-
const relativePath = relative(cwd, file);
|
|
131
|
-
for (const ref of refs) {
|
|
132
|
-
if (!knowledgeToFiles.has(ref)) {
|
|
133
|
-
knowledgeToFiles.set(ref, []);
|
|
134
|
-
}
|
|
135
|
-
knowledgeToFiles.get(ref).push(relativePath);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
fileToKnowledge: Object.fromEntries(
|
|
141
|
-
Array.from(fileRefs.entries()).map(([file, refs]) => [
|
|
142
|
-
relative(cwd, file),
|
|
143
|
-
refs
|
|
144
|
-
])
|
|
145
|
-
),
|
|
146
|
-
knowledgeToFiles: Object.fromEntries(knowledgeToFiles)
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Update knowledge registry with current file references
|
|
152
|
-
* @param {string} cwd - Working directory
|
|
153
|
-
* @returns {Object} Update result
|
|
154
|
-
*/
|
|
155
|
-
export function updateReferences(cwd = process.cwd()) {
|
|
156
|
-
const registry = getKnowledgeRegistry(cwd);
|
|
157
|
-
if (!registry) {
|
|
158
|
-
return { error: 'No knowledge registry found. Run sync first.' };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const refMap = buildReferenceMap(cwd);
|
|
162
|
-
const result = { updated: [], orphaned: [], newRefs: [] };
|
|
163
|
-
|
|
164
|
-
// Update each knowledge item with its references
|
|
165
|
-
for (const [id, item] of Object.entries(registry.items)) {
|
|
166
|
-
const currentFiles = refMap.knowledgeToFiles[id] || [];
|
|
167
|
-
const previousFiles = item.relatedFiles || [];
|
|
168
|
-
|
|
169
|
-
// Find changes
|
|
170
|
-
const added = currentFiles.filter(f => !previousFiles.includes(f));
|
|
171
|
-
const removed = previousFiles.filter(f => !currentFiles.includes(f));
|
|
172
|
-
|
|
173
|
-
if (added.length > 0 || removed.length > 0) {
|
|
174
|
-
registry.items[id].relatedFiles = currentFiles;
|
|
175
|
-
result.updated.push({ id, added, removed });
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Check for orphaned items (no references)
|
|
179
|
-
if (currentFiles.length === 0 && item.status === 'active') {
|
|
180
|
-
result.orphaned.push(id);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Track new references for metrics
|
|
184
|
-
if (added.length > 0) {
|
|
185
|
-
result.newRefs.push({ id, files: added });
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
saveKnowledgeRegistry(registry, cwd);
|
|
190
|
-
return result;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Add a manual reference link
|
|
195
|
-
* @param {string} knowledgeId - Knowledge item ID
|
|
196
|
-
* @param {string} target - Target (file path, decision ID, task ID)
|
|
197
|
-
* @param {string} targetType - Type: 'file', 'decision', 'task'
|
|
198
|
-
* @param {string} cwd - Working directory
|
|
199
|
-
* @returns {Object|null}
|
|
200
|
-
*/
|
|
201
|
-
export function addReference(knowledgeId, target, targetType, cwd = process.cwd()) {
|
|
202
|
-
const registry = getKnowledgeRegistry(cwd);
|
|
203
|
-
if (!registry || !registry.items[knowledgeId]) {
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const item = registry.items[knowledgeId];
|
|
208
|
-
|
|
209
|
-
switch (targetType) {
|
|
210
|
-
case 'file':
|
|
211
|
-
item.relatedFiles = item.relatedFiles || [];
|
|
212
|
-
if (!item.relatedFiles.includes(target)) {
|
|
213
|
-
item.relatedFiles.push(target);
|
|
214
|
-
}
|
|
215
|
-
break;
|
|
216
|
-
case 'decision':
|
|
217
|
-
item.relatedDecisions = item.relatedDecisions || [];
|
|
218
|
-
if (!item.relatedDecisions.includes(target)) {
|
|
219
|
-
item.relatedDecisions.push(target);
|
|
220
|
-
}
|
|
221
|
-
break;
|
|
222
|
-
case 'task':
|
|
223
|
-
item.relatedTasks = item.relatedTasks || [];
|
|
224
|
-
if (!item.relatedTasks.includes(target)) {
|
|
225
|
-
item.relatedTasks.push(target);
|
|
226
|
-
}
|
|
227
|
-
break;
|
|
228
|
-
default:
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
saveKnowledgeRegistry(registry, cwd);
|
|
233
|
-
return item;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Remove a reference link
|
|
238
|
-
* @param {string} knowledgeId - Knowledge item ID
|
|
239
|
-
* @param {string} target - Target to remove
|
|
240
|
-
* @param {string} targetType - Type: 'file', 'decision', 'task'
|
|
241
|
-
* @param {string} cwd - Working directory
|
|
242
|
-
* @returns {Object|null}
|
|
243
|
-
*/
|
|
244
|
-
export function removeReference(knowledgeId, target, targetType, cwd = process.cwd()) {
|
|
245
|
-
const registry = getKnowledgeRegistry(cwd);
|
|
246
|
-
if (!registry || !registry.items[knowledgeId]) {
|
|
247
|
-
return null;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const item = registry.items[knowledgeId];
|
|
251
|
-
|
|
252
|
-
switch (targetType) {
|
|
253
|
-
case 'file':
|
|
254
|
-
item.relatedFiles = (item.relatedFiles || []).filter(f => f !== target);
|
|
255
|
-
break;
|
|
256
|
-
case 'decision':
|
|
257
|
-
item.relatedDecisions = (item.relatedDecisions || []).filter(d => d !== target);
|
|
258
|
-
break;
|
|
259
|
-
case 'task':
|
|
260
|
-
item.relatedTasks = (item.relatedTasks || []).filter(t => t !== target);
|
|
261
|
-
break;
|
|
262
|
-
default:
|
|
263
|
-
return null;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
saveKnowledgeRegistry(registry, cwd);
|
|
267
|
-
return item;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Find broken references (references to non-existent files)
|
|
272
|
-
* @param {string} cwd - Working directory
|
|
273
|
-
* @returns {Object[]} Broken references
|
|
274
|
-
*/
|
|
275
|
-
export function findBrokenReferences(cwd = process.cwd()) {
|
|
276
|
-
const registry = getKnowledgeRegistry(cwd);
|
|
277
|
-
if (!registry) {
|
|
278
|
-
return [];
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const broken = [];
|
|
282
|
-
|
|
283
|
-
for (const [id, item] of Object.entries(registry.items)) {
|
|
284
|
-
const brokenFiles = (item.relatedFiles || []).filter(file => {
|
|
285
|
-
const fullPath = join(cwd, file);
|
|
286
|
-
return !existsSync(fullPath);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
const brokenDecisions = (item.relatedDecisions || []).filter(decId => {
|
|
290
|
-
return !registry.items[decId];
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
if (brokenFiles.length > 0 || brokenDecisions.length > 0) {
|
|
294
|
-
broken.push({
|
|
295
|
-
id,
|
|
296
|
-
brokenFiles,
|
|
297
|
-
brokenDecisions
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
return broken;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Get reference statistics
|
|
307
|
-
* @param {string} cwd - Working directory
|
|
308
|
-
* @returns {Object}
|
|
309
|
-
*/
|
|
310
|
-
export function getReferenceStats(cwd = process.cwd()) {
|
|
311
|
-
const registry = getKnowledgeRegistry(cwd);
|
|
312
|
-
if (!registry) {
|
|
313
|
-
return { totalRefs: 0, avgRefsPerItem: 0, orphaned: 0, mostReferenced: [] };
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const items = Object.values(registry.items);
|
|
317
|
-
let totalRefs = 0;
|
|
318
|
-
let orphaned = 0;
|
|
319
|
-
const refCounts = [];
|
|
320
|
-
|
|
321
|
-
for (const item of items) {
|
|
322
|
-
const refCount = (item.relatedFiles || []).length +
|
|
323
|
-
(item.relatedDecisions || []).length +
|
|
324
|
-
(item.relatedTasks || []).length;
|
|
325
|
-
totalRefs += refCount;
|
|
326
|
-
refCounts.push({ id: item.id, title: item.title, count: refCount });
|
|
327
|
-
|
|
328
|
-
if (refCount === 0 && item.status === 'active') {
|
|
329
|
-
orphaned++;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Sort by ref count descending
|
|
334
|
-
refCounts.sort((a, b) => b.count - a.count);
|
|
335
|
-
|
|
336
|
-
return {
|
|
337
|
-
totalRefs,
|
|
338
|
-
avgRefsPerItem: items.length > 0 ? (totalRefs / items.length).toFixed(1) : 0,
|
|
339
|
-
orphaned,
|
|
340
|
-
mostReferenced: refCounts.slice(0, 5)
|
|
341
|
-
};
|
|
342
|
-
}
|
|
1
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, relative } from 'path';
|
|
3
|
+
import {
|
|
4
|
+
getKnowledgeRegistry,
|
|
5
|
+
saveKnowledgeRegistry,
|
|
6
|
+
updateKnowledgeItem
|
|
7
|
+
} from './lifecycle.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Scan a file for knowledge references (DEC-XXX, DE-XXX)
|
|
11
|
+
* @param {string} filePath - Path to file
|
|
12
|
+
* @returns {string[]} Array of referenced IDs
|
|
13
|
+
*/
|
|
14
|
+
export function scanFileForReferences(filePath) {
|
|
15
|
+
if (!existsSync(filePath)) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const content = readFileSync(filePath, 'utf8');
|
|
21
|
+
const references = new Set();
|
|
22
|
+
|
|
23
|
+
// Match DEC-XXX and DE-XXX patterns
|
|
24
|
+
const decisionRegex = /DEC-\d+/g;
|
|
25
|
+
const deadendRegex = /DE-\d+/g;
|
|
26
|
+
|
|
27
|
+
let match;
|
|
28
|
+
while ((match = decisionRegex.exec(content)) !== null) {
|
|
29
|
+
references.add(match[0]);
|
|
30
|
+
}
|
|
31
|
+
while ((match = deadendRegex.exec(content)) !== null) {
|
|
32
|
+
references.add(match[0]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return Array.from(references);
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Recursively scan directory for knowledge references
|
|
43
|
+
* @param {string} dir - Directory to scan
|
|
44
|
+
* @param {Object} options - Options
|
|
45
|
+
* @param {string[]} options.exclude - Patterns to exclude
|
|
46
|
+
* @returns {Map<string, string[]>} Map of file path to referenced IDs
|
|
47
|
+
*/
|
|
48
|
+
export function scanDirectoryForReferences(dir, options = {}) {
|
|
49
|
+
const exclude = options.exclude || [
|
|
50
|
+
'node_modules', '.git', '.ai', 'dist', 'build', 'coverage',
|
|
51
|
+
'.next', '.nuxt', '.output', 'vendor', '__pycache__'
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const results = new Map();
|
|
55
|
+
|
|
56
|
+
function walk(currentDir) {
|
|
57
|
+
try {
|
|
58
|
+
const entries = readdirSync(currentDir);
|
|
59
|
+
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const fullPath = join(currentDir, entry);
|
|
62
|
+
|
|
63
|
+
// Skip excluded directories
|
|
64
|
+
if (exclude.some(pattern => entry === pattern || fullPath.includes(pattern))) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const stat = statSync(fullPath);
|
|
70
|
+
|
|
71
|
+
if (stat.isDirectory()) {
|
|
72
|
+
walk(fullPath);
|
|
73
|
+
} else if (stat.isFile()) {
|
|
74
|
+
// Skip binary and non-text files
|
|
75
|
+
if (isScannable(entry)) {
|
|
76
|
+
const refs = scanFileForReferences(fullPath);
|
|
77
|
+
if (refs.length > 0) {
|
|
78
|
+
results.set(fullPath, refs);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// Skip files we can't access
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// Skip directories we can't access
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
walk(dir);
|
|
92
|
+
return results;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if a file should be scanned for references
|
|
97
|
+
* @param {string} filename - File name
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
function isScannable(filename) {
|
|
101
|
+
const scannableExtensions = [
|
|
102
|
+
'.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
|
|
103
|
+
'.py', '.rb', '.go', '.rs', '.java', '.kt', '.scala',
|
|
104
|
+
'.c', '.cpp', '.h', '.hpp', '.cs',
|
|
105
|
+
'.php', '.swift', '.m',
|
|
106
|
+
'.md', '.mdx', '.txt', '.rst',
|
|
107
|
+
'.yml', '.yaml', '.json', '.toml',
|
|
108
|
+
'.html', '.css', '.scss', '.less',
|
|
109
|
+
'.sql', '.graphql', '.prisma',
|
|
110
|
+
'.sh', '.bash', '.zsh', '.fish',
|
|
111
|
+
'.vue', '.svelte', '.astro'
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
const extension = filename.substring(filename.lastIndexOf('.'));
|
|
115
|
+
return scannableExtensions.includes(extension);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build a reference map: which files reference which knowledge items
|
|
120
|
+
* @param {string} cwd - Working directory
|
|
121
|
+
* @returns {Object} Reference map
|
|
122
|
+
*/
|
|
123
|
+
export function buildReferenceMap(cwd = process.cwd()) {
|
|
124
|
+
const fileRefs = scanDirectoryForReferences(cwd);
|
|
125
|
+
|
|
126
|
+
// Build inverse map: knowledge ID -> files
|
|
127
|
+
const knowledgeToFiles = new Map();
|
|
128
|
+
|
|
129
|
+
for (const [file, refs] of fileRefs) {
|
|
130
|
+
const relativePath = relative(cwd, file);
|
|
131
|
+
for (const ref of refs) {
|
|
132
|
+
if (!knowledgeToFiles.has(ref)) {
|
|
133
|
+
knowledgeToFiles.set(ref, []);
|
|
134
|
+
}
|
|
135
|
+
knowledgeToFiles.get(ref).push(relativePath);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
fileToKnowledge: Object.fromEntries(
|
|
141
|
+
Array.from(fileRefs.entries()).map(([file, refs]) => [
|
|
142
|
+
relative(cwd, file),
|
|
143
|
+
refs
|
|
144
|
+
])
|
|
145
|
+
),
|
|
146
|
+
knowledgeToFiles: Object.fromEntries(knowledgeToFiles)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Update knowledge registry with current file references
|
|
152
|
+
* @param {string} cwd - Working directory
|
|
153
|
+
* @returns {Object} Update result
|
|
154
|
+
*/
|
|
155
|
+
export function updateReferences(cwd = process.cwd()) {
|
|
156
|
+
const registry = getKnowledgeRegistry(cwd);
|
|
157
|
+
if (!registry) {
|
|
158
|
+
return { error: 'No knowledge registry found. Run sync first.' };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const refMap = buildReferenceMap(cwd);
|
|
162
|
+
const result = { updated: [], orphaned: [], newRefs: [] };
|
|
163
|
+
|
|
164
|
+
// Update each knowledge item with its references
|
|
165
|
+
for (const [id, item] of Object.entries(registry.items)) {
|
|
166
|
+
const currentFiles = refMap.knowledgeToFiles[id] || [];
|
|
167
|
+
const previousFiles = item.relatedFiles || [];
|
|
168
|
+
|
|
169
|
+
// Find changes
|
|
170
|
+
const added = currentFiles.filter(f => !previousFiles.includes(f));
|
|
171
|
+
const removed = previousFiles.filter(f => !currentFiles.includes(f));
|
|
172
|
+
|
|
173
|
+
if (added.length > 0 || removed.length > 0) {
|
|
174
|
+
registry.items[id].relatedFiles = currentFiles;
|
|
175
|
+
result.updated.push({ id, added, removed });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check for orphaned items (no references)
|
|
179
|
+
if (currentFiles.length === 0 && item.status === 'active') {
|
|
180
|
+
result.orphaned.push(id);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Track new references for metrics
|
|
184
|
+
if (added.length > 0) {
|
|
185
|
+
result.newRefs.push({ id, files: added });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
saveKnowledgeRegistry(registry, cwd);
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Add a manual reference link
|
|
195
|
+
* @param {string} knowledgeId - Knowledge item ID
|
|
196
|
+
* @param {string} target - Target (file path, decision ID, task ID)
|
|
197
|
+
* @param {string} targetType - Type: 'file', 'decision', 'task'
|
|
198
|
+
* @param {string} cwd - Working directory
|
|
199
|
+
* @returns {Object|null}
|
|
200
|
+
*/
|
|
201
|
+
export function addReference(knowledgeId, target, targetType, cwd = process.cwd()) {
|
|
202
|
+
const registry = getKnowledgeRegistry(cwd);
|
|
203
|
+
if (!registry || !registry.items[knowledgeId]) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const item = registry.items[knowledgeId];
|
|
208
|
+
|
|
209
|
+
switch (targetType) {
|
|
210
|
+
case 'file':
|
|
211
|
+
item.relatedFiles = item.relatedFiles || [];
|
|
212
|
+
if (!item.relatedFiles.includes(target)) {
|
|
213
|
+
item.relatedFiles.push(target);
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
case 'decision':
|
|
217
|
+
item.relatedDecisions = item.relatedDecisions || [];
|
|
218
|
+
if (!item.relatedDecisions.includes(target)) {
|
|
219
|
+
item.relatedDecisions.push(target);
|
|
220
|
+
}
|
|
221
|
+
break;
|
|
222
|
+
case 'task':
|
|
223
|
+
item.relatedTasks = item.relatedTasks || [];
|
|
224
|
+
if (!item.relatedTasks.includes(target)) {
|
|
225
|
+
item.relatedTasks.push(target);
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
default:
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
saveKnowledgeRegistry(registry, cwd);
|
|
233
|
+
return item;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Remove a reference link
|
|
238
|
+
* @param {string} knowledgeId - Knowledge item ID
|
|
239
|
+
* @param {string} target - Target to remove
|
|
240
|
+
* @param {string} targetType - Type: 'file', 'decision', 'task'
|
|
241
|
+
* @param {string} cwd - Working directory
|
|
242
|
+
* @returns {Object|null}
|
|
243
|
+
*/
|
|
244
|
+
export function removeReference(knowledgeId, target, targetType, cwd = process.cwd()) {
|
|
245
|
+
const registry = getKnowledgeRegistry(cwd);
|
|
246
|
+
if (!registry || !registry.items[knowledgeId]) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const item = registry.items[knowledgeId];
|
|
251
|
+
|
|
252
|
+
switch (targetType) {
|
|
253
|
+
case 'file':
|
|
254
|
+
item.relatedFiles = (item.relatedFiles || []).filter(f => f !== target);
|
|
255
|
+
break;
|
|
256
|
+
case 'decision':
|
|
257
|
+
item.relatedDecisions = (item.relatedDecisions || []).filter(d => d !== target);
|
|
258
|
+
break;
|
|
259
|
+
case 'task':
|
|
260
|
+
item.relatedTasks = (item.relatedTasks || []).filter(t => t !== target);
|
|
261
|
+
break;
|
|
262
|
+
default:
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
saveKnowledgeRegistry(registry, cwd);
|
|
267
|
+
return item;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Find broken references (references to non-existent files)
|
|
272
|
+
* @param {string} cwd - Working directory
|
|
273
|
+
* @returns {Object[]} Broken references
|
|
274
|
+
*/
|
|
275
|
+
export function findBrokenReferences(cwd = process.cwd()) {
|
|
276
|
+
const registry = getKnowledgeRegistry(cwd);
|
|
277
|
+
if (!registry) {
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const broken = [];
|
|
282
|
+
|
|
283
|
+
for (const [id, item] of Object.entries(registry.items)) {
|
|
284
|
+
const brokenFiles = (item.relatedFiles || []).filter(file => {
|
|
285
|
+
const fullPath = join(cwd, file);
|
|
286
|
+
return !existsSync(fullPath);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const brokenDecisions = (item.relatedDecisions || []).filter(decId => {
|
|
290
|
+
return !registry.items[decId];
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
if (brokenFiles.length > 0 || brokenDecisions.length > 0) {
|
|
294
|
+
broken.push({
|
|
295
|
+
id,
|
|
296
|
+
brokenFiles,
|
|
297
|
+
brokenDecisions
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return broken;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get reference statistics
|
|
307
|
+
* @param {string} cwd - Working directory
|
|
308
|
+
* @returns {Object}
|
|
309
|
+
*/
|
|
310
|
+
export function getReferenceStats(cwd = process.cwd()) {
|
|
311
|
+
const registry = getKnowledgeRegistry(cwd);
|
|
312
|
+
if (!registry) {
|
|
313
|
+
return { totalRefs: 0, avgRefsPerItem: 0, orphaned: 0, mostReferenced: [] };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const items = Object.values(registry.items);
|
|
317
|
+
let totalRefs = 0;
|
|
318
|
+
let orphaned = 0;
|
|
319
|
+
const refCounts = [];
|
|
320
|
+
|
|
321
|
+
for (const item of items) {
|
|
322
|
+
const refCount = (item.relatedFiles || []).length +
|
|
323
|
+
(item.relatedDecisions || []).length +
|
|
324
|
+
(item.relatedTasks || []).length;
|
|
325
|
+
totalRefs += refCount;
|
|
326
|
+
refCounts.push({ id: item.id, title: item.title, count: refCount });
|
|
327
|
+
|
|
328
|
+
if (refCount === 0 && item.status === 'active') {
|
|
329
|
+
orphaned++;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Sort by ref count descending
|
|
334
|
+
refCounts.sort((a, b) => b.count - a.count);
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
totalRefs,
|
|
338
|
+
avgRefsPerItem: items.length > 0 ? (totalRefs / items.length).toFixed(1) : 0,
|
|
339
|
+
orphaned,
|
|
340
|
+
mostReferenced: refCounts.slice(0, 5)
|
|
341
|
+
};
|
|
342
|
+
}
|