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.
Files changed (64) hide show
  1. package/CHANGELOG.md +149 -93
  2. package/CREDITS.md +14 -14
  3. package/LICENSE.md +76 -76
  4. package/LIMITATIONS.md +33 -23
  5. package/PRIVACY.md +30 -30
  6. package/README.md +170 -117
  7. package/SECURITY.md +78 -46
  8. package/action/post-comment.mjs +94 -89
  9. package/action.yml +62 -62
  10. package/bin/knosky.mjs +279 -105
  11. package/core/CONTRACT.md +70 -70
  12. package/core/append-only-checkpoint.mjs +215 -0
  13. package/core/audit-writer.mjs +317 -0
  14. package/core/benchmark-results.mjs +225 -225
  15. package/core/bundle.mjs +178 -178
  16. package/core/churn.mjs +23 -23
  17. package/core/ci.mjs +268 -268
  18. package/core/comparison.mjs +189 -189
  19. package/core/config.mjs +189 -189
  20. package/core/constants.mjs +13 -13
  21. package/core/contract.mjs +123 -123
  22. package/core/cross-repo.mjs +111 -111
  23. package/core/decision-codes.mjs +92 -0
  24. package/core/destination.mjs +161 -161
  25. package/core/district-classification.mjs +111 -0
  26. package/core/doctor-scorecard.mjs +369 -0
  27. package/core/domain-store.mjs +347 -0
  28. package/core/edges.mjs +43 -43
  29. package/core/escalate.mjs +68 -68
  30. package/core/freshness.mjs +198 -194
  31. package/core/fs-indexer.mjs +218 -218
  32. package/core/key-store.mjs +348 -348
  33. package/core/layout.mjs +46 -46
  34. package/core/ledger.mjs +176 -141
  35. package/core/local-ipc-identity.mjs +500 -0
  36. package/core/lod.mjs +155 -155
  37. package/core/mode-b.mjs +410 -0
  38. package/core/multi-model-benchmark.mjs +405 -405
  39. package/core/net-lockdown.mjs +421 -0
  40. package/core/onboarding.mjs +223 -223
  41. package/core/operator-auth.mjs +317 -0
  42. package/core/overlays.mjs +45 -45
  43. package/core/policy-lattice.mjs +142 -0
  44. package/core/pr-comment.mjs +198 -198
  45. package/core/protocol-spec.mjs +460 -460
  46. package/core/provenance.mjs +320 -0
  47. package/core/retrieve.mjs +63 -63
  48. package/core/route.mjs +304 -304
  49. package/core/schema.mjs +275 -275
  50. package/core/signing-tiers.mjs +1265 -0
  51. package/core/swarm-bench.mjs +106 -0
  52. package/core/swarm-coordinator.mjs +867 -0
  53. package/core/trust-root-rekey.mjs +410 -0
  54. package/mcp/server.mjs +264 -108
  55. package/package.json +56 -46
  56. package/renderer/art/kenney/buildingTiles_sheet.xml +130 -130
  57. package/renderer/art/kenney/cityDetails_sheet.xml +12 -12
  58. package/renderer/art/kenney/landscapeTiles_sheet.xml +129 -129
  59. package/renderer/art/kenney/sheet_allCars.xml +545 -545
  60. package/renderer/build-rich.mjs +43 -43
  61. package/renderer/city.template.html +808 -808
  62. package/ssot/decision-codes.json +133 -0
  63. package/ssot/ladder-l0-l3.md +232 -0
  64. 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
+ }