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
@@ -1,405 +1,405 @@
1
- // KnoSky simultaneous multi-model benchmark harness (SAT-459).
2
- //
3
- // Defines the artifact schema and validators for a multi-model benchmark run:
4
- // the same benchmark tasks from the comparison-run protocol (core/comparison.mjs)
5
- // executed simultaneously across all supported model families in parallel — not
6
- // sequentially — and collected into a single artifact for analysis.
7
- //
8
- // Paul's explicit instruction: "run simultaneous tests" -- every model family
9
- // (Claude, GPT, Gemini, DeepSeek, Grok) receives the same tasks at the same
10
- // time via Promise.all. This module owns the protocol shape only; it makes no
11
- // network calls itself. A separate, not-yet-built tools/-layer helper (following
12
- // the CI-only HTTP-dispatch pattern already used elsewhere in this repo's build
13
- // tooling) is expected to supply the real dispatchFn at call time -- see D-176.
14
- //
15
- // Pure Node stdlib, ESM — no new deps.
16
-
17
- import { PROTOCOL_VERSION } from './schema.mjs';
18
- // NOTE (D-176, post-PR#50 review): this file does NOT import validateComparisonRun from
19
- // core/comparison.mjs -- it defines and validates its own multi-model artifact shape and
20
- // never delegates to the single-model comparison-run validator. An earlier version had an
21
- // unused import of it (dead code, flagged by review); removed rather than left in.
22
-
23
- // ---------------------------------------------------------------------------
24
- // Artifact type constant
25
- // ---------------------------------------------------------------------------
26
-
27
- /** @type {string} */
28
- export const MULTI_MODEL_ARTIFACT_TYPE = 'multi-model-benchmark';
29
-
30
- // ---------------------------------------------------------------------------
31
- // Supported model families — the canonical list for simultaneous dispatch.
32
- // Names must be stable identifiers (used as keys in per-model result maps).
33
- // ---------------------------------------------------------------------------
34
-
35
- /**
36
- * The five model families that the simultaneous harness fans out to.
37
- * Each entry is a { family, modelId } pair:
38
- * family — short name used as a key in multi-model result artifacts
39
- * modelId — the LiteLLM / routing model string for the dispatch layer
40
- *
41
- * @type {Array<{ family: string, modelId: string }>}
42
- */
43
- export const MODEL_FAMILIES = [
44
- { family: 'claude', modelId: 'sathia-standard' },
45
- { family: 'gpt', modelId: 'sathia-gpt' },
46
- { family: 'gemini', modelId: 'sathia-gemini' },
47
- { family: 'deepseek', modelId: 'sathia-deepseek' },
48
- { family: 'grok', modelId: 'sathia-grok' },
49
- ];
50
-
51
- /**
52
- * The set of valid family names — used in validation.
53
- * @type {Set<string>}
54
- */
55
- const KNOWN_FAMILIES = new Set(MODEL_FAMILIES.map(m => m.family));
56
-
57
- // ---------------------------------------------------------------------------
58
- // Internal helpers
59
- // ---------------------------------------------------------------------------
60
-
61
- /**
62
- * Return true when `p` is a relative-safe file path:
63
- * — non-empty string
64
- * — does not start with `/` (Unix absolute) or Windows drive letter
65
- * — contains no `..` path segment
66
- * @param {string} p
67
- * @returns {boolean}
68
- */
69
- function isRelativeSafePath(p) {
70
- if (!p || typeof p !== 'string') return false;
71
- if (p.startsWith('/')) return false;
72
- if (/^[A-Za-z]:[\\\/]/.test(p)) return false;
73
- if (p.split(/[/\\]/).some(s => s === '..')) return false;
74
- return true;
75
- }
76
-
77
- /**
78
- * Validate one per-model result object.
79
- * Returns an array of violation strings; empty means valid.
80
- *
81
- * @param {unknown} result
82
- * @param {string} label — e.g. "results[claude]"
83
- * @returns {string[]}
84
- */
85
- function validateModelResult(result, label) {
86
- const errors = [];
87
-
88
- if (!result || typeof result !== 'object') {
89
- errors.push(`${label} must be an object`);
90
- return errors;
91
- }
92
-
93
- // family: known non-empty string
94
- if (typeof result.family !== 'string' || !KNOWN_FAMILIES.has(result.family)) {
95
- errors.push(
96
- `${label}.family must be one of [${[...KNOWN_FAMILIES].join(', ')}], ` +
97
- `got: ${JSON.stringify(result.family)}`,
98
- );
99
- }
100
-
101
- // model_id: non-empty string
102
- if (typeof result.model_id !== 'string' || result.model_id.length === 0) {
103
- errors.push(`${label}.model_id must be a non-empty string, got: ${JSON.stringify(result.model_id)}`);
104
- }
105
-
106
- // status: 'ok' | 'failed'
107
- if (result.status !== 'ok' && result.status !== 'failed') {
108
- errors.push(`${label}.status must be "ok" or "failed", got: ${JSON.stringify(result.status)}`);
109
- }
110
-
111
- // error: string or null
112
- if (result.error !== null && typeof result.error !== 'string') {
113
- errors.push(`${label}.error must be null or a string, got: ${JSON.stringify(result.error)}`);
114
- }
115
-
116
- // metrics: required when status === 'ok', must pass comparison-run side validation
117
- if (result.status === 'ok') {
118
- if (!result.metrics || typeof result.metrics !== 'object') {
119
- errors.push(`${label}.metrics must be an object when status is "ok"`);
120
- } else {
121
- // metrics carries tokens_in / tokens_out / tool_calls / time_to_relevant_file_ms / correct
122
- const m = result.metrics;
123
- if (!Number.isInteger(m.tokens_in) || m.tokens_in < 0) {
124
- errors.push(`${label}.metrics.tokens_in must be a non-negative integer, got: ${JSON.stringify(m.tokens_in)}`);
125
- }
126
- if (!Number.isInteger(m.tokens_out) || m.tokens_out < 0) {
127
- errors.push(`${label}.metrics.tokens_out must be a non-negative integer, got: ${JSON.stringify(m.tokens_out)}`);
128
- }
129
- if (!Number.isInteger(m.tool_calls) || m.tool_calls < 0) {
130
- errors.push(`${label}.metrics.tool_calls must be a non-negative integer, got: ${JSON.stringify(m.tool_calls)}`);
131
- }
132
- const ttrf = m.time_to_relevant_file_ms;
133
- if (ttrf !== null && !(typeof ttrf === 'number' && ttrf >= 0)) {
134
- errors.push(
135
- `${label}.metrics.time_to_relevant_file_ms must be null or a non-negative number, ` +
136
- `got: ${JSON.stringify(ttrf)}`,
137
- );
138
- }
139
- if (typeof m.correct !== 'boolean') {
140
- errors.push(`${label}.metrics.correct must be a boolean, got: ${JSON.stringify(m.correct)}`);
141
- }
142
- }
143
- }
144
-
145
- return errors;
146
- }
147
-
148
- // ---------------------------------------------------------------------------
149
- // makeMultiModelRun
150
- // ---------------------------------------------------------------------------
151
-
152
- /**
153
- * Construct a KnoSky `multi-model-benchmark` artifact envelope.
154
- *
155
- * The artifact records:
156
- * - the task that was run (same fields as comparison-run: id, description, target_files)
157
- * - per-model results collected simultaneously via Promise.all dispatch
158
- * - a summary: which models succeeded, total latency, fastest model
159
- *
160
- * @param {object} opts
161
- * @param {string} opts.task_id Short identifier for this task.
162
- * @param {string} opts.task_description Human-readable description of the task.
163
- * @param {string[]} opts.target_files Relative paths to the "relevant" files.
164
- * @param {object[]} opts.results Per-model result objects (one per family).
165
- * @param {object} [opts.dispatch] Dispatch metadata: how models were invoked.
166
- * @param {string} [opts.dispatch.mode] Always "parallel" — simultaneous Promise.all.
167
- * @param {number} [opts.dispatch.started_at_ms] Wall-clock ms when the fan-out started.
168
- * @param {number} [opts.dispatch.settled_at_ms] Wall-clock ms when all settled.
169
- * @returns {object}
170
- */
171
- export function makeMultiModelRun({
172
- task_id,
173
- task_description,
174
- target_files = [],
175
- results = [],
176
- dispatch = null,
177
- } = {}) {
178
- // Derive summary from results
179
- const succeeded = results.filter(r => r && r.status === 'ok').map(r => r.family);
180
- const failed = results.filter(r => r && r.status === 'failed').map(r => r.family);
181
-
182
- // Fastest model = min time_to_relevant_file_ms among successful runs that found the file
183
- let fastest_family = null;
184
- let minTtrf = Infinity;
185
- for (const r of results) {
186
- if (r && r.status === 'ok' && r.metrics) {
187
- const t = r.metrics.time_to_relevant_file_ms;
188
- if (typeof t === 'number' && t >= 0 && t < minTtrf) {
189
- minTtrf = t;
190
- fastest_family = r.family;
191
- }
192
- }
193
- }
194
-
195
- return {
196
- knosky_protocol: PROTOCOL_VERSION,
197
- artifact_type: MULTI_MODEL_ARTIFACT_TYPE,
198
- advisory: true,
199
- generated_at: new Date().toISOString(),
200
- task_id,
201
- task_description,
202
- target_files,
203
- dispatch: dispatch || { mode: 'parallel' },
204
- results,
205
- summary: {
206
- total: results.length,
207
- succeeded: succeeded.length,
208
- failed: failed.length,
209
- succeeded_families: succeeded,
210
- failed_families: failed,
211
- fastest_family,
212
- },
213
- };
214
- }
215
-
216
- // ---------------------------------------------------------------------------
217
- // validateMultiModelRun
218
- // ---------------------------------------------------------------------------
219
-
220
- /**
221
- * Validate a KnoSky `multi-model-benchmark` document.
222
- * Collects every violation; `ok` is true only when `errors` is empty.
223
- *
224
- * @param {object} doc
225
- * @returns {{ ok: boolean, errors: string[] }}
226
- */
227
- export function validateMultiModelRun(doc) {
228
- const errors = [];
229
-
230
- if (!doc || typeof doc !== 'object') {
231
- return { ok: false, errors: ['doc must be an object'] };
232
- }
233
-
234
- if (doc.knosky_protocol !== PROTOCOL_VERSION) {
235
- errors.push(
236
- `knosky_protocol must be "${PROTOCOL_VERSION}", got: ${JSON.stringify(doc.knosky_protocol)}`,
237
- );
238
- }
239
-
240
- if (doc.artifact_type !== MULTI_MODEL_ARTIFACT_TYPE) {
241
- errors.push(
242
- `artifact_type must be "${MULTI_MODEL_ARTIFACT_TYPE}", ` +
243
- `got: ${JSON.stringify(doc.artifact_type)}`,
244
- );
245
- }
246
-
247
- if (doc.advisory !== true) {
248
- errors.push(`advisory must be true, got: ${JSON.stringify(doc.advisory)}`);
249
- }
250
-
251
- // task_id: non-empty string
252
- if (typeof doc.task_id !== 'string' || doc.task_id.length === 0) {
253
- errors.push(`task_id must be a non-empty string, got: ${JSON.stringify(doc.task_id)}`);
254
- }
255
-
256
- // task_description: non-empty string
257
- if (typeof doc.task_description !== 'string' || doc.task_description.length === 0) {
258
- errors.push(
259
- `task_description must be a non-empty string, got: ${JSON.stringify(doc.task_description)}`,
260
- );
261
- }
262
-
263
- // target_files: non-empty array of relative-safe path strings
264
- if (!Array.isArray(doc.target_files) || doc.target_files.length === 0) {
265
- errors.push('target_files must be a non-empty array');
266
- } else {
267
- for (let i = 0; i < doc.target_files.length; i++) {
268
- const p = doc.target_files[i];
269
- if (typeof p !== 'string' || p.length === 0) {
270
- errors.push(`target_files[${i}] must be a non-empty string`);
271
- } else if (!isRelativeSafePath(p)) {
272
- errors.push(
273
- `target_files[${i}] must be a relative path with no ".." segments: ${JSON.stringify(p)}`,
274
- );
275
- }
276
- }
277
- }
278
-
279
- // dispatch: object with mode === 'parallel'
280
- if (!doc.dispatch || typeof doc.dispatch !== 'object') {
281
- errors.push('dispatch must be an object');
282
- } else if (doc.dispatch.mode !== 'parallel') {
283
- errors.push(
284
- `dispatch.mode must be "parallel" (simultaneous fan-out), ` +
285
- `got: ${JSON.stringify(doc.dispatch.mode)}`,
286
- );
287
- }
288
-
289
- // results: non-empty array
290
- if (!Array.isArray(doc.results) || doc.results.length === 0) {
291
- errors.push('results must be a non-empty array');
292
- } else {
293
- for (let i = 0; i < doc.results.length; i++) {
294
- errors.push(...validateModelResult(doc.results[i], `results[${i}]`));
295
- }
296
- }
297
-
298
- // summary: structural check
299
- if (!doc.summary || typeof doc.summary !== 'object') {
300
- errors.push('summary must be an object');
301
- } else {
302
- const s = doc.summary;
303
- if (!Number.isInteger(s.total) || s.total < 0) {
304
- errors.push(`summary.total must be a non-negative integer, got: ${JSON.stringify(s.total)}`);
305
- }
306
- if (!Number.isInteger(s.succeeded) || s.succeeded < 0) {
307
- errors.push(`summary.succeeded must be a non-negative integer, got: ${JSON.stringify(s.succeeded)}`);
308
- }
309
- if (!Number.isInteger(s.failed) || s.failed < 0) {
310
- errors.push(`summary.failed must be a non-negative integer, got: ${JSON.stringify(s.failed)}`);
311
- }
312
- if (!Array.isArray(s.succeeded_families)) {
313
- errors.push('summary.succeeded_families must be an array');
314
- }
315
- if (!Array.isArray(s.failed_families)) {
316
- errors.push('summary.failed_families must be an array');
317
- }
318
- // fastest_family: null or a known family string
319
- if (s.fastest_family !== null && typeof s.fastest_family !== 'string') {
320
- errors.push(
321
- `summary.fastest_family must be null or a string, got: ${JSON.stringify(s.fastest_family)}`,
322
- );
323
- }
324
- }
325
-
326
- return { ok: errors.length === 0, errors };
327
- }
328
-
329
- // ---------------------------------------------------------------------------
330
- // runMultiModelBenchmark
331
- // ---------------------------------------------------------------------------
332
-
333
- /**
334
- * Execute one benchmark task simultaneously across all model families.
335
- *
336
- * This is the harness entry point that implements Paul's "simultaneous tests"
337
- * requirement: all models are dispatched in a single Promise.all fan-out so
338
- * no model waits for another to finish.
339
- *
340
- * `dispatch` is an async function provided by the caller (typically the tools/
341
- * layer, which owns the HTTPS + LiteLLM wiring). It receives a { family,
342
- * modelId, task } descriptor and must resolve to a per-model result object:
343
- *
344
- * {
345
- * family: string, // must match MODEL_FAMILIES[n].family
346
- * model_id: string,
347
- * status: 'ok' | 'failed',
348
- * error: string | null, // non-null on failure
349
- * metrics: { // present when status === 'ok'
350
- * tokens_in: number,
351
- * tokens_out: number,
352
- * tool_calls: number,
353
- * time_to_relevant_file_ms: number | null,
354
- * correct: boolean,
355
- * } | null,
356
- * }
357
- *
358
- * Failed dispatches (rejected promise, uncaught throw) are caught and stored
359
- * as `{ status: 'failed', error: <message> }` — one model failing never
360
- * prevents the others from completing (Promise.allSettled semantics on top
361
- * of the individual error handling).
362
- *
363
- * @param {object} task
364
- * @param {string} task.task_id
365
- * @param {string} task.task_description
366
- * @param {string[]} task.target_files
367
- * @param {Function} dispatchFn async (descriptor) => modelResult
368
- * @param {Array<{ family: string, modelId: string }>} [models] Defaults to MODEL_FAMILIES.
369
- * @returns {Promise<object>} A validated multi-model-benchmark artifact.
370
- */
371
- export async function runMultiModelBenchmark(task, dispatchFn, models = MODEL_FAMILIES) {
372
- const startedAtMs = Date.now();
373
-
374
- // Fan out simultaneously — Promise.all so all models start at the same time.
375
- // Individual dispatch errors are caught per-slot so one failure cannot abort others.
376
- const results = await Promise.all(
377
- models.map(async ({ family, modelId }) => {
378
- try {
379
- return await dispatchFn({ family, modelId, task });
380
- } catch (err) {
381
- return {
382
- family,
383
- model_id: modelId,
384
- status: 'failed',
385
- error: err && err.message ? err.message : String(err),
386
- metrics: null,
387
- };
388
- }
389
- }),
390
- );
391
-
392
- const settledAtMs = Date.now();
393
-
394
- return makeMultiModelRun({
395
- task_id: task.task_id,
396
- task_description: task.task_description,
397
- target_files: task.target_files,
398
- results,
399
- dispatch: {
400
- mode: 'parallel',
401
- started_at_ms: startedAtMs,
402
- settled_at_ms: settledAtMs,
403
- },
404
- });
405
- }
1
+ // KnoSky simultaneous multi-model benchmark harness (SAT-459).
2
+ //
3
+ // Defines the artifact schema and validators for a multi-model benchmark run:
4
+ // the same benchmark tasks from the comparison-run protocol (core/comparison.mjs)
5
+ // executed simultaneously across all supported model families in parallel — not
6
+ // sequentially — and collected into a single artifact for analysis.
7
+ //
8
+ // Paul's explicit instruction: "run simultaneous tests" -- every model family
9
+ // (Claude, GPT, Gemini, DeepSeek, Grok) receives the same tasks at the same
10
+ // time via Promise.all. This module owns the protocol shape only; it makes no
11
+ // network calls itself. A separate, not-yet-built tools/-layer helper (following
12
+ // the CI-only HTTP-dispatch pattern already used elsewhere in this repo's build
13
+ // tooling) is expected to supply the real dispatchFn at call time -- see D-176.
14
+ //
15
+ // Pure Node stdlib, ESM — no new deps.
16
+
17
+ import { PROTOCOL_VERSION } from './schema.mjs';
18
+ // NOTE (D-176, post-PR#50 review): this file does NOT import validateComparisonRun from
19
+ // core/comparison.mjs -- it defines and validates its own multi-model artifact shape and
20
+ // never delegates to the single-model comparison-run validator. An earlier version had an
21
+ // unused import of it (dead code, flagged by review); removed rather than left in.
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Artifact type constant
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /** @type {string} */
28
+ export const MULTI_MODEL_ARTIFACT_TYPE = 'multi-model-benchmark';
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Supported model families — the canonical list for simultaneous dispatch.
32
+ // Names must be stable identifiers (used as keys in per-model result maps).
33
+ // ---------------------------------------------------------------------------
34
+
35
+ /**
36
+ * The five model families that the simultaneous harness fans out to.
37
+ * Each entry is a { family, modelId } pair:
38
+ * family — short name used as a key in multi-model result artifacts
39
+ * modelId — the LiteLLM / routing model string for the dispatch layer
40
+ *
41
+ * @type {Array<{ family: string, modelId: string }>}
42
+ */
43
+ export const MODEL_FAMILIES = [
44
+ { family: 'claude', modelId: 'sathia-standard' },
45
+ { family: 'gpt', modelId: 'sathia-gpt' },
46
+ { family: 'gemini', modelId: 'sathia-gemini' },
47
+ { family: 'deepseek', modelId: 'sathia-deepseek' },
48
+ { family: 'grok', modelId: 'sathia-grok' },
49
+ ];
50
+
51
+ /**
52
+ * The set of valid family names — used in validation.
53
+ * @type {Set<string>}
54
+ */
55
+ const KNOWN_FAMILIES = new Set(MODEL_FAMILIES.map(m => m.family));
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Internal helpers
59
+ // ---------------------------------------------------------------------------
60
+
61
+ /**
62
+ * Return true when `p` is a relative-safe file path:
63
+ * — non-empty string
64
+ * — does not start with `/` (Unix absolute) or Windows drive letter
65
+ * — contains no `..` path segment
66
+ * @param {string} p
67
+ * @returns {boolean}
68
+ */
69
+ function isRelativeSafePath(p) {
70
+ if (!p || typeof p !== 'string') return false;
71
+ if (p.startsWith('/')) return false;
72
+ if (/^[A-Za-z]:[\\\/]/.test(p)) return false;
73
+ if (p.split(/[/\\]/).some(s => s === '..')) return false;
74
+ return true;
75
+ }
76
+
77
+ /**
78
+ * Validate one per-model result object.
79
+ * Returns an array of violation strings; empty means valid.
80
+ *
81
+ * @param {unknown} result
82
+ * @param {string} label — e.g. "results[claude]"
83
+ * @returns {string[]}
84
+ */
85
+ function validateModelResult(result, label) {
86
+ const errors = [];
87
+
88
+ if (!result || typeof result !== 'object') {
89
+ errors.push(`${label} must be an object`);
90
+ return errors;
91
+ }
92
+
93
+ // family: known non-empty string
94
+ if (typeof result.family !== 'string' || !KNOWN_FAMILIES.has(result.family)) {
95
+ errors.push(
96
+ `${label}.family must be one of [${[...KNOWN_FAMILIES].join(', ')}], ` +
97
+ `got: ${JSON.stringify(result.family)}`,
98
+ );
99
+ }
100
+
101
+ // model_id: non-empty string
102
+ if (typeof result.model_id !== 'string' || result.model_id.length === 0) {
103
+ errors.push(`${label}.model_id must be a non-empty string, got: ${JSON.stringify(result.model_id)}`);
104
+ }
105
+
106
+ // status: 'ok' | 'failed'
107
+ if (result.status !== 'ok' && result.status !== 'failed') {
108
+ errors.push(`${label}.status must be "ok" or "failed", got: ${JSON.stringify(result.status)}`);
109
+ }
110
+
111
+ // error: string or null
112
+ if (result.error !== null && typeof result.error !== 'string') {
113
+ errors.push(`${label}.error must be null or a string, got: ${JSON.stringify(result.error)}`);
114
+ }
115
+
116
+ // metrics: required when status === 'ok', must pass comparison-run side validation
117
+ if (result.status === 'ok') {
118
+ if (!result.metrics || typeof result.metrics !== 'object') {
119
+ errors.push(`${label}.metrics must be an object when status is "ok"`);
120
+ } else {
121
+ // metrics carries tokens_in / tokens_out / tool_calls / time_to_relevant_file_ms / correct
122
+ const m = result.metrics;
123
+ if (!Number.isInteger(m.tokens_in) || m.tokens_in < 0) {
124
+ errors.push(`${label}.metrics.tokens_in must be a non-negative integer, got: ${JSON.stringify(m.tokens_in)}`);
125
+ }
126
+ if (!Number.isInteger(m.tokens_out) || m.tokens_out < 0) {
127
+ errors.push(`${label}.metrics.tokens_out must be a non-negative integer, got: ${JSON.stringify(m.tokens_out)}`);
128
+ }
129
+ if (!Number.isInteger(m.tool_calls) || m.tool_calls < 0) {
130
+ errors.push(`${label}.metrics.tool_calls must be a non-negative integer, got: ${JSON.stringify(m.tool_calls)}`);
131
+ }
132
+ const ttrf = m.time_to_relevant_file_ms;
133
+ if (ttrf !== null && !(typeof ttrf === 'number' && ttrf >= 0)) {
134
+ errors.push(
135
+ `${label}.metrics.time_to_relevant_file_ms must be null or a non-negative number, ` +
136
+ `got: ${JSON.stringify(ttrf)}`,
137
+ );
138
+ }
139
+ if (typeof m.correct !== 'boolean') {
140
+ errors.push(`${label}.metrics.correct must be a boolean, got: ${JSON.stringify(m.correct)}`);
141
+ }
142
+ }
143
+ }
144
+
145
+ return errors;
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // makeMultiModelRun
150
+ // ---------------------------------------------------------------------------
151
+
152
+ /**
153
+ * Construct a KnoSky `multi-model-benchmark` artifact envelope.
154
+ *
155
+ * The artifact records:
156
+ * - the task that was run (same fields as comparison-run: id, description, target_files)
157
+ * - per-model results collected simultaneously via Promise.all dispatch
158
+ * - a summary: which models succeeded, total latency, fastest model
159
+ *
160
+ * @param {object} opts
161
+ * @param {string} opts.task_id Short identifier for this task.
162
+ * @param {string} opts.task_description Human-readable description of the task.
163
+ * @param {string[]} opts.target_files Relative paths to the "relevant" files.
164
+ * @param {object[]} opts.results Per-model result objects (one per family).
165
+ * @param {object} [opts.dispatch] Dispatch metadata: how models were invoked.
166
+ * @param {string} [opts.dispatch.mode] Always "parallel" — simultaneous Promise.all.
167
+ * @param {number} [opts.dispatch.started_at_ms] Wall-clock ms when the fan-out started.
168
+ * @param {number} [opts.dispatch.settled_at_ms] Wall-clock ms when all settled.
169
+ * @returns {object}
170
+ */
171
+ export function makeMultiModelRun({
172
+ task_id,
173
+ task_description,
174
+ target_files = [],
175
+ results = [],
176
+ dispatch = null,
177
+ } = {}) {
178
+ // Derive summary from results
179
+ const succeeded = results.filter(r => r && r.status === 'ok').map(r => r.family);
180
+ const failed = results.filter(r => r && r.status === 'failed').map(r => r.family);
181
+
182
+ // Fastest model = min time_to_relevant_file_ms among successful runs that found the file
183
+ let fastest_family = null;
184
+ let minTtrf = Infinity;
185
+ for (const r of results) {
186
+ if (r && r.status === 'ok' && r.metrics) {
187
+ const t = r.metrics.time_to_relevant_file_ms;
188
+ if (typeof t === 'number' && t >= 0 && t < minTtrf) {
189
+ minTtrf = t;
190
+ fastest_family = r.family;
191
+ }
192
+ }
193
+ }
194
+
195
+ return {
196
+ knosky_protocol: PROTOCOL_VERSION,
197
+ artifact_type: MULTI_MODEL_ARTIFACT_TYPE,
198
+ advisory: true,
199
+ generated_at: new Date().toISOString(),
200
+ task_id,
201
+ task_description,
202
+ target_files,
203
+ dispatch: dispatch || { mode: 'parallel' },
204
+ results,
205
+ summary: {
206
+ total: results.length,
207
+ succeeded: succeeded.length,
208
+ failed: failed.length,
209
+ succeeded_families: succeeded,
210
+ failed_families: failed,
211
+ fastest_family,
212
+ },
213
+ };
214
+ }
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // validateMultiModelRun
218
+ // ---------------------------------------------------------------------------
219
+
220
+ /**
221
+ * Validate a KnoSky `multi-model-benchmark` document.
222
+ * Collects every violation; `ok` is true only when `errors` is empty.
223
+ *
224
+ * @param {object} doc
225
+ * @returns {{ ok: boolean, errors: string[] }}
226
+ */
227
+ export function validateMultiModelRun(doc) {
228
+ const errors = [];
229
+
230
+ if (!doc || typeof doc !== 'object') {
231
+ return { ok: false, errors: ['doc must be an object'] };
232
+ }
233
+
234
+ if (doc.knosky_protocol !== PROTOCOL_VERSION) {
235
+ errors.push(
236
+ `knosky_protocol must be "${PROTOCOL_VERSION}", got: ${JSON.stringify(doc.knosky_protocol)}`,
237
+ );
238
+ }
239
+
240
+ if (doc.artifact_type !== MULTI_MODEL_ARTIFACT_TYPE) {
241
+ errors.push(
242
+ `artifact_type must be "${MULTI_MODEL_ARTIFACT_TYPE}", ` +
243
+ `got: ${JSON.stringify(doc.artifact_type)}`,
244
+ );
245
+ }
246
+
247
+ if (doc.advisory !== true) {
248
+ errors.push(`advisory must be true, got: ${JSON.stringify(doc.advisory)}`);
249
+ }
250
+
251
+ // task_id: non-empty string
252
+ if (typeof doc.task_id !== 'string' || doc.task_id.length === 0) {
253
+ errors.push(`task_id must be a non-empty string, got: ${JSON.stringify(doc.task_id)}`);
254
+ }
255
+
256
+ // task_description: non-empty string
257
+ if (typeof doc.task_description !== 'string' || doc.task_description.length === 0) {
258
+ errors.push(
259
+ `task_description must be a non-empty string, got: ${JSON.stringify(doc.task_description)}`,
260
+ );
261
+ }
262
+
263
+ // target_files: non-empty array of relative-safe path strings
264
+ if (!Array.isArray(doc.target_files) || doc.target_files.length === 0) {
265
+ errors.push('target_files must be a non-empty array');
266
+ } else {
267
+ for (let i = 0; i < doc.target_files.length; i++) {
268
+ const p = doc.target_files[i];
269
+ if (typeof p !== 'string' || p.length === 0) {
270
+ errors.push(`target_files[${i}] must be a non-empty string`);
271
+ } else if (!isRelativeSafePath(p)) {
272
+ errors.push(
273
+ `target_files[${i}] must be a relative path with no ".." segments: ${JSON.stringify(p)}`,
274
+ );
275
+ }
276
+ }
277
+ }
278
+
279
+ // dispatch: object with mode === 'parallel'
280
+ if (!doc.dispatch || typeof doc.dispatch !== 'object') {
281
+ errors.push('dispatch must be an object');
282
+ } else if (doc.dispatch.mode !== 'parallel') {
283
+ errors.push(
284
+ `dispatch.mode must be "parallel" (simultaneous fan-out), ` +
285
+ `got: ${JSON.stringify(doc.dispatch.mode)}`,
286
+ );
287
+ }
288
+
289
+ // results: non-empty array
290
+ if (!Array.isArray(doc.results) || doc.results.length === 0) {
291
+ errors.push('results must be a non-empty array');
292
+ } else {
293
+ for (let i = 0; i < doc.results.length; i++) {
294
+ errors.push(...validateModelResult(doc.results[i], `results[${i}]`));
295
+ }
296
+ }
297
+
298
+ // summary: structural check
299
+ if (!doc.summary || typeof doc.summary !== 'object') {
300
+ errors.push('summary must be an object');
301
+ } else {
302
+ const s = doc.summary;
303
+ if (!Number.isInteger(s.total) || s.total < 0) {
304
+ errors.push(`summary.total must be a non-negative integer, got: ${JSON.stringify(s.total)}`);
305
+ }
306
+ if (!Number.isInteger(s.succeeded) || s.succeeded < 0) {
307
+ errors.push(`summary.succeeded must be a non-negative integer, got: ${JSON.stringify(s.succeeded)}`);
308
+ }
309
+ if (!Number.isInteger(s.failed) || s.failed < 0) {
310
+ errors.push(`summary.failed must be a non-negative integer, got: ${JSON.stringify(s.failed)}`);
311
+ }
312
+ if (!Array.isArray(s.succeeded_families)) {
313
+ errors.push('summary.succeeded_families must be an array');
314
+ }
315
+ if (!Array.isArray(s.failed_families)) {
316
+ errors.push('summary.failed_families must be an array');
317
+ }
318
+ // fastest_family: null or a known family string
319
+ if (s.fastest_family !== null && typeof s.fastest_family !== 'string') {
320
+ errors.push(
321
+ `summary.fastest_family must be null or a string, got: ${JSON.stringify(s.fastest_family)}`,
322
+ );
323
+ }
324
+ }
325
+
326
+ return { ok: errors.length === 0, errors };
327
+ }
328
+
329
+ // ---------------------------------------------------------------------------
330
+ // runMultiModelBenchmark
331
+ // ---------------------------------------------------------------------------
332
+
333
+ /**
334
+ * Execute one benchmark task simultaneously across all model families.
335
+ *
336
+ * This is the harness entry point that implements Paul's "simultaneous tests"
337
+ * requirement: all models are dispatched in a single Promise.all fan-out so
338
+ * no model waits for another to finish.
339
+ *
340
+ * `dispatch` is an async function provided by the caller (typically the tools/
341
+ * layer, which owns the HTTPS + LiteLLM wiring). It receives a { family,
342
+ * modelId, task } descriptor and must resolve to a per-model result object:
343
+ *
344
+ * {
345
+ * family: string, // must match MODEL_FAMILIES[n].family
346
+ * model_id: string,
347
+ * status: 'ok' | 'failed',
348
+ * error: string | null, // non-null on failure
349
+ * metrics: { // present when status === 'ok'
350
+ * tokens_in: number,
351
+ * tokens_out: number,
352
+ * tool_calls: number,
353
+ * time_to_relevant_file_ms: number | null,
354
+ * correct: boolean,
355
+ * } | null,
356
+ * }
357
+ *
358
+ * Failed dispatches (rejected promise, uncaught throw) are caught and stored
359
+ * as `{ status: 'failed', error: <message> }` — one model failing never
360
+ * prevents the others from completing (Promise.allSettled semantics on top
361
+ * of the individual error handling).
362
+ *
363
+ * @param {object} task
364
+ * @param {string} task.task_id
365
+ * @param {string} task.task_description
366
+ * @param {string[]} task.target_files
367
+ * @param {Function} dispatchFn async (descriptor) => modelResult
368
+ * @param {Array<{ family: string, modelId: string }>} [models] Defaults to MODEL_FAMILIES.
369
+ * @returns {Promise<object>} A validated multi-model-benchmark artifact.
370
+ */
371
+ export async function runMultiModelBenchmark(task, dispatchFn, models = MODEL_FAMILIES) {
372
+ const startedAtMs = Date.now();
373
+
374
+ // Fan out simultaneously — Promise.all so all models start at the same time.
375
+ // Individual dispatch errors are caught per-slot so one failure cannot abort others.
376
+ const results = await Promise.all(
377
+ models.map(async ({ family, modelId }) => {
378
+ try {
379
+ return await dispatchFn({ family, modelId, task });
380
+ } catch (err) {
381
+ return {
382
+ family,
383
+ model_id: modelId,
384
+ status: 'failed',
385
+ error: err && err.message ? err.message : String(err),
386
+ metrics: null,
387
+ };
388
+ }
389
+ }),
390
+ );
391
+
392
+ const settledAtMs = Date.now();
393
+
394
+ return makeMultiModelRun({
395
+ task_id: task.task_id,
396
+ task_description: task.task_description,
397
+ target_files: task.target_files,
398
+ results,
399
+ dispatch: {
400
+ mode: 'parallel',
401
+ started_at_ms: startedAtMs,
402
+ settled_at_ms: settledAtMs,
403
+ },
404
+ });
405
+ }