@fermindi/pwn-cli 0.1.0 → 0.2.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.
Files changed (46) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +265 -251
  3. package/cli/batch.js +333 -333
  4. package/cli/codespaces.js +303 -303
  5. package/cli/index.js +98 -91
  6. package/cli/inject.js +78 -53
  7. package/cli/knowledge.js +531 -531
  8. package/cli/migrate.js +466 -0
  9. package/cli/notify.js +135 -135
  10. package/cli/patterns.js +665 -665
  11. package/cli/status.js +91 -91
  12. package/cli/validate.js +61 -61
  13. package/package.json +70 -70
  14. package/src/core/inject.js +208 -128
  15. package/src/core/state.js +91 -91
  16. package/src/core/validate.js +202 -202
  17. package/src/core/workspace.js +176 -176
  18. package/src/index.js +20 -20
  19. package/src/knowledge/gc.js +308 -308
  20. package/src/knowledge/lifecycle.js +401 -401
  21. package/src/knowledge/promote.js +364 -364
  22. package/src/knowledge/references.js +342 -342
  23. package/src/patterns/matcher.js +218 -218
  24. package/src/patterns/registry.js +375 -375
  25. package/src/patterns/triggers.js +423 -423
  26. package/src/services/batch-service.js +849 -849
  27. package/src/services/notification-service.js +342 -342
  28. package/templates/codespaces/devcontainer.json +52 -52
  29. package/templates/codespaces/setup.sh +70 -70
  30. package/templates/workspace/.ai/README.md +164 -164
  31. package/templates/workspace/.ai/agents/README.md +204 -204
  32. package/templates/workspace/.ai/agents/claude.md +625 -625
  33. package/templates/workspace/.ai/config/README.md +79 -79
  34. package/templates/workspace/.ai/config/notifications.template.json +20 -20
  35. package/templates/workspace/.ai/memory/deadends.md +79 -79
  36. package/templates/workspace/.ai/memory/decisions.md +58 -58
  37. package/templates/workspace/.ai/memory/patterns.md +65 -65
  38. package/templates/workspace/.ai/patterns/backend/README.md +126 -126
  39. package/templates/workspace/.ai/patterns/frontend/README.md +103 -103
  40. package/templates/workspace/.ai/patterns/index.md +256 -256
  41. package/templates/workspace/.ai/patterns/triggers.json +1087 -1087
  42. package/templates/workspace/.ai/patterns/universal/README.md +141 -141
  43. package/templates/workspace/.ai/state.template.json +8 -8
  44. package/templates/workspace/.ai/tasks/active.md +77 -77
  45. package/templates/workspace/.ai/tasks/backlog.md +95 -95
  46. 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
+ }