@ijfw/memory-server 1.3.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/bin/ijfw +27 -0
- package/bin/ijfw-dashboard +180 -0
- package/bin/ijfw-dispatch-plan +41 -0
- package/bin/ijfw-memorize +273 -0
- package/bin/ijfw-memory +51 -0
- package/fixtures/demo-target.js +28 -0
- package/package.json +53 -0
- package/src/api-client.js +190 -0
- package/src/audit-roster.js +315 -0
- package/src/caps.js +37 -0
- package/src/cold-scan-runner.mjs +37 -0
- package/src/compute/edges.js +155 -0
- package/src/compute/extract.js +560 -0
- package/src/compute/fts5.js +420 -0
- package/src/compute/graph-auto-index.js +191 -0
- package/src/compute/graph-lock.js +114 -0
- package/src/compute/index.js +18 -0
- package/src/compute/migration-runner.js +116 -0
- package/src/compute/migrations/001-initial.js +23 -0
- package/src/compute/migrations/002-porter-stemming-source.js +139 -0
- package/src/compute/migrations/003-tier-semantic.js +69 -0
- package/src/compute/migrations/004-kg-tables.js +83 -0
- package/src/compute/migrations/005-stale-candidate.js +72 -0
- package/src/compute/python-resolver.js +106 -0
- package/src/compute/runner-vm.js +185 -0
- package/src/compute/runner.js +416 -0
- package/src/compute/sandbox-detect.js +122 -0
- package/src/compute/sandbox-linux.js +164 -0
- package/src/compute/sandbox-macos.js +167 -0
- package/src/compute/sandbox-windows.js +63 -0
- package/src/compute/schema.sql +118 -0
- package/src/compute/staleness.js +239 -0
- package/src/compute/synonyms.js +367 -0
- package/src/compute/traverse.js +180 -0
- package/src/cost/aggregator.js +229 -0
- package/src/cost/pricing.js +134 -0
- package/src/cost/readers/claude.js +179 -0
- package/src/cost/readers/codex.js +131 -0
- package/src/cost/readers/gemini.js +111 -0
- package/src/cost/savings.js +243 -0
- package/src/cross-dispatcher.js +437 -0
- package/src/cross-orchestrator-cli.js +1885 -0
- package/src/cross-orchestrator.js +598 -0
- package/src/cross-project-search.js +114 -0
- package/src/dashboard-client.html +1180 -0
- package/src/dashboard-server.js +895 -0
- package/src/design-companion.js +81 -0
- package/src/dispatch/colon-syntax.js +732 -0
- package/src/dispatch-planner.js +235 -0
- package/src/dream/cooldown.js +105 -0
- package/src/dream/runner.mjs +373 -0
- package/src/dream/staleness-wiring.js +195 -0
- package/src/feedback-detector.js +57 -0
- package/src/hero-line.js +115 -0
- package/src/importers/claude-mem.js +152 -0
- package/src/importers/cli.js +311 -0
- package/src/importers/common.js +84 -0
- package/src/importers/discover.js +235 -0
- package/src/importers/rtk.js +107 -0
- package/src/intent-router.js +221 -0
- package/src/lib/atomic-io.js +201 -0
- package/src/lib/cache.js +33 -0
- package/src/lib/npm-view.js +104 -0
- package/src/lib/status-card.js +95 -0
- package/src/lib/token.js +85 -0
- package/src/memory/fts5.js +349 -0
- package/src/memory/migration-runner.js +116 -0
- package/src/memory/migrations/001-fts5-init.js +26 -0
- package/src/memory/migrations/002-tier-semantic.js +60 -0
- package/src/memory/migrations/003-stale-candidate.js +60 -0
- package/src/memory/reader.js +300 -0
- package/src/memory/recall-counter.js +76 -0
- package/src/memory/schema.sql +79 -0
- package/src/memory/search.js +431 -0
- package/src/memory/staleness.js +237 -0
- package/src/memory/tier-promotion.js +377 -0
- package/src/memory/tokenize.js +63 -0
- package/src/project-type-detector.js +866 -0
- package/src/prompt-check.js +171 -0
- package/src/ralph-allowlist.js +88 -0
- package/src/receipts.js +129 -0
- package/src/redactor.js +107 -0
- package/src/sandbox.js +275 -0
- package/src/sanitizer.js +69 -0
- package/src/scan-resume.js +167 -0
- package/src/schema.js +82 -0
- package/src/search-bm25.js +108 -0
- package/src/server.js +1414 -0
- package/src/swarm-config.js +80 -0
- package/src/trident/dispatch.js +211 -0
- package/src/trident/lens-health.js +253 -0
- package/src/update-apply.js +79 -0
- package/src/update-check.js +136 -0
- package/src/vectors.js +178 -0
- package/templates/design/bento-grid.md +84 -0
- package/templates/design/brutalist-luxe.md +82 -0
- package/templates/design/cinematic-dark.md +82 -0
- package/templates/design/data-dense-dashboard.md +88 -0
- package/templates/design/editorial-warm.md +81 -0
- package/templates/design/glassmorphic.md +84 -0
- package/templates/design/magazine-editorial.md +84 -0
- package/templates/design/maximalist-vibrant.md +85 -0
- package/templates/design/neo-swiss-tech.md +85 -0
- package/templates/design/swiss-minimal.md +80 -0
- package/templates/design/terminal-native.md +83 -0
- package/templates/design/warm-organic.md +84 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
// --- Cross-dispatcher (Phase 7, Wave 1) ---
|
|
2
|
+
//
|
|
3
|
+
// Shared dispatcher for /cross-audit, /cross-research, /cross-critique.
|
|
4
|
+
// All modes use a JSON-in-fenced-block + prose response contract so that
|
|
5
|
+
// parseResponse can round-trip structured data regardless of auditor.
|
|
6
|
+
//
|
|
7
|
+
// Zero external deps. ES module.
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Internal templates
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
14
|
+
|
|
15
|
+
// Format-contract footer shared by all templates -- tells auditors the exact
|
|
16
|
+
// fenced block schema to use so parseResponse can extract it reliably.
|
|
17
|
+
function formatContract(schema) {
|
|
18
|
+
return `
|
|
19
|
+
## Response format (required)
|
|
20
|
+
|
|
21
|
+
Return your findings in **two parts**:
|
|
22
|
+
|
|
23
|
+
1. A single fenced JSON block -- the machine-readable payload:
|
|
24
|
+
|
|
25
|
+
\`\`\`json
|
|
26
|
+
${schema}
|
|
27
|
+
\`\`\`
|
|
28
|
+
|
|
29
|
+
2. After the block, any prose commentary you wish to add for context.
|
|
30
|
+
|
|
31
|
+
Rules:
|
|
32
|
+
- The \`\`\`json fence must appear exactly once.
|
|
33
|
+
- Every object key listed above is required; use an empty string for unknown values.
|
|
34
|
+
- Do not nest arrays inside arrays.
|
|
35
|
+
- Confidence values: "high" | "medium" | "low".
|
|
36
|
+
- Severity values: "critical" | "high" | "medium" | "low".`.trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const TEMPLATES = {
|
|
40
|
+
audit: {
|
|
41
|
+
general: {
|
|
42
|
+
system: `You are a precise, adversarial code and design auditor. You find real problems -- not style preferences. Every finding must be actionable. If a dimension is clean, say so explicitly rather than omitting it. Findings: blunt and specific. Wrapping prose: neutral tone.`,
|
|
43
|
+
format: formatContract(`[
|
|
44
|
+
{
|
|
45
|
+
"severity": "high",
|
|
46
|
+
"dimension": "correctness",
|
|
47
|
+
"location": "file.js:42",
|
|
48
|
+
"issue": "one-sentence description of the problem",
|
|
49
|
+
"whyItMatters": "one-sentence consequence if left unaddressed",
|
|
50
|
+
"fix": "one-sentence recommended action"
|
|
51
|
+
}
|
|
52
|
+
]`),
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
research: {
|
|
56
|
+
benchmarks: {
|
|
57
|
+
system: `You are a research specialist focused on performance benchmarks, empirical comparisons, and quantitative data. Surface concrete numbers, published benchmarks, and real-world measurements. Attribute every claim to a source where possible.`,
|
|
58
|
+
format: formatContract(`[
|
|
59
|
+
{
|
|
60
|
+
"claim": "the finding or data point",
|
|
61
|
+
"evidence": "supporting detail or measurement",
|
|
62
|
+
"source": "paper, repo, URL, or 'unpublished observation'",
|
|
63
|
+
"confidence": "high"
|
|
64
|
+
}
|
|
65
|
+
]`),
|
|
66
|
+
},
|
|
67
|
+
citations: {
|
|
68
|
+
system: `You are a research specialist focused on academic citations, authoritative references, and prior art. Surface papers, RFCs, specifications, and documented precedents. When citing, include enough detail for the reader to locate the source.`,
|
|
69
|
+
format: formatContract(`[
|
|
70
|
+
{
|
|
71
|
+
"claim": "the finding or referenced position",
|
|
72
|
+
"evidence": "summary of the source's argument or data",
|
|
73
|
+
"source": "author, title, year, URL",
|
|
74
|
+
"confidence": "high"
|
|
75
|
+
}
|
|
76
|
+
]`),
|
|
77
|
+
},
|
|
78
|
+
synthesis: {
|
|
79
|
+
system: `You are a synthesis analyst. You have received research from THREE independent sources: Codex (benchmarks angle), Gemini (citations angle), and the in-session caller (observations angle). Your job is to find consensus, surface contradictions, flag open questions, and produce a coherent synthesis across all three -- not a summary of each. Be rigorous: if two claims conflict, say so; do not average them. Cluster semantically equivalent claims even when the wording differs -- that is your unique value over the lexical dispatcher merge.`,
|
|
80
|
+
format: formatContract(`[
|
|
81
|
+
{
|
|
82
|
+
"claim": "synthesised finding",
|
|
83
|
+
"evidence": "which sources support this and how",
|
|
84
|
+
"source": "codex | gemini | both | inferred",
|
|
85
|
+
"confidence": "high"
|
|
86
|
+
}
|
|
87
|
+
]`),
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
critique: {
|
|
91
|
+
technical: {
|
|
92
|
+
system: `You are a technical adversary. Your role is to find weaknesses in implementation, architecture, and engineering choices. Focus on correctness, scalability, failure modes, and technical debt. Be concrete -- every counter-argument must name a condition under which the weakness manifests.`,
|
|
93
|
+
format: formatContract(`[
|
|
94
|
+
{
|
|
95
|
+
"counterArg": "the specific weakness or challenge",
|
|
96
|
+
"conditions": "the scenario or context under which this weakness applies",
|
|
97
|
+
"mitigation": "how the weakness could be addressed or accepted",
|
|
98
|
+
"severity": "high"
|
|
99
|
+
}
|
|
100
|
+
]`),
|
|
101
|
+
},
|
|
102
|
+
strategic: {
|
|
103
|
+
system: `You are a strategic adversary. Your role is to find weaknesses in positioning, market assumptions, prioritisation, and long-term viability. Focus on adoption risks, competitive landscape, and resource constraints. Be concrete -- every counter-argument must name a condition under which the weakness manifests.`,
|
|
104
|
+
format: formatContract(`[
|
|
105
|
+
{
|
|
106
|
+
"counterArg": "the specific strategic weakness",
|
|
107
|
+
"conditions": "the scenario or context under which this weakness applies",
|
|
108
|
+
"mitigation": "how the weakness could be addressed or accepted",
|
|
109
|
+
"severity": "medium"
|
|
110
|
+
}
|
|
111
|
+
]`),
|
|
112
|
+
},
|
|
113
|
+
ux: {
|
|
114
|
+
system: `You are a UX and adoption adversary. Your role is to find weaknesses in user experience, onboarding, learnability, and real-world adoption. Focus on friction points, mental models, and the gap between what the system does and what users expect. Be concrete -- every counter-argument must name a condition under which the weakness manifests.`,
|
|
115
|
+
format: formatContract(`[
|
|
116
|
+
{
|
|
117
|
+
"counterArg": "the specific UX or adoption weakness",
|
|
118
|
+
"conditions": "the scenario or context under which this weakness applies",
|
|
119
|
+
"mitigation": "how the weakness could be addressed or accepted",
|
|
120
|
+
"severity": "medium"
|
|
121
|
+
}
|
|
122
|
+
]`),
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// getTemplate
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
export function getTemplate(mode, angle) {
|
|
132
|
+
const modeTemplates = TEMPLATES[mode];
|
|
133
|
+
if (!modeTemplates) throw new Error(`Unknown mode: ${mode}. Valid: audit | research | critique`);
|
|
134
|
+
const template = modeTemplates[angle];
|
|
135
|
+
if (!template) {
|
|
136
|
+
const valid = Object.keys(modeTemplates).join(' | ');
|
|
137
|
+
throw new Error(`Unknown angle "${angle}" for mode "${mode}". Valid: ${valid}`);
|
|
138
|
+
}
|
|
139
|
+
return { system: template.system, format: template.format };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// assignRoles
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
// Mode → required angles → preferred auditor ids (in priority order).
|
|
147
|
+
// These are the "natural fit" assignments per PHASE7-PLAN-v2.md.
|
|
148
|
+
const ROLE_PREFERENCES = {
|
|
149
|
+
audit: [{ angle: 'general', preferred: ['codex', 'gemini', 'opencode', 'aider', 'copilot', 'claude'] }],
|
|
150
|
+
research: [
|
|
151
|
+
{ angle: 'benchmarks', preferred: ['codex', 'opencode', 'aider'] },
|
|
152
|
+
{ angle: 'citations', preferred: ['gemini', 'claude', 'copilot'] },
|
|
153
|
+
{ angle: 'synthesis', preferred: ['claude'] }, // always Claude -- see spec
|
|
154
|
+
],
|
|
155
|
+
critique: [
|
|
156
|
+
{ angle: 'technical', preferred: ['codex', 'opencode', 'aider'] },
|
|
157
|
+
{ angle: 'strategic', preferred: ['gemini', 'copilot'] },
|
|
158
|
+
{ angle: 'ux', preferred: ['claude', 'gemini'] },
|
|
159
|
+
],
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export function assignRoles(mode, roster, self) {
|
|
163
|
+
const roleDefs = ROLE_PREFERENCES[mode];
|
|
164
|
+
if (!roleDefs) throw new Error(`Unknown mode: ${mode}`);
|
|
165
|
+
|
|
166
|
+
// roster is an array of auditor id strings that are installed.
|
|
167
|
+
const installed = new Set(Array.isArray(roster) ? roster : []);
|
|
168
|
+
|
|
169
|
+
const roles = [];
|
|
170
|
+
const missing = [];
|
|
171
|
+
|
|
172
|
+
for (const { angle, preferred } of roleDefs) {
|
|
173
|
+
// Synthesis in research always goes to a fresh Claude session --
|
|
174
|
+
// even when self=claude. The spec is explicit: "synthesis is fresh Claude
|
|
175
|
+
// not the caller." So for synthesis we never exclude Claude.
|
|
176
|
+
const isSynthesis = mode === 'research' && angle === 'synthesis';
|
|
177
|
+
|
|
178
|
+
// Critique: caller's own angle is dropped -- they contribute in-session.
|
|
179
|
+
// We still assign it to someone else if possible.
|
|
180
|
+
const assignablePreferred = preferred.filter(id => {
|
|
181
|
+
if (!isSynthesis && id === self) return false;
|
|
182
|
+
return true;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Find first preferred that's installed.
|
|
186
|
+
const pick = assignablePreferred.find(id => installed.has(id));
|
|
187
|
+
|
|
188
|
+
if (pick) {
|
|
189
|
+
roles.push({ auditorId: pick, angle });
|
|
190
|
+
} else {
|
|
191
|
+
// No installed auditor for this angle.
|
|
192
|
+
missing.push({ angle, wanted: assignablePreferred[0] || preferred[0] });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { roles, missing };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// buildRequest
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
export function buildRequest(mode, target, auditorId, angle, priorResponses = null) {
|
|
204
|
+
const { system, format } = getTemplate(mode, angle);
|
|
205
|
+
|
|
206
|
+
const isSynthesis = mode === 'research' && angle === 'synthesis';
|
|
207
|
+
|
|
208
|
+
let priorSection = '';
|
|
209
|
+
if (isSynthesis && priorResponses) {
|
|
210
|
+
priorSection = `
|
|
211
|
+
## Prior research (Phase A -- synthesise across these)
|
|
212
|
+
|
|
213
|
+
${priorResponses}
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return `# IJFW Cross-${capitalise(mode)} Request
|
|
220
|
+
Auditor: ${auditorId}
|
|
221
|
+
Mode: ${mode}
|
|
222
|
+
Angle: ${angle}
|
|
223
|
+
Stamp: ${new Date().toISOString()}
|
|
224
|
+
|
|
225
|
+
## Your role
|
|
226
|
+
|
|
227
|
+
${system}
|
|
228
|
+
|
|
229
|
+
## Operating constraints (mandatory)
|
|
230
|
+
|
|
231
|
+
- You ARE the auditor. Do not delegate this work.
|
|
232
|
+
- Do not shell out, do not invoke other CLIs, do not call gemini/codex/claude/aider/opencode/copilot, do not spawn subagents.
|
|
233
|
+
- Do not attempt to convene additional auditors -- the orchestrator already runs them in parallel.
|
|
234
|
+
- Produce findings inline in the response format below. Nothing else.
|
|
235
|
+
- If the target is clean for your angle, return an empty findings array and say so in the prose.
|
|
236
|
+
|
|
237
|
+
${format}
|
|
238
|
+
${priorSection}
|
|
239
|
+
## Target
|
|
240
|
+
|
|
241
|
+
${target}`.trim();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function capitalise(s) {
|
|
245
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// parseResponse
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
export function parseResponse(_mode, raw) {
|
|
253
|
+
if (typeof raw !== 'string') return { items: [], prose: '' };
|
|
254
|
+
|
|
255
|
+
// Extract first ```json fence.
|
|
256
|
+
const match = raw.match(/```json\s*([\s\S]*?)```/);
|
|
257
|
+
let items = [];
|
|
258
|
+
if (match) {
|
|
259
|
+
try {
|
|
260
|
+
const parsed = JSON.parse(match[1].trim());
|
|
261
|
+
items = Array.isArray(parsed) ? parsed : [];
|
|
262
|
+
} catch {
|
|
263
|
+
items = [];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Prose is everything outside the fence (before + after), trimmed.
|
|
268
|
+
const prose = raw.replace(/```json[\s\S]*?```/, '').trim();
|
|
269
|
+
|
|
270
|
+
return { items, prose };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// scoreRebuttalSurvival
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
// Deterministic structural rubric -- NOT length-based (length bias was a
|
|
278
|
+
// dogfood-critique finding, consensus between Codex + Gemini). Scores by
|
|
279
|
+
// presence of falsifiability signals, actionable mitigation verbs, concrete
|
|
280
|
+
// code-level evidence, and explicit severity tier. Same input → same score.
|
|
281
|
+
const _CONDITION_MARKERS = /\b(when|if|once|under|during|assuming|in ci|in prod|at runtime|in production)\b/i;
|
|
282
|
+
const _MITIGATION_VERBS = /\b(add|implement|replace|route|switch|pin|lock|gate|require|drop|move|rename|enforce|promote|defer|merge|split|refactor|rewrite|extract|parse|validate|sandbox|isolate|audit)\b/i;
|
|
283
|
+
const _CODE_EVIDENCE = /`[^`]+`|\bline \d+|\bcommit [0-9a-f]{6,}|\.(js|md|sh|json|ts|py|mjs|cjs)\b|[A-Za-z_][A-Za-z0-9_.]*\(\)|\bfile:/i;
|
|
284
|
+
|
|
285
|
+
export function scoreRebuttalSurvival(counterArg) {
|
|
286
|
+
if (!counterArg || typeof counterArg !== 'object') return 1;
|
|
287
|
+
const { conditions = '', mitigation = '', counterArg: arg = '', severity = '' } = counterArg;
|
|
288
|
+
|
|
289
|
+
let score = 1;
|
|
290
|
+
if (['high', 'critical'].includes(String(severity).toLowerCase())) score++;
|
|
291
|
+
if (typeof conditions === 'string' && _CONDITION_MARKERS.test(conditions)) score++;
|
|
292
|
+
if (typeof mitigation === 'string' && _MITIGATION_VERBS.test(mitigation)) score++;
|
|
293
|
+
if (typeof arg === 'string' && _CODE_EVIDENCE.test(arg)) score++;
|
|
294
|
+
|
|
295
|
+
return Math.min(5, Math.max(1, score));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// mergeResponses
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
export function mergeResponses(mode, responses) {
|
|
303
|
+
if (mode === 'audit') return mergeAudit(responses);
|
|
304
|
+
if (mode === 'research') return mergeResearch(responses);
|
|
305
|
+
if (mode === 'critique') return mergeCritique(responses);
|
|
306
|
+
throw new Error(`Unknown mode: ${mode}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function mergeAudit(responses) {
|
|
310
|
+
const all = responses.flatMap(r => (r && Array.isArray(r.items) ? r.items : []));
|
|
311
|
+
return all.slice().sort((a, b) => {
|
|
312
|
+
const sa = SEVERITY_ORDER[String(a.severity).toLowerCase()] ?? 99;
|
|
313
|
+
const sb = SEVERITY_ORDER[String(b.severity).toLowerCase()] ?? 99;
|
|
314
|
+
return sa - sb;
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function normaliseClaim(claim) {
|
|
319
|
+
return String(claim).toLowerCase().trim().replace(/\s+/g, ' ');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function mergeResearch(responses) {
|
|
323
|
+
// Lexical clustering only -- exact normalised text match. Semantic clustering
|
|
324
|
+
// (paraphrases, opposing directions) is DELEGATED to the Claude synthesis
|
|
325
|
+
// pass (see research template line: "if two claims conflict, say so"). If
|
|
326
|
+
// the caller has not yet run Phase B, `synthesisPending` flags that the
|
|
327
|
+
// consensus here is lexical-only and the authoritative matrix comes from
|
|
328
|
+
// synthesis. This was M2 in DOGFOOD-CRITIQUE.md -- Codex flagged that exact
|
|
329
|
+
// normalisation misses semantic equivalence; delegating fixes it without
|
|
330
|
+
// baking a similarity heuristic into the dispatcher.
|
|
331
|
+
const hasSynthesis = responses.some(r => r && r.items && r.items.some(i => i && i.synthesis === true));
|
|
332
|
+
|
|
333
|
+
const buckets = new Map();
|
|
334
|
+
responses.forEach((r, auditorIdx) => {
|
|
335
|
+
const items = r && Array.isArray(r.items) ? r.items : [];
|
|
336
|
+
for (const item of items) {
|
|
337
|
+
const key = normaliseClaim(item.claim || '');
|
|
338
|
+
if (!key) continue;
|
|
339
|
+
if (!buckets.has(key)) buckets.set(key, []);
|
|
340
|
+
buckets.get(key).push({ item, auditorIdx });
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const consensus = [];
|
|
345
|
+
const contested = [];
|
|
346
|
+
const unique = {};
|
|
347
|
+
const openQuestions = [];
|
|
348
|
+
|
|
349
|
+
for (const [, entries] of buckets) {
|
|
350
|
+
if (entries.length >= 2) {
|
|
351
|
+
const confidences = new Set(entries.map(e => String(e.item.confidence || '').toLowerCase()));
|
|
352
|
+
const evidences = new Set(entries.map(e => String(e.item.evidence || '').toLowerCase().trim()));
|
|
353
|
+
if (confidences.size > 1 || evidences.size > 1) {
|
|
354
|
+
contested.push(...entries.map(e => e.item));
|
|
355
|
+
} else {
|
|
356
|
+
consensus.push(entries[0].item);
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
const { item, auditorIdx } = entries[0];
|
|
360
|
+
if (!unique[auditorIdx]) unique[auditorIdx] = [];
|
|
361
|
+
unique[auditorIdx].push(item);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return { consensus, contested, unique, openQuestions, synthesisPending: !hasSynthesis };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function mergeCritique(responses) {
|
|
369
|
+
const all = responses.flatMap(r => (r && Array.isArray(r.items) ? r.items : []));
|
|
370
|
+
return all.slice().sort((a, b) => {
|
|
371
|
+
const sa = scoreRebuttalSurvival(a);
|
|
372
|
+
const sb = scoreRebuttalSurvival(b);
|
|
373
|
+
if (sb !== sa) return sb - sa; // DESC survival
|
|
374
|
+
const ra = SEVERITY_ORDER[String(a.severity).toLowerCase()] ?? 99;
|
|
375
|
+
const rb = SEVERITY_ORDER[String(b.severity).toLowerCase()] ?? 99;
|
|
376
|
+
return ra - rb; // DESC severity (lower index = higher sev)
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
// Budget guard (Step 10B.6)
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
// Rough per-token list prices (USD) for each provider family.
|
|
385
|
+
// Used only for pre-flight estimation -- not for billing.
|
|
386
|
+
// Prices are input-side costs at standard rates as of 2026.
|
|
387
|
+
const PROVIDER_PRICE_PER_TOKEN = {
|
|
388
|
+
codex: 0.000_015, // OpenAI o4-mini input ~$15/M
|
|
389
|
+
opencode: 0.000_015,
|
|
390
|
+
aider: 0.000_015,
|
|
391
|
+
gemini: 0.000_000_5, // Gemini 1.5 Flash input ~$0.50/M
|
|
392
|
+
copilot: 0.000_010, // GPT-4o input ~$10/M (conservative)
|
|
393
|
+
claude: 0.000_003, // Sonnet input ~$3/M
|
|
394
|
+
anthropic: 0.000_003,
|
|
395
|
+
};
|
|
396
|
+
const DEFAULT_PRICE_PER_TOKEN = 0.000_010; // fallback for unknown providers
|
|
397
|
+
|
|
398
|
+
// estimateCost(target, picks) -- rough cost in USD for one runCrossOp call.
|
|
399
|
+
// char-count / 4 approximates token count; multiply by provider price.
|
|
400
|
+
export function estimateCost(target, picks) {
|
|
401
|
+
const charCount = typeof target === 'string' ? target.length : 0;
|
|
402
|
+
const tokens = charCount / 4;
|
|
403
|
+
let total = 0;
|
|
404
|
+
for (const pick of picks) {
|
|
405
|
+
const price = PROVIDER_PRICE_PER_TOKEN[pick.id] ?? DEFAULT_PRICE_PER_TOKEN;
|
|
406
|
+
total += tokens * price;
|
|
407
|
+
}
|
|
408
|
+
return total;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// checkBudget({ target, picks, receipts, sessionStart, env }) -- returns null
|
|
412
|
+
// if within budget, or a string error message to emit to stderr before exit 2.
|
|
413
|
+
//
|
|
414
|
+
// Post-flight accumulation only: first call is always allowed; the guard
|
|
415
|
+
// refuses when accumulated prior receipts + estimated next call exceed budget.
|
|
416
|
+
// (first-call surprise is unavoidable -- budget enforces on 2nd+ calls.)
|
|
417
|
+
export function checkBudget({ target, picks, receipts, sessionStart, env = {} }) {
|
|
418
|
+
const raw = env.IJFW_AUDIT_BUDGET_USD;
|
|
419
|
+
const budget = raw !== undefined ? parseFloat(raw) : 2.00;
|
|
420
|
+
if (!isFinite(budget) || budget <= 0) return null; // invalid → no guard
|
|
421
|
+
|
|
422
|
+
// Sum cost_usd from receipts in current session window.
|
|
423
|
+
const accumulated = receipts
|
|
424
|
+
.filter(r => r && r.timestamp && new Date(r.timestamp) >= sessionStart)
|
|
425
|
+
.reduce((sum, r) => sum + (typeof r.cost_usd === 'number' ? r.cost_usd : 0), 0);
|
|
426
|
+
|
|
427
|
+
const estimated = estimateCost(target, picks);
|
|
428
|
+
|
|
429
|
+
if (accumulated + estimated > budget) {
|
|
430
|
+
const fmt = (n) => `$${n.toFixed(2)}`;
|
|
431
|
+
return (
|
|
432
|
+
`Budget ${fmt(budget)} reached (accumulated ${fmt(accumulated)} + next ~${fmt(estimated)}). ` +
|
|
433
|
+
`Raise IJFW_AUDIT_BUDGET_USD to continue.`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
437
|
+
}
|