@dogfood-lab/study-swarm 1.3.0 → 2.0.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 +27 -0
- package/PROTOCOL.md +2 -2
- package/README.es.md +8 -2
- package/README.fr.md +15 -9
- package/README.hi.md +8 -2
- package/README.it.md +15 -9
- package/README.ja.md +15 -9
- package/README.md +8 -2
- package/README.pt-BR.md +8 -2
- package/README.zh.md +8 -2
- package/SECURITY.md +2 -2
- package/bin/study-swarm.mjs +279 -49
- package/examples/study-swarm-canon-rollback.lock.json +20 -20
- package/examples/study-swarm-lock.dispatch.md +3 -3
- package/examples/study-swarm-lock.lock.json +15 -15
- package/package.json +1 -1
package/bin/study-swarm.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// study-swarm — thin CLI for the research-grounded design protocol.
|
|
3
3
|
// Zero runtime dependencies. Commands: protocol | new | lint | help | version.
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, statSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, statSync, readdirSync, realpathSync } from 'node:fs';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { dirname, resolve, join } from 'node:path';
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
@@ -19,9 +19,15 @@ USAGE
|
|
|
19
19
|
COMMANDS
|
|
20
20
|
protocol Print the locked protocol (the five steps + halt rules).
|
|
21
21
|
new <slug> Scaffold a dispatch file <slug>.dispatch.md to fill in.
|
|
22
|
-
lint [--json] <path...>
|
|
22
|
+
lint [--json] [--strict] <path...>
|
|
23
|
+
Check dispatches' citations against the sourcing standard.
|
|
23
24
|
A <path> may be a file, a directory (linted recursively for
|
|
24
25
|
*.dispatch.md), or "-" to read one dispatch from stdin.
|
|
26
|
+
--strict also flags orphan citations (a finding no Step-5 choice
|
|
27
|
+
references by number or author — "citations without a connection
|
|
28
|
+
are noise"); opt-in, so the default CI gate is unchanged.
|
|
29
|
+
lock --init <dispatch> Scaffold <dispatch>.orchestration.json — a fill-in-the-blanks
|
|
30
|
+
harness record to feed to "lock <dispatch> --from".
|
|
25
31
|
lock <dispatch> --from <orchestration.json>
|
|
26
32
|
Emit <dispatch>.lock.json — pin (per Step-2 agent) the resolved
|
|
27
33
|
model + SHA-256 of the byte-exact prompt + SHA-256 of the tool
|
|
@@ -39,6 +45,10 @@ COMMANDS
|
|
|
39
45
|
Fail closed (exit 1) for any dispatch carrying an unresolved
|
|
40
46
|
evidence-withdrawn flag — the andon that HALTS a withdrawn finding's
|
|
41
47
|
dependents until it is removed or re-grounded. Gates CI.
|
|
48
|
+
requalify --status <corpus-dir> [--json]
|
|
49
|
+
Read-only evidence-health VIEW of a corpus: withdrawn vs resolved
|
|
50
|
+
counts, a breakdown by reason and resolution mode, per-dispatch
|
|
51
|
+
lines. Informational (exit 0), unlike the --check gate.
|
|
42
52
|
requalify --resolve <dispatch> <identifier> --mode removed|regrounded [--note <text>]
|
|
43
53
|
Clear a flag once the finding is removed (the citation is gone) or
|
|
44
54
|
re-grounded (re-verified clean by the sibling runner; --note records
|
|
@@ -47,12 +57,13 @@ COMMANDS
|
|
|
47
57
|
version Print the version.
|
|
48
58
|
|
|
49
59
|
EXIT CODES
|
|
50
|
-
0 ok / lint clean
|
|
51
|
-
1 lint
|
|
60
|
+
0 ok / lint clean / verify clean
|
|
61
|
+
1 a gate failed: a lint sourcing violation, lock --verify drift, or an
|
|
62
|
+
unresolved evidence-withdrawn flag (requalify --check)
|
|
52
63
|
2 usage or runtime error
|
|
53
64
|
|
|
54
65
|
NOTE
|
|
55
|
-
lint checks citation FORM (Step 3: author + year + a resolvable arXiv/DOI/URL,
|
|
66
|
+
lint checks citation FORM (Step 3: author + year + a resolvable arXiv/DOI/URL/RFC,
|
|
56
67
|
no "studies show…" gestures) — it does not judge whether a source is legitimate
|
|
57
68
|
or actually supports the claim. That is Step 4, below.
|
|
58
69
|
|
|
@@ -135,17 +146,63 @@ function cmdNew(slug) {
|
|
|
135
146
|
// --- lint core ------------------------------------------------------------
|
|
136
147
|
|
|
137
148
|
const YEAR = /\b(19|20)\d{2}\b/;
|
|
138
|
-
|
|
149
|
+
// A resolvable identifier: an arXiv id, a DOI, a direct URL, or a bare RFC number
|
|
150
|
+
// (RFC parity keeps `lint` in step with the sourcing standard and with normIdent, which
|
|
151
|
+
// already treats `RFC NNNN` as first-class in withdraw/requalify).
|
|
152
|
+
const ID = /(arxiv:\s*\d{4}\.\d{4,5}|10\.\d{4,9}\/\S+|https?:\/\/\S+|\brfc[\s/-]?\d{3,5}\b)/i;
|
|
139
153
|
const PLACEHOLDER = /arXiv:_{2,}|<finding>|<authors>|<year>|<implication>/i;
|
|
140
154
|
const BANNED = /\b(studies show|research suggests|it'?s well[- ]established|well[- ]established that)\b/i;
|
|
141
155
|
// An author cite: a capitalized name (Unicode-aware, so "Buçinca" counts), optionally
|
|
142
156
|
// followed by "et al.", "&", "and", or further surnames, immediately before the year.
|
|
143
|
-
// Accepts "Huang et al. 2023", "Walters & Wilder 2023", "Panickssery, Bowman & Feng 2024"
|
|
157
|
+
// Accepts "Huang et al. 2023", "Walters & Wilder 2023", "Panickssery, Bowman & Feng 2024",
|
|
158
|
+
// and space-separated org authors ("OASIS CSAF Technical Committee 2022");
|
|
144
159
|
// flags an author-less finding like "**Foo.** 2024 (arXiv:…)".
|
|
145
|
-
|
|
160
|
+
// The inner group requires a non-empty separator per iteration (`,?\s+`, never the old
|
|
161
|
+
// empty-matchable `\s*,?\s*`) and is bounded ({0,24}), so it is linear-time — the previous
|
|
162
|
+
// form had catastrophic backtracking (ReDoS) on a long capitalized/`and`-joined run with no
|
|
163
|
+
// trailing year, hanging the CI-gating `lint` command.
|
|
164
|
+
const AUTHOR = /\p{Lu}[\p{L}.'’-]+(?:,?\s+(?:&|and|et al\.?|\p{Lu}[\p{L}.'’-]+)){0,24}\s+\(?(?:19|20)\d{2}/u;
|
|
165
|
+
|
|
166
|
+
// --- strict mode: Step-5 connection / orphan-citation check (opt-in --strict) --------------
|
|
167
|
+
// Step 5 requires each finding to inform a design choice — "citations without a connection are
|
|
168
|
+
// noise" (PROTOCOL.md). This makes the protocol's one otherwise-unexecutable failure mode ("orphan
|
|
169
|
+
// citation") deterministic: a Step-3 finding whose number OR first-author token is never referenced
|
|
170
|
+
// in the "Step 5 / Architecture" section is flagged. Deliberately LIBERAL about what counts as a
|
|
171
|
+
// reference — a missed orphan is safer than a false orphan — so it never fails a dispatch that
|
|
172
|
+
// connected a finding in any reasonable numeric ("(findings 1, 3)") or prose ("Kim 2025") form.
|
|
173
|
+
const escRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
174
|
+
// The first-author surname token of a finding: the first Capitalized word after the bold **title**.
|
|
175
|
+
function authorTokenOf(findingText) {
|
|
176
|
+
const afterBold = String(findingText).replace(/^\s*\d+\.\s*/, '').replace(/^\s*\*\*[^*]*\*\*/, '');
|
|
177
|
+
const m = afterBold.match(/\p{Lu}[\p{L}.'’-]+/u);
|
|
178
|
+
return m ? m[0] : null;
|
|
179
|
+
}
|
|
180
|
+
// Finding numbers referenced in a Step-5 body: any "#N", plus every integer following the word
|
|
181
|
+
// "finding"/"findings" (covers "(findings 1, 3 and 5)"). Liberal.
|
|
182
|
+
function referencedNumbers(body) {
|
|
183
|
+
const nums = new Set();
|
|
184
|
+
for (const m of body.matchAll(/#\s*(\d+)/g)) nums.add(Number(m[1]));
|
|
185
|
+
for (const m of body.matchAll(/\bfindings?\b[\s:#-]*((?:\d+[\s,#&-]*(?:and\s+)?)+)/gi)) {
|
|
186
|
+
for (const d of m[1].match(/\d+/g) || []) nums.add(Number(d));
|
|
187
|
+
}
|
|
188
|
+
return nums;
|
|
189
|
+
}
|
|
190
|
+
// The Step-5 / Architecture section body (last matching heading → next heading/EOF), or null.
|
|
191
|
+
function step5Body(lines) {
|
|
192
|
+
let s = -1;
|
|
193
|
+
for (let i = 0; i < lines.length; i++) {
|
|
194
|
+
const h = lines[i].match(/^#{1,6}\s+(.*?)\s*$/);
|
|
195
|
+
if (h && /(step\s*5|architecture)/i.test(h[1])) s = i;
|
|
196
|
+
}
|
|
197
|
+
if (s === -1) return null;
|
|
198
|
+
let e = lines.length;
|
|
199
|
+
for (let i = s + 1; i < lines.length; i++) { if (/^#{1,6}\s/.test(lines[i])) { e = i; break; } }
|
|
200
|
+
return lines.slice(s + 1, e).join('\n');
|
|
201
|
+
}
|
|
146
202
|
|
|
147
|
-
// Check one dispatch's text. Returns a structured result; never exits.
|
|
148
|
-
|
|
203
|
+
// Check one dispatch's text. Returns a structured result; never exits. `strict` adds the Step-5
|
|
204
|
+
// orphan-citation check (opt-in, so the default CI gate stays stable).
|
|
205
|
+
function lintText(label, raw, strict) {
|
|
149
206
|
const lines = raw.split(/\r?\n/);
|
|
150
207
|
const problems = []; // { finding, line, rule, message }
|
|
151
208
|
const add = (rule, message, line = null, finding = null) => problems.push({ finding, line, rule, message });
|
|
@@ -186,13 +243,15 @@ function lintText(label, raw) {
|
|
|
186
243
|
findings.forEach((f, i) => {
|
|
187
244
|
const n = i + 1;
|
|
188
245
|
if (PLACEHOLDER.test(f.text)) add('placeholder', `finding ${n}: still has template placeholders — fill it in.`, f.line, n);
|
|
189
|
-
// Strip identifiers before the year check so
|
|
190
|
-
// (e.g. 2402 in arXiv:2402.01817)
|
|
191
|
-
|
|
246
|
+
// Strip identifiers before the year check so digits inside a citation can't masquerade
|
|
247
|
+
// as a publication year: an arXiv id's YYMM prefix (e.g. 2402 in arXiv:2402.01817), a DOI,
|
|
248
|
+
// or a year-like URL path segment (e.g. /2024/ in https://host/2024/paper). URLs are
|
|
249
|
+
// stripped first so a DOI-bearing URL is removed whole.
|
|
250
|
+
const fNoIds = f.text.replace(/https?:\/\/\S+/gi, '').replace(/arxiv:\s*\d{4}\.\d{4,5}/gi, '').replace(/10\.\d{4,9}\/\S+/g, '');
|
|
192
251
|
if (!YEAR.test(fNoIds)) add('missing-year', `finding ${n}: missing a year (spell it out, e.g. "2024" — an arXiv id alone is not a year).`, f.line, n);
|
|
193
252
|
if (!AUTHOR.test(f.text)) add('missing-author', `finding ${n}: missing an author before the year (e.g. "Huang et al. 2023").`, f.line, n);
|
|
194
253
|
const idm = f.text.match(ID);
|
|
195
|
-
if (!idm) add('missing-id', `finding ${n}: missing an identifier (arXiv:NNNN.NNNNN, DOI, or
|
|
254
|
+
if (!idm) add('missing-id', `finding ${n}: missing an identifier (arXiv:NNNN.NNNNN, DOI, URL, or RFC number).`, f.line, n);
|
|
196
255
|
const ym = fNoIds.match(YEAR);
|
|
197
256
|
const ident = idm ? idm[0].replace(/\s+/g, '').replace(/[).,;]+$/, '') : null;
|
|
198
257
|
parsed.push({ finding: n, year: ym ? ym[0] : null, identifier: ident });
|
|
@@ -208,30 +267,69 @@ function lintText(label, raw) {
|
|
|
208
267
|
}
|
|
209
268
|
});
|
|
210
269
|
|
|
270
|
+
// --strict: flag orphan citations — a finding no Step-5 choice connects to (Step 5 / orphan-citation).
|
|
271
|
+
if (strict) {
|
|
272
|
+
const body = step5Body(lines);
|
|
273
|
+
if (body === null) {
|
|
274
|
+
add('no-step5', 'strict: no "Step 5" / "Architecture" section found to check finding connections against.');
|
|
275
|
+
} else {
|
|
276
|
+
const refs = referencedNumbers(body);
|
|
277
|
+
findings.forEach((f, i) => {
|
|
278
|
+
const n = i + 1;
|
|
279
|
+
const tok = authorTokenOf(f.text);
|
|
280
|
+
// Match the author token OR (for a hyphenated surname like "Garcia-Molina") its first
|
|
281
|
+
// component, so a Step-5 reference to just "Garcia" still connects — the liberal direction.
|
|
282
|
+
const alts = tok ? [...new Set([tok, tok.split('-')[0]])].filter((t) => t.length >= 2) : [];
|
|
283
|
+
const tokHit = alts.some((t) => new RegExp('(?<![\\p{L}])' + escRe(t) + '(?![\\p{L}])', 'iu').test(body));
|
|
284
|
+
if (!refs.has(n) && !tokHit) {
|
|
285
|
+
add('orphan-citation', `finding ${n}: no Step-5 choice references it (by number or author) — a citation without a connection is noise (Step 5).`, f.line, n);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
211
291
|
return { file: label, ok: problems.length === 0, findingCount: findings.length, problems, findings: parsed };
|
|
212
292
|
}
|
|
213
293
|
|
|
214
|
-
// Recursively collect
|
|
215
|
-
|
|
294
|
+
// Recursively collect files whose name matches `re` under `dir`, sorted for determinism.
|
|
295
|
+
// Resilient by design:
|
|
296
|
+
// - an unreadable directory (EACCES/EPERM/ENOTDIR) is SKIPPED with a stderr note, never fatal, so a
|
|
297
|
+
// single bad node does not abort a whole-corpus lint / withdraw / requalify (PH-02);
|
|
298
|
+
// - symlinked entries are not followed, and a realpath `seen` set breaks directory-junction cycles
|
|
299
|
+
// (common on Windows) that would otherwise recurse until stack / path-length exhaustion (PH-03).
|
|
300
|
+
// Skips node_modules/.git.
|
|
301
|
+
function walkFiles(dir, re, seen) {
|
|
302
|
+
seen = seen || new Set();
|
|
303
|
+
let real; try { real = realpathSync(dir); } catch { real = dir; }
|
|
304
|
+
if (seen.has(real)) return []; // already visited via another path — junction/symlink cycle guard
|
|
305
|
+
seen.add(real);
|
|
306
|
+
let entries;
|
|
307
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); }
|
|
308
|
+
catch (err) { process.stderr.write(`study-swarm: skipping ${dir}: ${err && err.code ? err.code : err.message}\n`); return []; }
|
|
216
309
|
const out = [];
|
|
217
|
-
for (const entry of
|
|
310
|
+
for (const entry of entries) {
|
|
218
311
|
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
312
|
+
if (entry.isSymbolicLink()) continue; // don't follow symlinks (cycle + directory-escape safety)
|
|
219
313
|
const full = join(dir, entry.name);
|
|
220
|
-
if (entry.isDirectory()) out.push(...
|
|
221
|
-
else if (
|
|
314
|
+
if (entry.isDirectory()) out.push(...walkFiles(full, re, seen));
|
|
315
|
+
else if (re.test(entry.name)) out.push(full);
|
|
222
316
|
}
|
|
223
317
|
return out.sort();
|
|
224
318
|
}
|
|
319
|
+
function walkDispatches(dir) { return walkFiles(dir, /\.dispatch\.md$/i); }
|
|
225
320
|
|
|
226
321
|
function readTarget(p) {
|
|
227
322
|
try { return { label: p, raw: readFileSync(p, 'utf8') }; }
|
|
228
323
|
catch (err) { fail(2, `cannot read ${p}: ${err && err.code ? err.code : err.message}`); }
|
|
229
324
|
}
|
|
230
325
|
|
|
326
|
+
const LINT_SCHEMA = 'study-swarm.lint/v1'; // versioned handle for --json consumers (FG-03)
|
|
327
|
+
|
|
231
328
|
function cmdLint(args) {
|
|
232
329
|
const json = args.includes('--json');
|
|
233
|
-
const
|
|
234
|
-
|
|
330
|
+
const strict = args.includes('--strict'); // opt-in: also flag Step-5 orphan citations (FG-01)
|
|
331
|
+
const paths = args.filter((a) => a !== '--json' && a !== '--strict');
|
|
332
|
+
if (paths.length === 0) fail(2, 'usage: study-swarm lint [--json] [--strict] <file|dir|-> [more...]');
|
|
235
333
|
|
|
236
334
|
const targets = [];
|
|
237
335
|
for (const p of paths) {
|
|
@@ -252,20 +350,23 @@ function cmdLint(args) {
|
|
|
252
350
|
}
|
|
253
351
|
}
|
|
254
352
|
|
|
255
|
-
const results = targets.map((t) => lintText(t.label, t.raw));
|
|
353
|
+
const results = targets.map((t) => lintText(t.label, t.raw, strict));
|
|
256
354
|
const anyFail = results.some((r) => !r.ok);
|
|
257
355
|
|
|
258
356
|
if (json) {
|
|
259
|
-
|
|
357
|
+
// A versioned envelope so a CI/roleos consumer can detect a shape change (FG-03), matching the
|
|
358
|
+
// schema/version pattern the lock, sidecar, and receipt objects already carry.
|
|
359
|
+
const meta = { schema: LINT_SCHEMA, study_swarm_version: VERSION };
|
|
360
|
+
const payload = results.length === 1 ? { ...meta, ...results[0] } : { ...meta, ok: !anyFail, files: results };
|
|
260
361
|
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
261
362
|
process.exit(anyFail ? 1 : 0);
|
|
262
363
|
}
|
|
263
364
|
|
|
264
365
|
for (const r of results) {
|
|
265
366
|
if (r.ok) {
|
|
266
|
-
process.stdout.write(`ok ${r.file}: ${r.findingCount} finding(s), all sourced.\n`);
|
|
367
|
+
process.stdout.write(`ok ${r.file}: ${r.findingCount} finding(s), all sourced${strict ? ' and connected' : ''}.\n`);
|
|
267
368
|
} else {
|
|
268
|
-
process.stderr.write(`x ${r.file}: ${r.problems.length} sourcing issue(s)\n`);
|
|
369
|
+
process.stderr.write(`x ${r.file}: ${r.problems.length} ${strict ? 'issue(s)' : 'sourcing issue(s)'}\n`);
|
|
269
370
|
for (const pr of r.problems) process.stderr.write(` - ${pr.message}\n`);
|
|
270
371
|
}
|
|
271
372
|
}
|
|
@@ -274,6 +375,9 @@ function cmdLint(args) {
|
|
|
274
375
|
`\nStep 3 (sourcing FORM) is satisfied — this does NOT confirm the citations exist or support the claim.\n` +
|
|
275
376
|
`Run Step 4 (existence + groundedness, a different model family): roleos verify-citations <file>\n`,
|
|
276
377
|
);
|
|
378
|
+
} else {
|
|
379
|
+
// Symmetry with the clean-path nudge: tell the user what to do next (H1).
|
|
380
|
+
process.stderr.write(`\nFix the issue(s) above, then re-run study-swarm lint. (This checks Step 3 sourcing FORM${strict ? ' + Step 5 connections' : ''} only.)\n`);
|
|
277
381
|
}
|
|
278
382
|
process.exit(anyFail ? 1 : 0);
|
|
279
383
|
}
|
|
@@ -284,17 +388,26 @@ function cmdLint(args) {
|
|
|
284
388
|
// (resolved models + byte-exact prompts + tool schemas + verifier receipt); the CLI only
|
|
285
389
|
// canonicalizes + hashes + validates it. No network, no model calls (L2).
|
|
286
390
|
|
|
287
|
-
const LOCK_SCHEMA = 'dispatch.lock/
|
|
391
|
+
const LOCK_SCHEMA = 'dispatch.lock/v2';
|
|
288
392
|
|
|
289
393
|
// Self-describing digest "sha256-<base64>" — the W3C Subresource Integrity form: algorithm-
|
|
290
394
|
// prefixed (so it's algorithm-agile) and used fail-closed on mismatch (L9; lock dispatch finding 38).
|
|
291
395
|
function sriBytes(buf) { return 'sha256-' + createHash('sha256').update(buf).digest('base64'); }
|
|
396
|
+
// Domain-separation tags (v2): a TEXT preimage and a structured-JSON (JCS) preimage are hashed in
|
|
397
|
+
// DISJOINT spaces, so a prompt whose literal text happens to equal some tool schema's canonical JSON
|
|
398
|
+
// can never produce the same digest as that schema (the tagged-hash / DSSE "hash known bytes with a
|
|
399
|
+
// context" rule the lock dispatch cites — TUF/Rekor/CT). Without a tag, jcsDigest({}) === sriText('{}').
|
|
400
|
+
// The tag carries the schema major, so bumping it is itself a lock-format change — hence the v1 -> v2
|
|
401
|
+
// bump on LOCK_SCHEMA / WITHDRAWN_SCHEMA / RECEIPT_SCHEMA, and the schema gate on read that turns a
|
|
402
|
+
// stale-format lock into a clear "regenerate" message instead of a confusing hash mismatch.
|
|
403
|
+
const DOMAIN_TEXT = 'study-swarm/v2/text\n';
|
|
404
|
+
const DOMAIN_JCS = 'study-swarm/v2/jcs\n';
|
|
292
405
|
// Normalize TEXT before hashing so the same content hashes identically across platforms — strip a
|
|
293
406
|
// BOM, fold CRLF/CR -> LF, NFC-normalize. Without this, a CRLF working tree (Windows) and an LF
|
|
294
407
|
// checkout (git/CI) produce different hashes — the exact cross-platform drift our Q2 findings warn
|
|
295
408
|
// about (RFC 8259 BOM, UAX #15 NFC, and CRLF/LF). Applied to every text input that gets hashed.
|
|
296
409
|
function normText(s) { s = String(s); if (s.charCodeAt(0) === 0xFEFF) s = s.slice(1); return s.replace(/\r\n?/g, '\n').normalize('NFC'); }
|
|
297
|
-
function sriText(str) { return sriBytes(Buffer.from(normText(str), 'utf8')); }
|
|
410
|
+
function sriText(str) { return sriBytes(Buffer.from(DOMAIN_TEXT + normText(str), 'utf8')); }
|
|
298
411
|
|
|
299
412
|
// RFC 8785 (JCS) canonical JSON, for the structured JSON the CLI assembles ITSELF (the tool
|
|
300
413
|
// surface and the lock body): NFC-normalize strings, sort object keys by UTF-16 code unit (JS
|
|
@@ -321,7 +434,7 @@ function jcs(value) {
|
|
|
321
434
|
};
|
|
322
435
|
return ser(value);
|
|
323
436
|
}
|
|
324
|
-
function jcsDigest(value) { return sriBytes(Buffer.from(jcs(value), 'utf8')); }
|
|
437
|
+
function jcsDigest(value) { return sriBytes(Buffer.from(DOMAIN_JCS + jcs(value), 'utf8')); }
|
|
325
438
|
|
|
326
439
|
// The lock sits beside its dispatch: <dir>/<stem>.lock.json (stem strips a trailing .dispatch.md).
|
|
327
440
|
function lockPathFor(dispatch) {
|
|
@@ -351,8 +464,14 @@ function buildLockObject(dispatchPath, orchestration) {
|
|
|
351
464
|
if (s.params && typeof s.params === 'object') rec.params = s.params;
|
|
352
465
|
// L7 — output hash for DRIFT DETECTION only (not determinism). The harness may ship the raw
|
|
353
466
|
// output (the CLI hashes it) OR a pre-computed output_sha256 (large outputs needn't be shipped).
|
|
354
|
-
|
|
355
|
-
|
|
467
|
+
// A caller-supplied digest is validated to the SRI sha256- shape here, so a malformed hash is
|
|
468
|
+
// rejected where it enters rather than mis-surfacing as "drift" on a later verify (PH-05).
|
|
469
|
+
if (typeof s.output_sha256 === 'string') {
|
|
470
|
+
if (!/^sha256-[A-Za-z0-9+/]+=*$/.test(s.output_sha256)) {
|
|
471
|
+
fail(2, `orchestration step ${i + 1} output_sha256 is not an "sha256-<base64>" digest: "${s.output_sha256}"`);
|
|
472
|
+
}
|
|
473
|
+
rec.output_sha256 = s.output_sha256;
|
|
474
|
+
} else if (s.output !== undefined) rec.output_sha256 = typeof s.output === 'string' ? sriText(s.output) : jcsDigest(s.output);
|
|
356
475
|
return rec;
|
|
357
476
|
});
|
|
358
477
|
const lock = {
|
|
@@ -372,12 +491,27 @@ function buildLockObject(dispatchPath, orchestration) {
|
|
|
372
491
|
return lock;
|
|
373
492
|
}
|
|
374
493
|
|
|
494
|
+
// A forward-compat guard (PH-04): an artifact whose `schema` string names a version this CLI does
|
|
495
|
+
// not write is reported as "regenerate", not as a confusing hash/integrity mismatch — because the
|
|
496
|
+
// hash preimage (the domain tag) changes with the schema major, a v1 artifact read by a v2 CLI would
|
|
497
|
+
// otherwise fail self-integrity with a misleading "the body was edited" accusation. Absent schema =
|
|
498
|
+
// no gate (the self-integrity check still applies). Returns a message string, or null when in-version.
|
|
499
|
+
function staleSchema(stored, expected, kind, path) {
|
|
500
|
+
if (stored && typeof stored === 'object' && typeof stored.schema === 'string' && stored.schema !== expected) {
|
|
501
|
+
return `${path}: ${kind} is schema "${stored.schema}"; this study-swarm v${VERSION} understands "${expected}" — regenerate it (the hash format changed between schema versions).`;
|
|
502
|
+
}
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
|
|
375
506
|
// Verify a lock: self-integrity always; source-drift too when an orchestration record is supplied.
|
|
376
507
|
// Strict-match, fail-closed (L8): returns a list of problems (empty = clean).
|
|
377
508
|
function verifyLockObject(dispatchPath, lockPath, orchestration) {
|
|
378
509
|
let stored;
|
|
379
510
|
try { stored = JSON.parse(readFileSync(lockPath, 'utf8')); }
|
|
380
511
|
catch (err) { fail(2, `cannot read lock ${lockPath}: ${err && err.code ? err.code : err.message}`); }
|
|
512
|
+
// 0) Stale-format gate — a wrong-schema lock is "regenerate", not a hash mismatch (PH-04).
|
|
513
|
+
const stale = staleSchema(stored, LOCK_SCHEMA, 'lock', lockPath);
|
|
514
|
+
if (stale) return [stale];
|
|
381
515
|
const problems = [];
|
|
382
516
|
// 1) Self-integrity — recompute lock_sha256 over the stored body (detects a hand-edited lock).
|
|
383
517
|
if (!stored || typeof stored !== 'object' || typeof stored.lock_sha256 !== 'string') {
|
|
@@ -408,7 +542,46 @@ function verifyLockObject(dispatchPath, lockPath, orchestration) {
|
|
|
408
542
|
return problems;
|
|
409
543
|
}
|
|
410
544
|
|
|
545
|
+
// FG-05 — scaffold the orchestration.json the harness must supply, mirroring what `new` does for a
|
|
546
|
+
// dispatch. A deterministic Write of a template (no network, no models); refuses to overwrite. The
|
|
547
|
+
// orchestration record is the harder artifact to hand-author, and its shape was documented only in
|
|
548
|
+
// the large worked examples — this gives a fill-in-the-blanks starting point.
|
|
549
|
+
const orchTemplate = () => JSON.stringify({
|
|
550
|
+
_note: 'study-swarm orchestration record — the harness-emitted input to `study-swarm lock <dispatch> --from <this file>`. One steps[] entry per Step-2 research agent; replace every <...> placeholder. A full worked record: examples/study-swarm-lock.orchestration.json. Optional per step: params, schema_dialect, output_sha256 (an "sha256-<base64>" digest for drift detection).',
|
|
551
|
+
steps: [
|
|
552
|
+
{
|
|
553
|
+
question_id: '<Q1-short-slug>',
|
|
554
|
+
resolved_model: '<resolved model id, e.g. claude-opus-4-8 — never a floating alias>',
|
|
555
|
+
prompt: '<the byte-exact prompt string this research agent was given>',
|
|
556
|
+
tool_schema: { type: 'object', properties: {} },
|
|
557
|
+
schema_dialect: 'https://json-schema.org/draft/2020-12/schema',
|
|
558
|
+
},
|
|
559
|
+
],
|
|
560
|
+
verification: {
|
|
561
|
+
runner: 'roleos verify-citations',
|
|
562
|
+
tool: 'prism verify --type citations',
|
|
563
|
+
verifier_family: '<a DIFFERENT model family than the synthesizer>',
|
|
564
|
+
receipt_id: '<prism-receipt-id>',
|
|
565
|
+
receipt_chain_sha256: '<the verifier receipt chain hash>',
|
|
566
|
+
},
|
|
567
|
+
}, null, 2) + '\n';
|
|
568
|
+
|
|
569
|
+
function orchPathFor(dispatch) {
|
|
570
|
+
const base = dispatch.split(/[\\/]/).pop().replace(/(\.dispatch)?\.md$/i, '');
|
|
571
|
+
return join(dirname(dispatch), `${base}.orchestration.json`);
|
|
572
|
+
}
|
|
573
|
+
|
|
411
574
|
function cmdLock(args) {
|
|
575
|
+
if (args.includes('--init')) { // FG-05 — scaffold the orchestration record for a dispatch
|
|
576
|
+
const dispatch = args.filter((a) => a !== '--init')[0];
|
|
577
|
+
if (!dispatch) fail(2, 'usage: study-swarm lock --init <dispatch>');
|
|
578
|
+
if (!existsSync(dispatch)) fail(2, `dispatch not found: ${dispatch}`);
|
|
579
|
+
const out = orchPathFor(dispatch);
|
|
580
|
+
if (existsSync(out)) fail(2, `refusing to overwrite existing ${out}`);
|
|
581
|
+
writeFileSync(out, orchTemplate(), 'utf8');
|
|
582
|
+
process.stdout.write(`Created ${out}\nFill in each step (one per Step-2 research agent), then: study-swarm lock ${dispatch} --from ${out}\n`);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
412
585
|
const verify = args.includes('--verify');
|
|
413
586
|
const rest = args.filter((a) => a !== '--verify');
|
|
414
587
|
let orchPath = null;
|
|
@@ -460,8 +633,8 @@ function cmdLock(args) {
|
|
|
460
633
|
// receipts deterministically (file reads, JSON I/O, SHA-256); the actual re-verification of a
|
|
461
634
|
// re-grounded finding defers to the sibling runner (C12, honest ceiling). No network, no models.
|
|
462
635
|
|
|
463
|
-
const WITHDRAWN_SCHEMA = 'dispatch.withdrawn/
|
|
464
|
-
const RECEIPT_SCHEMA = 'withdrawal-receipt/
|
|
636
|
+
const WITHDRAWN_SCHEMA = 'dispatch.withdrawn/v2';
|
|
637
|
+
const RECEIPT_SCHEMA = 'withdrawal-receipt/v2';
|
|
465
638
|
// A CLOSED, machine-readable reason enum — never free text (C3; OpenVEX/CSAF/CycloneDX: a status
|
|
466
639
|
// must carry a structured justification, a bare flag is non-conformant).
|
|
467
640
|
const WITHDRAW_REASONS = ['fabricated', 'misattributed', 'retracted', 'verifier-flipped', 'other'];
|
|
@@ -489,17 +662,8 @@ function withdrawnPathFor(dispatch) {
|
|
|
489
662
|
return join(dirname(dispatch), `${base}.withdrawn.json`);
|
|
490
663
|
}
|
|
491
664
|
|
|
492
|
-
// Recursively collect files matching a regex (
|
|
493
|
-
function walkByExt(dir, re) {
|
|
494
|
-
const out = [];
|
|
495
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
496
|
-
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
497
|
-
const full = join(dir, entry.name);
|
|
498
|
-
if (entry.isDirectory()) out.push(...walkByExt(full, re));
|
|
499
|
-
else if (re.test(entry.name)) out.push(full);
|
|
500
|
-
}
|
|
501
|
-
return out.sort();
|
|
502
|
-
}
|
|
665
|
+
// Recursively collect files matching a regex (delegates to the resilient shared walker).
|
|
666
|
+
function walkByExt(dir, re) { return walkFiles(dir, re); }
|
|
503
667
|
|
|
504
668
|
// The finding numbers in one dispatch whose citation normalizes to `want` (reuses the lint parser,
|
|
505
669
|
// so Step 3 and the compensator agree on what a citation is).
|
|
@@ -578,14 +742,19 @@ function cmdWithdraw(args) {
|
|
|
578
742
|
const detail = f.detail ? String(f.detail) : '';
|
|
579
743
|
|
|
580
744
|
const deps = findDependents(corpus, identifier);
|
|
581
|
-
if (deps.length === 0) fail(2, `no dispatch in ${corpus} cites ${identifier} (normalized: ${want}) — nothing to withdraw
|
|
745
|
+
if (deps.length === 0) fail(2, `no dispatch in ${corpus} cites ${identifier} (normalized: ${want}) — nothing to withdraw. Check the identifier spelling and the --from directory; "study-swarm lint ${corpus}" lists the citations the tool can see.`);
|
|
582
746
|
|
|
583
747
|
const dependents = [];
|
|
584
748
|
for (const d of deps) {
|
|
585
749
|
const body = loadSidecar(d.path);
|
|
586
750
|
const existing = body.withdrawals.find((w) => w.identifier === want);
|
|
587
|
-
// Idempotent: an identical withdrawal (same id + reason + detail
|
|
588
|
-
|
|
751
|
+
// Idempotent: an identical withdrawal (same id + reason + detail + finding numbers, still
|
|
752
|
+
// withdrawn) is a no-op. The findings array is part of the identity so that re-withdrawing
|
|
753
|
+
// after the dispatch was edited (a citation moved to a different finding #) refreshes the
|
|
754
|
+
// stale numbers instead of being skipped as "identical".
|
|
755
|
+
const sameFindings = existing && Array.isArray(existing.findings) &&
|
|
756
|
+
existing.findings.length === d.findings.length && existing.findings.every((v, i) => v === d.findings[i]);
|
|
757
|
+
const identical = existing && existing.status === 'withdrawn' && existing.reason === String(f.reason) && (existing.detail || '') === detail && sameFindings;
|
|
589
758
|
if (!identical) {
|
|
590
759
|
if (existing) {
|
|
591
760
|
existing.reason = String(f.reason); existing.detail = detail; existing.status = 'withdrawn'; existing.resolution = null; existing.findings = d.findings;
|
|
@@ -622,14 +791,67 @@ function cmdWithdraw(args) {
|
|
|
622
791
|
process.stdout.write(
|
|
623
792
|
`\nYou may have relied on this finding. Each flagged dispatch now HALTS "study-swarm requalify --check"\n` +
|
|
624
793
|
`until the finding is removed or re-grounded — re-ground or override.\n` +
|
|
625
|
-
|
|
794
|
+
`${f.receipt ? `Receipt written to ${String(f.receipt)}` : 'No receipt file written — re-run with --receipt <path> or --json to capture it'} — receipt_sha256 ${receipt.receipt_sha256}\n`);
|
|
626
795
|
process.exit(0);
|
|
627
796
|
}
|
|
628
797
|
|
|
629
798
|
function cmdRequalify(args) {
|
|
630
799
|
if (args.includes('--check')) return requalifyCheck(args.filter((a) => a !== '--check'));
|
|
631
800
|
if (args.includes('--resolve')) return requalifyResolve(args.filter((a) => a !== '--resolve'));
|
|
632
|
-
|
|
801
|
+
if (args.includes('--status')) return requalifyStatus(args.filter((a) => a !== '--status'));
|
|
802
|
+
fail(2, 'usage: study-swarm requalify --check <corpus-dir> | study-swarm requalify --status <corpus-dir> [--json] | study-swarm requalify --resolve <dispatch> <identifier> --mode removed|regrounded [--note <text>]');
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const STATUS_SCHEMA = 'study-swarm.status/v1';
|
|
806
|
+
|
|
807
|
+
// FG-04 — the read-only evidence-health VIEW of a corpus (distinct from --check, the CI gate). Walks
|
|
808
|
+
// the .withdrawn.json sidecars and aggregates: withdrawn vs resolved counts, a breakdown by reason
|
|
809
|
+
// and by resolution mode, and a per-dispatch line. Informational — it never fails closed (exit 0),
|
|
810
|
+
// so it composes in a report without gating a build the way --check does.
|
|
811
|
+
function requalifyStatus(args) {
|
|
812
|
+
const f = parseFlags(args, new Set());
|
|
813
|
+
const corpus = f._[0];
|
|
814
|
+
if (!corpus) fail(2, 'usage: study-swarm requalify --status <corpus-dir> [--json]');
|
|
815
|
+
if (!existsSync(corpus)) fail(2, `corpus not found: ${corpus}`);
|
|
816
|
+
const sidecars = statSync(corpus).isDirectory() ? walkByExt(corpus, /\.withdrawn\.json$/i) : [corpus];
|
|
817
|
+
const totals = { withdrawn: 0, resolved: 0 };
|
|
818
|
+
const by_reason = {};
|
|
819
|
+
const by_mode = {};
|
|
820
|
+
const dispatches = [];
|
|
821
|
+
const problems = [];
|
|
822
|
+
for (const sc of sidecars) {
|
|
823
|
+
let stored;
|
|
824
|
+
try { stored = JSON.parse(readFileSync(sc, 'utf8')); }
|
|
825
|
+
catch (err) { problems.push(`${sc}: not valid JSON (${err.message})`); continue; }
|
|
826
|
+
if (!stored || typeof stored !== 'object' || Array.isArray(stored)) { problems.push(`${sc}: sidecar is not a JSON object`); continue; }
|
|
827
|
+
const entries = [];
|
|
828
|
+
for (const w of stored.withdrawals || []) {
|
|
829
|
+
const status = w.status === 'resolved' ? 'resolved' : 'withdrawn';
|
|
830
|
+
totals[status] += 1;
|
|
831
|
+
if (w.reason) by_reason[w.reason] = (by_reason[w.reason] || 0) + 1;
|
|
832
|
+
const mode = status === 'resolved' && w.resolution ? w.resolution.mode : null;
|
|
833
|
+
if (mode) by_mode[mode] = (by_mode[mode] || 0) + 1;
|
|
834
|
+
entries.push({ identifier: w.identifier, reason: w.reason, status, mode, findings: w.findings || [] });
|
|
835
|
+
}
|
|
836
|
+
dispatches.push({ dispatch: stored.dispatch, sidecar: sc.split(/[\\/]/).pop(), withdrawals: entries });
|
|
837
|
+
}
|
|
838
|
+
const kv = (o) => Object.keys(o).sort().map((k) => `${k}=${o[k]}`).join(', ') || '(none)';
|
|
839
|
+
if (f.json) {
|
|
840
|
+
process.stdout.write(JSON.stringify({ schema: STATUS_SCHEMA, study_swarm_version: VERSION, corpus, totals, by_reason, by_mode, dispatches, problems }) + '\n');
|
|
841
|
+
process.exit(0);
|
|
842
|
+
}
|
|
843
|
+
process.stdout.write(`study-swarm requalify --status ${corpus}: ${dispatches.length} sidecar(s), ${totals.withdrawn + totals.resolved} withdrawal(s)\n`);
|
|
844
|
+
process.stdout.write(` ${totals.withdrawn} unresolved (evidence-withdrawn), ${totals.resolved} resolved\n`);
|
|
845
|
+
process.stdout.write(` by reason: ${kv(by_reason)}\n`);
|
|
846
|
+
process.stdout.write(` by resolution mode: ${kv(by_mode)}\n`);
|
|
847
|
+
for (const d of dispatches) {
|
|
848
|
+
for (const w of d.withdrawals) {
|
|
849
|
+
const tag = w.status === 'resolved' ? `resolved: ${w.mode}` : 'withdrawn';
|
|
850
|
+
process.stdout.write(` - ${d.dispatch}: ${w.identifier} (${w.reason}) [${tag}] findings ${(w.findings || []).map((n) => '#' + n).join(', ')}\n`);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
for (const p of problems) process.stderr.write(` ! ${p}\n`);
|
|
854
|
+
process.exit(0);
|
|
633
855
|
}
|
|
634
856
|
|
|
635
857
|
function requalifyCheck(args) {
|
|
@@ -645,6 +867,14 @@ function requalifyCheck(args) {
|
|
|
645
867
|
let stored;
|
|
646
868
|
try { stored = JSON.parse(readFileSync(sc, 'utf8')); }
|
|
647
869
|
catch (err) { problems.push(`${sc}: not valid JSON (${err.message})`); continue; }
|
|
870
|
+
// A non-object sidecar (null / array / scalar) is a reportable problem, not a crash — the per-file
|
|
871
|
+
// loop keeps checking the rest of the corpus and the offending file is named (PH-01).
|
|
872
|
+
if (!stored || typeof stored !== 'object' || Array.isArray(stored)) {
|
|
873
|
+
problems.push(`${sc}: sidecar is not a JSON object`); continue;
|
|
874
|
+
}
|
|
875
|
+
// Stale-format gate — a wrong-schema sidecar is "regenerate", not a self-integrity failure (PH-04).
|
|
876
|
+
const stale = staleSchema(stored, WITHDRAWN_SCHEMA, 'sidecar', sc);
|
|
877
|
+
if (stale) { problems.push(stale); continue; }
|
|
648
878
|
// Self-integrity: a hand-edited sidecar (e.g. a status forged to "resolved") fails closed.
|
|
649
879
|
if (typeof stored.withdrawn_sha256 !== 'string' || stored.withdrawn_sha256 !== withSha(stored, 'withdrawn_sha256').withdrawn_sha256) {
|
|
650
880
|
problems.push(`${sc}: withdrawn_sha256 self-integrity mismatch (the sidecar was hand-edited)`);
|
|
@@ -692,7 +922,7 @@ function requalifyResolve(args) {
|
|
|
692
922
|
}
|
|
693
923
|
if (mode === 'removed') {
|
|
694
924
|
const still = findingsCiting(dispatch, want);
|
|
695
|
-
if (still.length) fail(1, `${dispatch} still cites ${want} (findings ${still.map((n) => '#' + n).join(', ')}) — cannot resolve --mode removed
|
|
925
|
+
if (still.length) fail(1, `${dispatch} still cites ${want} (findings ${still.map((n) => '#' + n).join(', ')}) — cannot resolve --mode removed while the citation is present.\n Two ways forward:\n • remove the citation from the dispatch, then re-run --mode removed; or\n • if it was re-verified in place, use --mode regrounded --note "<attestation>".`);
|
|
696
926
|
} else if (mode === 'regrounded' && !f.note) {
|
|
697
927
|
fail(2, '--mode regrounded requires --note <attestation> — the CLI records that the sibling runner re-verified the finding, it does not itself re-verify');
|
|
698
928
|
}
|
|
@@ -1,63 +1,63 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schema": "dispatch.lock/
|
|
3
|
-
"study_swarm_version": "
|
|
4
|
-
"protocol_sha256": "sha256-
|
|
5
|
-
"dispatch_sha256": "sha256-
|
|
2
|
+
"schema": "dispatch.lock/v2",
|
|
3
|
+
"study_swarm_version": "2.0.0",
|
|
4
|
+
"protocol_sha256": "sha256-9tVI9RrD1qKHB/j/Ec9+//Q6y5coRtOvVcfDyiH46eQ=",
|
|
5
|
+
"dispatch_sha256": "sha256-O6rba+NrJhL4s6qSzaMPgQ5hhWB7mQWD1lEIQJjOJ9I=",
|
|
6
6
|
"steps": [
|
|
7
7
|
{
|
|
8
8
|
"question_id": "Q1-revocation-propagation",
|
|
9
9
|
"resolved_model": "claude-opus-4-8",
|
|
10
|
-
"prompt_sha256": "sha256-
|
|
11
|
-
"tool_schema_sha256": "sha256-
|
|
10
|
+
"prompt_sha256": "sha256-itBKUD1vfwbDaQFlItEbIrWRx4auszx3/MeDUggQRI4=",
|
|
11
|
+
"tool_schema_sha256": "sha256-rAn5uvRldR9ctw1Sb8ZkXIuVRzA8spJBRmFJqkAqUO0=",
|
|
12
12
|
"schema_dialect": "https://json-schema.org/draft/2020-12/schema",
|
|
13
13
|
"params": {
|
|
14
14
|
"effort": "high"
|
|
15
15
|
},
|
|
16
|
-
"output_sha256": "sha256-
|
|
16
|
+
"output_sha256": "sha256-nIWvzFM74zz5s6DddRBXeoZXUUrn5W5YB7L9symaFXo="
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"question_id": "Q2-status-propagation-states",
|
|
20
20
|
"resolved_model": "claude-opus-4-8",
|
|
21
|
-
"prompt_sha256": "sha256-
|
|
22
|
-
"tool_schema_sha256": "sha256-
|
|
21
|
+
"prompt_sha256": "sha256-rLMa1hQeYobjvEeCWjmhhwjx0ZV7A/JG4xN+Gn/Taus=",
|
|
22
|
+
"tool_schema_sha256": "sha256-rAn5uvRldR9ctw1Sb8ZkXIuVRzA8spJBRmFJqkAqUO0=",
|
|
23
23
|
"schema_dialect": "https://json-schema.org/draft/2020-12/schema",
|
|
24
24
|
"params": {
|
|
25
25
|
"effort": "high"
|
|
26
26
|
},
|
|
27
|
-
"output_sha256": "sha256-
|
|
27
|
+
"output_sha256": "sha256-PY9+TX8amO4HEnSj2m5667eCPzSxQoJLLlRifWNjSAE="
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"question_id": "Q3-scholarly-retraction",
|
|
31
31
|
"resolved_model": "claude-opus-4-8",
|
|
32
|
-
"prompt_sha256": "sha256-
|
|
33
|
-
"tool_schema_sha256": "sha256-
|
|
32
|
+
"prompt_sha256": "sha256-edU5+lj80xAt/lICOwERznjxy7Xavrt4zkRbHAL4lL8=",
|
|
33
|
+
"tool_schema_sha256": "sha256-rAn5uvRldR9ctw1Sb8ZkXIuVRzA8spJBRmFJqkAqUO0=",
|
|
34
34
|
"schema_dialect": "https://json-schema.org/draft/2020-12/schema",
|
|
35
35
|
"params": {
|
|
36
36
|
"effort": "high"
|
|
37
37
|
},
|
|
38
|
-
"output_sha256": "sha256-
|
|
38
|
+
"output_sha256": "sha256-ffW4pz8IyjoeRqgNHe6xTfnvo8zs9wKSCZJczG4qBTc="
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
"question_id": "Q4-sound-compensators",
|
|
42
42
|
"resolved_model": "claude-opus-4-8",
|
|
43
|
-
"prompt_sha256": "sha256-
|
|
44
|
-
"tool_schema_sha256": "sha256-
|
|
43
|
+
"prompt_sha256": "sha256-f2549e25ZsyA+QgebqdIeNsh3a+u/iAer+t439zh+J4=",
|
|
44
|
+
"tool_schema_sha256": "sha256-rAn5uvRldR9ctw1Sb8ZkXIuVRzA8spJBRmFJqkAqUO0=",
|
|
45
45
|
"schema_dialect": "https://json-schema.org/draft/2020-12/schema",
|
|
46
46
|
"params": {
|
|
47
47
|
"effort": "high"
|
|
48
48
|
},
|
|
49
|
-
"output_sha256": "sha256-
|
|
49
|
+
"output_sha256": "sha256-e8mm2j3uOX0pUvdgMpck8OMqtvs+NG3FuVMn4LMQhDU="
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
"question_id": "Q5-stale-tombstone-contrastive",
|
|
53
53
|
"resolved_model": "claude-opus-4-8",
|
|
54
|
-
"prompt_sha256": "sha256-
|
|
55
|
-
"tool_schema_sha256": "sha256-
|
|
54
|
+
"prompt_sha256": "sha256-Ml4IoYnSajRyAjaWyjrA44kdzpmDGbrSFGAPOfYUPVo=",
|
|
55
|
+
"tool_schema_sha256": "sha256-rAn5uvRldR9ctw1Sb8ZkXIuVRzA8spJBRmFJqkAqUO0=",
|
|
56
56
|
"schema_dialect": "https://json-schema.org/draft/2020-12/schema",
|
|
57
57
|
"params": {
|
|
58
58
|
"effort": "high"
|
|
59
59
|
},
|
|
60
|
-
"output_sha256": "sha256-
|
|
60
|
+
"output_sha256": "sha256-wpEjen2OeX44jBIkf50iRfY4VLQMWCcJyoiyBYq33lI="
|
|
61
61
|
}
|
|
62
62
|
],
|
|
63
63
|
"verification": {
|
|
@@ -74,5 +74,5 @@
|
|
|
74
74
|
"citations_sha256": "70a0d84650477090be49925bf4b53ab52b8fdb88ebf5d2487f3000ea96fc810a",
|
|
75
75
|
"receipt_chain_sha256": "dcd57fc2ec8d66992e9762e1e551aee75829dda2bf9cc0837bc3106f48cece94"
|
|
76
76
|
},
|
|
77
|
-
"lock_sha256": "sha256-
|
|
77
|
+
"lock_sha256": "sha256-soRuGNSctKGPLxebfbZtCx+nA/oWtDTeFaf0TRIwYhg="
|
|
78
78
|
}
|