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.
@@ -0,0 +1,301 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { NubosPilotError } = require('./core.cjs');
6
+
7
+ const SCHEMA_DIR = path.join(__dirname, 'schemas', 'data');
8
+ const PATTERN_INPUT_MAX = 64 * 1024;
9
+ const _cache = new Map();
10
+
11
+ function _hasOwn(obj, key) {
12
+ return Object.prototype.hasOwnProperty.call(obj, key);
13
+ }
14
+
15
+ function _deepFreeze(obj) {
16
+ if (obj && typeof obj === 'object' && !Object.isFrozen(obj)) {
17
+ Object.freeze(obj);
18
+ for (const key of Object.keys(obj)) _deepFreeze(obj[key]);
19
+ }
20
+ return obj;
21
+ }
22
+
23
+ function _deepEqual(a, b) {
24
+ if (a === b) return true;
25
+ if (typeof a !== typeof b) return false;
26
+ if (a === null || b === null) return a === b;
27
+ if (typeof a !== 'object') return false;
28
+ const aArr = Array.isArray(a);
29
+ if (aArr !== Array.isArray(b)) return false;
30
+ if (aArr) {
31
+ if (a.length !== b.length) return false;
32
+ for (let i = 0; i < a.length; i += 1) if (!_deepEqual(a[i], b[i])) return false;
33
+ return true;
34
+ }
35
+ const aKeys = Object.keys(a);
36
+ if (aKeys.length !== Object.keys(b).length) return false;
37
+ for (const key of aKeys) {
38
+ if (!_hasOwn(b, key) || !_deepEqual(a[key], b[key])) return false;
39
+ }
40
+ return true;
41
+ }
42
+
43
+ function _loadSchema(name) {
44
+ if (_cache.has(name)) return _cache.get(name);
45
+ if (!/^[a-z0-9][a-z0-9.\-]*$/.test(String(name))) {
46
+ throw new NubosPilotError(
47
+ 'data-schema-not-found',
48
+ 'Invalid data-schema name: ' + JSON.stringify(name),
49
+ { name },
50
+ );
51
+ }
52
+ const p = path.join(SCHEMA_DIR, name + '.json');
53
+ let raw;
54
+ try { raw = fs.readFileSync(p, 'utf-8'); }
55
+ catch (err) {
56
+ throw new NubosPilotError(
57
+ 'data-schema-not-found',
58
+ 'Unknown data schema: ' + String(name),
59
+ { name, cause: err && err.code, available: listSchemas() },
60
+ );
61
+ }
62
+ let schema;
63
+ try { schema = JSON.parse(raw); }
64
+ catch (err) {
65
+ throw new NubosPilotError(
66
+ 'data-schema-corrupt',
67
+ 'Data schema ' + name + '.json is not valid JSON: ' + (err && err.message),
68
+ { name },
69
+ );
70
+ }
71
+ _deepFreeze(schema);
72
+ _cache.set(name, schema);
73
+ return schema;
74
+ }
75
+
76
+ function listSchemas() {
77
+ let entries;
78
+ try { entries = fs.readdirSync(SCHEMA_DIR); }
79
+ catch { return []; }
80
+ return entries
81
+ .filter((f) => f.endsWith('.json'))
82
+ .map((f) => f.slice(0, -5))
83
+ .sort();
84
+ }
85
+
86
+ function _typeOf(value) {
87
+ if (value === null) return 'null';
88
+ if (Array.isArray(value)) return 'array';
89
+ if (Number.isInteger(value)) return 'integer';
90
+ return typeof value;
91
+ }
92
+
93
+ function _matchesType(value, type) {
94
+ if (type === 'integer') return typeof value === 'number' && Number.isInteger(value);
95
+ if (type === 'number') return typeof value === 'number' && Number.isFinite(value);
96
+ if (type === 'array') return Array.isArray(value);
97
+ if (type === 'object') return value !== null && typeof value === 'object' && !Array.isArray(value);
98
+ if (type === 'null') return value === null;
99
+ return typeof value === type;
100
+ }
101
+
102
+ function _segmentsToPath(segments) {
103
+ if (!segments.length) return '';
104
+ return '/' + segments.map((s) => String(s)).join('/');
105
+ }
106
+
107
+ function _lastNamed(segments) {
108
+ for (let i = segments.length - 1; i >= 0; i -= 1) {
109
+ if (typeof segments[i] === 'string') return segments[i];
110
+ }
111
+ return null;
112
+ }
113
+
114
+ function _firstIndex(segments) {
115
+ for (const s of segments) if (typeof s === 'number') return s;
116
+ return null;
117
+ }
118
+
119
+ function _push(errors, segments, keyword, message, extra) {
120
+ const err = {
121
+ instancePath: _segmentsToPath(segments),
122
+ keyword,
123
+ message,
124
+ field: _lastNamed(segments),
125
+ index: _firstIndex(segments),
126
+ };
127
+ if (extra) Object.assign(err, extra);
128
+ errors.push(err);
129
+ }
130
+
131
+ function _validateNode(value, schema, segments, errors) {
132
+ if (!schema || typeof schema !== 'object') return;
133
+
134
+ if ('const' in schema && !_deepEqual(value, schema.const)) {
135
+ _push(errors, segments, 'const', _label(segments) + ' must equal ' + JSON.stringify(schema.const),
136
+ { expected: schema.const, actual: value });
137
+ return;
138
+ }
139
+
140
+ if (schema.type) {
141
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
142
+ if (!types.some((t) => _matchesType(value, t))) {
143
+ _push(errors, segments, 'type',
144
+ _label(segments) + ' must be ' + types.join(' or ') + ' (got ' + _typeOf(value) + ')',
145
+ { expected: schema.type, actual: _typeOf(value) });
146
+ return;
147
+ }
148
+ }
149
+
150
+ if (Array.isArray(schema.enum) && !schema.enum.some((e) => _deepEqual(e, value))) {
151
+ _push(errors, segments, 'enum',
152
+ _label(segments) + ' must be one of ' + JSON.stringify(schema.enum) + ' (got ' + JSON.stringify(value) + ')',
153
+ { expected: schema.enum, actual: value });
154
+ }
155
+
156
+ if (typeof value === 'string') _validateString(value, schema, segments, errors);
157
+ if (typeof value === 'number') _validateNumber(value, schema, segments, errors);
158
+ if (Array.isArray(value)) _validateArray(value, schema, segments, errors);
159
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
160
+ _validateObject(value, schema, segments, errors);
161
+ }
162
+ }
163
+
164
+ function _label(segments) {
165
+ const named = _lastNamed(segments);
166
+ const idx = _firstIndex(segments);
167
+ if (named && idx !== null && segments[segments.length - 1] === named) {
168
+ return named;
169
+ }
170
+ return named || (idx !== null ? '[' + idx + ']' : 'value');
171
+ }
172
+
173
+ function _validateString(value, schema, segments, errors) {
174
+ if (typeof schema.minLength === 'number' && value.length < schema.minLength) {
175
+ _push(errors, segments, 'minLength',
176
+ _label(segments) + ' must be at least ' + schema.minLength + ' characters',
177
+ { expected: schema.minLength, actual: value.length });
178
+ }
179
+ if (typeof schema.maxLength === 'number' && value.length > schema.maxLength) {
180
+ _push(errors, segments, 'maxLength',
181
+ _label(segments) + ' must be at most ' + schema.maxLength + ' characters',
182
+ { expected: schema.maxLength, actual: value.length });
183
+ }
184
+ if (typeof schema.maxBytes === 'number') {
185
+ const bytes = Buffer.byteLength(value, 'utf-8');
186
+ if (bytes > schema.maxBytes) {
187
+ _push(errors, segments, 'maxBytes',
188
+ _label(segments) + ' exceeds ' + schema.maxBytes + ' bytes (got ' + bytes + ')',
189
+ { expected: schema.maxBytes, actual: bytes });
190
+ }
191
+ }
192
+ if (typeof schema.pattern === 'string') {
193
+ if (value.length > PATTERN_INPUT_MAX) {
194
+ _push(errors, segments, 'pattern',
195
+ _label(segments) + ' is too long (' + value.length + ' chars) to match ' + schema.pattern,
196
+ { expected: schema.pattern, actual: value.length });
197
+ } else if (!new RegExp(schema.pattern).test(value)) {
198
+ _push(errors, segments, 'pattern',
199
+ _label(segments) + ' must match ' + schema.pattern,
200
+ { expected: schema.pattern, actual: value });
201
+ }
202
+ }
203
+ }
204
+
205
+ function _validateNumber(value, schema, segments, errors) {
206
+ if (typeof schema.minimum === 'number' && value < schema.minimum) {
207
+ _push(errors, segments, 'minimum',
208
+ _label(segments) + ' must be >= ' + schema.minimum + ' (got ' + value + ')',
209
+ { expected: schema.minimum, actual: value });
210
+ }
211
+ if (typeof schema.exclusiveMinimum === 'number' && value <= schema.exclusiveMinimum) {
212
+ _push(errors, segments, 'exclusiveMinimum',
213
+ _label(segments) + ' must be > ' + schema.exclusiveMinimum + ' (got ' + value + ')',
214
+ { expected: schema.exclusiveMinimum, actual: value });
215
+ }
216
+ if (typeof schema.maximum === 'number' && value > schema.maximum) {
217
+ _push(errors, segments, 'maximum',
218
+ _label(segments) + ' must be <= ' + schema.maximum + ' (got ' + value + ')',
219
+ { expected: schema.maximum, actual: value });
220
+ }
221
+ }
222
+
223
+ function _validateArray(value, schema, segments, errors) {
224
+ if (typeof schema.minItems === 'number' && value.length < schema.minItems) {
225
+ _push(errors, segments, 'minItems',
226
+ _label(segments) + ' must have at least ' + schema.minItems + ' items',
227
+ { expected: schema.minItems, actual: value.length });
228
+ }
229
+ if (typeof schema.maxItems === 'number' && value.length > schema.maxItems) {
230
+ _push(errors, segments, 'maxItems',
231
+ _label(segments) + ' must have at most ' + schema.maxItems + ' items',
232
+ { expected: schema.maxItems, actual: value.length });
233
+ }
234
+ if (schema.items) {
235
+ for (let i = 0; i < value.length; i += 1) {
236
+ _validateNode(value[i], schema.items, segments.concat(i), errors);
237
+ }
238
+ }
239
+ }
240
+
241
+ function _validateObject(value, schema, segments, errors) {
242
+ if (Array.isArray(schema.required)) {
243
+ for (const field of schema.required) {
244
+ if (!_hasOwn(value, field) || value[field] === undefined) {
245
+ _push(errors, segments.concat(field), 'required',
246
+ _objLabel(segments) + ' missing required field "' + field + '"',
247
+ { field });
248
+ }
249
+ }
250
+ }
251
+ const props = schema.properties || {};
252
+ const addl = schema.additionalProperties;
253
+ if (addl === false) {
254
+ for (const key of Object.keys(value)) {
255
+ if (!_hasOwn(props, key) && value[key] !== undefined) {
256
+ _push(errors, segments.concat(key), 'additionalProperties',
257
+ _objLabel(segments) + ' has unknown field "' + key + '"',
258
+ { field: key });
259
+ }
260
+ }
261
+ } else if (addl && typeof addl === 'object') {
262
+ for (const key of Object.keys(value)) {
263
+ if (!_hasOwn(props, key) && value[key] !== undefined) {
264
+ _validateNode(value[key], addl, segments.concat(key), errors);
265
+ }
266
+ }
267
+ }
268
+ for (const key of Object.keys(props)) {
269
+ if (_hasOwn(value, key) && value[key] !== undefined) {
270
+ _validateNode(value[key], props[key], segments.concat(key), errors);
271
+ }
272
+ }
273
+ }
274
+
275
+ function _objLabel(segments) {
276
+ const idx = _firstIndex(segments);
277
+ const named = _lastNamed(segments);
278
+ if (named) return named;
279
+ if (idx !== null) return '[' + idx + ']';
280
+ return 'object';
281
+ }
282
+
283
+ function validate(value, schemaName) {
284
+ const schema = typeof schemaName === 'string' ? _loadSchema(schemaName) : schemaName;
285
+ const errors = [];
286
+ _validateNode(value, schema, [], errors);
287
+ return errors;
288
+ }
289
+
290
+ function assertValid(value, schemaName, code, baseDetails) {
291
+ const errors = validate(value, schemaName);
292
+ if (errors.length === 0) return;
293
+ const first = errors[0];
294
+ throw new NubosPilotError(
295
+ code,
296
+ first.message,
297
+ Object.assign({ schema: schemaName, errors }, first, baseDetails || {}),
298
+ );
299
+ }
300
+
301
+ module.exports = { validate, assertValid, listSchemas, _loadSchema, SCHEMA_DIR };
@@ -0,0 +1,242 @@
1
+ 'use strict';
2
+
3
+ const { test } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+
6
+ const { validate, assertValid, listSchemas, _loadSchema } = require('./validate.cjs');
7
+ const { NubosPilotError } = require('./core.cjs');
8
+ const learnings = require('./learnings.cjs');
9
+ const memory = require('./memory.cjs');
10
+ const messaging = require('./messaging.cjs');
11
+
12
+ const RECORD = {
13
+ type: 'object',
14
+ required: ['fingerprint', 'occurrence'],
15
+ properties: {
16
+ fingerprint: { type: 'string', pattern: '^[a-f0-9]{16}$' },
17
+ occurrence: { type: 'integer', minimum: 1 },
18
+ pattern: { type: 'string', maxBytes: 8 },
19
+ status: { type: 'string', enum: ['a', 'b'] },
20
+ tags: { type: 'array', items: { type: 'string' } },
21
+ },
22
+ };
23
+
24
+ test('VAL-1: valid object yields no errors', () => {
25
+ assert.deepEqual(validate({ fingerprint: 'aaaaaaaaaaaaaaaa', occurrence: 3 }, RECORD), []);
26
+ });
27
+
28
+ test('VAL-2: missing required field reports field name', () => {
29
+ const errs = validate({ fingerprint: 'aaaaaaaaaaaaaaaa' }, RECORD);
30
+ assert.equal(errs.length, 1);
31
+ assert.equal(errs[0].keyword, 'required');
32
+ assert.equal(errs[0].field, 'occurrence');
33
+ });
34
+
35
+ test('VAL-3: type mismatch reports expected/actual', () => {
36
+ const errs = validate({ fingerprint: 'aaaaaaaaaaaaaaaa', occurrence: 'x' }, RECORD);
37
+ assert.equal(errs[0].keyword, 'type');
38
+ assert.equal(errs[0].field, 'occurrence');
39
+ assert.equal(errs[0].actual, 'string');
40
+ });
41
+
42
+ test('VAL-4: pattern mismatch', () => {
43
+ const errs = validate({ fingerprint: 'NOTHEX', occurrence: 1 }, RECORD);
44
+ assert.equal(errs[0].keyword, 'pattern');
45
+ assert.match(errs[0].message, /fingerprint/);
46
+ });
47
+
48
+ test('VAL-5: minimum violation', () => {
49
+ const errs = validate({ fingerprint: 'aaaaaaaaaaaaaaaa', occurrence: 0 }, RECORD);
50
+ assert.equal(errs[0].keyword, 'minimum');
51
+ });
52
+
53
+ test('VAL-6: non-integer rejected by integer type', () => {
54
+ const errs = validate({ fingerprint: 'aaaaaaaaaaaaaaaa', occurrence: 1.5 }, RECORD);
55
+ assert.equal(errs[0].keyword, 'type');
56
+ });
57
+
58
+ test('VAL-7: maxBytes counts UTF-8 bytes', () => {
59
+ const errs = validate({ fingerprint: 'aaaaaaaaaaaaaaaa', occurrence: 1, pattern: 'üüüüü' }, RECORD);
60
+ assert.equal(errs[0].keyword, 'maxBytes');
61
+ assert.equal(errs[0].actual, 10);
62
+ });
63
+
64
+ test('VAL-8: enum violation', () => {
65
+ const errs = validate({ fingerprint: 'aaaaaaaaaaaaaaaa', occurrence: 1, status: 'z' }, RECORD);
66
+ assert.equal(errs[0].keyword, 'enum');
67
+ });
68
+
69
+ test('VAL-9: nested array items validated with index in path', () => {
70
+ const errs = validate({ fingerprint: 'aaaaaaaaaaaaaaaa', occurrence: 1, tags: ['ok', 5] }, RECORD);
71
+ assert.equal(errs[0].keyword, 'type');
72
+ assert.equal(errs[0].index, 1);
73
+ assert.equal(errs[0].instancePath, '/tags/1');
74
+ });
75
+
76
+ test('VAL-10: assertValid throws NubosPilotError with given code and errors detail', () => {
77
+ assert.throws(
78
+ () => assertValid({ fingerprint: 'NOTHEX' }, RECORD, 'demo-corrupt', { path: '/tmp/x' }),
79
+ (err) => err instanceof NubosPilotError
80
+ && err.code === 'demo-corrupt'
81
+ && Array.isArray(err.details.errors)
82
+ && err.details.path === '/tmp/x',
83
+ );
84
+ });
85
+
86
+ test('VAL-11: assertValid is a no-op when valid', () => {
87
+ assert.doesNotThrow(() => assertValid({ fingerprint: 'aaaaaaaaaaaaaaaa', occurrence: 1 }, RECORD, 'demo-corrupt'));
88
+ });
89
+
90
+ test('VAL-12: unknown schema name throws data-schema-not-found', () => {
91
+ assert.throws(
92
+ () => validate({}, 'no-such-schema'),
93
+ (err) => err instanceof NubosPilotError && err.code === 'data-schema-not-found',
94
+ );
95
+ });
96
+
97
+ test('VAL-13: invalid schema name is rejected before fs access', () => {
98
+ assert.throws(
99
+ () => validate({}, '../etc/passwd'),
100
+ (err) => err instanceof NubosPilotError && err.code === 'data-schema-not-found',
101
+ );
102
+ });
103
+
104
+ test('VAL-14: learnings.v1 is registered and loadable', () => {
105
+ assert.ok(listSchemas().includes('learnings.v1'));
106
+ const schema = _loadSchema('learnings.v1');
107
+ assert.equal(schema.$id, 'learnings.v1');
108
+ });
109
+
110
+ test('VAL-15: learnings.v1 pattern maxBytes stays in sync with MAX_PATTERN_BYTES', () => {
111
+ const schema = _loadSchema('learnings.v1');
112
+ const patternSchema = schema.properties.learnings.items.properties.pattern;
113
+ assert.equal(
114
+ patternSchema.maxBytes,
115
+ learnings.MAX_PATTERN_BYTES,
116
+ 'learnings.v1.json pattern.maxBytes drifted from learnings.MAX_PATTERN_BYTES',
117
+ );
118
+ });
119
+
120
+ test('VAL-16: learnings.v1 outcome maxBytes stays in sync with MAX_OUTCOME_BYTES', () => {
121
+ const schema = _loadSchema('learnings.v1');
122
+ const outcomeSchema = schema.properties.learnings.items.properties.outcome;
123
+ assert.equal(
124
+ outcomeSchema.maxBytes,
125
+ learnings.MAX_OUTCOME_BYTES,
126
+ 'learnings.v1.json outcome.maxBytes drifted from learnings.MAX_OUTCOME_BYTES',
127
+ );
128
+ });
129
+
130
+ test('VAL-17: additionalProperties:false flags inherited-name own keys (no prototype-chain bypass)', () => {
131
+ const schema = { type: 'object', additionalProperties: false, properties: { a: { type: 'string' } } };
132
+ const input = JSON.parse('{"a":"x","constructor":1,"toString":1}');
133
+ const errs = validate(input, schema);
134
+ const unknown = errs.filter((e) => e.keyword === 'additionalProperties').map((e) => e.field).sort();
135
+ assert.deepEqual(unknown, ['constructor', 'toString']);
136
+ });
137
+
138
+ test('VAL-18: required is satisfied only by own properties, not Object.prototype members', () => {
139
+ const schema = { type: 'object', required: ['toString', 'fingerprint'] };
140
+ const errs = validate({}, schema);
141
+ const missing = errs.filter((e) => e.keyword === 'required').map((e) => e.field).sort();
142
+ assert.deepEqual(missing, ['fingerprint', 'toString']);
143
+ });
144
+
145
+ test('VAL-19: a property named like a prototype member does not validate the inherited value', () => {
146
+ const schema = { type: 'object', properties: { toString: { type: 'string' } } };
147
+ const errs = validate(JSON.parse('{"a":1}'), schema);
148
+ assert.deepEqual(errs, []);
149
+ });
150
+
151
+ test('VAL-20: pattern does not run on pathological oversized input (ReDoS guard)', () => {
152
+ const schema = { type: 'string', pattern: '^(a+)+$' };
153
+ const errs = validate('a'.repeat(70000) + '!', schema);
154
+ assert.equal(errs[0].keyword, 'pattern');
155
+ assert.match(errs[0].message, /too long/);
156
+ });
157
+
158
+ test('VAL-21: enum/const equality is key-order independent', () => {
159
+ const enumSchema = { enum: [{ a: 1, b: 2 }] };
160
+ assert.deepEqual(validate(JSON.parse('{"b":2,"a":1}'), enumSchema), []);
161
+ const constSchema = { const: { a: 1, b: 2 } };
162
+ assert.deepEqual(validate(JSON.parse('{"b":2,"a":1}'), constSchema), []);
163
+ });
164
+
165
+ test('VAL-23: memory-record.v1 type enum stays in sync with TYPE_ENUM', () => {
166
+ const schema = _loadSchema('memory-record.v1');
167
+ assert.deepEqual(schema.properties.type.enum, [...memory.TYPE_ENUM]);
168
+ });
169
+
170
+ test('VAL-24: memory-record.v1 provenance enum stays in sync with PROVENANCE_ENUM', () => {
171
+ const schema = _loadSchema('memory-record.v1');
172
+ const nonNull = schema.properties.provenance.enum.filter((e) => e !== null);
173
+ assert.deepEqual(nonNull, [...memory.PROVENANCE_ENUM]);
174
+ assert.ok(schema.properties.provenance.enum.includes(null));
175
+ });
176
+
177
+ test('VAL-25: memory-manifest.v1 accepts both init and rebuilt manifests', () => {
178
+ assert.deepEqual(validate({ schema_version: 1, model: 'm', dim: 384, alpha: 0.6, created_at: 'x' }, 'memory-manifest.v1'), []);
179
+ assert.deepEqual(validate({ schema_version: 1, model: 'm', dim: 384, alpha: 0.6, rebuilt_at: 'x' }, 'memory-manifest.v1'), []);
180
+ assert.equal(validate({ schema_version: 1, model: 'm' }, 'memory-manifest.v1').length, 1);
181
+ });
182
+
183
+ test('VAL-26: additionalProperties as a schema validates every map value', () => {
184
+ const schema = { type: 'object', additionalProperties: { type: 'object', required: ['sha256'], properties: { sha256: { type: 'string' } } } };
185
+ assert.deepEqual(validate({ 'a.js': { sha256: 'x' }, 'b.js': { sha256: 'y' } }, schema), []);
186
+ const errs = validate({ 'a.js': { sha256: 'x' }, 'b.js': { size: 5 } }, schema);
187
+ assert.equal(errs[0].keyword, 'required');
188
+ assert.equal(errs[0].field, 'sha256');
189
+ assert.equal(errs[0].instancePath, '/b.js/sha256');
190
+ });
191
+
192
+ test('VAL-27: codebase-manifest.v1 rejects a file entry missing sha256', () => {
193
+ const errs = validate({ schema_version: 1, files: { 'a.js': { size: 10, ext: '.js' } } }, 'codebase-manifest.v1');
194
+ assert.equal(errs[0].keyword, 'required');
195
+ assert.equal(errs[0].field, 'sha256');
196
+ });
197
+
198
+ test('VAL-28: message.v1 id/from/to patterns stay in sync with messaging regexes', () => {
199
+ const schema = _loadSchema('message.v1');
200
+ assert.equal(schema.properties.id.pattern, messaging.ID_RE.source);
201
+ assert.equal(schema.properties.from.pattern, messaging.AGENT_RE.source);
202
+ assert.equal(schema.properties.to.pattern, messaging.AGENT_RE.source);
203
+ });
204
+
205
+ test('VAL-29: message.v1 body maxBytes stays in sync with MAX_BODY_BYTES', () => {
206
+ const schema = _loadSchema('message.v1');
207
+ assert.equal(schema.properties.body.maxBytes, messaging.MAX_BODY_BYTES);
208
+ });
209
+
210
+ test('VAL-32: message.v1 kind enum stays in sync with KIND_ENUM', () => {
211
+ const schema = _loadSchema('message.v1');
212
+ assert.deepEqual(schema.properties.kind.enum, [...messaging.KIND_ENUM]);
213
+ });
214
+
215
+ test('VAL-30: metrics-record.v1 accepts coexisting record shapes (buildRecord + session-aggregate)', () => {
216
+ const buildShape = {
217
+ agent: 'a', tier: 't', resolved_model: 'm', phase: '10', plan: 'p', task: 'k',
218
+ started_at: 'x', ended_at: 'y', duration_ms: 1000, tokens_in: 100, tokens_out: 50,
219
+ retry_count: 0, status: 'ok', runtime: 'claude', error: null,
220
+ };
221
+ const sessionShape = {
222
+ schema_version: 2, started_at: 'x', phase: 'P1', agent: 'a', tier: 't', resolved_model: 'm',
223
+ plan: 'PL1', task: 'TA1', tokens_in: 10, tokens_out: 20, duration_ms: 100, status: 'ok',
224
+ runtime: 'claude', retry_count: 0,
225
+ };
226
+ assert.deepEqual(validate(buildShape, 'metrics-record.v1'), []);
227
+ assert.deepEqual(validate(sessionShape, 'metrics-record.v1'), []);
228
+ });
229
+
230
+ test('VAL-31: metrics-record.v1 rejects a record whose arithmetic field is the wrong type', () => {
231
+ const errs = validate({ agent: 'a', tokens_in: 'not-a-number' }, 'metrics-record.v1');
232
+ assert.equal(errs[0].keyword, 'type');
233
+ assert.equal(errs[0].field, 'tokens_in');
234
+ });
235
+
236
+ test('VAL-22: circular / non-serializable input does not throw a raw TypeError', () => {
237
+ const circular = { a: 1 };
238
+ circular.self = circular;
239
+ let errs;
240
+ assert.doesNotThrow(() => { errs = validate(circular, { const: { a: 1 } }); });
241
+ assert.equal(errs[0].keyword, 'const');
242
+ });
package/np-tools.cjs CHANGED
@@ -42,6 +42,7 @@ const topLevelCommands = {
42
42
  'config-get': require('./bin/np-tools/config.cjs'),
43
43
  'scan-codebase': require('./bin/np-tools/scan-codebase.cjs'),
44
44
  'update-docs': require('./bin/np-tools/update-docs.cjs'),
45
+ 'graph-impact': require('./bin/np-tools/graph-impact.cjs'),
45
46
  'doctor': require('./bin/np-tools/doctor.cjs'),
46
47
  'generate-slug': require('./bin/np-tools/slug.cjs'),
47
48
  'metrics': require('./bin/np-tools/metrics.cjs'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nubos-pilot",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Self-hosted AI pilot for any codebase. Researcher and critic agents plan, execute and verify each change.",
5
5
  "homepage": "https://pilot.nubos.cloud",
6
6
  "repository": {
@@ -43,6 +43,8 @@
43
43
  "check-coverage": "node bin/check-coverage.cjs",
44
44
  "docs:generate": "node scripts/generate-docs.cjs",
45
45
  "docs:check": "node scripts/generate-docs.cjs --check",
46
+ "attributions": "node scripts/generate-attributions.cjs",
47
+ "attributions:check": "node scripts/generate-attributions.cjs --check",
46
48
  "ci": "npm run test && npm run check-coverage"
47
49
  }
48
50
  }