nubos-pilot 1.2.1 → 1.2.3
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 +43 -1
- package/agents/np-architect.md +2 -0
- package/agents/np-executor.md +1 -1
- package/agents/np-learnings-extractor.md +54 -0
- package/agents/np-planner.md +1 -1
- package/agents/np-security-reviewer.md +9 -0
- package/bin/np-tools/_commands.cjs +5 -0
- package/bin/np-tools/derive-tier.cjs +86 -0
- package/bin/np-tools/derive-tier.test.cjs +83 -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/learnings.cjs +105 -0
- package/bin/np-tools/learnings.test.cjs +66 -0
- package/bin/np-tools/loop-run-round.cjs +7 -1
- package/bin/np-tools/scan-codebase.cjs +21 -1
- package/bin/np-tools/skill-audit.cjs +79 -0
- package/bin/np-tools/skill-audit.test.cjs +86 -0
- package/bin/np-tools/verify-reliability.cjs +65 -0
- package/bin/np-tools/verify-reliability.test.cjs +69 -0
- package/lib/agents.test.cjs +1 -0
- 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/config-defaults.cjs +13 -0
- package/lib/config-schema.cjs +11 -0
- package/lib/eval-reliability.cjs +63 -0
- package/lib/eval-reliability.test.cjs +56 -0
- package/lib/install/claude-hooks-learnings.test.cjs +82 -0
- package/lib/install/claude-hooks.cjs +65 -4
- package/lib/install/claude-hooks.test.cjs +5 -2
- package/lib/learnings/capture-ledger.cjs +80 -0
- package/lib/learnings/capture-ledger.test.cjs +54 -0
- package/lib/learnings/extract.cjs +191 -0
- package/lib/learnings/extract.test.cjs +115 -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/nubosloop-audit.cjs +104 -0
- package/lib/nubosloop-skill-audit.test.cjs +98 -0
- package/lib/nubosloop.cjs +9 -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/tier-classify.cjs +67 -0
- package/lib/tier-classify.test.cjs +67 -0
- package/lib/validate.cjs +301 -0
- package/lib/validate.test.cjs +242 -0
- package/np-tools.cjs +5 -0
- package/package.json +3 -1
- package/skills/np-access-control/SKILL.md +42 -0
- package/skills/np-accessibility-audit/SKILL.md +41 -0
- package/skills/np-adr/SKILL.md +37 -0
- package/skills/np-api-design/SKILL.md +34 -0
- package/skills/np-caching-strategy/SKILL.md +38 -0
- package/skills/np-data-modeling/SKILL.md +37 -0
- package/skills/np-data-privacy/SKILL.md +39 -0
- package/skills/np-dependency-audit/SKILL.md +47 -0
- package/skills/np-encryption/SKILL.md +47 -0
- package/skills/np-error-handling/SKILL.md +37 -0
- package/skills/np-incident-response/SKILL.md +38 -0
- package/skills/np-llm-app-architecture/SKILL.md +50 -0
- package/skills/np-observability/SKILL.md +39 -0
- package/skills/np-performance/SKILL.md +38 -0
- package/skills/np-queue-design/SKILL.md +32 -0
- package/skills/np-rag-design/SKILL.md +43 -0
- package/skills/np-refactoring/SKILL.md +35 -0
- package/skills/np-resilience-patterns/SKILL.md +39 -0
- package/skills/np-secure-code-review/SKILL.md +46 -0
- package/skills/np-secure-design/SKILL.md +44 -0
- package/skills/np-service-boundary/SKILL.md +35 -0
- package/skills/np-system-design/SKILL.md +40 -0
- package/skills/np-test-strategy/SKILL.md +46 -0
- package/skills/np-threat-model/SKILL.md +42 -0
- package/templates/claude/payload/hooks/np-learnings-hook.cjs +55 -0
- package/workflows/architect-phase.md +21 -1
- package/workflows/execute-phase.md +66 -4
- package/workflows/verify-work.md +17 -4
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { VALID_TIERS } = require('./model-profiles.cjs');
|
|
4
|
+
|
|
5
|
+
// ADR-0013: a tier is a routing/meta property derived from OBSERVABLE task
|
|
6
|
+
// signals (files touched + risk keywords), never invented from implementation
|
|
7
|
+
// detail. classifyTier is advisory — the planner remains the decider; this
|
|
8
|
+
// helper only makes that decision evidence-based. Output is deterministic
|
|
9
|
+
// (no clock, no randomness) so a given task always classifies the same way.
|
|
10
|
+
|
|
11
|
+
const RISK_RE = /\b(auth|authn|authz|authoriz\w*|login|crypto|encrypt\w*|decrypt\w*|password|secret|credential|token|jwt|oauth|saml|session|payment|billing|invoice|permission|role|access[\s-]?control|migrat\w*|schema)\b/i;
|
|
12
|
+
const ARCH_RE = /\b(architect\w*|cross[\s-]?cutting|multi[\s-]?module|redesign|breaking[\s-]?change|public[\s-]?api|contract|interface|protocol|state[\s-]?machine|concurren\w*|distributed|orchestrat\w*)\b/i;
|
|
13
|
+
const TRIVIAL_RE = /\b(typo|comment|rename|docs?|readme|changelog|copy(?:writing)?|wording|spelling|version[\s-]?bump|bump[\s-]?version|lint|format(?:ting)?|whitespace|config[\s-]?value|constant|string[\s-]?literal)\b/i;
|
|
14
|
+
|
|
15
|
+
const SIZE_TO_TIER = Object.freeze({ trivial: 'haiku', standard: 'sonnet', large: 'opus' });
|
|
16
|
+
|
|
17
|
+
const LARGE_FILE_THRESHOLD = 6;
|
|
18
|
+
|
|
19
|
+
function _text(name, desc) {
|
|
20
|
+
return [String(name || ''), String(desc || '')].join(' ');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {{files_modified?: string[], name?: string, desc?: string}} task
|
|
25
|
+
* @returns {{tier: string, size: string, rationale: string, signals: {file_count: number, risk: boolean, arch: boolean, trivial: boolean}}}
|
|
26
|
+
*/
|
|
27
|
+
function classifyTier(task) {
|
|
28
|
+
const t = task || {};
|
|
29
|
+
const files = Array.isArray(t.files_modified) ? t.files_modified : [];
|
|
30
|
+
const fileCount = files.length;
|
|
31
|
+
const haystack = _text(t.name, t.desc) + ' ' + files.join(' ');
|
|
32
|
+
|
|
33
|
+
const risk = RISK_RE.test(haystack);
|
|
34
|
+
const arch = ARCH_RE.test(haystack);
|
|
35
|
+
const trivial = TRIVIAL_RE.test(haystack);
|
|
36
|
+
|
|
37
|
+
let size;
|
|
38
|
+
let rationale;
|
|
39
|
+
if (risk) {
|
|
40
|
+
size = 'large';
|
|
41
|
+
rationale = 'security/data-sensitive surface (auth, crypto, secrets, or migration) — escalate to the strongest tier';
|
|
42
|
+
} else if (arch || fileCount >= LARGE_FILE_THRESHOLD) {
|
|
43
|
+
size = 'large';
|
|
44
|
+
rationale = arch
|
|
45
|
+
? 'architectural / cross-cutting change — invariants span multiple units'
|
|
46
|
+
: 'broad change touching ' + fileCount + ' files — cross-file invariants likely';
|
|
47
|
+
} else if (fileCount <= 1 && trivial) {
|
|
48
|
+
size = 'trivial';
|
|
49
|
+
rationale = 'single-file mechanical edit (docs/rename/format/config) — narrow, low-risk';
|
|
50
|
+
} else {
|
|
51
|
+
size = 'standard';
|
|
52
|
+
rationale = 'ordinary single-concern implementation';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
tier: SIZE_TO_TIER[size],
|
|
57
|
+
size,
|
|
58
|
+
rationale,
|
|
59
|
+
signals: { file_count: fileCount, risk, arch, trivial },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isValidTier(tier) {
|
|
64
|
+
return VALID_TIERS.includes(tier);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { classifyTier, isValidTier, SIZE_TO_TIER, LARGE_FILE_THRESHOLD };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('node:assert');
|
|
5
|
+
const { classifyTier, isValidTier, SIZE_TO_TIER } = require('./tier-classify.cjs');
|
|
6
|
+
|
|
7
|
+
test('TC-1: security keyword forces large→opus regardless of file count', () => {
|
|
8
|
+
const r = classifyTier({ files_modified: ['app/Auth.php'], name: 'Add password reset flow' });
|
|
9
|
+
assert.strictEqual(r.size, 'large');
|
|
10
|
+
assert.strictEqual(r.tier, 'opus');
|
|
11
|
+
assert.strictEqual(r.signals.risk, true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('TC-2: migration path escalates to large', () => {
|
|
15
|
+
const r = classifyTier({ files_modified: ['db/migrations/003_add_col.sql'], name: 'add column' });
|
|
16
|
+
assert.strictEqual(r.tier, 'opus');
|
|
17
|
+
assert.strictEqual(r.signals.risk, true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('TC-3: many files → large even without risk/arch keywords', () => {
|
|
21
|
+
const r = classifyTier({ files_modified: ['a.ts', 'b.ts', 'c.ts', 'd.ts', 'e.ts', 'f.ts'], name: 'wire feature' });
|
|
22
|
+
assert.strictEqual(r.size, 'large');
|
|
23
|
+
assert.strictEqual(r.tier, 'opus');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('TC-4: architectural keyword → large', () => {
|
|
27
|
+
const r = classifyTier({ files_modified: ['svc.ts'], name: 'refactor the orchestration interface' });
|
|
28
|
+
assert.strictEqual(r.size, 'large');
|
|
29
|
+
assert.strictEqual(r.signals.arch, true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('TC-5: single-file doc/typo → trivial→haiku', () => {
|
|
33
|
+
const r = classifyTier({ files_modified: ['README.md'], name: 'fix typo in readme' });
|
|
34
|
+
assert.strictEqual(r.size, 'trivial');
|
|
35
|
+
assert.strictEqual(r.tier, 'haiku');
|
|
36
|
+
assert.strictEqual(r.signals.trivial, true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('TC-6: ordinary single-concern → standard→sonnet', () => {
|
|
40
|
+
const r = classifyTier({ files_modified: ['app/Service.php', 'app/Service.test.php'], name: 'add discount calculation' });
|
|
41
|
+
assert.strictEqual(r.size, 'standard');
|
|
42
|
+
assert.strictEqual(r.tier, 'sonnet');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('TC-7: trivial keyword but multiple files is NOT trivial', () => {
|
|
46
|
+
const r = classifyTier({ files_modified: ['a.ts', 'b.ts'], name: 'rename helper' });
|
|
47
|
+
assert.strictEqual(r.size, 'standard');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('TC-8: empty/missing input → standard, no throw', () => {
|
|
51
|
+
const r = classifyTier({});
|
|
52
|
+
assert.strictEqual(r.size, 'standard');
|
|
53
|
+
assert.strictEqual(r.signals.file_count, 0);
|
|
54
|
+
const r2 = classifyTier(null);
|
|
55
|
+
assert.strictEqual(r2.size, 'standard');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('TC-9: every emitted tier is a valid tier', () => {
|
|
59
|
+
for (const size of Object.keys(SIZE_TO_TIER)) {
|
|
60
|
+
assert.ok(isValidTier(SIZE_TO_TIER[size]), size + ' maps to a valid tier');
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('TC-10: deterministic — same input twice yields identical result', () => {
|
|
65
|
+
const input = { files_modified: ['x.ts'], name: 'add token validation' };
|
|
66
|
+
assert.deepStrictEqual(classifyTier(input), classifyTier(input));
|
|
67
|
+
});
|
package/lib/validate.cjs
ADDED
|
@@ -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 };
|