@stravica-ai/digital-operator-pa 0.1.6 → 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.
- package/dist/engineering-pa-cli/package.json +2 -0
- package/dist/engineering-pa-cli/src/research/assemble.mjs +281 -0
- package/dist/engineering-pa-cli/src/research/briefs/deep-extractor.md +141 -0
- package/dist/engineering-pa-cli/src/research/briefs/deep-verifier.md +95 -0
- package/dist/engineering-pa-cli/src/research/briefs/judge.md +108 -0
- package/dist/engineering-pa-cli/src/research/briefs/verifier.md +17 -0
- package/dist/engineering-pa-cli/src/research/commands/show.mjs +10 -1
- package/dist/engineering-pa-cli/src/research/engine/agents.mjs +163 -0
- package/dist/engineering-pa-cli/src/research/engine/deep.mjs +513 -0
- package/dist/engineering-pa-cli/src/research/engine/deepen.mjs +97 -0
- package/dist/engineering-pa-cli/src/research/engine/finalise.mjs +121 -0
- package/dist/engineering-pa-cli/src/research/engine/io.mjs +60 -0
- package/dist/engineering-pa-cli/src/research/engine/plan.mjs +129 -0
- package/dist/engineering-pa-cli/src/research/engine/rank.mjs +80 -0
- package/dist/engineering-pa-cli/src/research/engine/verify.mjs +52 -0
- package/dist/engineering-pa-cli/src/research/schema/cost.schema.json +19 -0
- package/dist/engineering-pa-cli/src/research/schema/judge.schema.json +35 -0
- package/dist/engineering-pa-cli/src/research/schema/rounds.schema.json +34 -0
- package/dist/engineering-pa-cli/src/research/store/cost.mjs +41 -0
- package/dist/engineering-pa-cli/src/research/store/index.mjs +31 -0
- package/dist/engineering-pa-cli/src/research/store/judge.mjs +53 -0
- package/dist/engineering-pa-cli/src/research/store/rounds.mjs +61 -0
- package/dist/engineering-pa-cli/src/research/workflows/research-deep.js +1364 -0
- package/dist/engineering-pa-cli/src/research/workflows/research-normal.js +561 -327
- package/dist/engineering-pa-cli/src/research/wrappers/deep.mjs +146 -0
- package/dist/engineering-pa-cli/src/research/wrappers/normal.mjs +100 -0
- package/dist/engineering-pa-core/docs/commands/research.md +438 -0
- package/dist/engineering-pa-core/docs/entity-location-map.md +1 -0
- package/dist/engineering-pa-core/knowledge/knowledge-memory/knowledge-retrieval-discipline.md +27 -0
- package/dist/engineering-pa-core/knowledge/operator-interface/push-back-discipline.md +1 -1
- package/dist/engineering-pa-scaffold/templates/CLAUDE.md.template +1 -1
- 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.)
|