knosky 0.6.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +149 -93
- package/CREDITS.md +14 -14
- package/LICENSE.md +76 -76
- package/LIMITATIONS.md +33 -23
- package/PRIVACY.md +30 -30
- package/README.md +170 -117
- package/SECURITY.md +78 -46
- package/action/post-comment.mjs +94 -89
- package/action.yml +62 -62
- package/bin/knosky.mjs +279 -105
- package/core/CONTRACT.md +70 -70
- package/core/append-only-checkpoint.mjs +215 -0
- package/core/audit-writer.mjs +317 -0
- package/core/benchmark-results.mjs +225 -225
- package/core/bundle.mjs +178 -178
- package/core/churn.mjs +23 -23
- package/core/ci.mjs +268 -268
- package/core/comparison.mjs +189 -189
- package/core/config.mjs +189 -189
- package/core/constants.mjs +13 -13
- package/core/contract.mjs +123 -123
- package/core/cross-repo.mjs +111 -111
- package/core/decision-codes.mjs +92 -0
- package/core/destination.mjs +161 -161
- package/core/district-classification.mjs +111 -0
- package/core/doctor-scorecard.mjs +369 -0
- package/core/domain-store.mjs +347 -0
- package/core/edges.mjs +43 -43
- package/core/escalate.mjs +68 -68
- package/core/freshness.mjs +198 -194
- package/core/fs-indexer.mjs +218 -218
- package/core/key-store.mjs +348 -348
- package/core/layout.mjs +46 -46
- package/core/ledger.mjs +176 -141
- package/core/local-ipc-identity.mjs +500 -0
- package/core/lod.mjs +155 -155
- package/core/mode-b.mjs +410 -0
- package/core/multi-model-benchmark.mjs +405 -405
- package/core/net-lockdown.mjs +421 -0
- package/core/onboarding.mjs +223 -223
- package/core/operator-auth.mjs +317 -0
- package/core/overlays.mjs +45 -45
- package/core/policy-lattice.mjs +142 -0
- package/core/pr-comment.mjs +198 -198
- package/core/protocol-spec.mjs +460 -460
- package/core/provenance.mjs +320 -0
- package/core/retrieve.mjs +63 -63
- package/core/route.mjs +304 -304
- package/core/schema.mjs +275 -275
- package/core/signing-tiers.mjs +1265 -0
- package/core/swarm-bench.mjs +106 -0
- package/core/swarm-coordinator.mjs +867 -0
- package/core/trust-root-rekey.mjs +410 -0
- package/mcp/server.mjs +264 -108
- package/package.json +56 -46
- package/renderer/art/kenney/buildingTiles_sheet.xml +130 -130
- package/renderer/art/kenney/cityDetails_sheet.xml +12 -12
- package/renderer/art/kenney/landscapeTiles_sheet.xml +129 -129
- package/renderer/art/kenney/sheet_allCars.xml +545 -545
- package/renderer/build-rich.mjs +43 -43
- package/renderer/city.template.html +808 -808
- package/ssot/decision-codes.json +133 -0
- package/ssot/ladder-l0-l3.md +232 -0
- package/ssot/tool-menu.json +130 -0
package/core/comparison.mjs
CHANGED
|
@@ -1,189 +1,189 @@
|
|
|
1
|
-
// KnoSky naive-vs-guided agent comparison protocol (SAT-458).
|
|
2
|
-
// Defines the artifact schema and validators for a single comparison run:
|
|
3
|
-
// tokens (in + out), tool-calls, time-to-relevant-file, correctness
|
|
4
|
-
// for both a naive agent and an identical task run with KnoSky guidance.
|
|
5
|
-
// Pure Node stdlib, ESM — no new deps.
|
|
6
|
-
|
|
7
|
-
import { PROTOCOL_VERSION } from './schema.mjs';
|
|
8
|
-
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// Artifact type constant
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
/** @type {string} */
|
|
14
|
-
export const COMPARISON_ARTIFACT_TYPE = 'comparison-run';
|
|
15
|
-
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// Internal helpers
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Return true when `p` is a relative-safe file path:
|
|
22
|
-
* — non-empty string
|
|
23
|
-
* — does not start with `/` (Unix absolute) or Windows drive letter
|
|
24
|
-
* — contains no `..` path segment
|
|
25
|
-
* @param {string} p
|
|
26
|
-
* @returns {boolean}
|
|
27
|
-
*/
|
|
28
|
-
function isRelativeSafePath(p) {
|
|
29
|
-
if (!p || typeof p !== 'string') return false;
|
|
30
|
-
if (p.startsWith('/')) return false;
|
|
31
|
-
if (/^[A-Za-z]:[\\\/]/.test(p)) return false;
|
|
32
|
-
if (p.split(/[/\\]/).some(s => s === '..')) return false;
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Validate one metric side object (either `naive` or `guided`).
|
|
38
|
-
* Returns an array of violation strings; empty means valid.
|
|
39
|
-
*
|
|
40
|
-
* @param {unknown} side
|
|
41
|
-
* @param {string} label — "naive" or "guided"
|
|
42
|
-
* @returns {string[]}
|
|
43
|
-
*/
|
|
44
|
-
function validateSide(side, label) {
|
|
45
|
-
const errors = [];
|
|
46
|
-
|
|
47
|
-
if (!side || typeof side !== 'object') {
|
|
48
|
-
errors.push(`${label} must be an object`);
|
|
49
|
-
return errors; // can't go further
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// tokens_in: non-negative integer
|
|
53
|
-
if (!Number.isInteger(side.tokens_in) || side.tokens_in < 0) {
|
|
54
|
-
errors.push(`${label}.tokens_in must be a non-negative integer, got: ${JSON.stringify(side.tokens_in)}`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// tokens_out: non-negative integer
|
|
58
|
-
if (!Number.isInteger(side.tokens_out) || side.tokens_out < 0) {
|
|
59
|
-
errors.push(`${label}.tokens_out must be a non-negative integer, got: ${JSON.stringify(side.tokens_out)}`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// tool_calls: non-negative integer
|
|
63
|
-
if (!Number.isInteger(side.tool_calls) || side.tool_calls < 0) {
|
|
64
|
-
errors.push(`${label}.tool_calls must be a non-negative integer, got: ${JSON.stringify(side.tool_calls)}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// time_to_relevant_file_ms: null (never found) or non-negative number
|
|
68
|
-
const ttrf = side.time_to_relevant_file_ms;
|
|
69
|
-
if (ttrf !== null && !(typeof ttrf === 'number' && ttrf >= 0)) {
|
|
70
|
-
errors.push(
|
|
71
|
-
`${label}.time_to_relevant_file_ms must be null or a non-negative number, got: ${JSON.stringify(ttrf)}`,
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// correct: boolean
|
|
76
|
-
if (typeof side.correct !== 'boolean') {
|
|
77
|
-
errors.push(`${label}.correct must be a boolean, got: ${JSON.stringify(side.correct)}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return errors;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// makeComparisonRun
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Construct a KnoSky `comparison-run` artifact envelope.
|
|
89
|
-
*
|
|
90
|
-
* @param {object} opts
|
|
91
|
-
* @param {string} opts.task_id Short identifier for this task.
|
|
92
|
-
* @param {string} opts.task_description Human-readable description of the task.
|
|
93
|
-
* @param {string[]} opts.target_files Relative paths to the "relevant" files for this task.
|
|
94
|
-
* @param {object} opts.naive Metrics for the naive agent (no KnoSky guidance).
|
|
95
|
-
* @param {number} opts.naive.tokens_in Input tokens consumed.
|
|
96
|
-
* @param {number} opts.naive.tokens_out Output tokens produced.
|
|
97
|
-
* @param {number} opts.naive.tool_calls Total tool/function calls made.
|
|
98
|
-
* @param {number|null} opts.naive.time_to_relevant_file_ms ms until first relevant-file hit, or null.
|
|
99
|
-
* @param {boolean} opts.naive.correct Whether the agent arrived at the correct answer.
|
|
100
|
-
* @param {object} opts.guided Same metric shape for the KnoSky-guided agent.
|
|
101
|
-
* @returns {object}
|
|
102
|
-
*/
|
|
103
|
-
export function makeComparisonRun({
|
|
104
|
-
task_id,
|
|
105
|
-
task_description,
|
|
106
|
-
target_files = [],
|
|
107
|
-
naive,
|
|
108
|
-
guided,
|
|
109
|
-
} = {}) {
|
|
110
|
-
return {
|
|
111
|
-
knosky_protocol: PROTOCOL_VERSION,
|
|
112
|
-
artifact_type: COMPARISON_ARTIFACT_TYPE,
|
|
113
|
-
advisory: true,
|
|
114
|
-
generated_at: new Date().toISOString(),
|
|
115
|
-
task_id,
|
|
116
|
-
task_description,
|
|
117
|
-
target_files,
|
|
118
|
-
naive,
|
|
119
|
-
guided,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
// validateComparisonRun
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Validate a KnoSky `comparison-run` document.
|
|
129
|
-
* Collects every violation; `ok` is true only when `errors` is empty.
|
|
130
|
-
*
|
|
131
|
-
* @param {object} doc
|
|
132
|
-
* @returns {{ ok: boolean, errors: string[] }}
|
|
133
|
-
*/
|
|
134
|
-
export function validateComparisonRun(doc) {
|
|
135
|
-
const errors = [];
|
|
136
|
-
|
|
137
|
-
if (!doc || typeof doc !== 'object') {
|
|
138
|
-
return { ok: false, errors: ['doc must be an object'] };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (doc.knosky_protocol !== PROTOCOL_VERSION) {
|
|
142
|
-
errors.push(
|
|
143
|
-
`knosky_protocol must be "${PROTOCOL_VERSION}", got: ${JSON.stringify(doc.knosky_protocol)}`,
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (doc.artifact_type !== COMPARISON_ARTIFACT_TYPE) {
|
|
148
|
-
errors.push(
|
|
149
|
-
`artifact_type must be "${COMPARISON_ARTIFACT_TYPE}", got: ${JSON.stringify(doc.artifact_type)}`,
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (doc.advisory !== true) {
|
|
154
|
-
errors.push(`advisory must be true, got: ${JSON.stringify(doc.advisory)}`);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// task_id: non-empty string
|
|
158
|
-
if (typeof doc.task_id !== 'string' || doc.task_id.length === 0) {
|
|
159
|
-
errors.push(`task_id must be a non-empty string, got: ${JSON.stringify(doc.task_id)}`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// task_description: non-empty string
|
|
163
|
-
if (typeof doc.task_description !== 'string' || doc.task_description.length === 0) {
|
|
164
|
-
errors.push(
|
|
165
|
-
`task_description must be a non-empty string, got: ${JSON.stringify(doc.task_description)}`,
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// target_files: non-empty array of relative-safe path strings
|
|
170
|
-
if (!Array.isArray(doc.target_files) || doc.target_files.length === 0) {
|
|
171
|
-
errors.push('target_files must be a non-empty array');
|
|
172
|
-
} else {
|
|
173
|
-
for (let i = 0; i < doc.target_files.length; i++) {
|
|
174
|
-
const p = doc.target_files[i];
|
|
175
|
-
if (typeof p !== 'string' || p.length === 0) {
|
|
176
|
-
errors.push(`target_files[${i}] must be a non-empty string`);
|
|
177
|
-
} else if (!isRelativeSafePath(p)) {
|
|
178
|
-
errors.push(`target_files[${i}] must be a relative path with no ".." segments: ${JSON.stringify(p)}`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// naive and guided sides
|
|
184
|
-
for (const side of ['naive', 'guided']) {
|
|
185
|
-
errors.push(...validateSide(doc[side], side));
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return { ok: errors.length === 0, errors };
|
|
189
|
-
}
|
|
1
|
+
// KnoSky naive-vs-guided agent comparison protocol (SAT-458).
|
|
2
|
+
// Defines the artifact schema and validators for a single comparison run:
|
|
3
|
+
// tokens (in + out), tool-calls, time-to-relevant-file, correctness
|
|
4
|
+
// for both a naive agent and an identical task run with KnoSky guidance.
|
|
5
|
+
// Pure Node stdlib, ESM — no new deps.
|
|
6
|
+
|
|
7
|
+
import { PROTOCOL_VERSION } from './schema.mjs';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Artifact type constant
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/** @type {string} */
|
|
14
|
+
export const COMPARISON_ARTIFACT_TYPE = 'comparison-run';
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Internal helpers
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Return true when `p` is a relative-safe file path:
|
|
22
|
+
* — non-empty string
|
|
23
|
+
* — does not start with `/` (Unix absolute) or Windows drive letter
|
|
24
|
+
* — contains no `..` path segment
|
|
25
|
+
* @param {string} p
|
|
26
|
+
* @returns {boolean}
|
|
27
|
+
*/
|
|
28
|
+
function isRelativeSafePath(p) {
|
|
29
|
+
if (!p || typeof p !== 'string') return false;
|
|
30
|
+
if (p.startsWith('/')) return false;
|
|
31
|
+
if (/^[A-Za-z]:[\\\/]/.test(p)) return false;
|
|
32
|
+
if (p.split(/[/\\]/).some(s => s === '..')) return false;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validate one metric side object (either `naive` or `guided`).
|
|
38
|
+
* Returns an array of violation strings; empty means valid.
|
|
39
|
+
*
|
|
40
|
+
* @param {unknown} side
|
|
41
|
+
* @param {string} label — "naive" or "guided"
|
|
42
|
+
* @returns {string[]}
|
|
43
|
+
*/
|
|
44
|
+
function validateSide(side, label) {
|
|
45
|
+
const errors = [];
|
|
46
|
+
|
|
47
|
+
if (!side || typeof side !== 'object') {
|
|
48
|
+
errors.push(`${label} must be an object`);
|
|
49
|
+
return errors; // can't go further
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// tokens_in: non-negative integer
|
|
53
|
+
if (!Number.isInteger(side.tokens_in) || side.tokens_in < 0) {
|
|
54
|
+
errors.push(`${label}.tokens_in must be a non-negative integer, got: ${JSON.stringify(side.tokens_in)}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// tokens_out: non-negative integer
|
|
58
|
+
if (!Number.isInteger(side.tokens_out) || side.tokens_out < 0) {
|
|
59
|
+
errors.push(`${label}.tokens_out must be a non-negative integer, got: ${JSON.stringify(side.tokens_out)}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// tool_calls: non-negative integer
|
|
63
|
+
if (!Number.isInteger(side.tool_calls) || side.tool_calls < 0) {
|
|
64
|
+
errors.push(`${label}.tool_calls must be a non-negative integer, got: ${JSON.stringify(side.tool_calls)}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// time_to_relevant_file_ms: null (never found) or non-negative number
|
|
68
|
+
const ttrf = side.time_to_relevant_file_ms;
|
|
69
|
+
if (ttrf !== null && !(typeof ttrf === 'number' && ttrf >= 0)) {
|
|
70
|
+
errors.push(
|
|
71
|
+
`${label}.time_to_relevant_file_ms must be null or a non-negative number, got: ${JSON.stringify(ttrf)}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// correct: boolean
|
|
76
|
+
if (typeof side.correct !== 'boolean') {
|
|
77
|
+
errors.push(`${label}.correct must be a boolean, got: ${JSON.stringify(side.correct)}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return errors;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// makeComparisonRun
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Construct a KnoSky `comparison-run` artifact envelope.
|
|
89
|
+
*
|
|
90
|
+
* @param {object} opts
|
|
91
|
+
* @param {string} opts.task_id Short identifier for this task.
|
|
92
|
+
* @param {string} opts.task_description Human-readable description of the task.
|
|
93
|
+
* @param {string[]} opts.target_files Relative paths to the "relevant" files for this task.
|
|
94
|
+
* @param {object} opts.naive Metrics for the naive agent (no KnoSky guidance).
|
|
95
|
+
* @param {number} opts.naive.tokens_in Input tokens consumed.
|
|
96
|
+
* @param {number} opts.naive.tokens_out Output tokens produced.
|
|
97
|
+
* @param {number} opts.naive.tool_calls Total tool/function calls made.
|
|
98
|
+
* @param {number|null} opts.naive.time_to_relevant_file_ms ms until first relevant-file hit, or null.
|
|
99
|
+
* @param {boolean} opts.naive.correct Whether the agent arrived at the correct answer.
|
|
100
|
+
* @param {object} opts.guided Same metric shape for the KnoSky-guided agent.
|
|
101
|
+
* @returns {object}
|
|
102
|
+
*/
|
|
103
|
+
export function makeComparisonRun({
|
|
104
|
+
task_id,
|
|
105
|
+
task_description,
|
|
106
|
+
target_files = [],
|
|
107
|
+
naive,
|
|
108
|
+
guided,
|
|
109
|
+
} = {}) {
|
|
110
|
+
return {
|
|
111
|
+
knosky_protocol: PROTOCOL_VERSION,
|
|
112
|
+
artifact_type: COMPARISON_ARTIFACT_TYPE,
|
|
113
|
+
advisory: true,
|
|
114
|
+
generated_at: new Date().toISOString(),
|
|
115
|
+
task_id,
|
|
116
|
+
task_description,
|
|
117
|
+
target_files,
|
|
118
|
+
naive,
|
|
119
|
+
guided,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// validateComparisonRun
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Validate a KnoSky `comparison-run` document.
|
|
129
|
+
* Collects every violation; `ok` is true only when `errors` is empty.
|
|
130
|
+
*
|
|
131
|
+
* @param {object} doc
|
|
132
|
+
* @returns {{ ok: boolean, errors: string[] }}
|
|
133
|
+
*/
|
|
134
|
+
export function validateComparisonRun(doc) {
|
|
135
|
+
const errors = [];
|
|
136
|
+
|
|
137
|
+
if (!doc || typeof doc !== 'object') {
|
|
138
|
+
return { ok: false, errors: ['doc must be an object'] };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (doc.knosky_protocol !== PROTOCOL_VERSION) {
|
|
142
|
+
errors.push(
|
|
143
|
+
`knosky_protocol must be "${PROTOCOL_VERSION}", got: ${JSON.stringify(doc.knosky_protocol)}`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (doc.artifact_type !== COMPARISON_ARTIFACT_TYPE) {
|
|
148
|
+
errors.push(
|
|
149
|
+
`artifact_type must be "${COMPARISON_ARTIFACT_TYPE}", got: ${JSON.stringify(doc.artifact_type)}`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (doc.advisory !== true) {
|
|
154
|
+
errors.push(`advisory must be true, got: ${JSON.stringify(doc.advisory)}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// task_id: non-empty string
|
|
158
|
+
if (typeof doc.task_id !== 'string' || doc.task_id.length === 0) {
|
|
159
|
+
errors.push(`task_id must be a non-empty string, got: ${JSON.stringify(doc.task_id)}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// task_description: non-empty string
|
|
163
|
+
if (typeof doc.task_description !== 'string' || doc.task_description.length === 0) {
|
|
164
|
+
errors.push(
|
|
165
|
+
`task_description must be a non-empty string, got: ${JSON.stringify(doc.task_description)}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// target_files: non-empty array of relative-safe path strings
|
|
170
|
+
if (!Array.isArray(doc.target_files) || doc.target_files.length === 0) {
|
|
171
|
+
errors.push('target_files must be a non-empty array');
|
|
172
|
+
} else {
|
|
173
|
+
for (let i = 0; i < doc.target_files.length; i++) {
|
|
174
|
+
const p = doc.target_files[i];
|
|
175
|
+
if (typeof p !== 'string' || p.length === 0) {
|
|
176
|
+
errors.push(`target_files[${i}] must be a non-empty string`);
|
|
177
|
+
} else if (!isRelativeSafePath(p)) {
|
|
178
|
+
errors.push(`target_files[${i}] must be a relative path with no ".." segments: ${JSON.stringify(p)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// naive and guided sides
|
|
184
|
+
for (const side of ['naive', 'guided']) {
|
|
185
|
+
errors.push(...validateSide(doc[side], side));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { ok: errors.length === 0, errors };
|
|
189
|
+
}
|