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/lib/learnings.cjs
CHANGED
|
@@ -6,6 +6,10 @@ const crypto = require('node:crypto');
|
|
|
6
6
|
|
|
7
7
|
const { projectStateDir, atomicWriteFileSync, withFileLock, NubosPilotError, safeAssign } = require('./core.cjs');
|
|
8
8
|
const { TASK_ID_RE, MILESTONE_ID_RE } = require('./ids.cjs');
|
|
9
|
+
const { assertValid } = require('./validate.cjs');
|
|
10
|
+
const { runMigrators } = require('./migrate.cjs');
|
|
11
|
+
|
|
12
|
+
const STORE_SCHEMA = 'learnings.v1';
|
|
9
13
|
|
|
10
14
|
const STOPWORDS = new Set([
|
|
11
15
|
'the','a','an','of','to','in','on','for','and','or','is','are','was','were',
|
|
@@ -96,14 +100,7 @@ function _readStore(cwd) {
|
|
|
96
100
|
);
|
|
97
101
|
}
|
|
98
102
|
if (obj.version === STORE_VERSION) {
|
|
99
|
-
|
|
100
|
-
throw new NubosPilotError(
|
|
101
|
-
'learnings-store-corrupt',
|
|
102
|
-
'learnings.json missing learnings[] array',
|
|
103
|
-
{ path: p, version: obj.version },
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
_assertLearningRecords(obj.learnings, p);
|
|
103
|
+
assertValid(obj, STORE_SCHEMA, 'learnings-store-corrupt', { path: p });
|
|
107
104
|
return obj;
|
|
108
105
|
}
|
|
109
106
|
const migrated = _migrate(obj, p);
|
|
@@ -120,99 +117,26 @@ function _readStore(cwd) {
|
|
|
120
117
|
);
|
|
121
118
|
}
|
|
122
119
|
|
|
123
|
-
const _REQUIRED_LEARNING_FIELDS = ['fingerprint', 'pattern', 'outcome', 'occurrence', 'first_seen', 'last_seen'];
|
|
124
|
-
|
|
125
120
|
function _assertLearningRecords(records, p) {
|
|
126
|
-
|
|
127
|
-
const r = records[i];
|
|
128
|
-
if (!r || typeof r !== 'object' || Array.isArray(r)) {
|
|
129
|
-
throw new NubosPilotError(
|
|
130
|
-
'learnings-store-corrupt',
|
|
131
|
-
'learnings[' + i + '] is not a JSON object',
|
|
132
|
-
{ path: p, index: i },
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
for (const field of _REQUIRED_LEARNING_FIELDS) {
|
|
136
|
-
if (!(field in r)) {
|
|
137
|
-
throw new NubosPilotError(
|
|
138
|
-
'learnings-store-corrupt',
|
|
139
|
-
'learnings[' + i + '] missing required field "' + field + '"',
|
|
140
|
-
{ path: p, index: i, field, required: _REQUIRED_LEARNING_FIELDS.slice() },
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
if (typeof r.fingerprint !== 'string' || !/^[a-f0-9]{16}$/.test(r.fingerprint)) {
|
|
145
|
-
throw new NubosPilotError(
|
|
146
|
-
'learnings-store-corrupt',
|
|
147
|
-
'learnings[' + i + '].fingerprint must be a 16-char hex string',
|
|
148
|
-
{ path: p, index: i, got: r.fingerprint },
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
if (typeof r.occurrence !== 'number' || r.occurrence < 1) {
|
|
152
|
-
throw new NubosPilotError(
|
|
153
|
-
'learnings-store-corrupt',
|
|
154
|
-
'learnings[' + i + '].occurrence must be a positive integer',
|
|
155
|
-
{ path: p, index: i, got: r.occurrence },
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
if (typeof r.pattern !== 'string') {
|
|
159
|
-
throw new NubosPilotError(
|
|
160
|
-
'learnings-store-corrupt',
|
|
161
|
-
'learnings[' + i + '].pattern must be a string',
|
|
162
|
-
{ path: p, index: i, got: typeof r.pattern },
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
if (Buffer.byteLength(r.pattern, 'utf-8') > MAX_PATTERN_BYTES) {
|
|
166
|
-
throw new NubosPilotError(
|
|
167
|
-
'learnings-store-corrupt',
|
|
168
|
-
'learnings[' + i + '].pattern exceeds MAX_PATTERN_BYTES',
|
|
169
|
-
{ path: p, index: i, max: MAX_PATTERN_BYTES },
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
if (typeof r.outcome !== 'string') {
|
|
173
|
-
throw new NubosPilotError(
|
|
174
|
-
'learnings-store-corrupt',
|
|
175
|
-
'learnings[' + i + '].outcome must be a string',
|
|
176
|
-
{ path: p, index: i, got: typeof r.outcome },
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
if ('tokens' in r && !Array.isArray(r.tokens)) {
|
|
180
|
-
throw new NubosPilotError(
|
|
181
|
-
'learnings-store-corrupt',
|
|
182
|
-
'learnings[' + i + '].tokens, when present, must be an array',
|
|
183
|
-
{ path: p, index: i, got: typeof r.tokens },
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
for (const arrField of ['task_ids', 'milestone_ids']) {
|
|
187
|
-
if (arrField in r && !Array.isArray(r[arrField])) {
|
|
188
|
-
throw new NubosPilotError(
|
|
189
|
-
'learnings-store-corrupt',
|
|
190
|
-
'learnings[' + i + '].' + arrField + ', when present, must be an array',
|
|
191
|
-
{ path: p, index: i, field: arrField, got: typeof r[arrField] },
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function _migrate(obj, p, migrators) {
|
|
199
|
-
const reg = migrators || MIGRATORS;
|
|
200
|
-
let cur = obj;
|
|
201
|
-
while (cur && cur.version !== STORE_VERSION) {
|
|
202
|
-
const next = reg[cur.version];
|
|
203
|
-
if (typeof next !== 'function') return null;
|
|
204
|
-
cur = next(cur);
|
|
205
|
-
if (!cur || typeof cur !== 'object') return null;
|
|
206
|
-
}
|
|
207
|
-
if (!Array.isArray(cur.learnings)) {
|
|
121
|
+
if (!Array.isArray(records)) {
|
|
208
122
|
throw new NubosPilotError(
|
|
209
123
|
'learnings-store-corrupt',
|
|
210
|
-
'
|
|
124
|
+
'learnings[] must be an array',
|
|
211
125
|
{ path: p },
|
|
212
126
|
);
|
|
213
127
|
}
|
|
214
|
-
|
|
215
|
-
|
|
128
|
+
assertValid({ version: STORE_VERSION, learnings: records }, STORE_SCHEMA, 'learnings-store-corrupt', { path: p });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function _migrate(obj, p, migrators) {
|
|
132
|
+
return runMigrators(obj, {
|
|
133
|
+
versionField: 'version',
|
|
134
|
+
targetVersion: STORE_VERSION,
|
|
135
|
+
migrators: migrators || MIGRATORS,
|
|
136
|
+
schema: STORE_SCHEMA,
|
|
137
|
+
code: 'learnings-store-corrupt',
|
|
138
|
+
details: { path: p },
|
|
139
|
+
});
|
|
216
140
|
}
|
|
217
141
|
|
|
218
142
|
function _evictIfOverCap(store, opts) {
|
package/lib/memory.cjs
CHANGED
|
@@ -4,6 +4,7 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const crypto = require('node:crypto');
|
|
6
6
|
const { atomicWriteFileSync, appendJsonl, withFileLockAsync, NubosPilotError, projectStateDir } = require('./core.cjs');
|
|
7
|
+
const { validate } = require('./validate.cjs');
|
|
7
8
|
|
|
8
9
|
let _memLog;
|
|
9
10
|
function _log() {
|
|
@@ -15,6 +16,19 @@ const TYPE_ENUM = new Set(['learning', 'handoff', 'critic', 'research']);
|
|
|
15
16
|
const PROVENANCE_ENUM = new Set(['VERIFIED', 'CITED', 'ASSUMED', 'CACHED']);
|
|
16
17
|
const SCHEMA_VERSION = 1;
|
|
17
18
|
|
|
19
|
+
const RECORD_SCHEMA = 'memory-record.v1';
|
|
20
|
+
const MANIFEST_SCHEMA = 'memory-manifest.v1';
|
|
21
|
+
|
|
22
|
+
const _RECORD_CODE_BY_FIELD = Object.freeze({
|
|
23
|
+
type: 'memory-invalid-type',
|
|
24
|
+
title: 'memory-missing-title',
|
|
25
|
+
body: 'memory-missing-body',
|
|
26
|
+
tags: 'memory-invalid-tags',
|
|
27
|
+
provenance: 'memory-invalid-provenance',
|
|
28
|
+
id: 'memory-invalid-id',
|
|
29
|
+
phase: 'memory-invalid-phase',
|
|
30
|
+
});
|
|
31
|
+
|
|
18
32
|
function _memoryRoot(cwd) {
|
|
19
33
|
return path.join(projectStateDir(cwd || process.cwd()), 'memory');
|
|
20
34
|
}
|
|
@@ -24,38 +38,14 @@ function _indexPath(cwd) { return path.join(_memoryRoot(cwd), 'index.usearch');
|
|
|
24
38
|
function _manifestPath(cwd) { return path.join(_memoryRoot(cwd), 'manifest.json'); }
|
|
25
39
|
|
|
26
40
|
function _validateRecord(record) {
|
|
27
|
-
if (!record || typeof record !== 'object') {
|
|
41
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
|
28
42
|
throw new NubosPilotError('memory-invalid-record', 'record must be object', {});
|
|
29
43
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
if (typeof record.title !== 'string' || record.title.length === 0) {
|
|
38
|
-
throw new NubosPilotError('memory-missing-title', 'title required as non-empty string', {});
|
|
39
|
-
}
|
|
40
|
-
if (typeof record.body !== 'string') {
|
|
41
|
-
throw new NubosPilotError('memory-missing-body', 'body required as string', { type: typeof record.body });
|
|
42
|
-
}
|
|
43
|
-
if (record.tags !== undefined && record.tags !== null && !Array.isArray(record.tags)) {
|
|
44
|
-
throw new NubosPilotError('memory-invalid-tags', 'tags must be array of strings or null', {});
|
|
45
|
-
}
|
|
46
|
-
if (record.provenance !== undefined && record.provenance !== null && !PROVENANCE_ENUM.has(record.provenance)) {
|
|
47
|
-
throw new NubosPilotError(
|
|
48
|
-
'memory-invalid-provenance',
|
|
49
|
-
`provenance must be one of [${[...PROVENANCE_ENUM].join(', ')}]`,
|
|
50
|
-
{ provenance: record.provenance },
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
if (record.id !== undefined && record.id !== null && typeof record.id !== 'string') {
|
|
54
|
-
throw new NubosPilotError('memory-invalid-id', 'id must be string or null', { id: record.id });
|
|
55
|
-
}
|
|
56
|
-
if (record.phase !== undefined && record.phase !== null && typeof record.phase !== 'string') {
|
|
57
|
-
throw new NubosPilotError('memory-invalid-phase', 'phase must be string or null', { phase: record.phase });
|
|
58
|
-
}
|
|
44
|
+
const errors = validate(record, RECORD_SCHEMA);
|
|
45
|
+
if (errors.length === 0) return;
|
|
46
|
+
const first = errors[0];
|
|
47
|
+
const code = _RECORD_CODE_BY_FIELD[first.field] || 'memory-invalid-record';
|
|
48
|
+
throw new NubosPilotError(code, first.message, { field: first.field, value: record[first.field] });
|
|
59
49
|
}
|
|
60
50
|
|
|
61
51
|
function _readRecordsJsonlWithStats(cwd) {
|
|
@@ -66,8 +56,11 @@ function _readRecordsJsonlWithStats(cwd) {
|
|
|
66
56
|
let skipped = 0;
|
|
67
57
|
for (const line of raw.split('\n')) {
|
|
68
58
|
if (!line.trim()) continue;
|
|
69
|
-
|
|
70
|
-
|
|
59
|
+
let rec;
|
|
60
|
+
try { rec = JSON.parse(line); }
|
|
61
|
+
catch { skipped += 1; continue; }
|
|
62
|
+
if (validate(rec, RECORD_SCHEMA).length > 0) { skipped += 1; continue; }
|
|
63
|
+
records.push(rec);
|
|
71
64
|
}
|
|
72
65
|
if (skipped > 0) {
|
|
73
66
|
_log().warn('skipped corrupt records.jsonl lines', {
|
|
@@ -92,7 +85,19 @@ function _writeManifest(cwd, manifest) {
|
|
|
92
85
|
function _readManifest(cwd) {
|
|
93
86
|
const p = _manifestPath(cwd);
|
|
94
87
|
if (!fs.existsSync(p)) return null;
|
|
95
|
-
|
|
88
|
+
let obj;
|
|
89
|
+
try { obj = JSON.parse(fs.readFileSync(p, 'utf-8')); } catch { return null; }
|
|
90
|
+
const errors = validate(obj, MANIFEST_SCHEMA);
|
|
91
|
+
if (errors.length > 0) {
|
|
92
|
+
_log().warn('ignoring corrupt memory manifest', {
|
|
93
|
+
event: 'memory-manifest-corrupt',
|
|
94
|
+
file: 'manifest.json',
|
|
95
|
+
violation: errors[0].message,
|
|
96
|
+
hint: 'manifest will be rewritten on next index/rebuild; run `memory-rebuild` if model/dim changed',
|
|
97
|
+
});
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return obj;
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
function _embeddingText(record) {
|
package/lib/messaging.cjs
CHANGED
|
@@ -4,6 +4,7 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const crypto = require('node:crypto');
|
|
6
6
|
const { atomicWriteFileSync, atomicCreateExclusiveSync, fsyncDir, appendJsonl, withFileLock, NubosPilotError, projectStateDir } = require('./core.cjs');
|
|
7
|
+
const { validate, assertValid } = require('./validate.cjs');
|
|
7
8
|
|
|
8
9
|
function _runId() {
|
|
9
10
|
try { return require('./run-context.cjs').getRunId(); }
|
|
@@ -13,6 +14,8 @@ function _runId() {
|
|
|
13
14
|
const KIND_ENUM = new Set(['request', 'response', 'notify']);
|
|
14
15
|
const AGENT_RE = /^[a-zA-Z0-9_-]+$/;
|
|
15
16
|
const ID_RE = /^\d+-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
17
|
+
const MAX_BODY_BYTES = 256 * 1024;
|
|
18
|
+
const MESSAGE_SCHEMA = 'message.v1';
|
|
16
19
|
|
|
17
20
|
function _messagesRoot(cwd) {
|
|
18
21
|
return path.join(projectStateDir(cwd || process.cwd()), 'messages');
|
|
@@ -81,11 +84,11 @@ function _appendManifest(cwd, event) {
|
|
|
81
84
|
function _readMessageFile(filePath) {
|
|
82
85
|
let raw;
|
|
83
86
|
try { raw = fs.readFileSync(filePath, 'utf-8'); } catch { return null; }
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
let parsed;
|
|
88
|
+
try { parsed = JSON.parse(raw); } catch { return null; }
|
|
89
|
+
if (validate(parsed, MESSAGE_SCHEMA).length > 0) return null;
|
|
90
|
+
Object.defineProperty(parsed, '__path', { value: filePath, enumerable: false });
|
|
91
|
+
return parsed;
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
function _scanDir(dir) {
|
|
@@ -118,7 +121,6 @@ function send(opts, cwd) {
|
|
|
118
121
|
if (typeof o.body !== 'string') {
|
|
119
122
|
throw new NubosPilotError('messages-missing-body', 'body required (string, may be empty)', {});
|
|
120
123
|
}
|
|
121
|
-
const MAX_BODY_BYTES = 256 * 1024;
|
|
122
124
|
if (Buffer.byteLength(o.body, 'utf-8') > MAX_BODY_BYTES) {
|
|
123
125
|
throw new NubosPilotError(
|
|
124
126
|
'messages-body-too-large',
|
|
@@ -157,6 +159,7 @@ function send(opts, cwd) {
|
|
|
157
159
|
in_reply_to: o.in_reply_to || null,
|
|
158
160
|
created_at: createdAt,
|
|
159
161
|
};
|
|
162
|
+
assertValid(message, MESSAGE_SCHEMA, 'messages-invalid-record', { id });
|
|
160
163
|
|
|
161
164
|
const workingDir = cwd || process.cwd();
|
|
162
165
|
const dir = _inboxDir(workingDir, o.to);
|
|
@@ -472,4 +475,7 @@ module.exports = {
|
|
|
472
475
|
pendingReplies,
|
|
473
476
|
sweepTaskOnCommit,
|
|
474
477
|
KIND_ENUM,
|
|
478
|
+
AGENT_RE,
|
|
479
|
+
ID_RE,
|
|
480
|
+
MAX_BODY_BYTES,
|
|
475
481
|
};
|
|
@@ -5,6 +5,9 @@ const path = require('node:path');
|
|
|
5
5
|
const readline = require('node:readline');
|
|
6
6
|
const { NubosPilotError, findProjectRoot } = require('./core.cjs');
|
|
7
7
|
const { SAFE_PHASE_RE } = require('./metrics.cjs');
|
|
8
|
+
const { validate } = require('./validate.cjs');
|
|
9
|
+
|
|
10
|
+
const RECORD_SCHEMA = 'metrics-record.v1';
|
|
8
11
|
|
|
9
12
|
let _maLog;
|
|
10
13
|
function _log() {
|
|
@@ -41,15 +44,24 @@ function _readJsonlLines(filePath, onRecord) {
|
|
|
41
44
|
rl.on('line', (raw) => {
|
|
42
45
|
const line = String(raw).trim();
|
|
43
46
|
if (!line) return;
|
|
47
|
+
let rec;
|
|
44
48
|
try {
|
|
45
|
-
|
|
46
|
-
onRecord(rec);
|
|
49
|
+
rec = JSON.parse(line);
|
|
47
50
|
} catch (_err) {
|
|
48
51
|
_log().warn('skipping malformed JSONL line', {
|
|
49
52
|
event: 'metrics-aggregate-malformed-line',
|
|
50
53
|
file: require('node:path').basename(filePath),
|
|
51
54
|
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (validate(rec, RECORD_SCHEMA).length > 0) {
|
|
58
|
+
_log().warn('skipping schema-invalid JSONL line', {
|
|
59
|
+
event: 'metrics-aggregate-invalid-line',
|
|
60
|
+
file: require('node:path').basename(filePath),
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
52
63
|
}
|
|
64
|
+
onRecord(rec);
|
|
53
65
|
});
|
|
54
66
|
rl.on('close', () => resolve());
|
|
55
67
|
rl.on('error', (err) => reject(err));
|
package/lib/migrate.cjs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { assertValid } = require('./validate.cjs');
|
|
4
|
+
|
|
5
|
+
const MAX_HOPS = 100;
|
|
6
|
+
|
|
7
|
+
function runMigrators(obj, opts) {
|
|
8
|
+
const o = opts || {};
|
|
9
|
+
const versionField = o.versionField || 'version';
|
|
10
|
+
const target = o.targetVersion;
|
|
11
|
+
const migrators = o.migrators || {};
|
|
12
|
+
let cur = obj;
|
|
13
|
+
let hops = 0;
|
|
14
|
+
while (cur && typeof cur === 'object' && !Array.isArray(cur) && cur[versionField] !== target) {
|
|
15
|
+
if (hops >= MAX_HOPS) return null;
|
|
16
|
+
hops += 1;
|
|
17
|
+
const key = cur[versionField];
|
|
18
|
+
if (!Object.prototype.hasOwnProperty.call(migrators, key)) return null;
|
|
19
|
+
const fn = migrators[key];
|
|
20
|
+
if (typeof fn !== 'function') return null;
|
|
21
|
+
cur = fn(cur);
|
|
22
|
+
if (!cur || typeof cur !== 'object' || Array.isArray(cur)) return null;
|
|
23
|
+
}
|
|
24
|
+
if (!cur || typeof cur !== 'object' || Array.isArray(cur)) return null;
|
|
25
|
+
if (o.schema) assertValid(cur, o.schema, o.code || 'data-migration-invalid', o.details || {});
|
|
26
|
+
return cur;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { runMigrators, MAX_HOPS };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
|
|
6
|
+
const { runMigrators, MAX_HOPS } = require('./migrate.cjs');
|
|
7
|
+
const { NubosPilotError } = require('./core.cjs');
|
|
8
|
+
|
|
9
|
+
test('MIG-1: object already at target version is returned unchanged', () => {
|
|
10
|
+
const obj = { version: 2, payload: 'x' };
|
|
11
|
+
const out = runMigrators(obj, { targetVersion: 2, migrators: {} });
|
|
12
|
+
assert.equal(out, obj);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('MIG-2: single hop runs the matching migrator', () => {
|
|
16
|
+
const out = runMigrators({ version: 1, n: 1 }, {
|
|
17
|
+
targetVersion: 2,
|
|
18
|
+
migrators: { 1: (v) => ({ version: 2, n: v.n + 1 }) },
|
|
19
|
+
});
|
|
20
|
+
assert.deepEqual(out, { version: 2, n: 2 });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('MIG-3: multi-hop chain runs each hop until target', () => {
|
|
24
|
+
const out = runMigrators({ version: 0, steps: [] }, {
|
|
25
|
+
targetVersion: 3,
|
|
26
|
+
migrators: {
|
|
27
|
+
0: (v) => ({ version: 1, steps: [...v.steps, 'a'] }),
|
|
28
|
+
1: (v) => ({ version: 2, steps: [...v.steps, 'b'] }),
|
|
29
|
+
2: (v) => ({ version: 3, steps: [...v.steps, 'c'] }),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
assert.deepEqual(out.steps, ['a', 'b', 'c']);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('MIG-4: missing migrator for a version returns null', () => {
|
|
36
|
+
const out = runMigrators({ version: 99 }, { targetVersion: 1, migrators: {} });
|
|
37
|
+
assert.equal(out, null);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('MIG-5: migrator returning a non-object returns null', () => {
|
|
41
|
+
const out = runMigrators({ version: 0 }, { targetVersion: 1, migrators: { 0: () => null } });
|
|
42
|
+
assert.equal(out, null);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('MIG-6: hop cap prevents an infinite migrator cycle', () => {
|
|
46
|
+
const out = runMigrators({ version: 0 }, {
|
|
47
|
+
targetVersion: 999,
|
|
48
|
+
migrators: { 0: () => ({ version: 0 }) },
|
|
49
|
+
});
|
|
50
|
+
assert.equal(out, null);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('MIG-7: pure function — input object is not mutated', () => {
|
|
54
|
+
const input = { version: 0, n: 1 };
|
|
55
|
+
runMigrators(input, { targetVersion: 1, migrators: { 0: (v) => ({ version: 1, n: v.n + 1 }) } });
|
|
56
|
+
assert.deepEqual(input, { version: 0, n: 1 });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('MIG-8: migrated shape is validated against the given schema', () => {
|
|
60
|
+
assert.throws(
|
|
61
|
+
() => runMigrators({ version: 0, learnings: [] }, {
|
|
62
|
+
targetVersion: 1,
|
|
63
|
+
migrators: { 0: () => ({ version: 1, learnings: [{ fingerprint: 'NOTHEX', occurrence: 1 }] }) },
|
|
64
|
+
schema: 'learnings.v1',
|
|
65
|
+
code: 'learnings-store-corrupt',
|
|
66
|
+
details: { path: '<test>' },
|
|
67
|
+
}),
|
|
68
|
+
(err) => err instanceof NubosPilotError && err.code === 'learnings-store-corrupt',
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('MIG-9: custom versionField is honoured', () => {
|
|
73
|
+
const out = runMigrators({ schema_version: 1, ok: true }, {
|
|
74
|
+
versionField: 'schema_version',
|
|
75
|
+
targetVersion: 1,
|
|
76
|
+
migrators: {},
|
|
77
|
+
});
|
|
78
|
+
assert.deepEqual(out, { schema_version: 1, ok: true });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('MIG-10: MAX_HOPS is a finite positive cap', () => {
|
|
82
|
+
assert.ok(Number.isInteger(MAX_HOPS) && MAX_HOPS > 0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('MIG-11: a version matching an Object.prototype member returns null, not a raw throw', () => {
|
|
86
|
+
for (const poison of ['valueOf', 'hasOwnProperty', 'toString', 'constructor', '__proto__']) {
|
|
87
|
+
let out;
|
|
88
|
+
assert.doesNotThrow(() => { out = runMigrators({ version: poison }, { targetVersion: 1, migrators: {} }); });
|
|
89
|
+
assert.equal(out, null);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "checkpoint.v1",
|
|
3
|
+
"title": "Task checkpoint (checkpoints/<task_id>.json)",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["schema_version"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"schema_version": { "type": "integer", "minimum": 1 },
|
|
8
|
+
"task_id": { "type": "string" },
|
|
9
|
+
"status": { "type": "string" },
|
|
10
|
+
"files_touched": { "type": "array" },
|
|
11
|
+
"nubosloop": { "type": "object" }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "codebase-manifest.v1",
|
|
3
|
+
"title": "Codebase hash manifest (codebase/.hashes.json)",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["schema_version"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"schema_version": { "type": "integer", "minimum": 1 },
|
|
8
|
+
"generated_at": { "type": ["string", "null"] },
|
|
9
|
+
"files": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"additionalProperties": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"required": ["sha256"],
|
|
14
|
+
"properties": {
|
|
15
|
+
"sha256": { "type": "string" },
|
|
16
|
+
"size": { "type": "integer", "minimum": 0 },
|
|
17
|
+
"ext": { "type": "string" }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "learnings.v1",
|
|
3
|
+
"title": "Learnings store (knowledge/learnings.json)",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["version", "learnings"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"version": { "type": "integer", "minimum": 1 },
|
|
8
|
+
"learnings": {
|
|
9
|
+
"type": "array",
|
|
10
|
+
"items": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"required": ["fingerprint", "pattern", "outcome", "occurrence", "first_seen", "last_seen"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"fingerprint": { "type": "string", "pattern": "^[a-f0-9]{16}$" },
|
|
15
|
+
"pattern": { "type": "string", "maxBytes": 4096 },
|
|
16
|
+
"outcome": { "type": "string", "maxBytes": 4096 },
|
|
17
|
+
"occurrence": { "type": "integer", "minimum": 1 },
|
|
18
|
+
"first_seen": { "type": "string" },
|
|
19
|
+
"last_seen": { "type": "string" },
|
|
20
|
+
"tokens": { "type": "array", "items": { "type": "string" } },
|
|
21
|
+
"task_ids": { "type": "array", "items": { "type": "string" } },
|
|
22
|
+
"milestone_ids": { "type": "array", "items": { "type": "string" } },
|
|
23
|
+
"outcome_history": { "type": "array" }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "memory-manifest.v1",
|
|
3
|
+
"title": "Memory manifest (memory/manifest.json)",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["schema_version", "model", "dim"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"schema_version": { "type": "integer", "minimum": 1 },
|
|
8
|
+
"model": { "type": "string", "minLength": 1 },
|
|
9
|
+
"dim": { "type": "integer", "minimum": 1 },
|
|
10
|
+
"alpha": { "type": "number" },
|
|
11
|
+
"created_at": { "type": "string" },
|
|
12
|
+
"rebuilt_at": { "type": "string" }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "memory-record.v1",
|
|
3
|
+
"title": "Memory record (memory/records.jsonl line)",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["type", "title", "body"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"type": { "enum": ["learning", "handoff", "critic", "research"] },
|
|
8
|
+
"title": { "type": "string", "minLength": 1 },
|
|
9
|
+
"body": { "type": "string" },
|
|
10
|
+
"tags": { "type": ["array", "null"] },
|
|
11
|
+
"provenance": { "enum": ["VERIFIED", "CITED", "ASSUMED", "CACHED", null] },
|
|
12
|
+
"id": { "type": ["string", "null"] },
|
|
13
|
+
"phase": { "type": ["string", "null"] },
|
|
14
|
+
"created_at": { "type": ["string", "null"] }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "message.v1",
|
|
3
|
+
"title": "Inter-agent message (messages/inbox/<agent>/<id>.json)",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["id", "from", "to", "phase", "kind", "subject", "body", "created_at"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"id": { "type": "string", "pattern": "^\\d+-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" },
|
|
8
|
+
"from": { "type": "string", "pattern": "^[a-zA-Z0-9_-]+$" },
|
|
9
|
+
"to": { "type": "string", "pattern": "^[a-zA-Z0-9_-]+$" },
|
|
10
|
+
"phase": { "type": "string", "minLength": 1 },
|
|
11
|
+
"round": { "type": ["integer", "null"], "minimum": 0 },
|
|
12
|
+
"kind": { "enum": ["request", "response", "notify"] },
|
|
13
|
+
"subject": { "type": "string", "minLength": 1 },
|
|
14
|
+
"body": { "type": "string", "maxBytes": 262144 },
|
|
15
|
+
"expects_reply": { "type": "boolean" },
|
|
16
|
+
"in_reply_to": { "type": ["string", "null"] },
|
|
17
|
+
"created_at": { "type": "string" }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "metrics-record.v1",
|
|
3
|
+
"title": "Metrics record (metrics/phase-<phase>.jsonl, meta.jsonl line)",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"duration_ms": { "type": ["number", "null"], "minimum": 0 },
|
|
7
|
+
"tokens_in": { "type": ["number", "null"] },
|
|
8
|
+
"tokens_out": { "type": ["number", "null"] },
|
|
9
|
+
"retry_count": { "type": ["number", "null"], "minimum": 0 }
|
|
10
|
+
}
|
|
11
|
+
}
|