@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.
Files changed (48) 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 +112 -91
  6. package/cli/inject.js +90 -67
  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/save.js +206 -0
  12. package/cli/status.js +91 -91
  13. package/cli/update.js +189 -0
  14. package/cli/validate.js +61 -61
  15. package/package.json +70 -70
  16. package/src/core/inject.js +300 -204
  17. package/src/core/state.js +91 -91
  18. package/src/core/validate.js +202 -202
  19. package/src/core/workspace.js +176 -176
  20. package/src/index.js +20 -20
  21. package/src/knowledge/gc.js +308 -308
  22. package/src/knowledge/lifecycle.js +401 -401
  23. package/src/knowledge/promote.js +364 -364
  24. package/src/knowledge/references.js +342 -342
  25. package/src/patterns/matcher.js +218 -218
  26. package/src/patterns/registry.js +375 -375
  27. package/src/patterns/triggers.js +423 -423
  28. package/src/services/batch-service.js +849 -849
  29. package/src/services/notification-service.js +342 -342
  30. package/templates/codespaces/devcontainer.json +52 -52
  31. package/templates/codespaces/setup.sh +70 -70
  32. package/templates/workspace/.ai/README.md +164 -164
  33. package/templates/workspace/.ai/agents/README.md +204 -204
  34. package/templates/workspace/.ai/agents/claude.md +625 -625
  35. package/templates/workspace/.ai/config/README.md +79 -79
  36. package/templates/workspace/.ai/config/notifications.template.json +20 -20
  37. package/templates/workspace/.ai/memory/deadends.md +79 -79
  38. package/templates/workspace/.ai/memory/decisions.md +58 -58
  39. package/templates/workspace/.ai/memory/patterns.md +65 -65
  40. package/templates/workspace/.ai/patterns/backend/README.md +126 -126
  41. package/templates/workspace/.ai/patterns/frontend/README.md +103 -103
  42. package/templates/workspace/.ai/patterns/index.md +256 -256
  43. package/templates/workspace/.ai/patterns/triggers.json +1087 -1087
  44. package/templates/workspace/.ai/patterns/universal/README.md +141 -141
  45. package/templates/workspace/.ai/state.template.json +8 -8
  46. package/templates/workspace/.ai/tasks/active.md +77 -77
  47. package/templates/workspace/.ai/tasks/backlog.md +95 -95
  48. 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
+ }