nubos-pilot 1.2.1 → 1.2.2
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.
- package/CHANGELOG.md +33 -1
- package/bin/np-tools/_commands.cjs +1 -0
- package/bin/np-tools/doctor.cjs +15 -2
- package/bin/np-tools/graph-impact.cjs +111 -0
- package/bin/np-tools/graph-impact.test.cjs +119 -0
- package/bin/np-tools/scan-codebase.cjs +21 -1
- package/lib/checkpoint.cjs +3 -0
- package/lib/codebase-graph.cjs +0 -0
- package/lib/codebase-graph.test.cjs +174 -0
- package/lib/codebase-manifest.cjs +3 -0
- package/lib/learnings.cjs +19 -95
- package/lib/memory.cjs +38 -33
- package/lib/messaging.cjs +12 -6
- package/lib/metrics-aggregate.cjs +14 -2
- package/lib/migrate.cjs +29 -0
- package/lib/migrate.test.cjs +91 -0
- package/lib/schemas/data/checkpoint.v1.json +13 -0
- package/lib/schemas/data/codebase-manifest.v1.json +22 -0
- package/lib/schemas/data/learnings.v1.json +28 -0
- package/lib/schemas/data/memory-manifest.v1.json +14 -0
- package/lib/schemas/data/memory-record.v1.json +16 -0
- package/lib/schemas/data/message.v1.json +19 -0
- package/lib/schemas/data/metrics-record.v1.json +11 -0
- package/lib/validate.cjs +301 -0
- package/lib/validate.test.cjs +242 -0
- package/np-tools.cjs +1 -0
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,39 @@ All notable changes to nubos-pilot are documented in this file. Format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning
|
|
5
5
|
follows [SemVer](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
## [1.
|
|
7
|
+
## [1.2.2] — 2026-06-05
|
|
8
|
+
|
|
9
|
+
A dependency graph for the codebase you work in, plus stricter checks on nubos-pilot's own data.
|
|
10
|
+
|
|
11
|
+
- `np:scan-codebase` now builds a module dependency graph and writes it to `.nubos-pilot/codebase/.graph.json`. The new `np:graph-impact` command shows what a change touches before you make it. It reports which modules depend on a file, what that file depends on, and any dependency cycle it sits in. The graph reads relative imports only. It builds no AST and adds no dependencies.
|
|
12
|
+
- Persisted state files are now validated on read against versioned schemas. A corrupt single-document store fails with a clear error code. A bad line in an append-only log is skipped, not fatal.
|
|
13
|
+
- The reference docs now list every error code. That list is generated from source and checked on each build, so it cannot drift from the code.
|
|
14
|
+
- Internal logging goes through one structured logger. A test keeps `console.*` out of `lib/` and `bin/np-tools/`.
|
|
15
|
+
- Added `ATTRIBUTIONS.md`. It names the third-party packages nubos-pilot uses and their licenses.
|
|
16
|
+
|
|
17
|
+
Full documentation at <https://pilot.nubos.cloud>.
|
|
18
|
+
|
|
19
|
+
## [1.2.1] — 2026-06-02
|
|
20
|
+
|
|
21
|
+
Two always-on quality layers that act while the agent writes code.
|
|
22
|
+
|
|
23
|
+
- In-session security review: nubos-pilot reviews the code it writes for
|
|
24
|
+
vulnerabilities while it works and fixes findings in the same session,
|
|
25
|
+
before they reach a pull request. Three non-blocking depths — an instant
|
|
26
|
+
per-edit pattern scan with no model call, a background semantic review of
|
|
27
|
+
the turn's diff at end of turn, and a deeper review that reads surrounding
|
|
28
|
+
code on each commit or push the agent makes.
|
|
29
|
+
- The security reviewer runs independently with a fresh context, reports each
|
|
30
|
+
finding once, and never blocks a write or commit. Extend it with custom
|
|
31
|
+
pattern rules and a review guidance file; built-in checks stay on.
|
|
32
|
+
- Requirements-aware executor: `/np:execute-phase` injects the milestone
|
|
33
|
+
success criteria into the executor as its acceptance target, so it writes
|
|
34
|
+
against the requirements from the first round, not just the verify command.
|
|
35
|
+
- New configuration blocks `security.*` and `conformance.*`.
|
|
36
|
+
|
|
37
|
+
Full documentation at <https://pilot.nubos.cloud>.
|
|
38
|
+
|
|
39
|
+
## [1.2.0] — 2026-05-25
|
|
8
40
|
|
|
9
41
|
Public release.
|
|
10
42
|
|
|
@@ -36,6 +36,7 @@ const COMMANDS = [
|
|
|
36
36
|
{ name: 'doctor', category: 'Install', description: '12-check install-integrity scan (--fix for auto-safe fixes)', description_de: '12-Check-Install-Integritäts-Scan (--fix für auto-sichere Fixes)' },
|
|
37
37
|
{ name: 'scan-codebase', category: 'Install', description: 'Initial deep codebase inventory → .nubos-pilot/codebase/ skill docs', description_de: 'Initiale tiefe Codebase-Inventur → .nubos-pilot/codebase/ Skill-Docs' },
|
|
38
38
|
{ name: 'update-docs', category: 'Install', description: 'Refresh stale module docs after code changes', description_de: 'Aktualisiert veraltete Modul-Docs nach Code-Änderungen' },
|
|
39
|
+
{ name: 'graph-impact', category: 'Utility', description: 'Query the module dependency graph (.graph.json from np:scan-codebase): impact (transitive dependents), dependencies, cluster, cycle membership. Flags: --module <id> | --path <relpath> | --cycles', description_de: 'Fragt den Modul-Dependency-Graphen ab (.graph.json aus np:scan-codebase): Impact (transitive Dependents), Dependencies, Cluster, Zyklus-Zugehörigkeit. Flags: --module <id> | --path <relpath> | --cycles' },
|
|
39
40
|
|
|
40
41
|
{ name: 'resolve-model', category: 'Utility', description: 'Resolve agent/tier to model alias or id (Tier×Profile matrix)', description_de: 'Löst Agent/Tier zu Model-Alias oder -ID auf (Tier×Profile-Matrix)' },
|
|
41
42
|
{ name: 'metrics', category: 'Utility', description: 'Record JSONL metrics entry (record | now | start-timestamp | end-timestamp)', description_de: 'Schreibt JSONL-Metrics-Eintrag (record | now | start-timestamp | end-timestamp)' },
|
package/bin/np-tools/doctor.cjs
CHANGED
|
@@ -399,14 +399,27 @@ function _checkNubosloopKnowledgeStore(projectRoot) {
|
|
|
399
399
|
}
|
|
400
400
|
try {
|
|
401
401
|
const parsed = JSON.parse(fs.readFileSync(learningsPath, 'utf-8'));
|
|
402
|
-
|
|
402
|
+
const { STORE_VERSION } = require('../../lib/learnings.cjs');
|
|
403
|
+
const { validate } = require('../../lib/validate.cjs');
|
|
404
|
+
const isObject = parsed && typeof parsed === 'object' && !Array.isArray(parsed);
|
|
405
|
+
let errors;
|
|
406
|
+
if (isObject && parsed.version === STORE_VERSION) {
|
|
407
|
+
errors = validate(parsed, 'learnings.v1');
|
|
408
|
+
} else if (!isObject || !Array.isArray(parsed.learnings)) {
|
|
409
|
+
errors = [{ message: 'expected JSON object with `version` and `learnings[]`' }];
|
|
410
|
+
} else {
|
|
411
|
+
errors = [];
|
|
412
|
+
}
|
|
413
|
+
if (errors.length) {
|
|
403
414
|
issues.push({
|
|
404
415
|
id: 'nubosloop-knowledge-store-corrupt',
|
|
405
416
|
severity: 'warn',
|
|
406
417
|
fixable: 'manual',
|
|
407
418
|
details: {
|
|
408
419
|
path: learningsPath,
|
|
409
|
-
|
|
420
|
+
violations: errors.length,
|
|
421
|
+
first: errors[0].message,
|
|
422
|
+
hint: 'store violates the learnings.v1 schema; remove or restore from a backup.',
|
|
410
423
|
},
|
|
411
424
|
});
|
|
412
425
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
5
|
+
const g = require('../../lib/codebase-graph.cjs');
|
|
6
|
+
|
|
7
|
+
function _parseArgs(args) {
|
|
8
|
+
const flags = { cwd: null, module: null, filePath: null, cycles: false };
|
|
9
|
+
for (let i = 0; i < (args || []).length; i++) {
|
|
10
|
+
const a = args[i];
|
|
11
|
+
if (a === '--cwd') flags.cwd = args[++i];
|
|
12
|
+
else if (a === '--module') flags.module = args[++i];
|
|
13
|
+
else if (a === '--path') flags.filePath = args[++i];
|
|
14
|
+
else if (a === '--cycles') flags.cycles = true;
|
|
15
|
+
}
|
|
16
|
+
return flags;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function _graphPath(projectRoot) {
|
|
20
|
+
return path.join(projectRoot, '.nubos-pilot', 'codebase', '.graph.json');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function _load(projectRoot) {
|
|
24
|
+
const p = _graphPath(projectRoot);
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
raw = fs.readFileSync(p, 'utf-8');
|
|
28
|
+
} catch {
|
|
29
|
+
throw new NubosPilotError(
|
|
30
|
+
'graph-not-found',
|
|
31
|
+
'module graph not found — run np:scan-codebase first',
|
|
32
|
+
{ path: '.nubos-pilot/codebase/.graph.json' },
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(raw);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
throw new NubosPilotError(
|
|
39
|
+
'graph-unreadable',
|
|
40
|
+
'module graph is not valid JSON — re-run np:scan-codebase',
|
|
41
|
+
{ path: '.nubos-pilot/codebase/.graph.json', cause: err && err.message },
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function _moduleForPath(graph, rel) {
|
|
47
|
+
const norm = rel.split(path.sep).join('/');
|
|
48
|
+
const dir = norm.includes('/') ? norm.slice(0, norm.lastIndexOf('/')) : '';
|
|
49
|
+
const node = (graph.nodes || []).find((n) => n.directory === dir);
|
|
50
|
+
return node ? node.id : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function run(args, ctx) {
|
|
54
|
+
const context = ctx || {};
|
|
55
|
+
const stdout = context.stdout || process.stdout;
|
|
56
|
+
const flags = _parseArgs(args);
|
|
57
|
+
const projectRoot = path.resolve(flags.cwd || context.cwd || process.cwd());
|
|
58
|
+
const graph = _load(projectRoot);
|
|
59
|
+
|
|
60
|
+
if (flags.cycles && !flags.module && !flags.filePath) {
|
|
61
|
+
stdout.write(JSON.stringify({
|
|
62
|
+
module_count: graph.module_count,
|
|
63
|
+
cycle_count: (graph.cycles || []).length,
|
|
64
|
+
cycles: graph.cycles || [],
|
|
65
|
+
}, null, 2));
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let moduleId = flags.module;
|
|
70
|
+
if (!moduleId && flags.filePath) {
|
|
71
|
+
moduleId = _moduleForPath(graph, flags.filePath);
|
|
72
|
+
if (!moduleId) {
|
|
73
|
+
throw new NubosPilotError(
|
|
74
|
+
'graph-path-unmapped',
|
|
75
|
+
'no module owns that path: ' + flags.filePath,
|
|
76
|
+
{ path: flags.filePath },
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!moduleId) {
|
|
81
|
+
throw new NubosPilotError(
|
|
82
|
+
'graph-missing-target',
|
|
83
|
+
'--module <id> or --path <relpath> required',
|
|
84
|
+
{},
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (!(graph.nodes || []).some((n) => n.id === moduleId)) {
|
|
88
|
+
throw new NubosPilotError(
|
|
89
|
+
'graph-unknown-module',
|
|
90
|
+
'module not in graph: ' + moduleId,
|
|
91
|
+
{ module: moduleId },
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
stdout.write(JSON.stringify({
|
|
96
|
+
module: moduleId,
|
|
97
|
+
direct_dependents: g.directDependents(graph, moduleId),
|
|
98
|
+
impact: g.transitiveDependents(graph, moduleId),
|
|
99
|
+
direct_dependencies: g.directDependencies(graph, moduleId),
|
|
100
|
+
transitive_dependencies: g.transitiveDependencies(graph, moduleId),
|
|
101
|
+
cluster: g.clusterOf(graph, moduleId),
|
|
102
|
+
in_cycle: g.cycleFor(graph, moduleId),
|
|
103
|
+
}, null, 2));
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { run, _parseArgs };
|
|
108
|
+
|
|
109
|
+
if (require.main === module) {
|
|
110
|
+
process.exit(run(process.argv.slice(2)) || 0);
|
|
111
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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 cli = require('./graph-impact.cjs');
|
|
8
|
+
|
|
9
|
+
const _sandboxes = [];
|
|
10
|
+
|
|
11
|
+
function makeSandbox() {
|
|
12
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-gi-'));
|
|
13
|
+
_sandboxes.push(dir);
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function writeGraph(root, graph) {
|
|
18
|
+
const dir = path.join(root, '.nubos-pilot', 'codebase');
|
|
19
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
20
|
+
fs.writeFileSync(path.join(dir, '.graph.json'), JSON.stringify(graph));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function capture() {
|
|
24
|
+
let buf = '';
|
|
25
|
+
return { stream: { write: (s) => { buf += s; } }, read: () => buf };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const SAMPLE = {
|
|
29
|
+
schema_version: 1,
|
|
30
|
+
module_count: 3,
|
|
31
|
+
edge_count: 2,
|
|
32
|
+
nodes: [
|
|
33
|
+
{ id: 'a', directory: 'a', primary_language: 'javascript', file_count: 1 },
|
|
34
|
+
{ id: 'b', directory: 'b', primary_language: 'javascript', file_count: 1 },
|
|
35
|
+
{ id: 'c', directory: 'c', primary_language: 'javascript', file_count: 1 },
|
|
36
|
+
],
|
|
37
|
+
edges: [
|
|
38
|
+
{ from: 'a', to: 'b', weight: 1 },
|
|
39
|
+
{ from: 'b', to: 'c', weight: 1 },
|
|
40
|
+
],
|
|
41
|
+
cycles: [],
|
|
42
|
+
clusters: [{ id: 0, members: ['a', 'b', 'c'] }],
|
|
43
|
+
metrics: { unresolved_internal_deps: 0, max_fan_in: 1, max_fan_out: 1, isolated_modules: 0 },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
while (_sandboxes.length) {
|
|
48
|
+
const dir = _sandboxes.pop();
|
|
49
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('GI-1: --module reports impact and dependencies', () => {
|
|
54
|
+
const root = makeSandbox();
|
|
55
|
+
writeGraph(root, SAMPLE);
|
|
56
|
+
const out = capture();
|
|
57
|
+
const rc = cli.run(['--module', 'c'], { cwd: root, stdout: out.stream });
|
|
58
|
+
assert.equal(rc, 0);
|
|
59
|
+
const res = JSON.parse(out.read());
|
|
60
|
+
assert.equal(res.module, 'c');
|
|
61
|
+
assert.deepEqual(res.direct_dependents, ['b']);
|
|
62
|
+
assert.deepEqual(res.impact, ['a', 'b']);
|
|
63
|
+
assert.deepEqual(res.transitive_dependencies, []);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('GI-2: --path maps a file to its owning module', () => {
|
|
67
|
+
const root = makeSandbox();
|
|
68
|
+
writeGraph(root, SAMPLE);
|
|
69
|
+
const out = capture();
|
|
70
|
+
cli.run(['--path', 'a/login.js'], { cwd: root, stdout: out.stream });
|
|
71
|
+
const res = JSON.parse(out.read());
|
|
72
|
+
assert.equal(res.module, 'a');
|
|
73
|
+
assert.deepEqual(res.direct_dependencies, ['b']);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('GI-3: missing graph throws graph-not-found', () => {
|
|
77
|
+
const root = makeSandbox();
|
|
78
|
+
assert.throws(
|
|
79
|
+
() => cli.run(['--module', 'a'], { cwd: root, stdout: capture().stream }),
|
|
80
|
+
(err) => err.code === 'graph-not-found',
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('GI-4: unknown module throws graph-unknown-module', () => {
|
|
85
|
+
const root = makeSandbox();
|
|
86
|
+
writeGraph(root, SAMPLE);
|
|
87
|
+
assert.throws(
|
|
88
|
+
() => cli.run(['--module', 'nope'], { cwd: root, stdout: capture().stream }),
|
|
89
|
+
(err) => err.code === 'graph-unknown-module',
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('GI-5: no target throws graph-missing-target', () => {
|
|
94
|
+
const root = makeSandbox();
|
|
95
|
+
writeGraph(root, SAMPLE);
|
|
96
|
+
assert.throws(
|
|
97
|
+
() => cli.run([], { cwd: root, stdout: capture().stream }),
|
|
98
|
+
(err) => err.code === 'graph-missing-target',
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('GI-6: --cycles dumps the cycle list', () => {
|
|
103
|
+
const root = makeSandbox();
|
|
104
|
+
writeGraph(root, Object.assign({}, SAMPLE, { cycles: [['a', 'b']] }));
|
|
105
|
+
const out = capture();
|
|
106
|
+
cli.run(['--cycles'], { cwd: root, stdout: out.stream });
|
|
107
|
+
const res = JSON.parse(out.read());
|
|
108
|
+
assert.equal(res.cycle_count, 1);
|
|
109
|
+
assert.deepEqual(res.cycles[0], ['a', 'b']);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('GI-7: unmappable --path throws graph-path-unmapped', () => {
|
|
113
|
+
const root = makeSandbox();
|
|
114
|
+
writeGraph(root, SAMPLE);
|
|
115
|
+
assert.throws(
|
|
116
|
+
() => cli.run(['--path', 'ghost/x.js'], { cwd: root, stdout: capture().stream }),
|
|
117
|
+
(err) => err.code === 'graph-path-unmapped',
|
|
118
|
+
);
|
|
119
|
+
});
|
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
moduleDocPath,
|
|
19
19
|
indexDocPath,
|
|
20
20
|
} = require('../../lib/codebase-docs.cjs');
|
|
21
|
+
const { buildModuleGraph } = require('../../lib/codebase-graph.cjs');
|
|
21
22
|
|
|
22
23
|
function _parseArgs(args) {
|
|
23
24
|
const flags = {
|
|
@@ -73,6 +74,16 @@ function _emitPlan(projectRoot, flags, stdout) {
|
|
|
73
74
|
projectRoot,
|
|
74
75
|
path.join(projectRoot, '.nubos-pilot', 'codebase', '.hashes.json'),
|
|
75
76
|
),
|
|
77
|
+
graph_path: path.relative(
|
|
78
|
+
projectRoot,
|
|
79
|
+
path.join(projectRoot, '.nubos-pilot', 'codebase', '.graph.json'),
|
|
80
|
+
),
|
|
81
|
+
graph: {
|
|
82
|
+
module_count: modulesResult.graph.module_count,
|
|
83
|
+
edge_count: modulesResult.graph.edge_count,
|
|
84
|
+
cycle_count: modulesResult.graph.cycles.length,
|
|
85
|
+
unresolved_internal_deps: modulesResult.graph.metrics.unresolved_internal_deps,
|
|
86
|
+
},
|
|
76
87
|
}, null, 2));
|
|
77
88
|
}
|
|
78
89
|
|
|
@@ -103,6 +114,15 @@ function _scanAndBuild(projectRoot, flags) {
|
|
|
103
114
|
fs.mkdirSync(path.dirname(indexMapPath), { recursive: true });
|
|
104
115
|
atomicWriteFileSync(indexMapPath, JSON.stringify(docIndex, null, 2) + '\n');
|
|
105
116
|
|
|
117
|
+
const graph = buildModuleGraph(modules.map((m) => m.facts));
|
|
118
|
+
const graphPath = path.join(
|
|
119
|
+
projectRoot,
|
|
120
|
+
'.nubos-pilot',
|
|
121
|
+
'codebase',
|
|
122
|
+
'.graph.json',
|
|
123
|
+
);
|
|
124
|
+
atomicWriteFileSync(graphPath, JSON.stringify(graph, null, 2) + '\n');
|
|
125
|
+
|
|
106
126
|
const indexPath = indexDocPath(projectRoot);
|
|
107
127
|
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
|
|
108
128
|
atomicWriteFileSync(indexPath, buildIndexDoc(modules, { project_name: flags.projectName || null }));
|
|
@@ -115,7 +135,7 @@ function _scanAndBuild(projectRoot, flags) {
|
|
|
115
135
|
atomicWriteFileSync(docPath, renderModuleDoc(mod.facts, null, hashLookup));
|
|
116
136
|
}
|
|
117
137
|
|
|
118
|
-
return { scan: scanResult, modules, manifest, hashLookup };
|
|
138
|
+
return { scan: scanResult, modules, manifest, hashLookup, graph };
|
|
119
139
|
}
|
|
120
140
|
|
|
121
141
|
function _applyProse(projectRoot, flags, stdout) {
|
package/lib/checkpoint.cjs
CHANGED
|
@@ -10,8 +10,10 @@ const {
|
|
|
10
10
|
} = require('./core.cjs');
|
|
11
11
|
const { parseState, serializeState } = require('./state.cjs');
|
|
12
12
|
const { TASK_ID_RE } = require('./ids.cjs');
|
|
13
|
+
const { assertValid } = require('./validate.cjs');
|
|
13
14
|
|
|
14
15
|
const CHECKPOINT_SCHEMA_VERSION = 1;
|
|
16
|
+
const STORE_SCHEMA = 'checkpoint.v1';
|
|
15
17
|
|
|
16
18
|
function _assertSafeTaskId(taskId) {
|
|
17
19
|
if (typeof taskId !== 'string' || !TASK_ID_RE.test(taskId)) {
|
|
@@ -78,6 +80,7 @@ function _assertCompatibleSchema(existing, cpPath) {
|
|
|
78
80
|
},
|
|
79
81
|
);
|
|
80
82
|
}
|
|
83
|
+
assertValid(existing, STORE_SCHEMA, 'checkpoint-corrupt', { path: cpPath });
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
function _sliceFromTaskId(taskId) {
|
|
Binary file
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
|
|
4
|
+
const g = require('./codebase-graph.cjs');
|
|
5
|
+
|
|
6
|
+
function fact(id, directory, files, extra) {
|
|
7
|
+
const source_paths = files.map((f) => f.path);
|
|
8
|
+
return Object.assign({
|
|
9
|
+
id,
|
|
10
|
+
name: directory || 'root',
|
|
11
|
+
directory: directory || '',
|
|
12
|
+
primary_language: 'javascript',
|
|
13
|
+
language_distribution: { javascript: files.length },
|
|
14
|
+
file_count: files.length,
|
|
15
|
+
source_paths,
|
|
16
|
+
symbols: [],
|
|
17
|
+
internal_deps: [],
|
|
18
|
+
external_deps: [],
|
|
19
|
+
files,
|
|
20
|
+
}, extra || {});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('CG-1: buildModuleGraph creates a node per fact', () => {
|
|
24
|
+
const facts = [
|
|
25
|
+
fact('src-auth', 'src/auth', [{ path: 'src/auth/login.js', language: 'javascript', symbols: [], deps: [] }]),
|
|
26
|
+
fact('src-db', 'src/db', [{ path: 'src/db/index.js', language: 'javascript', symbols: [], deps: [] }]),
|
|
27
|
+
];
|
|
28
|
+
const graph = g.buildModuleGraph(facts);
|
|
29
|
+
assert.equal(graph.module_count, 2);
|
|
30
|
+
assert.deepEqual(graph.nodes.map((n) => n.id), ['src-auth', 'src-db']);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('CG-2: relative import resolves to a cross-module edge', () => {
|
|
34
|
+
const facts = [
|
|
35
|
+
fact('src-auth', 'src/auth', [
|
|
36
|
+
{ path: 'src/auth/login.js', language: 'javascript', symbols: [], deps: ['../db', 'bcrypt'] },
|
|
37
|
+
]),
|
|
38
|
+
fact('src-db', 'src/db', [
|
|
39
|
+
{ path: 'src/db/index.js', language: 'javascript', symbols: [], deps: [] },
|
|
40
|
+
]),
|
|
41
|
+
];
|
|
42
|
+
const graph = g.buildModuleGraph(facts);
|
|
43
|
+
assert.equal(graph.edge_count, 1);
|
|
44
|
+
assert.deepEqual(graph.edges[0], { from: 'src-auth', to: 'src-db', weight: 1 });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('CG-3: external (non-relative) deps never become edges', () => {
|
|
48
|
+
const facts = [
|
|
49
|
+
fact('src-auth', 'src/auth', [
|
|
50
|
+
{ path: 'src/auth/login.js', language: 'javascript', symbols: [], deps: ['bcrypt', 'node:fs'] },
|
|
51
|
+
]),
|
|
52
|
+
];
|
|
53
|
+
const graph = g.buildModuleGraph(facts);
|
|
54
|
+
assert.equal(graph.edge_count, 0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('CG-4: same-module relative import is not a self-edge', () => {
|
|
58
|
+
const facts = [
|
|
59
|
+
fact('src-auth', 'src/auth', [
|
|
60
|
+
{ path: 'src/auth/login.js', language: 'javascript', symbols: [], deps: ['./session'] },
|
|
61
|
+
{ path: 'src/auth/session.js', language: 'javascript', symbols: [], deps: [] },
|
|
62
|
+
]),
|
|
63
|
+
];
|
|
64
|
+
const graph = g.buildModuleGraph(facts);
|
|
65
|
+
assert.equal(graph.edge_count, 0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('CG-5: repeated imports raise edge weight', () => {
|
|
69
|
+
const facts = [
|
|
70
|
+
fact('a', 'a', [
|
|
71
|
+
{ path: 'a/one.js', language: 'javascript', symbols: [], deps: ['../b'] },
|
|
72
|
+
{ path: 'a/two.js', language: 'javascript', symbols: [], deps: ['../b/helper'] },
|
|
73
|
+
]),
|
|
74
|
+
fact('b', 'b', [
|
|
75
|
+
{ path: 'b/index.js', language: 'javascript', symbols: [], deps: [] },
|
|
76
|
+
{ path: 'b/helper.js', language: 'javascript', symbols: [], deps: [] },
|
|
77
|
+
]),
|
|
78
|
+
];
|
|
79
|
+
const graph = g.buildModuleGraph(facts);
|
|
80
|
+
assert.equal(graph.edges.length, 1);
|
|
81
|
+
assert.equal(graph.edges[0].weight, 2);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('CG-6: unresolved internal-looking deps are counted, not edged', () => {
|
|
85
|
+
const facts = [
|
|
86
|
+
fact('a', 'a', [
|
|
87
|
+
{ path: 'a/one.js', language: 'javascript', symbols: [], deps: ['../nonexistent'] },
|
|
88
|
+
]),
|
|
89
|
+
];
|
|
90
|
+
const graph = g.buildModuleGraph(facts);
|
|
91
|
+
assert.equal(graph.edge_count, 0);
|
|
92
|
+
assert.equal(graph.metrics.unresolved_internal_deps, 1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('CG-7: Tarjan detects a 2-module cycle', () => {
|
|
96
|
+
const facts = [
|
|
97
|
+
fact('a', 'a', [{ path: 'a/x.js', language: 'javascript', symbols: [], deps: ['../b'] }]),
|
|
98
|
+
fact('b', 'b', [{ path: 'b/index.js', language: 'javascript', symbols: [], deps: ['../a'] }]),
|
|
99
|
+
];
|
|
100
|
+
const graph = g.buildModuleGraph(facts);
|
|
101
|
+
assert.equal(graph.cycles.length, 1);
|
|
102
|
+
assert.deepEqual(graph.cycles[0], ['a', 'b']);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('CG-8: acyclic graph reports no cycles', () => {
|
|
106
|
+
const facts = [
|
|
107
|
+
fact('a', 'a', [{ path: 'a/x.js', language: 'javascript', symbols: [], deps: ['../b'] }]),
|
|
108
|
+
fact('b', 'b', [{ path: 'b/index.js', language: 'javascript', symbols: [], deps: ['../c'] }]),
|
|
109
|
+
fact('c', 'c', [{ path: 'c/index.js', language: 'javascript', symbols: [], deps: [] }]),
|
|
110
|
+
];
|
|
111
|
+
const graph = g.buildModuleGraph(facts);
|
|
112
|
+
assert.equal(graph.cycles.length, 0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('CG-9: transitive dependents (impact) walk the reverse graph', () => {
|
|
116
|
+
const facts = [
|
|
117
|
+
fact('a', 'a', [{ path: 'a/x.js', language: 'javascript', symbols: [], deps: ['../b'] }]),
|
|
118
|
+
fact('b', 'b', [{ path: 'b/index.js', language: 'javascript', symbols: [], deps: ['../c'] }]),
|
|
119
|
+
fact('c', 'c', [{ path: 'c/index.js', language: 'javascript', symbols: [], deps: [] }]),
|
|
120
|
+
];
|
|
121
|
+
const graph = g.buildModuleGraph(facts);
|
|
122
|
+
assert.deepEqual(g.transitiveDependents(graph, 'c'), ['a', 'b']);
|
|
123
|
+
assert.deepEqual(g.directDependents(graph, 'c'), ['b']);
|
|
124
|
+
assert.deepEqual(g.transitiveDependencies(graph, 'a'), ['b', 'c']);
|
|
125
|
+
assert.deepEqual(g.directDependencies(graph, 'a'), ['b']);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('CG-10: deterministic clustering groups a connected component together', () => {
|
|
129
|
+
const facts = [
|
|
130
|
+
fact('a', 'a', [{ path: 'a/x.js', language: 'javascript', symbols: [], deps: ['../b'] }]),
|
|
131
|
+
fact('b', 'b', [{ path: 'b/index.js', language: 'javascript', symbols: [], deps: ['../a'] }]),
|
|
132
|
+
fact('lonely', 'lonely', [{ path: 'lonely/z.js', language: 'javascript', symbols: [], deps: [] }]),
|
|
133
|
+
];
|
|
134
|
+
const graph = g.buildModuleGraph(facts);
|
|
135
|
+
const first = g.buildModuleGraph(facts);
|
|
136
|
+
assert.deepEqual(graph.clusters, first.clusters);
|
|
137
|
+
const clusterAB = graph.clusters.find((c) => c.members.includes('a'));
|
|
138
|
+
assert.ok(clusterAB.members.includes('b'));
|
|
139
|
+
assert.ok(!clusterAB.members.includes('lonely'));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('CG-11: cycleFor and clusterOf locate a module', () => {
|
|
143
|
+
const facts = [
|
|
144
|
+
fact('a', 'a', [{ path: 'a/x.js', language: 'javascript', symbols: [], deps: ['../b'] }]),
|
|
145
|
+
fact('b', 'b', [{ path: 'b/index.js', language: 'javascript', symbols: [], deps: ['../a'] }]),
|
|
146
|
+
];
|
|
147
|
+
const graph = g.buildModuleGraph(facts);
|
|
148
|
+
assert.deepEqual(g.cycleFor(graph, 'a'), ['a', 'b']);
|
|
149
|
+
assert.equal(typeof g.clusterOf(graph, 'a'), 'number');
|
|
150
|
+
assert.equal(g.cycleFor(graph, 'missing'), null);
|
|
151
|
+
assert.equal(g.clusterOf(graph, 'missing'), null);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('CG-12: root-relative (/-prefixed) deps resolve from project root', () => {
|
|
155
|
+
const facts = [
|
|
156
|
+
fact('src-auth', 'src/auth', [
|
|
157
|
+
{ path: 'src/auth/login.js', language: 'javascript', symbols: [], deps: ['/src/db'] },
|
|
158
|
+
]),
|
|
159
|
+
fact('src-db', 'src/db', [
|
|
160
|
+
{ path: 'src/db/index.js', language: 'javascript', symbols: [], deps: [] },
|
|
161
|
+
]),
|
|
162
|
+
];
|
|
163
|
+
const graph = g.buildModuleGraph(facts);
|
|
164
|
+
assert.equal(graph.edge_count, 1);
|
|
165
|
+
assert.deepEqual(graph.edges[0], { from: 'src-auth', to: 'src-db', weight: 1 });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('CG-13: empty facts yield an empty graph', () => {
|
|
169
|
+
const graph = g.buildModuleGraph([]);
|
|
170
|
+
assert.equal(graph.module_count, 0);
|
|
171
|
+
assert.equal(graph.edge_count, 0);
|
|
172
|
+
assert.deepEqual(graph.cycles, []);
|
|
173
|
+
assert.deepEqual(graph.clusters, []);
|
|
174
|
+
});
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const { atomicWriteFileSync, NubosPilotError } = require('./core.cjs');
|
|
6
|
+
const { assertValid } = require('./validate.cjs');
|
|
6
7
|
|
|
7
8
|
const SCHEMA_VERSION = 1;
|
|
9
|
+
const STORE_SCHEMA = 'codebase-manifest.v1';
|
|
8
10
|
const CODEBASE_DIR_NAME = 'codebase';
|
|
9
11
|
const MANIFEST_FILENAME = '.hashes.json';
|
|
10
12
|
|
|
@@ -63,6 +65,7 @@ function readManifest(projectRoot) {
|
|
|
63
65
|
);
|
|
64
66
|
}
|
|
65
67
|
if (!parsed.files || typeof parsed.files !== 'object') parsed.files = {};
|
|
68
|
+
assertValid(parsed, STORE_SCHEMA, 'manifest-invalid-shape', { path: p });
|
|
66
69
|
return parsed;
|
|
67
70
|
}
|
|
68
71
|
|