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/schema.mjs
CHANGED
|
@@ -1,275 +1,275 @@
|
|
|
1
|
-
// KnoSky protocol artifact schemas and validators (SAT-427 / KSV2-P2).
|
|
2
|
-
// Covers: route.json and intent-manifest.json envelope shapes.
|
|
3
|
-
// Pure Node stdlib, ESM — no third-party dependencies.
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// Protocol version constant
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
/** @type {string} */
|
|
10
|
-
export const PROTOCOL_VERSION = '1.0';
|
|
11
|
-
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
// Internal helpers
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Return true if `str` is an absolute path (Unix or Windows).
|
|
18
|
-
* @param {string} str
|
|
19
|
-
* @returns {boolean}
|
|
20
|
-
*/
|
|
21
|
-
function isAbsolutePath(str) {
|
|
22
|
-
if (str.startsWith('/')) return true;
|
|
23
|
-
// Windows: C:\ or C:/
|
|
24
|
-
if (/^[A-Za-z]:[\\\/]/.test(str)) return true;
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Return true if `str` contains a `..` path segment.
|
|
30
|
-
* Splits on both `/` and `\`.
|
|
31
|
-
* @param {string} str
|
|
32
|
-
* @returns {boolean}
|
|
33
|
-
*/
|
|
34
|
-
function hasDotDotSegment(str) {
|
|
35
|
-
return str.split(/[/\\]/).some(seg => seg === '..');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Extract the path string from a route/manifest entry.
|
|
40
|
-
* An entry is either a plain string or an object with a `path` field.
|
|
41
|
-
* Returns null if the entry carries no recognisable path.
|
|
42
|
-
*
|
|
43
|
-
* @param {unknown} entry
|
|
44
|
-
* @returns {string|null}
|
|
45
|
-
*/
|
|
46
|
-
function extractPathString(entry) {
|
|
47
|
-
if (typeof entry === 'string') return entry;
|
|
48
|
-
if (entry !== null && typeof entry === 'object' && typeof entry.path === 'string') {
|
|
49
|
-
return entry.path;
|
|
50
|
-
}
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Validate the absolute-path invariant over an array of entries.
|
|
56
|
-
* Pushes error messages into `errors` for each violation found.
|
|
57
|
-
*
|
|
58
|
-
* @param {unknown[]} entries Array of string or { path } objects.
|
|
59
|
-
* @param {string} fieldName Label for error messages.
|
|
60
|
-
* @param {string[]} errors Accumulator array.
|
|
61
|
-
*/
|
|
62
|
-
function checkAbsolutePaths(entries, fieldName, errors) {
|
|
63
|
-
for (let i = 0; i < entries.length; i++) {
|
|
64
|
-
const p = extractPathString(entries[i]);
|
|
65
|
-
if (p === null) continue; // non-path entry — skip
|
|
66
|
-
if (isAbsolutePath(p)) {
|
|
67
|
-
errors.push(`${fieldName}[${i}] must not be an absolute path: ${JSON.stringify(p)}`);
|
|
68
|
-
}
|
|
69
|
-
if (hasDotDotSegment(p)) {
|
|
70
|
-
errors.push(`${fieldName}[${i}] must not contain a ".." path segment: ${JSON.stringify(p)}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
// makeRouteDoc
|
|
77
|
-
// ---------------------------------------------------------------------------
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Construct a KnoSky `route` artifact envelope.
|
|
81
|
-
*
|
|
82
|
-
* @param {object} opts
|
|
83
|
-
* @param {string} opts.destination Navigation target.
|
|
84
|
-
* @param {unknown[]} [opts.route] Ordered waypoints.
|
|
85
|
-
* @param {unknown[]} [opts.alternates] Alternate routes.
|
|
86
|
-
* @param {string[]} [opts.caveats] Advisory notes.
|
|
87
|
-
* @param {number} [opts.confidence] Confidence score [0..1].
|
|
88
|
-
* @param {string|null} [opts.source_rev] VCS revision that produced this doc.
|
|
89
|
-
* @param {number|null} [opts.ledger_seq] Ledger sequence number (SAT-444) —
|
|
90
|
-
* monotone commit count from the city that produced this route doc.
|
|
91
|
-
* null means the city predates ledger-anchored freshness.
|
|
92
|
-
* @returns {object}
|
|
93
|
-
*/
|
|
94
|
-
export function makeRouteDoc({
|
|
95
|
-
destination,
|
|
96
|
-
route = [],
|
|
97
|
-
alternates = [],
|
|
98
|
-
caveats = [],
|
|
99
|
-
confidence = 0,
|
|
100
|
-
source_rev = null,
|
|
101
|
-
ledger_seq = null,
|
|
102
|
-
} = {}) {
|
|
103
|
-
return {
|
|
104
|
-
knosky_protocol: PROTOCOL_VERSION,
|
|
105
|
-
artifact_type: 'route',
|
|
106
|
-
advisory: true,
|
|
107
|
-
generated_at: new Date().toISOString(),
|
|
108
|
-
source_rev,
|
|
109
|
-
ledger_seq,
|
|
110
|
-
destination,
|
|
111
|
-
route,
|
|
112
|
-
alternates,
|
|
113
|
-
caveats,
|
|
114
|
-
confidence,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ---------------------------------------------------------------------------
|
|
119
|
-
// validateRouteDoc
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Validate a KnoSky `route` document.
|
|
124
|
-
* Collects every violation; `ok` is true only when `errors` is empty.
|
|
125
|
-
*
|
|
126
|
-
* @param {object} doc
|
|
127
|
-
* @returns {{ ok: boolean, errors: string[] }}
|
|
128
|
-
*/
|
|
129
|
-
export function validateRouteDoc(doc) {
|
|
130
|
-
const errors = [];
|
|
131
|
-
|
|
132
|
-
if (doc.knosky_protocol !== '1.0') {
|
|
133
|
-
errors.push(`knosky_protocol must be "1.0", got: ${JSON.stringify(doc.knosky_protocol)}`);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (doc.artifact_type !== 'route') {
|
|
137
|
-
errors.push(`artifact_type must be "route", got: ${JSON.stringify(doc.artifact_type)}`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (doc.advisory !== true) {
|
|
141
|
-
errors.push(`advisory must be true, got: ${JSON.stringify(doc.advisory)}`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
for (const field of ['route', 'alternates', 'caveats']) {
|
|
145
|
-
if (!Array.isArray(doc[field])) {
|
|
146
|
-
errors.push(`${field} must be an array, got: ${JSON.stringify(doc[field])}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Number.isFinite rejects NaN and ±Infinity; the range check then enforces [0, 1].
|
|
151
|
-
if (!Number.isFinite(doc.confidence) || doc.confidence < 0 || doc.confidence > 1) {
|
|
152
|
-
errors.push(`confidence must be a number in [0, 1], got: ${JSON.stringify(doc.confidence)}`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ledger_seq: must be null or a non-negative integer (SAT-444)
|
|
156
|
-
if (doc.ledger_seq !== undefined && doc.ledger_seq !== null) {
|
|
157
|
-
if (typeof doc.ledger_seq !== 'number' || !Number.isInteger(doc.ledger_seq) || doc.ledger_seq < 0) {
|
|
158
|
-
errors.push(`ledger_seq must be null or a non-negative integer, got: ${JSON.stringify(doc.ledger_seq)}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Absolute-path invariant applies to route[] and alternates[]
|
|
163
|
-
if (Array.isArray(doc.route)) {
|
|
164
|
-
checkAbsolutePaths(doc.route, 'route', errors);
|
|
165
|
-
}
|
|
166
|
-
if (Array.isArray(doc.alternates)) {
|
|
167
|
-
checkAbsolutePaths(doc.alternates, 'alternates', errors);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return { ok: errors.length === 0, errors };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// ---------------------------------------------------------------------------
|
|
174
|
-
// makeIntentManifest
|
|
175
|
-
// ---------------------------------------------------------------------------
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Construct a KnoSky `intent-manifest` artifact envelope.
|
|
179
|
-
*
|
|
180
|
-
* @param {object} opts
|
|
181
|
-
* @param {Array<{ path: string, sha256: string }>} [opts.paths] Files covered.
|
|
182
|
-
* @param {unknown[]} [opts.edges] Dependency edges.
|
|
183
|
-
* @param {string|null} [opts.expiry] ISO-8601 expiry timestamp or null.
|
|
184
|
-
* @param {object} opts.secret_scan REQUIRED secret-scan result object.
|
|
185
|
-
* @param {number|null} [opts.ledger_seq] Ledger sequence number (SAT-444) —
|
|
186
|
-
* monotone commit count from the city that produced this manifest.
|
|
187
|
-
* null means the city predates ledger-anchored freshness.
|
|
188
|
-
* @returns {object}
|
|
189
|
-
*/
|
|
190
|
-
export function makeIntentManifest({
|
|
191
|
-
paths = [],
|
|
192
|
-
edges = [],
|
|
193
|
-
expiry = null,
|
|
194
|
-
secret_scan,
|
|
195
|
-
ledger_seq = null,
|
|
196
|
-
} = {}) {
|
|
197
|
-
return {
|
|
198
|
-
knosky_protocol: PROTOCOL_VERSION,
|
|
199
|
-
artifact_type: 'intent-manifest',
|
|
200
|
-
advisory: true,
|
|
201
|
-
generated_at: new Date().toISOString(),
|
|
202
|
-
paths,
|
|
203
|
-
edges,
|
|
204
|
-
expiry,
|
|
205
|
-
ledger_seq,
|
|
206
|
-
secret_scan,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// ---------------------------------------------------------------------------
|
|
211
|
-
// validateIntentManifest
|
|
212
|
-
// ---------------------------------------------------------------------------
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Validate a KnoSky `intent-manifest` document.
|
|
216
|
-
* Collects every violation; `ok` is true only when `errors` is empty.
|
|
217
|
-
*
|
|
218
|
-
* @param {object} doc
|
|
219
|
-
* @returns {{ ok: boolean, errors: string[] }}
|
|
220
|
-
*/
|
|
221
|
-
export function validateIntentManifest(doc) {
|
|
222
|
-
const errors = [];
|
|
223
|
-
|
|
224
|
-
if (doc.knosky_protocol !== '1.0') {
|
|
225
|
-
errors.push(`knosky_protocol must be "1.0", got: ${JSON.stringify(doc.knosky_protocol)}`);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (doc.artifact_type !== 'intent-manifest') {
|
|
229
|
-
errors.push(`artifact_type must be "intent-manifest", got: ${JSON.stringify(doc.artifact_type)}`);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (doc.advisory !== true) {
|
|
233
|
-
errors.push(`advisory must be true, got: ${JSON.stringify(doc.advisory)}`);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// paths must be an array of { path: string (non-empty), sha256: string }
|
|
237
|
-
if (!Array.isArray(doc.paths)) {
|
|
238
|
-
errors.push(`paths must be an array, got: ${JSON.stringify(doc.paths)}`);
|
|
239
|
-
} else {
|
|
240
|
-
for (let i = 0; i < doc.paths.length; i++) {
|
|
241
|
-
const entry = doc.paths[i];
|
|
242
|
-
if (!entry || typeof entry !== 'object') {
|
|
243
|
-
errors.push(`paths[${i}] must be an object`);
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
if (typeof entry.path !== 'string' || entry.path.length === 0) {
|
|
247
|
-
errors.push(`paths[${i}].path must be a non-empty string, got: ${JSON.stringify(entry.path)}`);
|
|
248
|
-
}
|
|
249
|
-
if (typeof entry.sha256 !== 'string') {
|
|
250
|
-
errors.push(`paths[${i}].sha256 must be a string, got: ${JSON.stringify(entry.sha256)}`);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Absolute-path invariant on paths[].path
|
|
255
|
-
checkAbsolutePaths(doc.paths, 'paths', errors);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// ledger_seq: must be null or a non-negative integer (SAT-444)
|
|
259
|
-
if (doc.ledger_seq !== undefined && doc.ledger_seq !== null) {
|
|
260
|
-
if (typeof doc.ledger_seq !== 'number' || !Number.isInteger(doc.ledger_seq) || doc.ledger_seq < 0) {
|
|
261
|
-
errors.push(`ledger_seq must be null or a non-negative integer, got: ${JSON.stringify(doc.ledger_seq)}`);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// secret_scan must be present and have a valid status
|
|
266
|
-
if (!doc.secret_scan || typeof doc.secret_scan !== 'object') {
|
|
267
|
-
errors.push('secret_scan is required and must be an object');
|
|
268
|
-
} else if (doc.secret_scan.status !== 'clean' && doc.secret_scan.status !== 'blocked') {
|
|
269
|
-
errors.push(
|
|
270
|
-
`secret_scan.status must be "clean" or "blocked", got: ${JSON.stringify(doc.secret_scan.status)}`,
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return { ok: errors.length === 0, errors };
|
|
275
|
-
}
|
|
1
|
+
// KnoSky protocol artifact schemas and validators (SAT-427 / KSV2-P2).
|
|
2
|
+
// Covers: route.json and intent-manifest.json envelope shapes.
|
|
3
|
+
// Pure Node stdlib, ESM — no third-party dependencies.
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Protocol version constant
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
/** @type {string} */
|
|
10
|
+
export const PROTOCOL_VERSION = '1.0';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Internal helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Return true if `str` is an absolute path (Unix or Windows).
|
|
18
|
+
* @param {string} str
|
|
19
|
+
* @returns {boolean}
|
|
20
|
+
*/
|
|
21
|
+
function isAbsolutePath(str) {
|
|
22
|
+
if (str.startsWith('/')) return true;
|
|
23
|
+
// Windows: C:\ or C:/
|
|
24
|
+
if (/^[A-Za-z]:[\\\/]/.test(str)) return true;
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Return true if `str` contains a `..` path segment.
|
|
30
|
+
* Splits on both `/` and `\`.
|
|
31
|
+
* @param {string} str
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
function hasDotDotSegment(str) {
|
|
35
|
+
return str.split(/[/\\]/).some(seg => seg === '..');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract the path string from a route/manifest entry.
|
|
40
|
+
* An entry is either a plain string or an object with a `path` field.
|
|
41
|
+
* Returns null if the entry carries no recognisable path.
|
|
42
|
+
*
|
|
43
|
+
* @param {unknown} entry
|
|
44
|
+
* @returns {string|null}
|
|
45
|
+
*/
|
|
46
|
+
function extractPathString(entry) {
|
|
47
|
+
if (typeof entry === 'string') return entry;
|
|
48
|
+
if (entry !== null && typeof entry === 'object' && typeof entry.path === 'string') {
|
|
49
|
+
return entry.path;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate the absolute-path invariant over an array of entries.
|
|
56
|
+
* Pushes error messages into `errors` for each violation found.
|
|
57
|
+
*
|
|
58
|
+
* @param {unknown[]} entries Array of string or { path } objects.
|
|
59
|
+
* @param {string} fieldName Label for error messages.
|
|
60
|
+
* @param {string[]} errors Accumulator array.
|
|
61
|
+
*/
|
|
62
|
+
function checkAbsolutePaths(entries, fieldName, errors) {
|
|
63
|
+
for (let i = 0; i < entries.length; i++) {
|
|
64
|
+
const p = extractPathString(entries[i]);
|
|
65
|
+
if (p === null) continue; // non-path entry — skip
|
|
66
|
+
if (isAbsolutePath(p)) {
|
|
67
|
+
errors.push(`${fieldName}[${i}] must not be an absolute path: ${JSON.stringify(p)}`);
|
|
68
|
+
}
|
|
69
|
+
if (hasDotDotSegment(p)) {
|
|
70
|
+
errors.push(`${fieldName}[${i}] must not contain a ".." path segment: ${JSON.stringify(p)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// makeRouteDoc
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Construct a KnoSky `route` artifact envelope.
|
|
81
|
+
*
|
|
82
|
+
* @param {object} opts
|
|
83
|
+
* @param {string} opts.destination Navigation target.
|
|
84
|
+
* @param {unknown[]} [opts.route] Ordered waypoints.
|
|
85
|
+
* @param {unknown[]} [opts.alternates] Alternate routes.
|
|
86
|
+
* @param {string[]} [opts.caveats] Advisory notes.
|
|
87
|
+
* @param {number} [opts.confidence] Confidence score [0..1].
|
|
88
|
+
* @param {string|null} [opts.source_rev] VCS revision that produced this doc.
|
|
89
|
+
* @param {number|null} [opts.ledger_seq] Ledger sequence number (SAT-444) —
|
|
90
|
+
* monotone commit count from the city that produced this route doc.
|
|
91
|
+
* null means the city predates ledger-anchored freshness.
|
|
92
|
+
* @returns {object}
|
|
93
|
+
*/
|
|
94
|
+
export function makeRouteDoc({
|
|
95
|
+
destination,
|
|
96
|
+
route = [],
|
|
97
|
+
alternates = [],
|
|
98
|
+
caveats = [],
|
|
99
|
+
confidence = 0,
|
|
100
|
+
source_rev = null,
|
|
101
|
+
ledger_seq = null,
|
|
102
|
+
} = {}) {
|
|
103
|
+
return {
|
|
104
|
+
knosky_protocol: PROTOCOL_VERSION,
|
|
105
|
+
artifact_type: 'route',
|
|
106
|
+
advisory: true,
|
|
107
|
+
generated_at: new Date().toISOString(),
|
|
108
|
+
source_rev,
|
|
109
|
+
ledger_seq,
|
|
110
|
+
destination,
|
|
111
|
+
route,
|
|
112
|
+
alternates,
|
|
113
|
+
caveats,
|
|
114
|
+
confidence,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// validateRouteDoc
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Validate a KnoSky `route` document.
|
|
124
|
+
* Collects every violation; `ok` is true only when `errors` is empty.
|
|
125
|
+
*
|
|
126
|
+
* @param {object} doc
|
|
127
|
+
* @returns {{ ok: boolean, errors: string[] }}
|
|
128
|
+
*/
|
|
129
|
+
export function validateRouteDoc(doc) {
|
|
130
|
+
const errors = [];
|
|
131
|
+
|
|
132
|
+
if (doc.knosky_protocol !== '1.0') {
|
|
133
|
+
errors.push(`knosky_protocol must be "1.0", got: ${JSON.stringify(doc.knosky_protocol)}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (doc.artifact_type !== 'route') {
|
|
137
|
+
errors.push(`artifact_type must be "route", got: ${JSON.stringify(doc.artifact_type)}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (doc.advisory !== true) {
|
|
141
|
+
errors.push(`advisory must be true, got: ${JSON.stringify(doc.advisory)}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const field of ['route', 'alternates', 'caveats']) {
|
|
145
|
+
if (!Array.isArray(doc[field])) {
|
|
146
|
+
errors.push(`${field} must be an array, got: ${JSON.stringify(doc[field])}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Number.isFinite rejects NaN and ±Infinity; the range check then enforces [0, 1].
|
|
151
|
+
if (!Number.isFinite(doc.confidence) || doc.confidence < 0 || doc.confidence > 1) {
|
|
152
|
+
errors.push(`confidence must be a number in [0, 1], got: ${JSON.stringify(doc.confidence)}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ledger_seq: must be null or a non-negative integer (SAT-444)
|
|
156
|
+
if (doc.ledger_seq !== undefined && doc.ledger_seq !== null) {
|
|
157
|
+
if (typeof doc.ledger_seq !== 'number' || !Number.isInteger(doc.ledger_seq) || doc.ledger_seq < 0) {
|
|
158
|
+
errors.push(`ledger_seq must be null or a non-negative integer, got: ${JSON.stringify(doc.ledger_seq)}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Absolute-path invariant applies to route[] and alternates[]
|
|
163
|
+
if (Array.isArray(doc.route)) {
|
|
164
|
+
checkAbsolutePaths(doc.route, 'route', errors);
|
|
165
|
+
}
|
|
166
|
+
if (Array.isArray(doc.alternates)) {
|
|
167
|
+
checkAbsolutePaths(doc.alternates, 'alternates', errors);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { ok: errors.length === 0, errors };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// makeIntentManifest
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Construct a KnoSky `intent-manifest` artifact envelope.
|
|
179
|
+
*
|
|
180
|
+
* @param {object} opts
|
|
181
|
+
* @param {Array<{ path: string, sha256: string }>} [opts.paths] Files covered.
|
|
182
|
+
* @param {unknown[]} [opts.edges] Dependency edges.
|
|
183
|
+
* @param {string|null} [opts.expiry] ISO-8601 expiry timestamp or null.
|
|
184
|
+
* @param {object} opts.secret_scan REQUIRED secret-scan result object.
|
|
185
|
+
* @param {number|null} [opts.ledger_seq] Ledger sequence number (SAT-444) —
|
|
186
|
+
* monotone commit count from the city that produced this manifest.
|
|
187
|
+
* null means the city predates ledger-anchored freshness.
|
|
188
|
+
* @returns {object}
|
|
189
|
+
*/
|
|
190
|
+
export function makeIntentManifest({
|
|
191
|
+
paths = [],
|
|
192
|
+
edges = [],
|
|
193
|
+
expiry = null,
|
|
194
|
+
secret_scan,
|
|
195
|
+
ledger_seq = null,
|
|
196
|
+
} = {}) {
|
|
197
|
+
return {
|
|
198
|
+
knosky_protocol: PROTOCOL_VERSION,
|
|
199
|
+
artifact_type: 'intent-manifest',
|
|
200
|
+
advisory: true,
|
|
201
|
+
generated_at: new Date().toISOString(),
|
|
202
|
+
paths,
|
|
203
|
+
edges,
|
|
204
|
+
expiry,
|
|
205
|
+
ledger_seq,
|
|
206
|
+
secret_scan,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// validateIntentManifest
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Validate a KnoSky `intent-manifest` document.
|
|
216
|
+
* Collects every violation; `ok` is true only when `errors` is empty.
|
|
217
|
+
*
|
|
218
|
+
* @param {object} doc
|
|
219
|
+
* @returns {{ ok: boolean, errors: string[] }}
|
|
220
|
+
*/
|
|
221
|
+
export function validateIntentManifest(doc) {
|
|
222
|
+
const errors = [];
|
|
223
|
+
|
|
224
|
+
if (doc.knosky_protocol !== '1.0') {
|
|
225
|
+
errors.push(`knosky_protocol must be "1.0", got: ${JSON.stringify(doc.knosky_protocol)}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (doc.artifact_type !== 'intent-manifest') {
|
|
229
|
+
errors.push(`artifact_type must be "intent-manifest", got: ${JSON.stringify(doc.artifact_type)}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (doc.advisory !== true) {
|
|
233
|
+
errors.push(`advisory must be true, got: ${JSON.stringify(doc.advisory)}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// paths must be an array of { path: string (non-empty), sha256: string }
|
|
237
|
+
if (!Array.isArray(doc.paths)) {
|
|
238
|
+
errors.push(`paths must be an array, got: ${JSON.stringify(doc.paths)}`);
|
|
239
|
+
} else {
|
|
240
|
+
for (let i = 0; i < doc.paths.length; i++) {
|
|
241
|
+
const entry = doc.paths[i];
|
|
242
|
+
if (!entry || typeof entry !== 'object') {
|
|
243
|
+
errors.push(`paths[${i}] must be an object`);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (typeof entry.path !== 'string' || entry.path.length === 0) {
|
|
247
|
+
errors.push(`paths[${i}].path must be a non-empty string, got: ${JSON.stringify(entry.path)}`);
|
|
248
|
+
}
|
|
249
|
+
if (typeof entry.sha256 !== 'string') {
|
|
250
|
+
errors.push(`paths[${i}].sha256 must be a string, got: ${JSON.stringify(entry.sha256)}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Absolute-path invariant on paths[].path
|
|
255
|
+
checkAbsolutePaths(doc.paths, 'paths', errors);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ledger_seq: must be null or a non-negative integer (SAT-444)
|
|
259
|
+
if (doc.ledger_seq !== undefined && doc.ledger_seq !== null) {
|
|
260
|
+
if (typeof doc.ledger_seq !== 'number' || !Number.isInteger(doc.ledger_seq) || doc.ledger_seq < 0) {
|
|
261
|
+
errors.push(`ledger_seq must be null or a non-negative integer, got: ${JSON.stringify(doc.ledger_seq)}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// secret_scan must be present and have a valid status
|
|
266
|
+
if (!doc.secret_scan || typeof doc.secret_scan !== 'object') {
|
|
267
|
+
errors.push('secret_scan is required and must be an object');
|
|
268
|
+
} else if (doc.secret_scan.status !== 'clean' && doc.secret_scan.status !== 'blocked') {
|
|
269
|
+
errors.push(
|
|
270
|
+
`secret_scan.status must be "clean" or "blocked", got: ${JSON.stringify(doc.secret_scan.status)}`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return { ok: errors.length === 0, errors };
|
|
275
|
+
}
|