thuban 0.3.2 → 0.3.3

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.
@@ -1,322 +1,322 @@
1
- /**
2
- * Sentinel Knowledge Interface
3
- *
4
- * Provides a unified API for other Orion systems to query codebase knowledge.
5
- * Used by: Astra (voice queries), CITADEL (enforcement), Forge (auto-widgets)
6
- *
7
- * @purpose Unified codebase knowledge API for Orion systems
8
- * @module sentinel
9
- * @layer infrastructure
10
- * @imports-from sentinel/dependency-graph.js, sentinel/widget-generator.js, sentinel/code-scanner.js
11
- * @exports-to astra-service, citadel/enforcer, forge, server
12
- * @side-effects File system reads
13
- * @critical-for System-wide codebase awareness
14
- */
15
-
16
- const path = require('path');
17
- const fs = require('fs');
18
- const DependencyGraph = require('./dependency-graph');
19
- const WidgetGenerator = require('./widget-generator');
20
- const CodeScanner = require('./code-scanner');
21
-
22
- // Singleton instances
23
- let dependencyGraph = null;
24
- let widgetGenerator = null;
25
- let codeScanner = null;
26
- let initialized = false;
27
- let rootPath = null;
28
-
29
- /**
30
- * Initialize Sentinel Knowledge (call once at startup)
31
- */
32
- async function initialize(config = {}) {
33
- rootPath = config.rootPath || process.cwd();
34
-
35
- console.log('[SENTINEL-KNOWLEDGE] Initializing codebase knowledge...');
36
-
37
- // Initialize dependency graph
38
- dependencyGraph = new DependencyGraph({ rootPath });
39
- await dependencyGraph.build();
40
-
41
- // Initialize widget generator (with graph)
42
- widgetGenerator = new WidgetGenerator({
43
- rootPath,
44
- dependencyGraph,
45
- dryRun: true // Default to dry run
46
- });
47
-
48
- // Initialize code scanner
49
- codeScanner = new CodeScanner({ rootPath });
50
-
51
- initialized = true;
52
- console.log('[SENTINEL-KNOWLEDGE] Codebase knowledge ready');
53
-
54
- return getStats();
55
- }
56
-
57
- /**
58
- * Ensure initialized
59
- */
60
- async function ensureInitialized() {
61
- if (!initialized) {
62
- await initialize();
63
- }
64
- }
65
-
66
- // ============================================================
67
- // QUERY FUNCTIONS (for Astra and other systems)
68
- // ============================================================
69
-
70
- /**
71
- * Get impact analysis for a file
72
- * "What will break if I change this file?"
73
- */
74
- async function getImpact(filePath) {
75
- await ensureInitialized();
76
-
77
- const fullPath = path.isAbsolute(filePath)
78
- ? filePath
79
- : path.join(rootPath, filePath);
80
-
81
- return dependencyGraph.getImpactAnalysis(fullPath);
82
- }
83
-
84
- /**
85
- * Get what a file depends on
86
- */
87
- async function getDependencies(filePath) {
88
- await ensureInitialized();
89
-
90
- const fullPath = path.isAbsolute(filePath)
91
- ? filePath
92
- : path.join(rootPath, filePath);
93
-
94
- const deps = dependencyGraph.getDependencies(fullPath);
95
- return deps.map(d => ({
96
- module: d.module,
97
- resolved: d.resolved ? path.relative(rootPath, d.resolved) : null
98
- }));
99
- }
100
-
101
- /**
102
- * Get what depends on a file
103
- */
104
- async function getDependents(filePath) {
105
- await ensureInitialized();
106
-
107
- const fullPath = path.isAbsolute(filePath)
108
- ? filePath
109
- : path.join(rootPath, filePath);
110
-
111
- const deps = dependencyGraph.getDependents(fullPath);
112
- return deps.map(d => path.relative(rootPath, d));
113
- }
114
-
115
- /**
116
- * Get most critical files in the codebase
117
- */
118
- async function getCriticalFiles(limit = 10) {
119
- await ensureInitialized();
120
- return dependencyGraph.getMostCriticalFiles(limit);
121
- }
122
-
123
- /**
124
- * Get module summary
125
- */
126
- async function getModules() {
127
- await ensureInitialized();
128
- return dependencyGraph.getModuleSummary();
129
- }
130
-
131
- /**
132
- * Get orphan files (potential dead code)
133
- */
134
- async function getOrphans() {
135
- await ensureInitialized();
136
- return dependencyGraph.getOrphanFiles();
137
- }
138
-
139
- /**
140
- * Get overall codebase stats
141
- */
142
- async function getStats() {
143
- await ensureInitialized();
144
- return {
145
- ...dependencyGraph.getStats(),
146
- initialized,
147
- rootPath
148
- };
149
- }
150
-
151
- /**
152
- * Natural language query handler (for Astra)
153
- */
154
- async function query(question) {
155
- await ensureInitialized();
156
-
157
- const q = question.toLowerCase();
158
-
159
- // Impact queries
160
- if (q.includes('break') || q.includes('impact') || q.includes('affect')) {
161
- const fileMatch = question.match(/['"]?([a-zA-Z0-9\-_./\\]+\.js)['"]?/);
162
- if (fileMatch) {
163
- const impact = await getImpact(fileMatch[1]);
164
- return {
165
- type: 'impact',
166
- file: fileMatch[1],
167
- result: impact,
168
- summary: `Changing ${fileMatch[1]} could affect ${impact.totalImpact} files. Risk level: ${impact.riskLevel.toUpperCase()}.`
169
- };
170
- }
171
- }
172
-
173
- // Dependency queries
174
- if (q.includes('depend') || q.includes('import') || q.includes('use')) {
175
- const fileMatch = question.match(/['"]?([a-zA-Z0-9\-_./\\]+\.js)['"]?/);
176
- if (fileMatch) {
177
- if (q.includes('on') || q.includes('import')) {
178
- // What does X depend on?
179
- const deps = await getDependencies(fileMatch[1]);
180
- return {
181
- type: 'dependencies',
182
- file: fileMatch[1],
183
- result: deps,
184
- summary: `${fileMatch[1]} depends on ${deps.length} files: ${deps.slice(0, 5).map(d => d.module).join(', ')}${deps.length > 5 ? '...' : ''}`
185
- };
186
- } else {
187
- // What depends on X?
188
- const deps = await getDependents(fileMatch[1]);
189
- return {
190
- type: 'dependents',
191
- file: fileMatch[1],
192
- result: deps,
193
- summary: `${deps.length} files depend on ${fileMatch[1]}: ${deps.slice(0, 5).join(', ')}${deps.length > 5 ? '...' : ''}`
194
- };
195
- }
196
- }
197
- }
198
-
199
- // Critical files query
200
- if (q.includes('critical') || q.includes('important') || q.includes('core')) {
201
- const critical = await getCriticalFiles(10);
202
- return {
203
- type: 'critical',
204
- result: critical,
205
- summary: `Top critical files: ${critical.slice(0, 5).map(f => f.relativePath).join(', ')}`
206
- };
207
- }
208
-
209
- // Module query
210
- if (q.includes('module') || q.includes('system') || q.includes('component')) {
211
- const modules = await getModules();
212
- return {
213
- type: 'modules',
214
- result: modules,
215
- summary: `Codebase has ${modules.length} modules. Largest: ${modules.slice(0, 3).map(m => `${m.name} (${m.fileCount} files)`).join(', ')}`
216
- };
217
- }
218
-
219
- // Orphan/dead code query
220
- if (q.includes('orphan') || q.includes('dead') || q.includes('unused')) {
221
- const orphans = await getOrphans();
222
- return {
223
- type: 'orphans',
224
- result: orphans,
225
- summary: `Found ${orphans.length} potentially unused files: ${orphans.slice(0, 5).map(o => o.relativePath).join(', ')}${orphans.length > 5 ? '...' : ''}`
226
- };
227
- }
228
-
229
- // Stats query
230
- if (q.includes('stat') || q.includes('overview') || q.includes('summary')) {
231
- const stats = await getStats();
232
- return {
233
- type: 'stats',
234
- result: stats,
235
- summary: `Codebase: ${stats.totalFiles} files, ${stats.totalImports} imports, ${stats.circularCount} circular dependencies`
236
- };
237
- }
238
-
239
- // Default
240
- return {
241
- type: 'unknown',
242
- result: null,
243
- summary: 'I can answer questions about: file impact, dependencies, critical files, modules, orphan/dead code, and codebase stats.'
244
- };
245
- }
246
-
247
- // ============================================================
248
- // WIDGET FUNCTIONS (for CITADEL and Forge)
249
- // ============================================================
250
-
251
- /**
252
- * Check if a file has a valid widget
253
- */
254
- function hasWidget(filePath) {
255
- try {
256
- const fullPath = path.isAbsolute(filePath)
257
- ? filePath
258
- : path.join(rootPath, filePath);
259
-
260
- const content = fs.readFileSync(fullPath, 'utf8');
261
- const widgetPattern = /\/\*\*[\s\S]*?@(purpose|module|layer)[\s\S]*?\*\//;
262
- return widgetPattern.test(content);
263
- } catch (e) {
264
- return false;
265
- }
266
- }
267
-
268
- /**
269
- * Generate widget for a file (returns string, doesn't write)
270
- */
271
- async function generateWidget(filePath, content = null) {
272
- await ensureInitialized();
273
-
274
- const fullPath = path.isAbsolute(filePath)
275
- ? filePath
276
- : path.join(rootPath, filePath);
277
-
278
- const widget = await widgetGenerator.generateWidget(fullPath, content);
279
- return widget.widgetString;
280
- }
281
-
282
- /**
283
- * Generate and prepend widget to content
284
- */
285
- async function addWidgetToContent(filePath, content) {
286
- const widgetString = await generateWidget(filePath, content);
287
- return widgetString + '\n\n' + content;
288
- }
289
-
290
- /**
291
- * Refresh the dependency graph (call after files change)
292
- */
293
- async function refresh() {
294
- if (dependencyGraph) {
295
- await dependencyGraph.build();
296
- }
297
- }
298
-
299
- // ============================================================
300
- // EXPORT
301
- // ============================================================
302
-
303
- module.exports = {
304
- // Initialization
305
- initialize,
306
- refresh,
307
-
308
- // Query functions (for Astra)
309
- query,
310
- getImpact,
311
- getDependencies,
312
- getDependents,
313
- getCriticalFiles,
314
- getModules,
315
- getOrphans,
316
- getStats,
317
-
318
- // Widget functions (for CITADEL/Forge)
319
- hasWidget,
320
- generateWidget,
321
- addWidgetToContent
322
- };
1
+ /**
2
+ * Sentinel Knowledge Interface
3
+ *
4
+ * Provides a unified API for other Orion systems to query codebase knowledge.
5
+ * Used by: Astra (voice queries), CITADEL (enforcement), Forge (auto-widgets)
6
+ *
7
+ * @purpose Unified codebase knowledge API for Orion systems
8
+ * @module sentinel
9
+ * @layer infrastructure
10
+ * @imports-from sentinel/dependency-graph.js, sentinel/widget-generator.js, sentinel/code-scanner.js
11
+ * @exports-to astra-service, citadel/enforcer, forge, server
12
+ * @side-effects File system reads
13
+ * @critical-for System-wide codebase awareness
14
+ */
15
+
16
+ const path = require('path');
17
+ const fs = require('fs');
18
+ const DependencyGraph = require('./dependency-graph');
19
+ const WidgetGenerator = require('./widget-generator');
20
+ const CodeScanner = require('./code-scanner');
21
+
22
+ // Singleton instances
23
+ let dependencyGraph = null;
24
+ let widgetGenerator = null;
25
+ let codeScanner = null;
26
+ let initialized = false;
27
+ let rootPath = null;
28
+
29
+ /**
30
+ * Initialize Sentinel Knowledge (call once at startup)
31
+ */
32
+ async function initialize(config = {}) {
33
+ rootPath = config.rootPath || process.cwd();
34
+
35
+ console.log('[SENTINEL-KNOWLEDGE] Initializing codebase knowledge...');
36
+
37
+ // Initialize dependency graph
38
+ dependencyGraph = new DependencyGraph({ rootPath });
39
+ await dependencyGraph.build();
40
+
41
+ // Initialize widget generator (with graph)
42
+ widgetGenerator = new WidgetGenerator({
43
+ rootPath,
44
+ dependencyGraph,
45
+ dryRun: true // Default to dry run
46
+ });
47
+
48
+ // Initialize code scanner
49
+ codeScanner = new CodeScanner({ rootPath });
50
+
51
+ initialized = true;
52
+ console.log('[SENTINEL-KNOWLEDGE] Codebase knowledge ready');
53
+
54
+ return getStats();
55
+ }
56
+
57
+ /**
58
+ * Ensure initialized
59
+ */
60
+ async function ensureInitialized() {
61
+ if (!initialized) {
62
+ await initialize();
63
+ }
64
+ }
65
+
66
+ // ============================================================
67
+ // QUERY FUNCTIONS (for Astra and other systems)
68
+ // ============================================================
69
+
70
+ /**
71
+ * Get impact analysis for a file
72
+ * "What will break if I change this file?"
73
+ */
74
+ async function getImpact(filePath) {
75
+ await ensureInitialized();
76
+
77
+ const fullPath = path.isAbsolute(filePath)
78
+ ? filePath
79
+ : path.join(rootPath, filePath);
80
+
81
+ return dependencyGraph.getImpactAnalysis(fullPath);
82
+ }
83
+
84
+ /**
85
+ * Get what a file depends on
86
+ */
87
+ async function getDependencies(filePath) {
88
+ await ensureInitialized();
89
+
90
+ const fullPath = path.isAbsolute(filePath)
91
+ ? filePath
92
+ : path.join(rootPath, filePath);
93
+
94
+ const deps = dependencyGraph.getDependencies(fullPath);
95
+ return deps.map(d => ({
96
+ module: d.module,
97
+ resolved: d.resolved ? path.relative(rootPath, d.resolved) : null
98
+ }));
99
+ }
100
+
101
+ /**
102
+ * Get what depends on a file
103
+ */
104
+ async function getDependents(filePath) {
105
+ await ensureInitialized();
106
+
107
+ const fullPath = path.isAbsolute(filePath)
108
+ ? filePath
109
+ : path.join(rootPath, filePath);
110
+
111
+ const deps = dependencyGraph.getDependents(fullPath);
112
+ return deps.map(d => path.relative(rootPath, d));
113
+ }
114
+
115
+ /**
116
+ * Get most critical files in the codebase
117
+ */
118
+ async function getCriticalFiles(limit = 10) {
119
+ await ensureInitialized();
120
+ return dependencyGraph.getMostCriticalFiles(limit);
121
+ }
122
+
123
+ /**
124
+ * Get module summary
125
+ */
126
+ async function getModules() {
127
+ await ensureInitialized();
128
+ return dependencyGraph.getModuleSummary();
129
+ }
130
+
131
+ /**
132
+ * Get orphan files (potential dead code)
133
+ */
134
+ async function getOrphans() {
135
+ await ensureInitialized();
136
+ return dependencyGraph.getOrphanFiles();
137
+ }
138
+
139
+ /**
140
+ * Get overall codebase stats
141
+ */
142
+ async function getStats() {
143
+ await ensureInitialized();
144
+ return {
145
+ ...dependencyGraph.getStats(),
146
+ initialized,
147
+ rootPath
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Natural language query handler (for Astra)
153
+ */
154
+ async function query(question) {
155
+ await ensureInitialized();
156
+
157
+ const q = question.toLowerCase();
158
+
159
+ // Impact queries
160
+ if (q.includes('break') || q.includes('impact') || q.includes('affect')) {
161
+ const fileMatch = question.match(/['"]?([a-zA-Z0-9\-_./\\]+\.js)['"]?/);
162
+ if (fileMatch) {
163
+ const impact = await getImpact(fileMatch[1]);
164
+ return {
165
+ type: 'impact',
166
+ file: fileMatch[1],
167
+ result: impact,
168
+ summary: `Changing ${fileMatch[1]} could affect ${impact.totalImpact} files. Risk level: ${impact.riskLevel.toUpperCase()}.`
169
+ };
170
+ }
171
+ }
172
+
173
+ // Dependency queries
174
+ if (q.includes('depend') || q.includes('import') || q.includes('use')) {
175
+ const fileMatch = question.match(/['"]?([a-zA-Z0-9\-_./\\]+\.js)['"]?/);
176
+ if (fileMatch) {
177
+ if (q.includes('on') || q.includes('import')) {
178
+ // What does X depend on?
179
+ const deps = await getDependencies(fileMatch[1]);
180
+ return {
181
+ type: 'dependencies',
182
+ file: fileMatch[1],
183
+ result: deps,
184
+ summary: `${fileMatch[1]} depends on ${deps.length} files: ${deps.slice(0, 5).map(d => d.module).join(', ')}${deps.length > 5 ? '...' : ''}`
185
+ };
186
+ } else {
187
+ // What depends on X?
188
+ const deps = await getDependents(fileMatch[1]);
189
+ return {
190
+ type: 'dependents',
191
+ file: fileMatch[1],
192
+ result: deps,
193
+ summary: `${deps.length} files depend on ${fileMatch[1]}: ${deps.slice(0, 5).join(', ')}${deps.length > 5 ? '...' : ''}`
194
+ };
195
+ }
196
+ }
197
+ }
198
+
199
+ // Critical files query
200
+ if (q.includes('critical') || q.includes('important') || q.includes('core')) {
201
+ const critical = await getCriticalFiles(10);
202
+ return {
203
+ type: 'critical',
204
+ result: critical,
205
+ summary: `Top critical files: ${critical.slice(0, 5).map(f => f.relativePath).join(', ')}`
206
+ };
207
+ }
208
+
209
+ // Module query
210
+ if (q.includes('module') || q.includes('system') || q.includes('component')) {
211
+ const modules = await getModules();
212
+ return {
213
+ type: 'modules',
214
+ result: modules,
215
+ summary: `Codebase has ${modules.length} modules. Largest: ${modules.slice(0, 3).map(m => `${m.name} (${m.fileCount} files)`).join(', ')}`
216
+ };
217
+ }
218
+
219
+ // Orphan/dead code query
220
+ if (q.includes('orphan') || q.includes('dead') || q.includes('unused')) {
221
+ const orphans = await getOrphans();
222
+ return {
223
+ type: 'orphans',
224
+ result: orphans,
225
+ summary: `Found ${orphans.length} potentially unused files: ${orphans.slice(0, 5).map(o => o.relativePath).join(', ')}${orphans.length > 5 ? '...' : ''}`
226
+ };
227
+ }
228
+
229
+ // Stats query
230
+ if (q.includes('stat') || q.includes('overview') || q.includes('summary')) {
231
+ const stats = await getStats();
232
+ return {
233
+ type: 'stats',
234
+ result: stats,
235
+ summary: `Codebase: ${stats.totalFiles} files, ${stats.totalImports} imports, ${stats.circularCount} circular dependencies`
236
+ };
237
+ }
238
+
239
+ // Default
240
+ return {
241
+ type: 'unknown',
242
+ result: null,
243
+ summary: 'I can answer questions about: file impact, dependencies, critical files, modules, orphan/dead code, and codebase stats.'
244
+ };
245
+ }
246
+
247
+ // ============================================================
248
+ // WIDGET FUNCTIONS (for CITADEL and Forge)
249
+ // ============================================================
250
+
251
+ /**
252
+ * Check if a file has a valid widget
253
+ */
254
+ function hasWidget(filePath) {
255
+ try {
256
+ const fullPath = path.isAbsolute(filePath)
257
+ ? filePath
258
+ : path.join(rootPath, filePath);
259
+
260
+ const content = fs.readFileSync(fullPath, 'utf8');
261
+ const widgetPattern = /\/\*\*[\s\S]*?@(purpose|module|layer)[\s\S]*?\*\//;
262
+ return widgetPattern.test(content);
263
+ } catch (e) {
264
+ return false;
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Generate widget for a file (returns string, doesn't write)
270
+ */
271
+ async function generateWidget(filePath, content = null) {
272
+ await ensureInitialized();
273
+
274
+ const fullPath = path.isAbsolute(filePath)
275
+ ? filePath
276
+ : path.join(rootPath, filePath);
277
+
278
+ const widget = await widgetGenerator.generateWidget(fullPath, content);
279
+ return widget.widgetString;
280
+ }
281
+
282
+ /**
283
+ * Generate and prepend widget to content
284
+ */
285
+ async function addWidgetToContent(filePath, content) {
286
+ const widgetString = await generateWidget(filePath, content);
287
+ return widgetString + '\n\n' + content;
288
+ }
289
+
290
+ /**
291
+ * Refresh the dependency graph (call after files change)
292
+ */
293
+ async function refresh() {
294
+ if (dependencyGraph) {
295
+ await dependencyGraph.build();
296
+ }
297
+ }
298
+
299
+ // ============================================================
300
+ // EXPORT
301
+ // ============================================================
302
+
303
+ module.exports = {
304
+ // Initialization
305
+ initialize,
306
+ refresh,
307
+
308
+ // Query functions (for Astra)
309
+ query,
310
+ getImpact,
311
+ getDependencies,
312
+ getDependents,
313
+ getCriticalFiles,
314
+ getModules,
315
+ getOrphans,
316
+ getStats,
317
+
318
+ // Widget functions (for CITADEL/Forge)
319
+ hasWidget,
320
+ generateWidget,
321
+ addWidgetToContent
322
+ };