nubos-pilot 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,450 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ const LANGUAGE_BY_EXT = Object.freeze({
5
+ '.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript', '.jsx': 'javascript',
6
+ '.ts': 'typescript', '.tsx': 'typescript', '.mts': 'typescript', '.cts': 'typescript',
7
+ '.py': 'python', '.pyi': 'python',
8
+ '.go': 'go',
9
+ '.rs': 'rust',
10
+ '.php': 'php',
11
+ '.rb': 'ruby',
12
+ '.java': 'java', '.kt': 'kotlin', '.kts': 'kotlin',
13
+ '.cs': 'csharp',
14
+ '.swift': 'swift',
15
+ '.c': 'c', '.h': 'c',
16
+ '.cpp': 'cpp', '.hpp': 'cpp', '.cc': 'cpp', '.hh': 'cpp',
17
+ '.ex': 'elixir', '.exs': 'elixir',
18
+ '.erl': 'erlang',
19
+ '.scala': 'scala',
20
+ '.sh': 'shell', '.bash': 'shell', '.zsh': 'shell',
21
+ '.sql': 'sql',
22
+ '.vue': 'vue', '.svelte': 'svelte',
23
+ '.html': 'html', '.css': 'css', '.scss': 'css', '.less': 'css',
24
+ '.json': 'json', '.yaml': 'yaml', '.yml': 'yaml', '.toml': 'toml',
25
+ '.md': 'markdown',
26
+ });
27
+
28
+ const NON_CODE_EXTS = new Set([
29
+ '.md', '.json', '.yaml', '.yml', '.toml', '.lock',
30
+ '.txt', '.log', '.csv', '.tsv',
31
+ '.html', '.css', '.scss', '.less',
32
+ ]);
33
+
34
+ const SYMBOL_PATTERNS = {
35
+ javascript: [
36
+ /^\s*export\s+(?:default\s+)?(?:async\s+)?(?:function|class)\s+([A-Za-z_$][\w$]*)/,
37
+ /^\s*export\s+(?:const|let|var)\s+([A-Za-z_$][\w$]*)/,
38
+ /^\s*export\s*\{\s*([^}]+)\s*\}/,
39
+ /^\s*module\.exports\s*\.\s*([A-Za-z_$][\w$]*)\s*=/,
40
+ /^\s*exports\s*\.\s*([A-Za-z_$][\w$]*)\s*=/,
41
+ ],
42
+ typescript: [
43
+ /^\s*export\s+(?:default\s+)?(?:async\s+)?(?:function|class|interface|type|enum|const|let|var)\s+([A-Za-z_$][\w$]*)/,
44
+ /^\s*export\s*\{\s*([^}]+)\s*\}/,
45
+ ],
46
+ python: [
47
+ /^def\s+([A-Za-z_][\w]*)\s*\(/,
48
+ /^class\s+([A-Za-z_][\w]*)\s*[:(]/,
49
+ /^async\s+def\s+([A-Za-z_][\w]*)\s*\(/,
50
+ ],
51
+ go: [
52
+ /^func\s+(?:\([^)]+\)\s+)?([A-Z][\w]*)/,
53
+ /^type\s+([A-Z][\w]*)\s+/,
54
+ /^(?:var|const)\s+([A-Z][\w]*)/,
55
+ ],
56
+ rust: [
57
+ /^\s*pub\s+(?:async\s+)?fn\s+([A-Za-z_][\w]*)/,
58
+ /^\s*pub\s+(?:struct|enum|trait|mod|const|static|type)\s+([A-Za-z_][\w]*)/,
59
+ ],
60
+ php: [
61
+ /^\s*(?:abstract\s+|final\s+)?class\s+([A-Za-z_][\w]*)/,
62
+ /^\s*interface\s+([A-Za-z_][\w]*)/,
63
+ /^\s*trait\s+([A-Za-z_][\w]*)/,
64
+ /^\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?function\s+([A-Za-z_][\w]*)/,
65
+ /^\s*namespace\s+([A-Za-z_\\][\w\\]*)/,
66
+ ],
67
+ ruby: [
68
+ /^\s*class\s+([A-Z][\w]*)/,
69
+ /^\s*module\s+([A-Z][\w]*)/,
70
+ /^\s*def\s+(?:self\.)?([a-z_][\w]*[?!=]?)/,
71
+ ],
72
+ java: [
73
+ /^\s*(?:public\s+|protected\s+|private\s+)?(?:abstract\s+|final\s+|static\s+)?(?:class|interface|enum|record)\s+([A-Za-z_][\w]*)/,
74
+ /^\s*package\s+([\w.]+)/,
75
+ ],
76
+ kotlin: [
77
+ /^\s*(?:public\s+|internal\s+|private\s+)?(?:abstract\s+|open\s+|final\s+)?(?:class|interface|object|data\s+class)\s+([A-Za-z_][\w]*)/,
78
+ /^\s*fun\s+([A-Za-z_][\w]*)/,
79
+ ],
80
+ csharp: [
81
+ /^\s*(?:public\s+|internal\s+|protected\s+|private\s+)?(?:abstract\s+|sealed\s+|static\s+)?(?:class|interface|struct|enum|record)\s+([A-Za-z_][\w]*)/,
82
+ /^\s*namespace\s+([\w.]+)/,
83
+ ],
84
+ swift: [
85
+ /^\s*(?:public\s+|open\s+|internal\s+)?(?:class|struct|enum|protocol|actor)\s+([A-Za-z_][\w]*)/,
86
+ /^\s*(?:public\s+|open\s+|internal\s+)?func\s+([A-Za-z_][\w]*)/,
87
+ ],
88
+ };
89
+
90
+ const IMPORT_PATTERNS = {
91
+ javascript: [
92
+ /^\s*import\s+(?:[^'"`]+\s+from\s+)?['"`]([^'"`]+)['"`]/,
93
+ /\brequire\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/,
94
+ ],
95
+ typescript: [
96
+ /^\s*import\s+(?:[^'"`]+\s+from\s+)?['"`]([^'"`]+)['"`]/,
97
+ /^\s*import\s+type\s+[^'"`]+from\s+['"`]([^'"`]+)['"`]/,
98
+ ],
99
+ python: [
100
+ /^(?:from\s+([\w.]+)\s+import)/,
101
+ /^import\s+([\w.]+)/,
102
+ ],
103
+ go: [
104
+ /^\s*import\s+"([^"]+)"/,
105
+ /^\s*"([^"]+)"/,
106
+ ],
107
+ rust: [
108
+ /^\s*use\s+([\w:]+)/,
109
+ ],
110
+ php: [
111
+ /^\s*use\s+([\w\\]+)/,
112
+ /^\s*require(?:_once)?\s+['"]([^'"]+)['"]/,
113
+ /^\s*include(?:_once)?\s+['"]([^'"]+)['"]/,
114
+ ],
115
+ ruby: [
116
+ /^\s*require\s+['"]([^'"]+)['"]/,
117
+ /^\s*require_relative\s+['"]([^'"]+)['"]/,
118
+ ],
119
+ java: [
120
+ /^\s*import\s+([\w.]+);/,
121
+ ],
122
+ kotlin: [
123
+ /^\s*import\s+([\w.]+)/,
124
+ ],
125
+ csharp: [
126
+ /^\s*using\s+([\w.]+);/,
127
+ ],
128
+ swift: [
129
+ /^\s*import\s+([\w.]+)/,
130
+ ],
131
+ };
132
+
133
+ function languageForExt(ext) {
134
+ return LANGUAGE_BY_EXT[ext] || 'unknown';
135
+ }
136
+
137
+ function isCodeExt(ext) {
138
+ if (!ext) return false;
139
+ if (NON_CODE_EXTS.has(ext)) return false;
140
+ return Object.prototype.hasOwnProperty.call(LANGUAGE_BY_EXT, ext);
141
+ }
142
+
143
+ function _moduleIdFromDir(dir) {
144
+ if (!dir || dir === '.' || dir === '') return 'root';
145
+ return dir.replace(/\//g, '-').replace(/[^a-zA-Z0-9_-]+/g, '-').toLowerCase();
146
+ }
147
+
148
+ function groupFilesIntoModules(files) {
149
+ const codeFiles = files.filter((f) => isCodeExt(f.ext));
150
+ const byDir = new Map();
151
+ for (const f of codeFiles) {
152
+ const dir = path.posix.dirname(f.path);
153
+ const key = dir === '.' ? '' : dir;
154
+ if (!byDir.has(key)) byDir.set(key, []);
155
+ byDir.get(key).push(f);
156
+ }
157
+
158
+ const modules = [];
159
+ for (const [dir, members] of byDir.entries()) {
160
+ const langCounts = {};
161
+ for (const f of members) {
162
+ const lang = languageForExt(f.ext);
163
+ langCounts[lang] = (langCounts[lang] || 0) + 1;
164
+ }
165
+ const primaryLanguage = Object.entries(langCounts).sort((a, b) => b[1] - a[1])[0][0];
166
+ const paths = members.map((m) => m.path).sort();
167
+ modules.push({
168
+ id: _moduleIdFromDir(dir),
169
+ name: dir === '' ? 'root' : dir,
170
+ directory: dir,
171
+ primary_language: primaryLanguage,
172
+ language_distribution: langCounts,
173
+ source_paths: paths,
174
+ file_count: members.length,
175
+ });
176
+ }
177
+
178
+ modules.sort((a, b) => a.directory.localeCompare(b.directory));
179
+ return modules;
180
+ }
181
+
182
+ function _readFirstLines(absPath, maxLines) {
183
+ try {
184
+ const raw = fs.readFileSync(absPath, 'utf-8');
185
+ return raw.split(/\r?\n/).slice(0, maxLines);
186
+ } catch {
187
+ return [];
188
+ }
189
+ }
190
+
191
+ function extractSymbols(absPath, language) {
192
+ const patterns = SYMBOL_PATTERNS[language];
193
+ if (!patterns) return [];
194
+ const lines = _readFirstLines(absPath, 2000);
195
+ const syms = new Set();
196
+ for (const line of lines) {
197
+ for (const pat of patterns) {
198
+ const match = line.match(pat);
199
+ if (match && match[1]) {
200
+ const raw = match[1].trim();
201
+ if (raw.includes(',')) {
202
+ for (const part of raw.split(',')) {
203
+ const clean = part.trim().split(/\s+as\s+/)[0].trim();
204
+ if (clean) syms.add(clean);
205
+ }
206
+ } else {
207
+ syms.add(raw);
208
+ }
209
+ }
210
+ }
211
+ }
212
+ return Array.from(syms).sort();
213
+ }
214
+
215
+ function extractDeps(absPath, language) {
216
+ const patterns = IMPORT_PATTERNS[language];
217
+ if (!patterns) return [];
218
+ const lines = _readFirstLines(absPath, 2000);
219
+ const deps = new Set();
220
+ let inGoImportBlock = false;
221
+ for (const line of lines) {
222
+ if (language === 'go') {
223
+ if (/^\s*import\s*\(/.test(line)) { inGoImportBlock = true; continue; }
224
+ if (inGoImportBlock && /^\s*\)/.test(line)) { inGoImportBlock = false; continue; }
225
+ if (!inGoImportBlock && !/^\s*import\s+["']/.test(line)) continue;
226
+ }
227
+ for (const pat of patterns) {
228
+ const match = line.match(pat);
229
+ if (match && match[1]) {
230
+ deps.add(match[1].trim());
231
+ }
232
+ }
233
+ }
234
+ return Array.from(deps).sort();
235
+ }
236
+
237
+ function buildModuleFacts(module, projectRoot) {
238
+ const root = path.resolve(projectRoot);
239
+ const symbols = new Set();
240
+ const deps = new Set();
241
+ const perFile = [];
242
+ for (const rel of module.source_paths) {
243
+ const abs = path.join(root, rel);
244
+ const lang = languageForExt(path.extname(rel).toLowerCase());
245
+ const fileSymbols = extractSymbols(abs, lang);
246
+ const fileDeps = extractDeps(abs, lang);
247
+ for (const s of fileSymbols) symbols.add(s);
248
+ for (const d of fileDeps) deps.add(d);
249
+ perFile.push({
250
+ path: rel,
251
+ language: lang,
252
+ symbols: fileSymbols,
253
+ deps: fileDeps,
254
+ });
255
+ }
256
+ const internalDeps = Array.from(deps).filter((d) => d.startsWith('.') || d.startsWith('/')).sort();
257
+ const externalDeps = Array.from(deps).filter((d) => !d.startsWith('.') && !d.startsWith('/')).sort();
258
+ return {
259
+ id: module.id,
260
+ name: module.name,
261
+ directory: module.directory,
262
+ primary_language: module.primary_language,
263
+ language_distribution: module.language_distribution,
264
+ file_count: module.file_count,
265
+ source_paths: module.source_paths,
266
+ symbols: Array.from(symbols).sort(),
267
+ internal_deps: internalDeps,
268
+ external_deps: externalDeps,
269
+ files: perFile,
270
+ };
271
+ }
272
+
273
+ function buildDocumenterPrompt(facts) {
274
+ const lines = [];
275
+ lines.push('You are the np-codebase-documenter agent.');
276
+ lines.push('Produce prose sections for the module below. Output JSON only:');
277
+ lines.push('');
278
+ lines.push('```json');
279
+ lines.push('{');
280
+ lines.push(' "description": "one-sentence summary",');
281
+ lines.push(' "purpose": "2-4 sentences on why this module exists",');
282
+ lines.push(' "key_concepts": ["bullet", "bullet"],');
283
+ lines.push(' "public_api": "markdown describing the public surface",');
284
+ lines.push(' "invariants": ["must-hold-true rules"],');
285
+ lines.push(' "gotchas": ["non-obvious behaviors"]');
286
+ lines.push('}');
287
+ lines.push('```');
288
+ lines.push('');
289
+ lines.push('Module facts (from deterministic parser — treat as ground truth):');
290
+ lines.push('```json');
291
+ lines.push(JSON.stringify(facts, null, 2));
292
+ lines.push('```');
293
+ lines.push('');
294
+ lines.push('Rules:');
295
+ lines.push('- Ground every claim in the facts. Do not invent symbols or deps.');
296
+ lines.push('- Keep prose in English.');
297
+ lines.push('- If the module is tiny or trivial, say so — do not pad.');
298
+ lines.push('- No marketing language.');
299
+ return lines.join('\n');
300
+ }
301
+
302
+ function renderModuleDoc(facts, prose, sourceHashes) {
303
+ const fm = [];
304
+ fm.push('---');
305
+ fm.push('name: ' + facts.name);
306
+ fm.push('description: ' + (prose && prose.description ? _escapeYaml(prose.description) : ''));
307
+ fm.push('kind: module');
308
+ fm.push('module_id: ' + facts.id);
309
+ fm.push('directory: ' + facts.directory);
310
+ fm.push('primary_language: ' + facts.primary_language);
311
+ fm.push('file_count: ' + facts.file_count);
312
+ fm.push('source_paths:');
313
+ for (const p of facts.source_paths) fm.push(' - ' + p);
314
+ fm.push('symbols:');
315
+ for (const s of facts.symbols) fm.push(' - ' + s);
316
+ if (facts.external_deps.length > 0) {
317
+ fm.push('external_deps:');
318
+ for (const d of facts.external_deps) fm.push(' - ' + d);
319
+ }
320
+ if (facts.internal_deps.length > 0) {
321
+ fm.push('internal_deps:');
322
+ for (const d of facts.internal_deps) fm.push(' - ' + d);
323
+ }
324
+ fm.push('source_hashes:');
325
+ for (const p of facts.source_paths) {
326
+ const h = sourceHashes && sourceHashes[p] ? sourceHashes[p] : '';
327
+ fm.push(' ' + p + ': ' + h);
328
+ }
329
+ fm.push('last_documented: ' + new Date().toISOString().slice(0, 10));
330
+ fm.push('---');
331
+ fm.push('');
332
+ fm.push('# ' + facts.name);
333
+ fm.push('');
334
+ fm.push('## Purpose');
335
+ fm.push('');
336
+ fm.push((prose && prose.purpose) || '_TBD — run np:update-docs to populate._');
337
+ fm.push('');
338
+ fm.push('## Key Concepts');
339
+ fm.push('');
340
+ const keys = (prose && Array.isArray(prose.key_concepts)) ? prose.key_concepts : [];
341
+ if (keys.length === 0) fm.push('_TBD_');
342
+ else for (const k of keys) fm.push('- ' + k);
343
+ fm.push('');
344
+ fm.push('## Public API');
345
+ fm.push('');
346
+ fm.push((prose && prose.public_api) || '_TBD_');
347
+ fm.push('');
348
+ fm.push('## Invariants');
349
+ fm.push('');
350
+ const invs = (prose && Array.isArray(prose.invariants)) ? prose.invariants : [];
351
+ if (invs.length === 0) fm.push('_None documented yet._');
352
+ else for (const inv of invs) fm.push('- ' + inv);
353
+ fm.push('');
354
+ fm.push('## Gotchas');
355
+ fm.push('');
356
+ const got = (prose && Array.isArray(prose.gotchas)) ? prose.gotchas : [];
357
+ if (got.length === 0) fm.push('_None documented yet._');
358
+ else for (const g of got) fm.push('- ' + g);
359
+ fm.push('');
360
+ fm.push('## Files');
361
+ fm.push('');
362
+ for (const file of facts.files) {
363
+ fm.push('### `' + file.path + '`');
364
+ fm.push('');
365
+ fm.push('- Language: ' + file.language);
366
+ if (file.symbols.length > 0) fm.push('- Symbols: ' + file.symbols.join(', '));
367
+ if (file.deps.length > 0) fm.push('- Deps: ' + file.deps.join(', '));
368
+ fm.push('');
369
+ }
370
+ return fm.join('\n');
371
+ }
372
+
373
+ function _escapeYaml(s) {
374
+ const str = String(s);
375
+ if (str.includes('\n') || str.includes(':') || str.includes('#')) {
376
+ return '"' + str.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
377
+ }
378
+ return str;
379
+ }
380
+
381
+ function buildIndexDoc(modules, meta) {
382
+ const lines = [];
383
+ lines.push('<!-- Generated by np:scan-codebase / np:update-docs. Do not edit by hand. -->');
384
+ lines.push('');
385
+ lines.push('# Codebase Index');
386
+ lines.push('');
387
+ if (meta && meta.project_name) {
388
+ lines.push('**Project:** ' + meta.project_name);
389
+ }
390
+ lines.push('**Generated:** ' + new Date().toISOString().slice(0, 10));
391
+ lines.push('**Modules:** ' + modules.length);
392
+ lines.push('');
393
+ lines.push('> Dev-Agents MUST read this index and relevant module docs before modifying code.');
394
+ lines.push('> After code changes, run `np:update-docs <path>` to refresh affected module docs.');
395
+ lines.push('');
396
+ lines.push('## Modules');
397
+ lines.push('');
398
+ for (const mod of modules) {
399
+ const line = '- [`' + (mod.directory || 'root') + '`](modules/' + mod.id + '.md) — ' +
400
+ mod.file_count + ' file' + (mod.file_count === 1 ? '' : 's') + ' · ' +
401
+ mod.primary_language;
402
+ lines.push(line);
403
+ }
404
+ lines.push('');
405
+ return lines.join('\n');
406
+ }
407
+
408
+ function moduleDocPath(projectRoot, moduleId) {
409
+ return path.join(
410
+ path.resolve(projectRoot),
411
+ '.nubos-pilot',
412
+ 'codebase',
413
+ 'modules',
414
+ moduleId + '.md',
415
+ );
416
+ }
417
+
418
+ function indexDocPath(projectRoot) {
419
+ return path.join(
420
+ path.resolve(projectRoot),
421
+ '.nubos-pilot',
422
+ 'codebase',
423
+ 'INDEX.md',
424
+ );
425
+ }
426
+
427
+ function buildDocIndexMap(modules) {
428
+ const index = {};
429
+ for (const mod of modules) {
430
+ const relDoc = path.posix.join('modules', mod.id + '.md');
431
+ index[relDoc] = mod.source_paths.slice();
432
+ }
433
+ return index;
434
+ }
435
+
436
+ module.exports = {
437
+ LANGUAGE_BY_EXT,
438
+ languageForExt,
439
+ isCodeExt,
440
+ groupFilesIntoModules,
441
+ extractSymbols,
442
+ extractDeps,
443
+ buildModuleFacts,
444
+ buildDocumenterPrompt,
445
+ renderModuleDoc,
446
+ buildIndexDoc,
447
+ buildDocIndexMap,
448
+ moduleDocPath,
449
+ indexDocPath,
450
+ };