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,216 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ const { NubosPilotError, atomicWriteFileSync } = require('../../lib/core.cjs');
5
+ const { scan } = require('../../lib/workspace-scan.cjs');
6
+ const { workspaceGitInfo } = require('../../lib/git.cjs');
7
+ const {
8
+ manifestFromScanFiles,
9
+ writeManifest,
10
+ readManifest,
11
+ diffManifest,
12
+ stalePathsForDocs,
13
+ } = require('../../lib/codebase-manifest.cjs');
14
+ const {
15
+ groupFilesIntoModules,
16
+ buildModuleFacts,
17
+ renderModuleDoc,
18
+ buildIndexDoc,
19
+ buildDocIndexMap,
20
+ moduleDocPath,
21
+ indexDocPath,
22
+ } = require('../../lib/codebase-docs.cjs');
23
+
24
+ function _parseArgs(args) {
25
+ const flags = {
26
+ cwd: null,
27
+ batchSize: 500,
28
+ maxFiles: 0,
29
+ applyProse: false,
30
+ moduleId: null,
31
+ proseFile: null,
32
+ paths: [],
33
+ };
34
+ for (let i = 0; i < (args || []).length; i++) {
35
+ const a = args[i];
36
+ if (a === '--cwd') flags.cwd = args[++i];
37
+ else if (a === '--batch-size') flags.batchSize = parseInt(args[++i], 10);
38
+ else if (a === '--max-files') flags.maxFiles = parseInt(args[++i], 10);
39
+ else if (a === '--apply-prose') flags.applyProse = true;
40
+ else if (a === '--module') flags.moduleId = args[++i];
41
+ else if (a === '--prose-file') flags.proseFile = args[++i];
42
+ else if (a === '--path') flags.paths.push(args[++i]);
43
+ }
44
+ return flags;
45
+ }
46
+
47
+ function _readDocIndex(projectRoot) {
48
+ const p = path.join(projectRoot, '.nubos-pilot', 'codebase', '.doc-index.json');
49
+ try {
50
+ return JSON.parse(fs.readFileSync(p, 'utf-8'));
51
+ } catch {
52
+ return {};
53
+ }
54
+ }
55
+
56
+ function _hashesLookupFromManifest(manifest) {
57
+ const lookup = {};
58
+ for (const [p, meta] of Object.entries(manifest.files || {})) {
59
+ lookup[p] = meta.sha256;
60
+ }
61
+ return lookup;
62
+ }
63
+
64
+ function _emitPlan(projectRoot, flags, stdout) {
65
+ const prev = readManifest(projectRoot);
66
+ const scanResult = scan({
67
+ cwd: projectRoot,
68
+ batchSize: flags.batchSize,
69
+ maxFiles: flags.maxFiles > 0 ? flags.maxFiles : undefined,
70
+ gitInfo: workspaceGitInfo,
71
+ });
72
+ const next = manifestFromScanFiles(scanResult.files);
73
+ const diff = diffManifest(prev, next);
74
+
75
+ const docIndex = _readDocIndex(projectRoot);
76
+ const staleInfo = stalePathsForDocs(diff, docIndex);
77
+
78
+ const groups = groupFilesIntoModules(scanResult.files);
79
+ const modulesById = new Map();
80
+ for (const g of groups) modulesById.set(g.id, g);
81
+
82
+ const staleModules = [];
83
+ for (const docPath of staleInfo.stale_docs) {
84
+ const parsedId = path.posix.basename(docPath).replace(/\.md$/, '');
85
+ const group = modulesById.get(parsedId);
86
+ if (!group) continue;
87
+ const facts = buildModuleFacts(group, projectRoot);
88
+ staleModules.push({
89
+ id: group.id,
90
+ directory: group.directory,
91
+ doc_path: docPath,
92
+ facts,
93
+ });
94
+ }
95
+
96
+ const hashLookup = _hashesLookupFromManifest(next);
97
+ const removedModules = [];
98
+ const currentIds = new Set(groups.map((g) => g.id));
99
+ for (const docRel of Object.keys(docIndex)) {
100
+ const id = path.posix.basename(docRel).replace(/\.md$/, '');
101
+ if (!currentIds.has(id)) removedModules.push({ id, doc_path: docRel });
102
+ }
103
+
104
+ const addedModules = [];
105
+ for (const g of groups) {
106
+ const relDoc = path.posix.join('modules', g.id + '.md');
107
+ if (!Object.prototype.hasOwnProperty.call(docIndex, relDoc)) {
108
+ const facts = buildModuleFacts(g, projectRoot);
109
+ addedModules.push({
110
+ id: g.id,
111
+ directory: g.directory,
112
+ doc_path: relDoc,
113
+ facts,
114
+ });
115
+ const absDoc = moduleDocPath(projectRoot, g.id);
116
+ if (!fs.existsSync(absDoc)) {
117
+ fs.mkdirSync(path.dirname(absDoc), { recursive: true });
118
+ atomicWriteFileSync(absDoc, renderModuleDoc(facts, null, hashLookup));
119
+ }
120
+ }
121
+ }
122
+
123
+ const newDocIndex = buildDocIndexMap(groups);
124
+ const indexMapPath = path.join(projectRoot, '.nubos-pilot', 'codebase', '.doc-index.json');
125
+ fs.mkdirSync(path.dirname(indexMapPath), { recursive: true });
126
+ atomicWriteFileSync(indexMapPath, JSON.stringify(newDocIndex, null, 2) + '\n');
127
+
128
+ const indexPath = indexDocPath(projectRoot);
129
+ atomicWriteFileSync(indexPath, buildIndexDoc(groups, {}));
130
+
131
+ writeManifest(projectRoot, next);
132
+
133
+ stdout.write(JSON.stringify({
134
+ mode: 'plan',
135
+ diff_summary: diff.summary,
136
+ touched_paths: staleInfo.touched_paths,
137
+ stale_modules: staleModules,
138
+ added_modules: addedModules,
139
+ removed_modules: removedModules,
140
+ }, null, 2));
141
+ }
142
+
143
+ function _applyProse(projectRoot, flags, stdout) {
144
+ if (!flags.moduleId) {
145
+ throw new NubosPilotError('update-docs-missing-module', '--apply-prose requires --module <id>', {});
146
+ }
147
+ if (!flags.proseFile) {
148
+ throw new NubosPilotError('update-docs-missing-prose', '--apply-prose requires --prose-file <path>', {});
149
+ }
150
+ let prose;
151
+ try {
152
+ prose = JSON.parse(fs.readFileSync(flags.proseFile, 'utf-8'));
153
+ } catch (err) {
154
+ throw new NubosPilotError(
155
+ 'update-docs-prose-unreadable',
156
+ 'prose file not readable or not valid JSON: ' + flags.proseFile,
157
+ { path: flags.proseFile, cause: err && err.message },
158
+ );
159
+ }
160
+
161
+ const scanResult = scan({
162
+ cwd: projectRoot,
163
+ batchSize: flags.batchSize,
164
+ maxFiles: flags.maxFiles > 0 ? flags.maxFiles : undefined,
165
+ gitInfo: workspaceGitInfo,
166
+ });
167
+ const groups = groupFilesIntoModules(scanResult.files);
168
+ const target = groups.find((g) => g.id === flags.moduleId);
169
+ if (!target) {
170
+ throw new NubosPilotError(
171
+ 'update-docs-module-not-found',
172
+ `module not found: ${flags.moduleId}`,
173
+ { moduleId: flags.moduleId },
174
+ );
175
+ }
176
+
177
+ const facts = buildModuleFacts(target, projectRoot);
178
+ const manifest = manifestFromScanFiles(scanResult.files);
179
+ const hashLookup = _hashesLookupFromManifest(manifest);
180
+
181
+ const docPath = moduleDocPath(projectRoot, target.id);
182
+ fs.mkdirSync(path.dirname(docPath), { recursive: true });
183
+ atomicWriteFileSync(docPath, renderModuleDoc(facts, prose, hashLookup));
184
+
185
+ writeManifest(projectRoot, manifest);
186
+
187
+ stdout.write(JSON.stringify({
188
+ mode: 'apply-prose',
189
+ module_id: target.id,
190
+ doc_path: path.relative(projectRoot, docPath),
191
+ }, null, 2));
192
+ }
193
+
194
+ function run(args, ctx) {
195
+ const context = ctx || {};
196
+ const stdout = context.stdout || process.stdout;
197
+ const flags = _parseArgs(args);
198
+ const projectRoot = path.resolve(flags.cwd || context.cwd || process.cwd());
199
+
200
+ const stateDir = path.join(projectRoot, '.nubos-pilot');
201
+ if (!fs.existsSync(stateDir)) {
202
+ throw new NubosPilotError(
203
+ 'update-docs-not-initialized',
204
+ '.nubos-pilot/ not found — run np:new-project first',
205
+ { cwd: projectRoot },
206
+ );
207
+ }
208
+
209
+ if (flags.applyProse) {
210
+ _applyProse(projectRoot, flags, stdout);
211
+ } else {
212
+ _emitPlan(projectRoot, flags, stdout);
213
+ }
214
+ }
215
+
216
+ module.exports = { run, _parseArgs };
@@ -0,0 +1,130 @@
1
+ const { test, afterEach } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const os = require('node:os');
6
+
7
+ const scanCmd = require('./scan-codebase.cjs');
8
+ const subcmd = require('./update-docs.cjs');
9
+
10
+ const _sandboxes = [];
11
+
12
+ function makeSandbox() {
13
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-ud-'));
14
+ fs.mkdirSync(path.join(dir, '.nubos-pilot'), { recursive: true });
15
+ _sandboxes.push(dir);
16
+ return dir;
17
+ }
18
+
19
+ function write(root, rel, content) {
20
+ const abs = path.join(root, rel);
21
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
22
+ fs.writeFileSync(abs, content);
23
+ }
24
+
25
+ function captureStdout() {
26
+ const chunks = [];
27
+ return {
28
+ stub: { write: (s) => chunks.push(String(s)) },
29
+ json: () => JSON.parse(chunks.join('')),
30
+ };
31
+ }
32
+
33
+ afterEach(() => {
34
+ while (_sandboxes.length) {
35
+ const dir = _sandboxes.pop();
36
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
37
+ }
38
+ });
39
+
40
+ test('UD-1: throws when .nubos-pilot missing', () => {
41
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-ud-bare-'));
42
+ _sandboxes.push(dir);
43
+ assert.throws(
44
+ () => subcmd.run([], { cwd: dir, stdout: captureStdout().stub }),
45
+ (err) => err.code === 'update-docs-not-initialized',
46
+ );
47
+ });
48
+
49
+ test('UD-2: detects added, changed, removed files against manifest', () => {
50
+ const root = makeSandbox();
51
+ write(root, 'src/auth/login.js', 'export function login(){}');
52
+ write(root, 'src/billing/invoice.js', 'export function invoice(){}');
53
+ scanCmd.run([], { cwd: root, stdout: captureStdout().stub });
54
+
55
+ write(root, 'src/auth/login.js', 'export function login(){ /* v2 */ }');
56
+ write(root, 'src/auth/session.js', 'export class Session {}');
57
+ fs.unlinkSync(path.join(root, 'src/billing/invoice.js'));
58
+
59
+ const cap = captureStdout();
60
+ subcmd.run([], { cwd: root, stdout: cap.stub });
61
+ const out = cap.json();
62
+
63
+ assert.equal(out.mode, 'plan');
64
+ assert.ok(out.diff_summary.changed >= 1);
65
+ assert.ok(out.diff_summary.added >= 1);
66
+ assert.ok(out.diff_summary.removed >= 1);
67
+
68
+ const staleIds = out.stale_modules.map((m) => m.id);
69
+ assert.ok(staleIds.includes('src-auth'));
70
+ });
71
+
72
+ test('UD-3: new module appears in added_modules and gets stub', () => {
73
+ const root = makeSandbox();
74
+ write(root, 'src/core/a.js', 'export function a(){}');
75
+ scanCmd.run([], { cwd: root, stdout: captureStdout().stub });
76
+
77
+ write(root, 'src/newmod/x.js', 'export function x(){}');
78
+
79
+ const cap = captureStdout();
80
+ subcmd.run([], { cwd: root, stdout: cap.stub });
81
+ const out = cap.json();
82
+
83
+ const addedIds = out.added_modules.map((m) => m.id);
84
+ assert.ok(addedIds.includes('src-newmod'));
85
+ assert.ok(fs.existsSync(path.join(root, '.nubos-pilot', 'codebase', 'modules', 'src-newmod.md')));
86
+ });
87
+
88
+ test('UD-4: removed module reported in removed_modules', () => {
89
+ const root = makeSandbox();
90
+ write(root, 'src/foo/a.js', 'export function a(){}');
91
+ write(root, 'src/bar/b.js', 'export function b(){}');
92
+ scanCmd.run([], { cwd: root, stdout: captureStdout().stub });
93
+
94
+ fs.rmSync(path.join(root, 'src/bar'), { recursive: true, force: true });
95
+
96
+ const cap = captureStdout();
97
+ subcmd.run([], { cwd: root, stdout: cap.stub });
98
+ const out = cap.json();
99
+ const removedIds = out.removed_modules.map((m) => m.id);
100
+ assert.ok(removedIds.includes('src-bar'));
101
+ });
102
+
103
+ test('UD-5: apply-prose writes prose into existing module doc', () => {
104
+ const root = makeSandbox();
105
+ write(root, 'src/auth/login.js', 'export function login(){}');
106
+ scanCmd.run([], { cwd: root, stdout: captureStdout().stub });
107
+
108
+ const proseFile = path.join(root, 'p.json');
109
+ fs.writeFileSync(proseFile, JSON.stringify({
110
+ description: 'Login',
111
+ purpose: 'Auth users.',
112
+ key_concepts: [],
113
+ public_api: '`login()`',
114
+ invariants: [],
115
+ gotchas: [],
116
+ }));
117
+
118
+ const cap = captureStdout();
119
+ subcmd.run(['--apply-prose', '--module', 'src-auth', '--prose-file', proseFile], {
120
+ cwd: root, stdout: cap.stub,
121
+ });
122
+ const out = cap.json();
123
+ assert.equal(out.mode, 'apply-prose');
124
+ const doc = fs.readFileSync(
125
+ path.join(root, '.nubos-pilot', 'codebase', 'modules', 'src-auth.md'),
126
+ 'utf-8',
127
+ );
128
+ assert.ok(doc.includes('description: Login'));
129
+ assert.ok(doc.includes('Auth users.'));
130
+ });
@@ -0,0 +1,273 @@
1
+ # ADR-0007: Codebase Documentation Layer as Shared Agent Memory
2
+
3
+ * Status: Accepted
4
+ * Date: 2026-04-20
5
+ * Supersedes: None
6
+ * Relates-to: [ADR-0001](0001-no-daemon-invariant.md), [ADR-0005](0005-three-orthogonal-file-trees.md)
7
+
8
+ ## Context and Problem Statement
9
+
10
+ Every dev-agent that nubos-pilot orchestrates (executor, code-fixer,
11
+ planner, researcher, code-reviewer, plus user-authored custom agents) reads
12
+ project source before it writes source. In practice this means each agent
13
+ re-derives context from raw files on every spawn: it opens the same
14
+ modules, re-discovers the same public APIs, and re-learns the same
15
+ invariants. Three failure modes follow:
16
+
17
+ * **Token spend** — the same module content is repeatedly paid for across
18
+ agent runs. Over a multi-phase project this dominates cost.
19
+ * **Drift** — two agents reading the same file at different times may
20
+ reach different conclusions (one spots an invariant the other misses),
21
+ and downstream decisions diverge.
22
+ * **Loss of hard-won context** — when a bug was fixed two phases ago with
23
+ a subtle timing workaround, the next agent has no way to know the
24
+ workaround exists. It will re-introduce the bug.
25
+
26
+ Nubos-pilot already commits to `.nubos-pilot/` as the Project-State tree
27
+ ([ADR-0005](0005-three-orthogonal-file-trees.md)). What is missing is a
28
+ canonical, incrementally-maintained description of the project's own
29
+ source tree that agents treat as ground truth. The planning artifacts
30
+ (PROJECT.md, REQUIREMENTS.md, phase CONTEXT.md files) describe *what* the
31
+ project is and *what* each phase should do — they deliberately do not
32
+ describe *how the code is shaped*.
33
+
34
+ ## Decision Drivers
35
+
36
+ * **Runtime agnostic** — the docs layer must function identically whether
37
+ the host is Claude Code, OpenAI Agents, Codex, or any other orchestrator.
38
+ No Claude-specific hooks, no Claude-specific invocation paths.
39
+ * **Language agnostic** — nubos-pilot ships into arbitrary third-party
40
+ projects; the layer must work for Node, Python, Go, Rust, PHP, Ruby,
41
+ Java, Kotlin, C#, Swift, and unknown-language files alike.
42
+ * **Cheap to keep fresh** — stale docs are worse than absent docs. The
43
+ update path must be incremental and must piggyback on the existing
44
+ `np:execute-*` workflow cadence.
45
+ * **Physically separated from source** — docs live under `.nubos-pilot/`
46
+ so they cannot pollute the user's code tree, cannot be mistaken for
47
+ source, and can be fully removed with a single directory deletion.
48
+ * **Inspectable and editable by humans** — plain Markdown with a skill-
49
+ style frontmatter header, not an opaque binary index.
50
+ * **Pluggable where speculative** — a deterministic parser handles
51
+ structure; an agent produces prose. Either component can be swapped
52
+ without invalidating the other.
53
+
54
+ ## Considered Options
55
+
56
+ * **Option A — No codebase documentation layer.** Status quo. Every agent
57
+ re-reads raw source. Reject: demonstrably wasteful and drift-prone at
58
+ the sizes nubos-pilot targets.
59
+ * **Option B — Single large CODEBASE.md summary.** One file per project.
60
+ Reject: dev-agents cannot selectively load what they need; file grows
61
+ unboundedly; a single write destroys a single read target.
62
+ * **Option C — Per-file docs mirroring the source tree.** One `.md` per
63
+ source file. Reject: explodes in large repos; does not express module
64
+ boundaries, which is where invariants live.
65
+ * **Option D — Module-level docs for coherent units, manifest-tracked,
66
+ skill-style frontmatter, incremental refresh.** Chosen.
67
+
68
+ ## Decision Outcome
69
+
70
+ Chosen: **Option D — Codebase Documentation Layer**. Module-granularity
71
+ docs under `.nubos-pilot/codebase/modules/<id>.md`, indexed by
72
+ `.nubos-pilot/codebase/INDEX.md`, tracked for staleness by
73
+ `.nubos-pilot/codebase/.hashes.json`, and mapped to source paths by
74
+ `.nubos-pilot/codebase/.doc-index.json`. The layer is created and
75
+ maintained by three workflows (`np:scan-codebase`, `np:update-docs`,
76
+ `np:discuss-project`) and is consumed by every dev-agent under a strict
77
+ read-first / write-back protocol.
78
+
79
+ ### Layout
80
+
81
+ ```
82
+ .nubos-pilot/
83
+ codebase/
84
+ INDEX.md # pointer list, generated
85
+ .hashes.json # per-source-file SHA-256 manifest
86
+ .doc-index.json # doc → source-paths mapping
87
+ modules/
88
+ <module-id>.md # one per coherent unit
89
+ ```
90
+
91
+ A "module" is a **coherent unit**, not a fixed shape. The initial grouping
92
+ is directory-based (all code files in one directory form one module), but
93
+ the contract is explicit: grouping may be overridden and refined in
94
+ future iterations to express bounded contexts, microservice boundaries,
95
+ or feature-level units without breaking the read-first protocol.
96
+
97
+ ### Skill-Style Frontmatter
98
+
99
+ Every module doc carries structured frontmatter that agents (and tooling)
100
+ can read without parsing the body:
101
+
102
+ ```yaml
103
+ ---
104
+ name: <human-readable name>
105
+ description: <one-sentence summary>
106
+ kind: module
107
+ module_id: <id>
108
+ directory: <repo-relative>
109
+ primary_language: <lang>
110
+ file_count: <n>
111
+ source_paths: [ ... ]
112
+ symbols: [ ... ] # exported API surface
113
+ external_deps: [ ... ]
114
+ internal_deps: [ ... ]
115
+ source_hashes:
116
+ <path>: <sha256> # per-file integrity anchor
117
+ last_documented: <date>
118
+ ---
119
+ ```
120
+
121
+ The body is human-readable Markdown with fixed sections: Purpose, Key
122
+ Concepts, Public API, Invariants, Gotchas, Files.
123
+
124
+ ### Hybrid Parser + Agent Generation
125
+
126
+ * **Deterministic parser** (`lib/codebase-docs.cjs`) extracts symbols and
127
+ imports from 11 languages via line-based regex patterns (JavaScript,
128
+ TypeScript, Python, Go, Rust, PHP, Ruby, Java, Kotlin, C#, Swift; others
129
+ documented as "unknown" but still scanned).
130
+ * **Agent** (`np-codebase-documenter`) receives the parser's facts and
131
+ produces strict-JSON prose sections. The agent prompt forbids inventing
132
+ symbols or behaviors; it grounds every claim in the facts or the source
133
+ it is allowed to read.
134
+ * **Render** combines both into the final `.md`. The agent never writes
135
+ files directly; the subcommand renders.
136
+
137
+ ### Staleness Detection
138
+
139
+ `.hashes.json` is the integrity anchor. On every `np:update-docs` run:
140
+
141
+ 1. Rescan the workspace.
142
+ 2. Diff the new hashes against `.hashes.json` → added / changed / removed
143
+ files.
144
+ 3. Map touched paths to modules via `.doc-index.json` → stale modules.
145
+ 4. Refresh only stale modules' prose via the documenter agent.
146
+ 5. Write back. Overwrite the manifest as the new baseline.
147
+
148
+ `np:doctor` surfaces three related issues — `codebase-not-scanned`,
149
+ `codebase-manifest-stale`, `codebase-tbd-docs` — with `fixable:
150
+ 'run-workflow'` so `--fix` prints a hint and does not prompt (honors
151
+ D-16 whitelist semantics from [ADR-0001](0001-no-daemon-invariant.md)
152
+ adjacent conventions).
153
+
154
+ ### Dev-Agent Protocol (runtime-agnostic)
155
+
156
+ **Pre-edit (read-first) — mandatory for every dev-agent:**
157
+
158
+ 1. Read `.nubos-pilot/codebase/INDEX.md`.
159
+ 2. For every source file the agent will touch, locate and read the
160
+ owning `.nubos-pilot/codebase/modules/<id>.md`.
161
+ 3. Respect Invariants and Gotchas as constraints; if a planned change
162
+ would violate an invariant, stop and report.
163
+
164
+ **Post-edit (write-back) — mandatory for every dev-agent that mutates
165
+ source:**
166
+
167
+ 1. Run `np:update-docs`.
168
+ 2. For each stale module in the diff, dispatch the `np-codebase-
169
+ documenter` agent with the provided facts and apply prose via
170
+ `np:update-docs --apply-prose`.
171
+
172
+ The protocol is deliberately not a runtime hook. Installing a
173
+ `PostToolUse` hook into `.claude/settings.json` would tie correctness to
174
+ a specific host. Keeping the protocol in agent prompts means the same
175
+ contract applies in Claude Code, OpenAI Agents, Codex, or any future
176
+ orchestrator that loads nubos-pilot's agent definitions.
177
+
178
+ ### Orthogonality Preservation
179
+
180
+ `.nubos-pilot/codebase/` is a strict sub-tree of the Project-State tree
181
+ ([ADR-0005](0005-three-orthogonal-file-trees.md)). It is owned by the
182
+ end user's project, mutated only through nubos-pilot workflows, never
183
+ touches Source or Install-Payload trees. The three-tree invariant holds.
184
+
185
+ ### `child_process` Boundary Preservation
186
+
187
+ `lib/workspace-scan.cjs` exposes the scan surface. Surface-audit (ADR-0001
188
+ adjacent) forbids `child_process` in `lib/*.cjs` outside the
189
+ `git.cjs` whitelist. The scanner therefore accepts an optional
190
+ `opts.gitInfo` callback; the git-info implementation lives in
191
+ `lib/git.cjs` and is passed in by the subcommand layer
192
+ (`bin/np-tools/*.cjs`). The `no-daemon` / `lib-is-pure` invariant holds.
193
+
194
+ ## Consequences
195
+
196
+ * Good, because every dev-agent starts with a curated summary of the
197
+ code it will touch, lowering token spend and reducing drift.
198
+ * Good, because Invariants and Gotchas persist across phases — a
199
+ workaround documented in module X's Gotchas section is seen by every
200
+ future agent that touches module X.
201
+ * Good, because incremental refresh (`np:update-docs`) costs only the
202
+ modules whose source hashes changed, so the steady-state price of
203
+ keeping docs fresh scales with change volume, not repo size.
204
+ * Good, because the layer is inspectable, editable, removable — a plain
205
+ directory of Markdown files, no database, no daemon.
206
+ * Good, because the split between deterministic parser and agent-
207
+ produced prose preserves ADR-0001's no-daemon invariant: the parser
208
+ is a library function, and the agent runs only inside a workflow the
209
+ user already invoked.
210
+ * Good, because language coverage is extensible — adding a new language
211
+ means adding a regex entry to `SYMBOL_PATTERNS` and `IMPORT_PATTERNS`
212
+ in `lib/codebase-docs.cjs`; no schema change, no manifest migration.
213
+ * Bad, because initial scans of large repos are expensive. Mitigation:
214
+ `np:scan-codebase` batches (user can pause between batches) and the
215
+ workflow shows a progress counter.
216
+ * Bad, because parser-extracted symbols are regex-best-effort, not AST-
217
+ precise. Mitigation: the documenter agent is instructed to omit
218
+ signatures it cannot confirm from source rather than guess, and the
219
+ Gotchas section allows surfacing parser gaps explicitly.
220
+ * Bad, because the protocol is contract-enforced in agent prompts, not
221
+ in the runtime. A custom agent that ignores the protocol can still
222
+ write source without refreshing docs. Mitigation: `np:doctor` reports
223
+ `codebase-manifest-stale` any time post-change refresh was skipped;
224
+ the user sees the drift.
225
+ * Neutral, because ADR-0002 (zero runtime deps) is not challenged —
226
+ the layer uses only Node built-ins plus the already-accepted
227
+ `yaml@^2.8` via `lib/codebase-manifest.cjs` (JSON only — not even
228
+ yaml in practice).
229
+
230
+ ## Pattern Conformance
231
+
232
+ * **S-1 atomic write + file lock** — every doc write in the codebase
233
+ layer goes through `atomicWriteFileSync`. No partial files.
234
+ * **S-2 NubosPilotError envelope** — all error paths in
235
+ `scan-codebase` / `update-docs` / `discuss-project` subcommands
236
+ throw typed errors (`scan-codebase-not-initialized`,
237
+ `update-docs-module-not-found`, `discuss-project-missing-field`,
238
+ `proposed-reqs-invalid-id`, etc.).
239
+ * **S-5 sandboxed tests** — every new test (65 across lib + bin)
240
+ creates a fresh tmp directory and tears it down in `afterEach`.
241
+ * **S-6 CJS module footer** — every new `.cjs` file ends with a
242
+ `module.exports = {...}` block.
243
+
244
+ ## More Information
245
+
246
+ * **Implementation:**
247
+ * `lib/workspace-scan.cjs` — sprachagnostischer Scanner (15 tests)
248
+ * `lib/codebase-manifest.cjs` — `.hashes.json` read/write/diff (10 tests)
249
+ * `lib/codebase-docs.cjs` — module grouping + symbol/import extraction + render (14 tests)
250
+ * `bin/np-tools/scan-codebase.cjs` — initial scan subcommand (6 tests)
251
+ * `bin/np-tools/update-docs.cjs` — incremental refresh subcommand (5 tests)
252
+ * `bin/np-tools/discuss-project.cjs` — project-level interview subcommand (13 tests)
253
+ * `agents/np-codebase-documenter.md` — runtime-agnostic documenter agent
254
+ * `workflows/scan-codebase.md`, `workflows/update-docs.md`,
255
+ `workflows/discuss-project.md`, `workflows/new-project.md`
256
+ * **Consumer updates:** `np-executor`, `np-code-fixer`, `np-planner`,
257
+ `np-researcher`, `np-code-reviewer` received the read-first / write-
258
+ back protocol in their agent frontmatter-adjacent prose.
259
+ * **Related ADRs:**
260
+ * [ADR-0001](0001-no-daemon-invariant.md) — the runtime-agnostic
261
+ protocol exists to avoid a daemon.
262
+ * [ADR-0002](0002-zero-runtime-dependencies.md) — layer adds no new
263
+ runtime deps.
264
+ * [ADR-0005](0005-three-orthogonal-file-trees.md) — `.nubos-pilot/
265
+ codebase/` is strictly inside the Project-State tree.
266
+
267
+ ---
268
+
269
+ *This ADR describes the source-level design of the Codebase Documentation
270
+ Layer. CI-gate enforcement of the read-first / write-back protocol
271
+ (static analysis of agent prompts) and release/publish of the new
272
+ workflows are deferred to later deploy-phase ADRs per the source-vs-
273
+ deploy separation in [ADR-0005](0005-three-orthogonal-file-trees.md).*
@@ -9,6 +9,8 @@ This directory contains MADR-full ADRs that codify scope invariants of nubos-pil
9
9
  - [`0003-max-six-unit-types.md`](0003-max-six-unit-types.md) — Milestone, Phase, Plan, Task, Todo, Backlog — no more (FND-03)
10
10
  - [`0004-atomic-commit-per-unit.md`](0004-atomic-commit-per-unit.md) — Every unit-completion = exactly one git commit (FND-04)
11
11
  - [`0005-three-orthogonal-file-trees.md`](0005-three-orthogonal-file-trees.md) — Source / Install-Payload / Project-State stay disjoint (FND-05)
12
+ - [`0006-yaml-dependency-amendment.md`](0006-yaml-dependency-amendment.md) — Accept `yaml@^2.8` as first runtime dep (amends ADR-0002)
13
+ - [`0007-codebase-docs-layer.md`](0007-codebase-docs-layer.md) — Skill-style codebase documentation under `.nubos-pilot/codebase/` as shared agent memory
12
14
 
13
15
  ## Status Lifecycle
14
16