@stravica-ai/digital-operator-pa 0.1.5 → 0.1.7

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 (33) hide show
  1. package/dist/engineering-pa-cli/package.json +2 -0
  2. package/dist/engineering-pa-cli/src/research/assemble.mjs +281 -0
  3. package/dist/engineering-pa-cli/src/research/briefs/deep-extractor.md +141 -0
  4. package/dist/engineering-pa-cli/src/research/briefs/deep-verifier.md +95 -0
  5. package/dist/engineering-pa-cli/src/research/briefs/judge.md +108 -0
  6. package/dist/engineering-pa-cli/src/research/briefs/verifier.md +17 -0
  7. package/dist/engineering-pa-cli/src/research/commands/show.mjs +10 -1
  8. package/dist/engineering-pa-cli/src/research/engine/agents.mjs +163 -0
  9. package/dist/engineering-pa-cli/src/research/engine/deep.mjs +513 -0
  10. package/dist/engineering-pa-cli/src/research/engine/deepen.mjs +97 -0
  11. package/dist/engineering-pa-cli/src/research/engine/finalise.mjs +121 -0
  12. package/dist/engineering-pa-cli/src/research/engine/io.mjs +60 -0
  13. package/dist/engineering-pa-cli/src/research/engine/plan.mjs +129 -0
  14. package/dist/engineering-pa-cli/src/research/engine/rank.mjs +80 -0
  15. package/dist/engineering-pa-cli/src/research/engine/verify.mjs +52 -0
  16. package/dist/engineering-pa-cli/src/research/schema/cost.schema.json +19 -0
  17. package/dist/engineering-pa-cli/src/research/schema/judge.schema.json +35 -0
  18. package/dist/engineering-pa-cli/src/research/schema/rounds.schema.json +34 -0
  19. package/dist/engineering-pa-cli/src/research/store/cost.mjs +41 -0
  20. package/dist/engineering-pa-cli/src/research/store/index.mjs +31 -0
  21. package/dist/engineering-pa-cli/src/research/store/judge.mjs +53 -0
  22. package/dist/engineering-pa-cli/src/research/store/rounds.mjs +61 -0
  23. package/dist/engineering-pa-cli/src/research/workflows/research-deep.js +1364 -0
  24. package/dist/engineering-pa-cli/src/research/workflows/research-normal.js +561 -327
  25. package/dist/engineering-pa-cli/src/research/wrappers/deep.mjs +146 -0
  26. package/dist/engineering-pa-cli/src/research/wrappers/normal.mjs +100 -0
  27. package/dist/engineering-pa-core/docs/commands/research.md +438 -0
  28. package/dist/engineering-pa-core/docs/entity-location-map.md +1 -0
  29. package/dist/engineering-pa-core/knowledge/knowledge-memory/knowledge-retrieval-discipline.md +27 -0
  30. package/dist/engineering-pa-core/knowledge/operator-interface/push-back-discipline.md +1 -1
  31. package/dist/engineering-pa-scaffold/templates/CLAUDE.md.template +1 -1
  32. package/dist/engineering-pa-workspace/app/index.html +4 -4
  33. package/package.json +1 -1
@@ -31,6 +31,8 @@
31
31
  "node": ">=24"
32
32
  },
33
33
  "scripts": {
34
+ "assemble": "node src/research/assemble.mjs",
35
+ "pretest": "node src/research/assemble.mjs --check",
34
36
  "test": "node --test --test-isolation=none 'test/**/*.test.mjs'"
35
37
  },
36
38
  "//": "test pins --test-isolation=none: default process-isolation mode silently undercounts (drops passed tests from totals) when a test awaits async work while stdout is patched by a capture() helper — see packages/cli/test/secrets. Counts must never lie."
@@ -0,0 +1,281 @@
1
+ // Zero-dependency assembler: wrappers/<mode>.mjs + engine/*.mjs → workflows/research-<mode>.js
2
+ //
3
+ // A wrapper is a real ES module (named imports from ../engine/*.mjs, one
4
+ // `export const meta = {…}` literal, and the run body). The assembler transitively
5
+ // closes the engine graph from the wrapper's imports, emits the reachable modules
6
+ // deps-first (Kahn, lexicographic tie-break), strips import/export syntax, and
7
+ // prepends the meta literal + a provenance header. Output is a single self-contained
8
+ // script the harness can run: plain JS, `export const meta` the sole export, zero
9
+ // imports, no Node APIs, no clock/randomness.
10
+ //
11
+ // Determinism: no clock, no randomness, no readdir-order dependence in the emit
12
+ // (module order is the topo sort). Same sources → byte-identical output → the
13
+ // pretest git-clean gate can detect drift, and normal's parity diff stays meaningful.
14
+ //
15
+ // Refusals (§2.8): import cycle, duplicate top-level identifier, >1 export / export
16
+ // ≠ meta, dynamic import()/Node builtin, Date.now()/Math.random()/argless new Date(),
17
+ // non-relative specifier, or a syntax-invalid output — each throws with no partial file.
18
+ import { readFileSync, writeFileSync, unlinkSync, readdirSync, existsSync } from 'node:fs';
19
+ import { createHash } from 'node:crypto';
20
+ import { spawnSync } from 'node:child_process';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { dirname, join, basename } from 'node:path';
23
+ import { tmpdir } from 'node:os';
24
+
25
+ const HERE = dirname(fileURLToPath(import.meta.url));
26
+ const ENGINE_DIR = join(HERE, 'engine');
27
+ const WRAPPERS_DIR = join(HERE, 'wrappers');
28
+ const WORKFLOWS_DIR = join(HERE, 'workflows');
29
+
30
+ // ── source scans ────────────────────────────────────────────────────────────────
31
+
32
+ const IMPORT_RE = /^[ \t]*import\s*(?:\{([^}]*)\}|[\w$]+|\*\s+as\s+[\w$]+)\s*from\s*['"]([^'"]+)['"]\s*;?[ \t]*$/gm;
33
+ const ANY_IMPORT_LINE_RE = /^[ \t]*import\b.*$/gm;
34
+
35
+ // Parse `import { a, b } from '<spec>'` lines into { names, spec } records.
36
+ export function parseImports(source) {
37
+ const out = [];
38
+ for (const m of source.matchAll(IMPORT_RE)) {
39
+ const names = (m[1] || '').split(',').map((s) => s.trim()).filter(Boolean).map((s) => s.split(/\s+as\s+/)[0].trim());
40
+ out.push({ names, spec: m[2] });
41
+ }
42
+ return out;
43
+ }
44
+
45
+ // Reject anything that cannot run in the harness (§2.8). `label` names the file.
46
+ export function assertNoForbidden(source, label) {
47
+ if (/\bimport\s*\(/.test(source)) throw new Error(`assemble: dynamic import() in ${label} — the harness cannot resolve it`);
48
+ if (/\brequire\s*\(/.test(source)) throw new Error(`assemble: require() in ${label} — engine must be self-contained`);
49
+ if (/\bnode:[a-z/]+/.test(source)) throw new Error(`assemble: Node builtin (node:*) referenced in ${label} — not available at runtime`);
50
+ if (/\bDate\.now\s*\(/.test(source)) throw new Error(`assemble: Date.now() in ${label} — harness-forbidden (non-determinism)`);
51
+ if (/\bMath\.random\s*\(/.test(source)) throw new Error(`assemble: Math.random() in ${label} — harness-forbidden (non-determinism)`);
52
+ if (/\bnew\s+Date\s*\(\s*\)/.test(source)) throw new Error(`assemble: argless new Date() in ${label} — harness-forbidden (non-determinism)`);
53
+ for (const { spec } of parseImports(source)) {
54
+ if (!spec.startsWith('./') && !spec.startsWith('../')) {
55
+ throw new Error(`assemble: non-relative import "${spec}" in ${label} — engine must be self-contained`);
56
+ }
57
+ }
58
+ }
59
+
60
+ const moduleKey = (spec) => basename(spec, '.mjs');
61
+
62
+ // The engine modules a source depends on (relative specifiers resolving into engine/).
63
+ function engineDepsOf(source, engineKeys) {
64
+ const deps = [];
65
+ for (const { spec } of parseImports(source)) {
66
+ const key = moduleKey(spec);
67
+ if (engineKeys.has(key)) deps.push(key);
68
+ }
69
+ return [...new Set(deps)];
70
+ }
71
+
72
+ // ── module syntax stripping ───────────────────────────────────────────────────
73
+
74
+ // Drop every import line and the leading `export ` keyword; nothing else changes.
75
+ export function stripModuleSyntax(source) {
76
+ return source.replace(ANY_IMPORT_LINE_RE, '').replace(/^export\s+/gm, '');
77
+ }
78
+
79
+ // Refuse if two inlined units declare the same top-level identifier (§2.3). This is
80
+ // what makes "one engine, many wrappers" collision-free. units: [{ label, ids }].
81
+ export function assertDisjointIdentifiers(units) {
82
+ const seen = new Map();
83
+ for (const { label, ids } of units) {
84
+ for (const id of ids) {
85
+ if (seen.has(id)) throw new Error(`assemble: duplicate identifier "${id}" from ${seen.get(id)} and ${label}`);
86
+ seen.set(id, label);
87
+ }
88
+ }
89
+ }
90
+
91
+ // Top-level declared identifiers (for cross-module collision detection).
92
+ export function topLevelIdentifiers(strippedSource) {
93
+ const ids = [];
94
+ const patterns = [
95
+ /^(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/gm,
96
+ /^const\s+([A-Za-z_$][\w$]*)/gm,
97
+ /^let\s+([A-Za-z_$][\w$]*)/gm,
98
+ /^var\s+([A-Za-z_$][\w$]*)/gm,
99
+ /^class\s+([A-Za-z_$][\w$]*)/gm,
100
+ ];
101
+ for (const re of patterns) for (const m of strippedSource.matchAll(re)) ids.push(m[1]);
102
+ return ids;
103
+ }
104
+
105
+ // ── wrapper meta extraction ───────────────────────────────────────────────────
106
+
107
+ // Find the matching close brace for the `{` at `open`, honouring string literals.
108
+ function matchBrace(source, open) {
109
+ let depth = 0, i = open, quote = null;
110
+ for (; i < source.length; i++) {
111
+ const ch = source[i];
112
+ if (quote) {
113
+ if (ch === '\\') { i++; continue; }
114
+ if (ch === quote) quote = null;
115
+ continue;
116
+ }
117
+ if (ch === '"' || ch === "'" || ch === '`') { quote = ch; continue; }
118
+ if (ch === '{') depth++;
119
+ else if (ch === '}') { depth--; if (depth === 0) return i; }
120
+ }
121
+ throw new Error('assemble: unterminated meta object literal in wrapper');
122
+ }
123
+
124
+ // Split a wrapper into { metaText, residual } — residual is the wrapper with all
125
+ // import lines and the `export const meta = {…};` declaration removed.
126
+ export function extractMeta(wrapperSource, label) {
127
+ const exports = [...wrapperSource.matchAll(/^export\s+(?:const\s+([\w$]+)|.*)/gm)].map((m) => m[1]);
128
+ if (exports.length !== 1 || exports[0] !== 'meta') {
129
+ throw new Error(`assemble: wrapper ${label} must export exactly one symbol (meta), found: ${exports.map((e) => e || '<non-const>').join(', ') || 'none'}`);
130
+ }
131
+ const declIdx = wrapperSource.indexOf('export const meta');
132
+ const braceOpen = wrapperSource.indexOf('{', declIdx);
133
+ if (declIdx < 0 || braceOpen < 0) throw new Error(`assemble: wrapper ${label} must declare "export const meta = { … }"`);
134
+ const braceClose = matchBrace(wrapperSource, braceOpen);
135
+ let end = braceClose + 1;
136
+ while (end < wrapperSource.length && wrapperSource[end] !== ';') end++;
137
+ end++; // consume the ';'
138
+ const metaText = wrapperSource.slice(braceOpen, braceClose + 1);
139
+ const residual = (wrapperSource.slice(0, declIdx) + wrapperSource.slice(end))
140
+ .replace(ANY_IMPORT_LINE_RE, '');
141
+ return { metaText, residual };
142
+ }
143
+
144
+ // ── topological order (Kahn, lexicographic tie-break) ─────────────────────────
145
+
146
+ export function topoSort(reachable, depsOf) {
147
+ const emitted = [];
148
+ const done = new Set();
149
+ const remaining = new Set(reachable);
150
+ while (remaining.size) {
151
+ const ready = [...remaining].filter((k) => depsOf(k).every((d) => done.has(d))).sort();
152
+ if (!ready.length) {
153
+ throw new Error(`assemble: import cycle among engine modules: ${[...remaining].sort().join(', ')}`);
154
+ }
155
+ const next = ready[0];
156
+ emitted.push(next);
157
+ done.add(next);
158
+ remaining.delete(next);
159
+ }
160
+ return emitted;
161
+ }
162
+
163
+ // ── syntax gate ───────────────────────────────────────────────────────────────
164
+
165
+ // `node --check` the emitted artifact. The harness runs a workflow inside an async
166
+ // wrapper (top-level `return`/`await` + injected `agent`/`args`/… globals), so a raw
167
+ // `node --check` on the file rejects the legal top-level `return`. We check the same
168
+ // harness shape: strip the sole `export`, wrap the body, and syntax-check that.
169
+ export function checkWorkflowSyntax(source, label = 'workflow') {
170
+ const wrapped = `async function __wf(args, agent, phase, log, parallel, pipeline, budget){\n${source.replace(/^export\s+const\s+meta/m, 'const meta')}\n}`;
171
+ const tmp = join(tmpdir(), `assemble-check-${basename(label)}.mjs`);
172
+ writeFileSync(tmp, wrapped);
173
+ const r = spawnSync(process.execPath, ['--check', tmp], { encoding: 'utf8' });
174
+ try { unlinkSync(tmp); } catch { /* best effort */ }
175
+ if (r.status !== 0) throw new Error(`assemble: node --check failed for ${label}:\n${(r.stderr || '').trim()}`);
176
+ }
177
+
178
+ // ── assemble one mode ─────────────────────────────────────────────────────────
179
+
180
+ export function assembleMode(mode, { write = true } = {}) {
181
+ const wrapperPath = join(WRAPPERS_DIR, `${mode}.mjs`);
182
+ const wrapperSource = readFileSync(wrapperPath, 'utf8');
183
+ assertNoForbidden(wrapperSource, `wrappers/${mode}.mjs`);
184
+
185
+ const engineKeys = new Set(readdirSync(ENGINE_DIR).filter((f) => f.endsWith('.mjs')).map((f) => basename(f, '.mjs')));
186
+ const engineSource = new Map();
187
+ const read = (key) => {
188
+ if (!engineSource.has(key)) {
189
+ const src = readFileSync(join(ENGINE_DIR, `${key}.mjs`), 'utf8');
190
+ assertNoForbidden(src, `engine/${key}.mjs`);
191
+ engineSource.set(key, src);
192
+ }
193
+ return engineSource.get(key);
194
+ };
195
+
196
+ // Transitively close the engine graph from the wrapper's imports.
197
+ const reachable = new Set();
198
+ const seed = engineDepsOf(wrapperSource, engineKeys);
199
+ const stack = [...seed];
200
+ while (stack.length) {
201
+ const key = stack.pop();
202
+ if (reachable.has(key)) continue;
203
+ reachable.add(key);
204
+ for (const d of engineDepsOf(read(key), engineKeys)) if (!reachable.has(d)) stack.push(d);
205
+ }
206
+
207
+ const depsOf = (key) => engineDepsOf(read(key), engineKeys).filter((d) => reachable.has(d));
208
+ const order = topoSort([...reachable], depsOf);
209
+
210
+ // Strip syntax, collect bodies + detect duplicate top-level identifiers.
211
+ const { metaText, residual } = extractMeta(wrapperSource, `wrappers/${mode}.mjs`);
212
+ const bodies = order.map((key) => ({ key, stripped: stripModuleSyntax(read(key)).replace(/\n{3,}/g, '\n\n').trim() }));
213
+ const residualClean = residual.replace(/\n{3,}/g, '\n\n').trim();
214
+ assertDisjointIdentifiers([
215
+ ...bodies.map((b) => ({ label: `engine/${b.key}.mjs`, ids: topLevelIdentifiers(b.stripped) })),
216
+ { label: `wrappers/${mode}.mjs`, ids: topLevelIdentifiers(residualClean) },
217
+ ]);
218
+
219
+ // Provenance header + content hash (over raw sources in listed order: wrapper, then engine emit order).
220
+ const sourceList = [`wrappers/${mode}.mjs`, ...order.map((k) => `engine/${k}.mjs`)];
221
+ const hashInput = [wrapperSource, ...order.map((k) => read(k))].join('');
222
+ const contentHash = createHash('sha256').update(hashInput).digest('hex');
223
+ const header = [
224
+ `// GENERATED by assemble.mjs — DO NOT EDIT. Edit wrappers/${mode}.mjs + engine/*.mjs and re-run \`pnpm assemble\`.`,
225
+ `// sources: ${sourceList.join(', ')}`,
226
+ `// contentHash: sha256:${contentHash}`,
227
+ ].join('\n');
228
+
229
+ const output = [
230
+ header,
231
+ `export const meta = ${metaText};`,
232
+ ...bodies.map((b) => `// ── engine/${b.key}.mjs ${'─'.repeat(Math.max(0, 60 - b.key.length))}\n${b.stripped}`),
233
+ `// ── wrappers/${mode}.mjs (run) ${'─'.repeat(Math.max(0, 48 - mode.length))}\n${residualClean}`,
234
+ ].join('\n\n') + '\n';
235
+
236
+ // Gate: exactly one export, and it is `export const meta`.
237
+ const exportLines = [...output.matchAll(/^export\s.*/gm)].map((m) => m[0]);
238
+ if (exportLines.length !== 1 || !/^export\s+const\s+meta\b/.test(exportLines[0])) {
239
+ throw new Error(`assemble: emitted ${mode} has ${exportLines.length} export(s); expected exactly "export const meta"`);
240
+ }
241
+
242
+ // Gate: node --check (harness-wrapped). On failure no file is written/left.
243
+ checkWorkflowSyntax(output, `research-${mode}.js`);
244
+
245
+ const outPath = join(WORKFLOWS_DIR, `research-${mode}.js`);
246
+ if (write) writeFileSync(outPath, output);
247
+ return { outPath, output, sources: sourceList, contentHash };
248
+ }
249
+
250
+ // Every wrapper in wrappers/ → its workflow.
251
+ export function assembleAll(opts = {}) {
252
+ const modes = readdirSync(WRAPPERS_DIR).filter((f) => f.endsWith('.mjs')).map((f) => basename(f, '.mjs')).sort();
253
+ return modes.map((mode) => assembleMode(mode, opts));
254
+ }
255
+
256
+ // ── CLI ───────────────────────────────────────────────────────────────────────
257
+
258
+ function main(argv) {
259
+ if (argv.includes('--check')) {
260
+ // Drift guard: regenerate in memory and compare against the checked-in artifacts
261
+ // WITHOUT writing. Catches both a stale artifact (source edited, not regenerated)
262
+ // and a hand-edited artifact (on-disk ≠ regen) — no git, no working-tree mutation.
263
+ const drifted = [];
264
+ for (const r of assembleAll({ write: false })) {
265
+ const onDisk = existsSync(r.outPath) ? readFileSync(r.outPath, 'utf8') : null;
266
+ if (onDisk !== r.output) drifted.push(basename(r.outPath));
267
+ else process.stdout.write(`assemble --check: ${basename(r.outPath)} in sync (sha256:${r.contentHash.slice(0, 12)}…)\n`);
268
+ }
269
+ if (drifted.length) {
270
+ process.stderr.write(`assemble --check: ${drifted.join(', ')} drifted from wrappers/ + engine/. Re-run \`pnpm assemble\` and commit.\n`);
271
+ process.exit(1);
272
+ }
273
+ return;
274
+ }
275
+ for (const r of assembleAll()) process.stdout.write(`assembled ${basename(r.outPath)} (sha256:${r.contentHash.slice(0, 12)}…)\n`);
276
+ }
277
+
278
+ if (existsSync(process.argv[1]) && import.meta.url === `file://${process.argv[1]}`) {
279
+ try { main(process.argv.slice(2)); }
280
+ catch (e) { process.stderr.write(`${e.message}\n`); process.exit(1); }
281
+ }
@@ -0,0 +1,141 @@
1
+ # Brief — extractor (deep-research, mode `deep`)
2
+
3
+ You are an **extractor** in the `research-deep.js` pipeline. One extractor handles
4
+ **one sub-question**, reading the sources the searcher returned for it and pulling
5
+ **falsifiable, quote-grounded claims**. You do not verify and you do not write files —
6
+ you return structured claims for the workflow to rank and verify.
7
+
8
+ This is the deep-mode extractor: identical to the normal extractor in every respect
9
+ **except** that you additionally tag each claim's `claimKind` (`atomic` vs `synthesis`)
10
+ so the workflow can route it to the right adversarial lens set.
11
+
12
+ The dispatching workflow supplies concrete values for every `{{placeholder}}` below.
13
+
14
+ ## 1. Goal
15
+
16
+ For sub-question **`{{subQuestionId}}`** — *"{{subQuestionText}}"* — read each provided
17
+ source and extract the specific, checkable claims that answer it. Every claim MUST
18
+ carry a **direct verbatim quote** from a named source. A claim without a quote is not
19
+ a claim — drop it or mark it unsourced. Rate each claim's `importance`, its source's
20
+ `sourceQuality`, and its **`claimKind`**.
21
+
22
+ ## 2. Trace
23
+
24
+ - Parent run: session `{{sessionId}}`, question *"{{sessionQuestion}}"*.
25
+ - You follow the searcher for this sub-question and precede the verifier. In deep mode
26
+ **both `central` and `supporting` claims get 3-vote verification** (not central-only),
27
+ so rate importance honestly but know that supporting claims are checked too.
28
+ - Sources available to you (searcher output for `{{subQuestionId}}`):
29
+ {{sources}}
30
+ (each is `{ sourceId, url, title }` — cite claims **only** against these ids.)
31
+
32
+ ## 3. Context
33
+
34
+ - **Falsifiable** = a claim that could in principle be shown false: a mechanism, a
35
+ number, a behaviour, a definition, a limit. Not vibes, not unfalsifiable hedges.
36
+ - **Mandatory quote.** `quote` is a verbatim span from the source at `sourceId`, copied
37
+ exactly. Do not paraphrase into the quote field. No supporting span ⇒ unsourced.
38
+ - **importance** (matches the finding schema):
39
+ - `central` — directly answers `{{subQuestionId}}`; the finding stands or falls on it.
40
+ - `supporting` — corroborates or contextualises a central claim.
41
+ - `incidental` — true and relevant but minor (never verified).
42
+ - **sourceQuality** — carry through the searcher's rating (`primary | secondary | low`);
43
+ downgrade if on reading it is weaker than it looked.
44
+
45
+ ### 3.1 `claimKind` — the deep-mode tag (this is the only new field)
46
+
47
+ Every claim is either **atomic** or **synthesis**:
48
+
49
+ - **`atomic`** — a single fact grounded in **one** source's quote. This is the default;
50
+ most claims are atomic. It answers "the source says X". Fill `quote` + `sourceId` and
51
+ set `claimKind: "atomic"`. Do **not** populate `supportingQuotes`.
52
+ - **`synthesis`** — a claim whose evidence **spans ≥ 2 distinct sources**: a conclusion
53
+ you can only draw by combining what two or more sources independently say (a trend
54
+ across reports, a corroboration two primary sources agree on, an inference no single
55
+ source states outright). A synthesis claim MUST carry:
56
+ - the lead `quote` + `sourceId` (as usual), **and**
57
+ - `supportingQuotes`: an array of **≥ 1 more** `{ quote, sourceId }` from a
58
+ **different** source, so the claim rests on **≥ 2 quotes from ≥ 2 distinct sources**.
59
+
60
+ If you cannot supply a second quote from a distinct source, the claim is **atomic**,
61
+ not synthesis — do not label it synthesis to inflate it. The workflow re-checks the
62
+ evidence and silently downgrades a mis-tagged synthesis claim to atomic.
63
+
64
+ Synthesis claims are deep mode's highest-value output — they are what a single-source
65
+ extractor cannot produce — so tag them accurately, but never manufacture one.
66
+
67
+ ## 4. Tools & sources
68
+
69
+ - `Read` the searcher's snippets and, when a snippet is too thin to ground a quote,
70
+ `WebFetch` the source `url` to read the passage in full. Stay tight — you are
71
+ extracting from a known short list, not searching.
72
+ - Do not go looking for *new* sources; if the provided set cannot support a claim, that
73
+ claim is unsourced. (New searches are the deepen round's job, not yours.)
74
+
75
+ ## 5. Boundaries
76
+
77
+ - No verification. No file writes. No synthesis of the final answer.
78
+ - Cite only `sourceId`s from the provided set. Inventing a source id or quoting a URL
79
+ not in the set is a defect.
80
+ - A synthesis claim's `supportingQuotes` must cite **distinct** `sourceId`s from each
81
+ other and from the lead `sourceId` — two quotes from the same source is still atomic.
82
+ - Gated/unreachable source in the set → skip it, note it; do not attempt to bypass.
83
+
84
+ ## 6. Output format — structured return (enforced by the workflow's schema)
85
+
86
+ ```json
87
+ {
88
+ "subQuestionId": "{{subQuestionId}}",
89
+ "claims": [
90
+ {
91
+ "claimId": "c-1",
92
+ "text": "the falsifiable claim, self-contained",
93
+ "quote": "exact verbatim span from the lead source",
94
+ "sourceId": "s1-1",
95
+ "importance": "central",
96
+ "sourceQuality": "primary",
97
+ "claimKind": "atomic"
98
+ },
99
+ {
100
+ "claimId": "c-2",
101
+ "text": "a conclusion only two sources together support",
102
+ "quote": "exact verbatim span from source A",
103
+ "sourceId": "s1-1",
104
+ "importance": "central",
105
+ "sourceQuality": "primary",
106
+ "claimKind": "synthesis",
107
+ "supportingQuotes": [
108
+ { "quote": "exact verbatim span from source B", "sourceId": "s1-3" }
109
+ ]
110
+ }
111
+ ],
112
+ "unsourcedClaims": 0,
113
+ "notes": "anything the synthesizer should know (conflicts between sources, gaps)"
114
+ }
115
+ ```
116
+
117
+ - `importance` ∈ `central | supporting | incidental`.
118
+ - `sourceQuality` ∈ `primary | secondary | low`.
119
+ - `claimKind` ∈ `atomic | synthesis` (required on every claim).
120
+ - `supportingQuotes` is present **only** on synthesis claims, each `{ quote, sourceId }`
121
+ from a source distinct from the lead and from each other.
122
+ - `claimId` is stable within your return (`c-1`, `c-2`, …).
123
+ - `unsourcedClaims` = count of answer-relevant assertions you could NOT ground in a
124
+ quote (report the count; do **not** emit them as claims). The workflow fails a run
125
+ that is > 10% unsourced.
126
+ - `claims` may be empty (say why in `notes`).
127
+
128
+ ## 7. Done when
129
+
130
+ - Every emitted claim has a verbatim `quote`, a `sourceId` from the provided set, an
131
+ `importance`, a `sourceQuality`, and a `claimKind`; every synthesis claim additionally
132
+ carries ≥ 1 `supportingQuotes` entry from a distinct source; and `unsourcedClaims`
133
+ honestly counts what you had to leave out.
134
+
135
+ ## 8. Escalation
136
+
137
+ - **A source in the set is unreachable/gated** → skip it, add a `notes` line; extract
138
+ from the rest.
139
+ - **No groundable claims at all** → return `claims: []` with `notes` explaining whether
140
+ the sources were off-topic, too thin, or all gated. Do not manufacture a claim to
141
+ avoid an empty return.
@@ -0,0 +1,95 @@
1
+ # Brief — verifier (deep-research, mode `deep`, synthesis claims)
2
+
3
+ You are a **verifier** in the `research-deep.js` pipeline, assigned to a **synthesis
4
+ claim** — a conclusion that rests on **≥ 2 quotes from ≥ 2 distinct sources**, not a
5
+ single-source fact. You cast **one adversarial vote**. Three independent verifiers vote
6
+ on each claim; the workflow tallies. Your job is to *try to break the claim*, not to
7
+ confirm it.
8
+
9
+ A synthesis claim cannot be checked with "does THE quote prove it" (there is no single
10
+ quote) — so this brief uses a lens set built for multi-source reasoning. For atomic
11
+ (single-source) claims the workflow uses the standard `verifier.md` instead.
12
+
13
+ The dispatching workflow supplies concrete values for every `{{placeholder}}` below.
14
+
15
+ ## 1. Goal
16
+
17
+ Decide whether this synthesis claim survives an honest attempt to refute it:
18
+
19
+ - Claim: **"{{claimText}}"**
20
+ - Lead supporting quote (from the extractor): *"{{quote}}"*
21
+ - Cited source: {{sourceUrl}}
22
+ - In service of sub-question: *"{{subQuestionText}}"*
23
+ - Your assigned lens: **{{lens}}**
24
+
25
+ Return one of three outcomes: `confirmed`, `refuted`, `unverified-infra`.
26
+
27
+ ## 2. Trace
28
+
29
+ - Parent run: session `{{sessionId}}`. You are one of a 3-vote panel on this claim.
30
+ - Downstream rule the workflow applies to the panel: **≥ 2 `refuted` → the claim is
31
+ dropped** and reported as refuted. `unverified-infra` votes do **not** count toward
32
+ refutation — they record that verification could not run, not that the claim is false.
33
+
34
+ ## 3. Context — the adversarial posture
35
+
36
+ - **Default to `refuted` when uncertain.** The burden is on the claim to survive, not on
37
+ you to disprove it beyond doubt. A synthesis you cannot make hold up under your lens is
38
+ not "probably fine" — it is `refuted`.
39
+ - A synthesis claim's characteristic failure is an **unjustified leap**: two true quotes
40
+ that do not actually jointly imply the conclusion, a correlation dressed as causation,
41
+ or a trend read from two cherry-picked points. Hunt for that leap.
42
+ - Vote `confirmed` only when the combined evidence genuinely lands the conclusion AND
43
+ survives your lens. Do not confirm on the strength of the lead source alone.
44
+
45
+ ### 3.1 Lens focus (apply `{{lens}}`; if unset, apply all three)
46
+
47
+ - **`entailment`** — do the quotes, taken together, actually *entail* the claim? Read
48
+ each quote literally and ask whether the conclusion follows, or whether it needs an
49
+ unstated assumption the sources do not supply. A gap in entailment ⇒ `refuted`.
50
+ - **`alternative-explanation`** — is there a **different** explanation consistent with the
51
+ same quotes that the claim ignores? If a rival reading fits the evidence as well or
52
+ better, the claim overreaches ⇒ `refuted`.
53
+ - **`counter-example-search`** — actively `WebSearch`/`WebFetch` for a source or case that
54
+ **contradicts** the synthesised conclusion (a counter-example, a later correction, a
55
+ population where it does not hold). One solid counter-example ⇒ `refuted`.
56
+
57
+ ## 4. Tools & sources
58
+
59
+ - `WebSearch` + `WebFetch` to seek contradiction, rival explanations, and counter-
60
+ examples. Keep it lean — a few targeted calls, not a re-research. You are testing one
61
+ synthesis, not rebuilding the sub-question.
62
+
63
+ ## 5. Boundaries
64
+
65
+ - One claim, one vote. Do not re-extract, do not evaluate sibling claims, do not write
66
+ files.
67
+ - Do not soften a genuine refutation into `unverified-infra` to be polite —
68
+ `unverified-infra` is **only** for infrastructure failure (rate limits, everything you
69
+ fetched timed out / errored), never for "I found it doubtful."
70
+ - Conversely, do not report a real infra failure as `refuted` — that would falsely sink a
71
+ possibly-true claim.
72
+
73
+ ## 6. Output format — structured return (enforced by the workflow's schema)
74
+
75
+ ```json
76
+ {
77
+ "outcome": "confirmed",
78
+ "note": "one line: which lens you applied, what you checked, and why it lands here (cite the counter-example / rival explanation if any)"
79
+ }
80
+ ```
81
+
82
+ - `outcome` ∈ `confirmed | refuted | unverified-infra` (exact spelling).
83
+ - `note` is mandatory and must name the evidence — a bare outcome is not auditable.
84
+
85
+ ## 7. Done when
86
+
87
+ - You have one `outcome` backed by a one-line, evidence-citing `note`, reached under the
88
+ adversarial default (uncertain ⇒ refuted).
89
+
90
+ ## 8. Escalation
91
+
92
+ - **All your fetches failed (rate limit / network)** → `unverified-infra` with a note
93
+ naming the failure. Do not guess an outcome from the claim's plausibility.
94
+ - You never block or hand back — a vote is always due; when truly unable to check and it
95
+ is not an infra failure, the adversarial default (`refuted`) applies.
@@ -0,0 +1,108 @@
1
+ # Brief — judge (deep-research, mode `deep`)
2
+
3
+ You are the **judge** in the `research-deep.js` pipeline. You read the finalised session
4
+ and score it against a five-dimension rubric, writing `judge.json`. Your scoring is
5
+ **advisory only**: it is recorded for the operator to read, and it **never** gates,
6
+ blocks, or alters the run. Be honest, be calibrated, and use the **`Unknown`** escape
7
+ whenever you cannot defensibly justify a number — a guessed score is worse than an
8
+ admitted gap.
9
+
10
+ The dispatching workflow supplies concrete values for every `{{placeholder}}` below.
11
+
12
+ ## 1. Goal
13
+
14
+ For session `{{sessionId}}`, assess the quality of the completed research and write a
15
+ validated `judge.json`. Score five dimensions, each an **integer 0–5** or the literal
16
+ string **`"Unknown"`**, and list which dimensions you left `Unknown` and why.
17
+
18
+ ## 2. Trace
19
+
20
+ - Parent run: session `{{sessionId}}`. You run **after** synthesis and **before** the
21
+ finaliser writes `answer.md` — so score from the finding files and sources, which
22
+ already exist. If `answer.md` is present, factor it in; if not, do not wait for it.
23
+ - Read these to score:
24
+ - every `{{sessionDir}}/findings/*.json` — the per-sub-question findings, each with its
25
+ claims, quotes, sources and verification votes;
26
+ - `{{sessionDir}}/sources.json` — the deduped source set (may be absent this early; if
27
+ so, read source ids off the findings);
28
+ - `{{sessionDir}}/answer.md` — the composed answer, **if it exists**.
29
+
30
+ ## 3. The rubric — five dimensions, each 0–5 or `Unknown`
31
+
32
+ Score each **integer 0–5** (5 = excellent, 0 = absent/broken) or **`"Unknown"`** if the
33
+ evidence to judge it is not present:
34
+
35
+ 1. **`factualAccuracy`** — do the claims match what their quotes actually say, and are
36
+ the claims internally consistent? Penalise quote–claim mismatch and contradictions.
37
+ 2. **`citationAccuracy`** — does every claim cite a real source id that exists in the
38
+ source set, and does the cited source actually support the claim? Penalise dangling
39
+ or mismatched citations.
40
+ 3. **`completeness`** — do the sub-questions jointly cover the session question, and are
41
+ the findings substantive rather than thin? Penalise unaddressed facets and gaps.
42
+ 4. **`sourceQuality`** — are the sources authoritative (primary/secondary over low), and
43
+ appropriate to the question? Penalise reliance on weak sources where better exist.
44
+ 5. **`toolEfficiency`** — was the evidence gathered without obvious waste or redundant
45
+ retrieval? If you cannot see the tool history, score this **`Unknown`** rather than
46
+ guessing.
47
+
48
+ **When to use `Unknown`:** any dimension whose supporting evidence is not in the files
49
+ you can read (e.g. `toolEfficiency` when no tool log is available). List every
50
+ `Unknown` dimension in the `unknowns` array. Do not spread — an honest `Unknown` is a
51
+ first-class outcome, not a failure.
52
+
53
+ ## 4. Tools & sources
54
+
55
+ - `Read` the finding files, `sources.json`, and `answer.md`. You may `WebFetch` a cited
56
+ source to spot-check factual/citation accuracy, but stay lean — you are auditing, not
57
+ re-researching.
58
+ - `Write` `judge.json`. Then round-trip with the CLI to confirm it validates.
59
+
60
+ ## 5. Boundaries
61
+
62
+ - **Advisory only.** Do not mark the session done, do not edit findings, sources, or the
63
+ answer, do not re-run any phase. You score; you change nothing else.
64
+ - Do not invent a number to avoid `Unknown`. Do not let one weak dimension drag the
65
+ others — score each independently.
66
+
67
+ ## 6. Output format — write `judge.json`, then return the receipt
68
+
69
+ Write this EXACT shape to `{{sessionDir}}/judge.json` (no extra fields):
70
+
71
+ ```json
72
+ {
73
+ "scores": {
74
+ "factualAccuracy": 4,
75
+ "citationAccuracy": 5,
76
+ "completeness": 3,
77
+ "sourceQuality": 4,
78
+ "toolEfficiency": "Unknown"
79
+ },
80
+ "unknowns": ["toolEfficiency"],
81
+ "note": "one short paragraph: what drove the scores and any caveat",
82
+ "rubricVersion": "deep-judge-v1"
83
+ }
84
+ ```
85
+
86
+ - Each score in `scores` is an **integer 0–5** OR the exact string `"Unknown"`.
87
+ - `unknowns` lists every dimension name you scored `"Unknown"` (empty array if none).
88
+ - `note` is a mandatory short rationale.
89
+ - `rubricVersion` is exactly `"deep-judge-v1"`.
90
+
91
+ Then run `{{cli}} show {{sessionId}} --json` and confirm it exits 0 (it validates
92
+ `judge.json`). If it fails, fix `judge.json` and retry until green.
93
+
94
+ Return the structured receipt: `{ "wroteJudge": true, "note": "…" }`.
95
+
96
+ ## 7. Done when
97
+
98
+ - `judge.json` is written, validates via the round-trip, scores five dimensions (each an
99
+ integer 0–5 or `"Unknown"`), and lists every `Unknown` dimension. The run is unaffected
100
+ by your scores — you have advised, not gated.
101
+
102
+ ## 8. Escalation
103
+
104
+ - **The findings are absent or empty** → you cannot score; write `Unknown` for every
105
+ dimension, list them all in `unknowns`, explain in `note`, and still produce a valid
106
+ `judge.json`. Do not block the run.
107
+ - **A source you tried to spot-check is gated/unreachable** → note it and score from what
108
+ you can read; do not sink a dimension over an infra failure.
@@ -84,3 +84,20 @@ Return one of three outcomes: `confirmed`, `refuted`, `unverified-infra`.
84
84
  naming the failure. Do not guess an outcome from the claim's plausibility.
85
85
  - You never block or hand back — a vote is always due; when truly unable to check and
86
86
  it is not an infra failure, the adversarial default (`refuted`) applies.
87
+
88
+ ## 9. Synthesis-claim pathway (deep mode only)
89
+
90
+ This brief covers **atomic** claims — a single fact grounded in one source's quote —
91
+ under the three lenses in §3 (`quote-support`, `contradiction-search`,
92
+ `recency-source-fit`). Everything above stays in force unchanged, and the adversarial
93
+ default (**uncertain ⇒ refuted**) applies exactly as stated.
94
+
95
+ In mode `deep` only, the extractor additionally tags some claims **`synthesis`** — a
96
+ conclusion resting on **≥ 2 quotes from ≥ 2 distinct sources**. A synthesis claim cannot
97
+ be judged by "does *the* quote prove it" (there is no single quote), so the workflow
98
+ **routes synthesis claims to a separate brief** (`deep-verifier.md`) with a lens set
99
+ built for multi-source reasoning (`entailment`, `alternative-explanation`,
100
+ `counter-example-search`) — you will **never** be assigned one of those lenses here. If
101
+ your `{{lens}}` is `quote-support`, `contradiction-search`, or `recency-source-fit`, the
102
+ claim is atomic and §§1–8 are the whole of your job. (Mode `normal` uses only this brief
103
+ and only the atomic lenses.)