@goodandready/dsh-moa 0.2.70 → 0.2.71
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/lib/client.js +58 -0
- package/lib/moa-adversarial-tdd.js +219 -0
- package/lib/moa-ast-merge.js +230 -0
- package/lib/moa-schema.js +8 -0
- package/lib/moa-speculative.js +195 -0
- package/lib/moa-workbench.js +6 -0
- package/lib/pipeline/candidates.js +42 -2
- package/lib/pipeline/runner.js +10 -5
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -2055,6 +2055,64 @@ window.__ModuleLoader__.load({
|
|
|
2055
2055
|
"(" + (t("aggregator.workbench_companion_note") || "safe shadow Git checkpoints before file promotion") + ")"
|
|
2056
2056
|
)
|
|
2057
2057
|
),
|
|
2058
|
+
/* AST-Level Composite Merge (#248) */
|
|
2059
|
+
React.createElement(
|
|
2060
|
+
"div",
|
|
2061
|
+
{ style: { display: "flex", flexDirection: "column", gap: 6, padding: "8px 12px", background: "var(--dsw-alias-bg-layer-2)", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2)", marginTop: 8 } },
|
|
2062
|
+
React.createElement(
|
|
2063
|
+
"label",
|
|
2064
|
+
{ style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 13, fontWeight: 500 } },
|
|
2065
|
+
React.createElement("input", {
|
|
2066
|
+
type: "checkbox",
|
|
2067
|
+
checked: Boolean(currentPreset.ast_composite_merge_enabled),
|
|
2068
|
+
onChange: (e) => updateCurrentPreset((p) => ({ ...p, ast_composite_merge_enabled: e.target.checked })),
|
|
2069
|
+
style: { accentColor: "var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))", cursor: "pointer" }
|
|
2070
|
+
}),
|
|
2071
|
+
t("aggregator.ast_merge_title") || "\u{1F333} AST-Level Composite Merge (Semantic Tree Synthesis)"
|
|
2072
|
+
),
|
|
2073
|
+
React.createElement(
|
|
2074
|
+
"div",
|
|
2075
|
+
{ style: { fontSize: 12, color: "var(--dsw-alias-label-secondary)", marginLeft: 22 } },
|
|
2076
|
+
t("aggregator.ast_merge_desc") || "Merge imports, type signatures, JSDoc, and function bodies at the AST declaration level without duplicate import collisions."
|
|
2077
|
+
),
|
|
2078
|
+
React.createElement(
|
|
2079
|
+
"div",
|
|
2080
|
+
{ style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary)", marginLeft: 22, marginTop: 2, display: "flex", alignItems: "center", gap: 6 } },
|
|
2081
|
+
"\u{1F517} Companion Plugin:",
|
|
2082
|
+
React.createElement(
|
|
2083
|
+
"a",
|
|
2084
|
+
{ href: "https://github.com/GooDAnDReaDY/dsh-context-lens", target: "_blank", rel: "noopener noreferrer", style: { color: "var(--dsw-alias-state-brand-primary)", textDecoration: "underline" } },
|
|
2085
|
+
"@goodandready/dsh-context-lens"
|
|
2086
|
+
)
|
|
2087
|
+
)
|
|
2088
|
+
),
|
|
2089
|
+
/* Speculative MoA (#247) & Adversarial TDD Loop (#246) */
|
|
2090
|
+
React.createElement(
|
|
2091
|
+
"div",
|
|
2092
|
+
{ style: { display: "flex", flexDirection: "column", gap: 6, padding: "8px 12px", background: "var(--dsw-alias-bg-layer-2)", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2)", marginTop: 8 } },
|
|
2093
|
+
React.createElement(
|
|
2094
|
+
"label",
|
|
2095
|
+
{ style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 13, fontWeight: 500 } },
|
|
2096
|
+
React.createElement("input", {
|
|
2097
|
+
type: "checkbox",
|
|
2098
|
+
checked: Boolean(currentPreset.speculative_moa_enabled),
|
|
2099
|
+
onChange: (e) => updateCurrentPreset((p) => ({ ...p, speculative_moa_enabled: e.target.checked })),
|
|
2100
|
+
style: { accentColor: "var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))", cursor: "pointer" }
|
|
2101
|
+
}),
|
|
2102
|
+
t("aggregator.speculative_moa_title") || "\u26A1 Speculative MoA (Fast Draft + Multi-Auditor Diffs)"
|
|
2103
|
+
),
|
|
2104
|
+
React.createElement(
|
|
2105
|
+
"label",
|
|
2106
|
+
{ style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 13, fontWeight: 500, marginTop: 4 } },
|
|
2107
|
+
React.createElement("input", {
|
|
2108
|
+
type: "checkbox",
|
|
2109
|
+
checked: Boolean(currentPreset.adversarial_tdd_enabled),
|
|
2110
|
+
onChange: (e) => updateCurrentPreset((p) => ({ ...p, adversarial_tdd_enabled: e.target.checked })),
|
|
2111
|
+
style: { accentColor: "var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))", cursor: "pointer" }
|
|
2112
|
+
}),
|
|
2113
|
+
t("aggregator.adversarial_tdd_title") || "\u{1F6E1}\uFE0F Adversarial TDD Loop (Auto-Generated Red-Team Unit Tests)"
|
|
2114
|
+
)
|
|
2115
|
+
),
|
|
2058
2116
|
/* Multimodal Visual MoA Consensus (#250) */
|
|
2059
2117
|
React.createElement(
|
|
2060
2118
|
"div",
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adversarial TDD Loop with Auto-Generated Unit Test Gate (#246)
|
|
3
|
+
* Generates an autonomous unit test suite (covering edge cases, nullability, and security bounds)
|
|
4
|
+
* from the user prompt via a Red-Team / Test Engineer model, injects the generated test files
|
|
5
|
+
* into each candidate's isolated workspace, executes them via moa-test-gate.js, and formats
|
|
6
|
+
* an objective pass/fail verification report for the Judge panel.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs/promises'
|
|
10
|
+
import fsSync from 'node:fs'
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
import { logger } from './logger.js'
|
|
13
|
+
import { slotLabel } from './moa-prompts.js'
|
|
14
|
+
import { extractFileBlocks } from './workspace/fs.js'
|
|
15
|
+
import { isSafeRelativePath, assertPathContained } from './workspace/security.js'
|
|
16
|
+
import { resolveCandidateDir, runCandidateTestGate } from './moa-test-gate.js'
|
|
17
|
+
|
|
18
|
+
export const RED_TEAM_TDD_SYSTEM_PROMPT = `You are an Adversarial Red-Team Test Engineer for a Mixture-of-Agents coding pipeline.
|
|
19
|
+
Given the user's coding prompt and any existing file signatures, generate a self-contained Node.js unit test file using \`node:test\` and \`node:assert/strict\`.
|
|
20
|
+
Rules:
|
|
21
|
+
1. Output the test file inside a markdown fence with \`path=__moa_adversarial.test.mjs\`.
|
|
22
|
+
2. Include edge-case assertions (empty inputs, null/undefined handling, boundary values, and security/injection guards).
|
|
23
|
+
3. Ensure the test file is syntactically valid ES module code that exits with 0 when all assertions pass and non-zero on failure.`
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generates an adversarial unit test suite from the user prompt.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} params
|
|
29
|
+
* @returns {Promise<{ testFiles: Array<{ relativePath: string, content: string }>, testCommand: string, generatorModel: string }>}
|
|
30
|
+
*/
|
|
31
|
+
export async function generateAdversarialTestSuite({
|
|
32
|
+
userPrompt = '',
|
|
33
|
+
referenceOutputs = [],
|
|
34
|
+
redTeamSlot = null,
|
|
35
|
+
callLlm,
|
|
36
|
+
signal = null,
|
|
37
|
+
}) {
|
|
38
|
+
if (typeof callLlm !== 'function') {
|
|
39
|
+
return { testFiles: [], testCommand: '', generatorModel: 'none' }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Pick a dedicated Red-Team / Test Engineer slot
|
|
43
|
+
const chosenSlot = (redTeamSlot?.provider && redTeamSlot?.model)
|
|
44
|
+
? redTeamSlot
|
|
45
|
+
: (referenceOutputs.find((r) => r?.slot?.role_persona === 'tester')?.slot || referenceOutputs[0]?.slot || { provider: 'default', model: 'default' })
|
|
46
|
+
|
|
47
|
+
const sampleFiles = []
|
|
48
|
+
for (const ref of referenceOutputs) {
|
|
49
|
+
if (!ref?.ok || !Array.isArray(ref.files)) continue
|
|
50
|
+
for (const f of ref.files) {
|
|
51
|
+
const rel = f.relativePath || f.path
|
|
52
|
+
if (rel && !sampleFiles.includes(rel)) sampleFiles.push(rel)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const userMsg = [
|
|
57
|
+
`### User Task Specification:\n${userPrompt}`,
|
|
58
|
+
sampleFiles.length > 0 ? `\n### Target Candidate Files to Test:\n${sampleFiles.map((f) => `- \`${f}\``).join('\n')}` : '',
|
|
59
|
+
`\nGenerate the adversarial unit test file with \`\`\`js path=__moa_adversarial.test.mjs.`,
|
|
60
|
+
].filter(Boolean).join('\n')
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const res = await callLlm({
|
|
64
|
+
provider: chosenSlot.provider,
|
|
65
|
+
model: chosenSlot.model,
|
|
66
|
+
messages: [
|
|
67
|
+
{ role: 'system', content: RED_TEAM_TDD_SYSTEM_PROMPT },
|
|
68
|
+
{ role: 'user', content: userMsg },
|
|
69
|
+
],
|
|
70
|
+
temperature: 0.2,
|
|
71
|
+
maxTokens: 2048,
|
|
72
|
+
signal,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const rawText = typeof res === 'string' ? res : (res?.content || res?.text || '')
|
|
76
|
+
let files = extractFileBlocks(rawText).filter((f) =>
|
|
77
|
+
/\.(test|spec)\.(mjs|js|cjs|ts)$/i.test(f.relativePath || '') || (f.relativePath || '').includes('adversarial')
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if (files.length === 0) {
|
|
81
|
+
const codeMatch = /```(?:js|javascript|mjs)\r?\n([\s\S]*?)```/i.exec(rawText)
|
|
82
|
+
if (codeMatch && codeMatch[1].trim()) {
|
|
83
|
+
files = [{ relativePath: '__moa_adversarial.test.mjs', content: codeMatch[1].trim() + '\n' }]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (files.length === 0) {
|
|
88
|
+
return { testFiles: [], testCommand: '', generatorModel: slotLabel(chosenSlot) }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const primaryTestFile = files[0].relativePath
|
|
92
|
+
return {
|
|
93
|
+
testFiles: files,
|
|
94
|
+
testCommand: `node --test ${primaryTestFile}`,
|
|
95
|
+
generatorModel: slotLabel(chosenSlot),
|
|
96
|
+
}
|
|
97
|
+
} catch (err) {
|
|
98
|
+
logger.warn('[dsh-moa:adversarial-tdd] Failed to generate adversarial unit tests:', err?.message || err)
|
|
99
|
+
return { testFiles: [], testCommand: '', generatorModel: slotLabel(chosenSlot) }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Runs the Adversarial TDD Loop across all candidate workspaces and attaches results.
|
|
105
|
+
*
|
|
106
|
+
* @param {object} params
|
|
107
|
+
* @returns {Promise<{
|
|
108
|
+
* executed: boolean,
|
|
109
|
+
* generatorModel: string,
|
|
110
|
+
* testCommand: string,
|
|
111
|
+
* testFiles: string[],
|
|
112
|
+
* results: Array<{ candidateIndex: number, label: string, passed: boolean, exitCode: number, summary: string }>,
|
|
113
|
+
* directive: string
|
|
114
|
+
* }>}
|
|
115
|
+
*/
|
|
116
|
+
export async function runAdversarialTddLoop({
|
|
117
|
+
cwd,
|
|
118
|
+
runId,
|
|
119
|
+
userPrompt = '',
|
|
120
|
+
referenceOutputs = [],
|
|
121
|
+
preset = {},
|
|
122
|
+
callLlm,
|
|
123
|
+
execFn = null,
|
|
124
|
+
onProgress = null,
|
|
125
|
+
signal = null,
|
|
126
|
+
}) {
|
|
127
|
+
const candidatesWithFiles = (referenceOutputs || []).filter((r) => r.ok && Array.isArray(r.files) && r.files.length > 0)
|
|
128
|
+
if (candidatesWithFiles.length === 0) {
|
|
129
|
+
return { executed: false, generatorModel: 'none', testCommand: '', testFiles: [], results: [], directive: '' }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (typeof onProgress === 'function') {
|
|
133
|
+
onProgress('🛡️ *Adversarial TDD Loop: Generating Red-Team unit test suite from prompt...*\n')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const suite = await generateAdversarialTestSuite({
|
|
137
|
+
userPrompt,
|
|
138
|
+
referenceOutputs: candidatesWithFiles,
|
|
139
|
+
redTeamSlot: preset?.aggregator,
|
|
140
|
+
callLlm,
|
|
141
|
+
signal,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
if (suite.testFiles.length === 0 || !suite.testCommand) {
|
|
145
|
+
return { executed: false, generatorModel: suite.generatorModel, testCommand: '', testFiles: [], results: [], directive: '' }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (typeof onProgress === 'function') {
|
|
149
|
+
onProgress(`🧪 *Adversarial TDD Loop: Executing \`${suite.testCommand}\` against ${candidatesWithFiles.length} candidate workspace(s)...*\n`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const timeoutMs = (typeof preset?.test_gate_timeout_sec === 'number' && preset.test_gate_timeout_sec > 0
|
|
153
|
+
? preset.test_gate_timeout_sec
|
|
154
|
+
: 15) * 1000
|
|
155
|
+
|
|
156
|
+
const results = []
|
|
157
|
+
for (let i = 0; i < referenceOutputs.length; i++) {
|
|
158
|
+
const ref = referenceOutputs[i]
|
|
159
|
+
if (!ref.ok || !Array.isArray(ref.files) || ref.files.length === 0) continue
|
|
160
|
+
const candIdx = ref.index ?? (i + 1)
|
|
161
|
+
const candDir = resolveCandidateDir(cwd, candIdx, { runId })
|
|
162
|
+
if (!candDir || !fsSync.existsSync(candDir)) continue
|
|
163
|
+
|
|
164
|
+
// Materialize generated adversarial test files inside the candidate sandbox
|
|
165
|
+
for (const tf of suite.testFiles) {
|
|
166
|
+
if (!isSafeRelativePath(tf.relativePath)) continue
|
|
167
|
+
const dest = assertPathContained(candDir, tf.relativePath)
|
|
168
|
+
await fs.mkdir(path.dirname(dest), { recursive: true })
|
|
169
|
+
await fs.writeFile(dest, tf.content, 'utf8')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const gateRes = await runCandidateTestGate({
|
|
173
|
+
cwd,
|
|
174
|
+
candidateIndex: candIdx,
|
|
175
|
+
testCommand: suite.testCommand,
|
|
176
|
+
timeoutMs,
|
|
177
|
+
execFn,
|
|
178
|
+
runId,
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// Remove temporary adversarial test files from candidate workspace so they don't get promoted unintentionally
|
|
182
|
+
for (const tf of suite.testFiles) {
|
|
183
|
+
if (!isSafeRelativePath(tf.relativePath)) continue
|
|
184
|
+
try {
|
|
185
|
+
const dest = assertPathContained(candDir, tf.relativePath)
|
|
186
|
+
await fs.rm(dest, { force: true })
|
|
187
|
+
} catch {}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (gateRes) {
|
|
191
|
+
ref.testResult = gateRes
|
|
192
|
+
results.push({
|
|
193
|
+
candidateIndex: candIdx,
|
|
194
|
+
label: ref.label || `Candidate ${candIdx}`,
|
|
195
|
+
passed: Boolean(gateRes.passed),
|
|
196
|
+
exitCode: gateRes.exitCode ?? (gateRes.passed ? 0 : 1),
|
|
197
|
+
summary: gateRes.summary || (gateRes.passed ? 'PASSED' : 'FAILED'),
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const reportLines = [
|
|
203
|
+
`### 🛡️ Adversarial TDD Verification Report (Generated by Red-Team \`${suite.generatorModel}\`):`,
|
|
204
|
+
`Command executed in isolated sandbox: \`${suite.testCommand}\``,
|
|
205
|
+
...results.map(
|
|
206
|
+
(r) => `- **Candidate #${r.candidateIndex} (${r.label})**: **${r.passed ? 'PASSED ✅' : `FAILED ❌ (exit ${r.exitCode})`}** — ${r.summary}`
|
|
207
|
+
),
|
|
208
|
+
'Strongly favor candidates that pass the adversarial Red-Team unit test suite.',
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
executed: results.length > 0,
|
|
213
|
+
generatorModel: suite.generatorModel,
|
|
214
|
+
testCommand: suite.testCommand,
|
|
215
|
+
testFiles: suite.testFiles.map((f) => f.relativePath),
|
|
216
|
+
results,
|
|
217
|
+
directive: results.length > 0 ? reportLines.join('\n') : '',
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-Level Composite Merge Integration via dsh-context-lens (#248)
|
|
3
|
+
* Semantically merges candidate files at the AST/declaration level (deduplicating imports,
|
|
4
|
+
* combining type signatures, JSDoc comments, and function bodies) using dsh-context-lens
|
|
5
|
+
* when available via ctx.reflect / service(ctx, 'contextLens'), with a clean fallback
|
|
6
|
+
* to structured declaration merge and standard string composite directives.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import vm from 'node:vm'
|
|
10
|
+
import { service } from './services.js'
|
|
11
|
+
import { logger } from './logger.js'
|
|
12
|
+
import { buildCompositeBlockDirectives } from './moa-multi-judge.js'
|
|
13
|
+
import { extractFileBlocks } from './workspace/fs.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Checks whether dsh-context-lens service is available on the Cordis context.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveContextLensService(ctx, explicitLens = null) {
|
|
19
|
+
if (explicitLens) return explicitLens
|
|
20
|
+
const fromSvc = service(ctx, 'contextLens')
|
|
21
|
+
if (fromSvc) return fromSvc
|
|
22
|
+
try {
|
|
23
|
+
if (ctx?.reflect && typeof ctx.reflect.get === 'function') {
|
|
24
|
+
const refl = ctx.reflect.get('contextLens') || ctx.reflect.get('dsh-context-lens')
|
|
25
|
+
if (refl) return refl
|
|
26
|
+
}
|
|
27
|
+
} catch {}
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parses top-level imports, declarations, and JSDoc from JS/TS source code.
|
|
33
|
+
*/
|
|
34
|
+
export function parseModuleDeclarations(source = '') {
|
|
35
|
+
const lines = String(source || '').split(/\r?\n/)
|
|
36
|
+
const imports = []
|
|
37
|
+
const bodyLines = []
|
|
38
|
+
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
const trimmed = line.trim()
|
|
41
|
+
if (/^import\s+/.test(trimmed) && (trimmed.endsWith(';') || trimmed.includes(' from '))) {
|
|
42
|
+
imports.push(trimmed.replace(/;$/, ''))
|
|
43
|
+
} else {
|
|
44
|
+
bodyLines.push(line)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const bodyText = bodyLines.join('\n')
|
|
49
|
+
const declarations = new Map()
|
|
50
|
+
const declRegex = /((?:\/\*\*[\s\S]*?\*\/\s*)?)(export\s+(?:async\s+)?(?:function|class|const|let|type|interface)\s+([A-Za-z0-9_$]+)[\s\S]*?)(?=(?:\n(?:\/\*\*[\s\S]*?\*\/\s*)?export\s+(?:async\s+)?(?:function|class|const|let|type|interface)\s+)|$)/g
|
|
51
|
+
let match
|
|
52
|
+
while ((match = declRegex.exec(bodyText)) !== null) {
|
|
53
|
+
const jsdoc = (match[1] || '').trim()
|
|
54
|
+
const code = (match[2] || '').trim()
|
|
55
|
+
const name = match[3]
|
|
56
|
+
if (name) {
|
|
57
|
+
declarations.set(name, { name, jsdoc, code })
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { imports, declarations, rawBody: bodyText.trim() }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Validates JavaScript syntax using node:vm compileFunction (ignoring TypeScript-only syntax).
|
|
66
|
+
*/
|
|
67
|
+
export function isValidJsAstSyntax(code = '', filename = 'module.js') {
|
|
68
|
+
if (/\.tsx?$/i.test(filename)) return true
|
|
69
|
+
const stripped = String(code)
|
|
70
|
+
.replace(/^\s*import\s+[\s\S]*?from\s+['"][^'"]+['"];?\s*$/gm, '')
|
|
71
|
+
.replace(/^\s*import\s+['"][^'"]+['"];?\s*$/gm, '')
|
|
72
|
+
.replace(/^\s*export\s+default\s+/gm, 'const __default_export__ = ')
|
|
73
|
+
.replace(/^\s*export\s+(async\s+function|function|class|const|let|var)\b/gm, '$1')
|
|
74
|
+
.replace(/^\s*export\s*\{[^}]*\};?\s*$/gm, '')
|
|
75
|
+
try {
|
|
76
|
+
new vm.Script(stripped, { filename })
|
|
77
|
+
return true
|
|
78
|
+
} catch {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Merges multiple candidate versions of the same file at declaration/AST level:
|
|
85
|
+
* - Deduplicates all import statements without collisions
|
|
86
|
+
* - Combines the richest JSDoc documentation, type signature, and implementation per symbol
|
|
87
|
+
*/
|
|
88
|
+
export async function mergeCandidateAstFiles(candidates = [], options = {}) {
|
|
89
|
+
const lens = resolveContextLensService(options.ctx, options.contextLens)
|
|
90
|
+
const byPath = new Map()
|
|
91
|
+
|
|
92
|
+
for (const cand of candidates) {
|
|
93
|
+
if (!cand || cand.ok === false) continue
|
|
94
|
+
const files = Array.isArray(cand.files) && cand.files.length > 0
|
|
95
|
+
? cand.files
|
|
96
|
+
: extractFileBlocks(cand.text || '')
|
|
97
|
+
for (const f of files) {
|
|
98
|
+
const rel = f.relativePath || f.path
|
|
99
|
+
if (!rel || !f.content) continue
|
|
100
|
+
if (!byPath.has(rel)) byPath.set(rel, [])
|
|
101
|
+
byPath.get(rel).push({
|
|
102
|
+
candidateIndex: cand.index,
|
|
103
|
+
label: cand.label,
|
|
104
|
+
content: f.content,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const mergedFiles = []
|
|
110
|
+
for (const [relativePath, versions] of byPath.entries()) {
|
|
111
|
+
if (versions.length === 1) {
|
|
112
|
+
mergedFiles.push({
|
|
113
|
+
relativePath,
|
|
114
|
+
content: versions[0].content,
|
|
115
|
+
contributors: [versions[0].label || `Candidate ${versions[0].candidateIndex}`],
|
|
116
|
+
usedContextLens: Boolean(lens),
|
|
117
|
+
})
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// If dsh-context-lens provides a native mergeAst or analyzeAst method, use it first
|
|
122
|
+
if (lens && typeof lens.mergeAst === 'function') {
|
|
123
|
+
try {
|
|
124
|
+
const mergedCode = await lens.mergeAst({
|
|
125
|
+
filePath: relativePath,
|
|
126
|
+
versions: versions.map((v) => v.content),
|
|
127
|
+
})
|
|
128
|
+
if (mergedCode && typeof mergedCode === 'string' && isValidJsAstSyntax(mergedCode, relativePath)) {
|
|
129
|
+
mergedFiles.push({
|
|
130
|
+
relativePath,
|
|
131
|
+
content: mergedCode,
|
|
132
|
+
contributors: versions.map((v) => v.label || `Candidate ${v.candidateIndex}`),
|
|
133
|
+
usedContextLens: true,
|
|
134
|
+
})
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
logger.warn(`[dsh-moa:ast-merge] contextLens.mergeAst failed for ${relativePath}, using declaration AST merger:`, err?.message || err)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Declaration-level AST synthesis: deduplicate imports & merge JSDoc + bodies
|
|
143
|
+
const uniqueImports = new Set()
|
|
144
|
+
const mergedDecls = new Map()
|
|
145
|
+
|
|
146
|
+
for (const ver of versions) {
|
|
147
|
+
const parsed = parseModuleDeclarations(ver.content)
|
|
148
|
+
for (const imp of parsed.imports) uniqueImports.add(imp)
|
|
149
|
+
|
|
150
|
+
for (const [symName, decl] of parsed.declarations.entries()) {
|
|
151
|
+
if (!mergedDecls.has(symName)) {
|
|
152
|
+
mergedDecls.set(symName, { ...decl })
|
|
153
|
+
} else {
|
|
154
|
+
const existing = mergedDecls.get(symName)
|
|
155
|
+
// Keep the richest JSDoc comment from any candidate
|
|
156
|
+
if (decl.jsdoc && decl.jsdoc.length > (existing.jsdoc || '').length) {
|
|
157
|
+
existing.jsdoc = decl.jsdoc
|
|
158
|
+
}
|
|
159
|
+
// Prefer more comprehensive implementation if existing is a short stub
|
|
160
|
+
if (decl.code.length > existing.code.length && existing.code.length < 120) {
|
|
161
|
+
existing.code = decl.code
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let assembled = ''
|
|
168
|
+
if (uniqueImports.size > 0) {
|
|
169
|
+
assembled += Array.from(uniqueImports).map((i) => `${i};`).join('\n') + '\n\n'
|
|
170
|
+
}
|
|
171
|
+
if (mergedDecls.size > 0) {
|
|
172
|
+
assembled += Array.from(mergedDecls.values())
|
|
173
|
+
.map((d) => (d.jsdoc ? `${d.jsdoc}\n${d.code}` : d.code))
|
|
174
|
+
.join('\n\n') + '\n'
|
|
175
|
+
} else {
|
|
176
|
+
assembled = versions[0].content
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!isValidJsAstSyntax(assembled, relativePath)) {
|
|
180
|
+
assembled = versions[0].content
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
mergedFiles.push({
|
|
184
|
+
relativePath,
|
|
185
|
+
content: assembled,
|
|
186
|
+
contributors: versions.map((v) => v.label || `Candidate ${v.candidateIndex}`),
|
|
187
|
+
usedContextLens: Boolean(lens),
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return mergedFiles
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Builds the AST-level composite merge directive for the Judge/Curator.
|
|
196
|
+
* Gracefully falls back to standard buildCompositeBlockDirectives when dsh-context-lens is absent
|
|
197
|
+
* and no multi-version AST declarations are detected.
|
|
198
|
+
*/
|
|
199
|
+
export async function buildAstCompositeMergeDirective(candidates = [], options = {}) {
|
|
200
|
+
const lens = resolveContextLensService(options.ctx, options.contextLens)
|
|
201
|
+
const mergedFiles = await mergeCandidateAstFiles(candidates, options)
|
|
202
|
+
const baseDirective = buildCompositeBlockDirectives(candidates, options)
|
|
203
|
+
|
|
204
|
+
if (!lens && mergedFiles.length === 0) {
|
|
205
|
+
return {
|
|
206
|
+
degraded: true,
|
|
207
|
+
usedContextLens: false,
|
|
208
|
+
mergedFiles: [],
|
|
209
|
+
directive: baseDirective,
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const astBlocks = mergedFiles
|
|
214
|
+
.map((mf) => `#### AST Pre-Merged Preview: \`${mf.relativePath}\` (Contributors: ${mf.contributors.join(', ')})\n\`\`\`js path=${mf.relativePath}\n${mf.content.trim()}\n\`\`\``)
|
|
215
|
+
.join('\n\n')
|
|
216
|
+
|
|
217
|
+
const directive = [
|
|
218
|
+
baseDirective.trim(),
|
|
219
|
+
`\n### 🌳 AST-Level Semantic Composite Merge (${lens ? 'via `dsh-context-lens`' : 'Fallback Declaration Merger'}):`,
|
|
220
|
+
'Imports have been deduplicated and declarations (JSDoc, type signatures, and function bodies) have been aligned across candidates:',
|
|
221
|
+
astBlocks,
|
|
222
|
+
].join('\n\n')
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
degraded: !lens,
|
|
226
|
+
usedContextLens: Boolean(lens),
|
|
227
|
+
mergedFiles,
|
|
228
|
+
directive,
|
|
229
|
+
}
|
|
230
|
+
}
|
package/lib/moa-schema.js
CHANGED
|
@@ -72,6 +72,10 @@ export const PresetSchema = z.object({
|
|
|
72
72
|
self_healing_command: z.string().default("npm test"),
|
|
73
73
|
visual_moa_enabled: z.boolean().default(false),
|
|
74
74
|
elo_ranking_enabled: z.boolean().default(true),
|
|
75
|
+
ast_composite_merge_enabled: z.boolean().default(false),
|
|
76
|
+
speculative_moa_enabled: z.boolean().default(false),
|
|
77
|
+
speculative_draft_model: ModelSlotSchema.default({ provider: "", model: "" }),
|
|
78
|
+
adversarial_tdd_enabled: z.boolean().default(false),
|
|
75
79
|
})
|
|
76
80
|
|
|
77
81
|
export const Config = z.object({
|
|
@@ -90,6 +94,10 @@ export const Config = z.object({
|
|
|
90
94
|
self_healing_command: z.string().default("npm test").volatile(),
|
|
91
95
|
visual_moa_enabled: z.boolean().default(false).volatile(),
|
|
92
96
|
elo_ranking_enabled: z.boolean().default(true).volatile(),
|
|
97
|
+
ast_composite_merge_enabled: z.boolean().default(false).volatile(),
|
|
98
|
+
speculative_moa_enabled: z.boolean().default(false).volatile(),
|
|
99
|
+
speculative_draft_model: ModelSlotSchema.default({ provider: "", model: "" }).volatile(),
|
|
100
|
+
adversarial_tdd_enabled: z.boolean().default(false).volatile(),
|
|
93
101
|
smart_routing_model: ModelSlotSchema.default({ provider: '', model: '' }).volatile(),
|
|
94
102
|
prices: z.dict(PriceRow).default({}).volatile(),
|
|
95
103
|
presets: z.array(PresetSchema).default([
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speculative MoA with Fast-Draft Model and Multi-Auditor Diffs (#247)
|
|
3
|
+
* Generates a rapid initial draft using speculative_draft_model, dispatches the draft
|
|
4
|
+
* to reference models in parallel as diff auditors (producing targeted SEARCH/REPLACE
|
|
5
|
+
* or unified diff hunks), applies valid hunks to the draft, and gracefully falls back
|
|
6
|
+
* to standard parallel MoA if the draft model fails.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { logger } from './logger.js'
|
|
10
|
+
import { slotLabel } from './moa-prompts.js'
|
|
11
|
+
import { estimateTokenCost } from './pricing.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Applies SEARCH/REPLACE or unified diff patches onto a base draft text.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} draftText
|
|
17
|
+
* @param {string} patchText
|
|
18
|
+
* @returns {{ patchedText: string, appliedCount: number }}
|
|
19
|
+
*/
|
|
20
|
+
export function applyAuditorPatchHunks(draftText = '', patchText = '') {
|
|
21
|
+
let result = String(draftText || '')
|
|
22
|
+
let appliedCount = 0
|
|
23
|
+
|
|
24
|
+
// 1. Support SEARCH / REPLACE blocks:
|
|
25
|
+
// <<<<<<< SEARCH
|
|
26
|
+
// ...
|
|
27
|
+
// =======
|
|
28
|
+
// ...
|
|
29
|
+
// >>>>>>> REPLACE
|
|
30
|
+
const searchReplaceRegex = /<{5,9}\s*SEARCH\r?\n([\s\S]*?)\r?\n={5,9}\r?\n([\s\S]*?)\r?\n>{5,9}\s*REPLACE/g
|
|
31
|
+
let srMatch
|
|
32
|
+
while ((srMatch = searchReplaceRegex.exec(patchText)) !== null) {
|
|
33
|
+
const searchBlock = srMatch[1]
|
|
34
|
+
const replaceBlock = srMatch[2]
|
|
35
|
+
if (searchBlock && result.includes(searchBlock)) {
|
|
36
|
+
result = result.replace(searchBlock, replaceBlock)
|
|
37
|
+
appliedCount++
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. Support unified diff hunks (- oldLine / + newLine)
|
|
42
|
+
if (appliedCount === 0 && /```diff[\r\n]+([\s\S]*?)```/i.test(patchText)) {
|
|
43
|
+
const diffMatch = /```diff[\r\n]+([\s\S]*?)```/i.exec(patchText)
|
|
44
|
+
const diffLines = diffMatch[1].split(/\r?\n/)
|
|
45
|
+
const removed = []
|
|
46
|
+
const added = []
|
|
47
|
+
for (const line of diffLines) {
|
|
48
|
+
if (line.startsWith('---') || line.startsWith('+++') || line.startsWith('@@')) continue
|
|
49
|
+
if (line.startsWith('-')) removed.push(line.slice(1))
|
|
50
|
+
else if (line.startsWith('+')) added.push(line.slice(1))
|
|
51
|
+
}
|
|
52
|
+
if (removed.length > 0) {
|
|
53
|
+
const targetChunk = removed.join('\n')
|
|
54
|
+
const replacementChunk = added.join('\n')
|
|
55
|
+
if (result.includes(targetChunk)) {
|
|
56
|
+
result = result.replace(targetChunk, replacementChunk)
|
|
57
|
+
appliedCount++
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { patchedText: result, appliedCount }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Executes Speculative MoA:
|
|
67
|
+
* 1. Calls fast draft model (`speculative_draft_model` or first reference slot).
|
|
68
|
+
* 2. Dispatches draft to reference models in parallel as diff auditors.
|
|
69
|
+
* 3. Applies valid diff hunks onto draft so each auditor produces a refined candidate output.
|
|
70
|
+
* 4. If the draft model fails, returns `{ fallbackToStandard: true }` so the caller runs standard parallel MoA.
|
|
71
|
+
*/
|
|
72
|
+
export async function runSpeculativeMoa(references = [], messages = [], options = {}, callLlm, onProgress) {
|
|
73
|
+
const draftSlot = (options.draftModel?.provider && options.draftModel?.model)
|
|
74
|
+
? options.draftModel
|
|
75
|
+
: references[0]
|
|
76
|
+
|
|
77
|
+
if (!draftSlot || !draftSlot.provider || !draftSlot.model) {
|
|
78
|
+
return { fallbackToStandard: true, reason: 'No valid speculative draft model configured' }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const draftLabel = slotLabel(draftSlot)
|
|
82
|
+
if (typeof onProgress === 'function') {
|
|
83
|
+
onProgress(`⚡ *Speculative MoA: Generating fast initial draft with \`${draftLabel}\`...*\n`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const draftStart = Date.now()
|
|
87
|
+
let draftText = ''
|
|
88
|
+
let draftUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const draftRes = await callLlm({
|
|
92
|
+
provider: draftSlot.provider,
|
|
93
|
+
model: draftSlot.model,
|
|
94
|
+
messages,
|
|
95
|
+
systemPrompt: options.systemPrompt,
|
|
96
|
+
temperature: options.temperature ?? 0.3,
|
|
97
|
+
maxTokens: options.maxTokens ?? 4096,
|
|
98
|
+
signal: options.signal,
|
|
99
|
+
})
|
|
100
|
+
draftText = typeof draftRes === 'string' ? draftRes : (draftRes?.content || draftRes?.text || '')
|
|
101
|
+
if (!draftText || !draftText.trim()) {
|
|
102
|
+
throw new Error('Empty draft response from speculative draft model')
|
|
103
|
+
}
|
|
104
|
+
const rawUsage = (typeof draftRes === 'object' && draftRes?.usage) ? draftRes.usage : {
|
|
105
|
+
inputTokens: 100,
|
|
106
|
+
outputTokens: Math.max(1, Math.round(draftText.length / 4)),
|
|
107
|
+
}
|
|
108
|
+
draftUsage = estimateTokenCost(draftSlot, rawUsage, options.prices || {})
|
|
109
|
+
} catch (err) {
|
|
110
|
+
logger.warn(`[dsh-moa:speculative] Draft model ${draftLabel} failed (${err?.message || err}); falling back to standard parallel MoA.`)
|
|
111
|
+
if (typeof onProgress === 'function') {
|
|
112
|
+
onProgress(`⚠️ *Speculative draft model \`${draftLabel}\` failed; falling back to standard parallel MoA...*\n`)
|
|
113
|
+
}
|
|
114
|
+
return { fallbackToStandard: true, reason: err?.message || String(err) }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const draftDurationMs = Date.now() - draftStart
|
|
118
|
+
if (typeof onProgress === 'function') {
|
|
119
|
+
onProgress(`🔍 *Speculative MoA: Dispatching draft to ${references.length} parallel diff auditor(s)...*\n`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const lastUserPrompt = messages[messages.length - 1]?.content || ''
|
|
123
|
+
const auditTasks = references.map(async (slot, idx) => {
|
|
124
|
+
const candidateIndex = idx + 1
|
|
125
|
+
const label = slotLabel(slot)
|
|
126
|
+
const auditStart = Date.now()
|
|
127
|
+
|
|
128
|
+
const auditPrompt = [
|
|
129
|
+
`You are Speculative Auditor #${candidateIndex} (${label}).`,
|
|
130
|
+
`Review the fast draft below for the user task and provide targeted diff fixes (logic bugs, edge cases, types, security) using SEARCH/REPLACE blocks or \`\`\`diff hunks.`,
|
|
131
|
+
`\n### Original User Task:\n${lastUserPrompt}`,
|
|
132
|
+
`\n### Fast Draft Solution (${draftLabel}):\n${draftText}`,
|
|
133
|
+
`\nOutput your audit notes and any SEARCH/REPLACE or \`\`\`diff blocks needed to perfect the draft.`,
|
|
134
|
+
].join('\n')
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const auditRes = await callLlm({
|
|
138
|
+
provider: slot.provider,
|
|
139
|
+
model: slot.model,
|
|
140
|
+
messages: [{ role: 'user', content: auditPrompt }],
|
|
141
|
+
temperature: 0.2,
|
|
142
|
+
maxTokens: Math.min(options.maxTokens ?? 2048, 2048),
|
|
143
|
+
signal: options.signal,
|
|
144
|
+
})
|
|
145
|
+
const auditText = typeof auditRes === 'string' ? auditRes : (auditRes?.content || auditRes?.text || '')
|
|
146
|
+
const rawAuditUsage = (typeof auditRes === 'object' && auditRes?.usage) ? auditRes.usage : {
|
|
147
|
+
inputTokens: Math.max(1, Math.round(auditPrompt.length / 4)),
|
|
148
|
+
outputTokens: Math.max(1, Math.round(auditText.length / 4)),
|
|
149
|
+
}
|
|
150
|
+
const auditCost = estimateTokenCost(slot, rawAuditUsage, options.prices || {})
|
|
151
|
+
|
|
152
|
+
const { patchedText, appliedCount } = applyAuditorPatchHunks(draftText, auditText)
|
|
153
|
+
const combinedText = appliedCount > 0
|
|
154
|
+
? `${patchedText}\n\n#### Speculative Auditor Diff Notes (${label}, ${appliedCount} hunk(s) applied):\n${auditText}`
|
|
155
|
+
: `${draftText}\n\n#### Speculative Auditor Review (${label}):\n${auditText}`
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
index: candidateIndex,
|
|
159
|
+
slot,
|
|
160
|
+
label,
|
|
161
|
+
text: combinedText,
|
|
162
|
+
speculativeAppliedHunks: appliedCount,
|
|
163
|
+
speculativeDraftModel: draftLabel,
|
|
164
|
+
usage: {
|
|
165
|
+
inputTokens: auditCost.inputTokens + (idx === 0 ? draftUsage.inputTokens : 0),
|
|
166
|
+
outputTokens: auditCost.outputTokens + (idx === 0 ? draftUsage.outputTokens : 0),
|
|
167
|
+
totalTokens: auditCost.totalTokens + (idx === 0 ? draftUsage.totalTokens : 0),
|
|
168
|
+
},
|
|
169
|
+
costUsd: Number((auditCost.costUsd + (idx === 0 ? draftUsage.costUsd : 0)).toFixed(6)),
|
|
170
|
+
durationMs: (Date.now() - auditStart) + (idx === 0 ? draftDurationMs : 0),
|
|
171
|
+
ok: true,
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
return {
|
|
175
|
+
index: candidateIndex,
|
|
176
|
+
slot,
|
|
177
|
+
label,
|
|
178
|
+
text: draftText,
|
|
179
|
+
error: err?.message || String(err),
|
|
180
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
181
|
+
costUsd: 0,
|
|
182
|
+
durationMs: Date.now() - auditStart,
|
|
183
|
+
ok: false,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const referenceOutputs = await Promise.all(auditTasks)
|
|
189
|
+
return {
|
|
190
|
+
fallbackToStandard: false,
|
|
191
|
+
draftModel: draftLabel,
|
|
192
|
+
draftText,
|
|
193
|
+
referenceOutputs,
|
|
194
|
+
}
|
|
195
|
+
}
|
package/lib/moa-workbench.js
CHANGED
|
@@ -51,6 +51,12 @@ export const COMPANION_PLUGINS = [
|
|
|
51
51
|
url: 'https://github.com/GooDAnDReaDY/dsh-live-canvas',
|
|
52
52
|
description: 'Isolated live HTML/CSS/SVG preview environment and screenshot capture',
|
|
53
53
|
},
|
|
54
|
+
{
|
|
55
|
+
id: 'dsh-context-lens',
|
|
56
|
+
name: 'DSH Context Lens',
|
|
57
|
+
url: 'https://github.com/GooDAnDReaDY/dsh-context-lens',
|
|
58
|
+
description: 'Semantic AST parsing and symbol-level composite code merging',
|
|
59
|
+
},
|
|
54
60
|
]
|
|
55
61
|
|
|
56
62
|
/**
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { runSpeculativeMoa } from '../moa-speculative.js'
|
|
2
|
+
import { runAdversarialTddLoop } from '../moa-adversarial-tdd.js'
|
|
1
3
|
import { assignAdaptivePersonas } from '../moa-adaptive-personas.js'
|
|
2
4
|
import { runSubagentSwarm } from '../moa-swarm.js'
|
|
3
5
|
import { runReferencesParallel } from '../moa-candidates.js'
|
|
@@ -37,7 +39,29 @@ export async function stageCandidateRound1({
|
|
|
37
39
|
? assignAdaptivePersonas(effectiveReferences, typeof lastMsgText === 'string' ? lastMsgText : '').references
|
|
38
40
|
: effectiveReferences
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
let referenceOutputs = null
|
|
43
|
+
if (preset?.speculative_moa_enabled && !preset?.subagent_swarm_enabled) {
|
|
44
|
+
const specRes = await runSpeculativeMoa(
|
|
45
|
+
adaptiveRefs,
|
|
46
|
+
enrichedMessages,
|
|
47
|
+
{
|
|
48
|
+
draftModel: preset.speculative_draft_model,
|
|
49
|
+
systemPrompt: candidateSystemPrompt,
|
|
50
|
+
temperature: refTemp,
|
|
51
|
+
maxTokens,
|
|
52
|
+
prices,
|
|
53
|
+
signal,
|
|
54
|
+
},
|
|
55
|
+
callLlm,
|
|
56
|
+
onProgress
|
|
57
|
+
)
|
|
58
|
+
if (!specRes.fallbackToStandard && Array.isArray(specRes.referenceOutputs)) {
|
|
59
|
+
referenceOutputs = specRes.referenceOutputs
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!referenceOutputs) {
|
|
64
|
+
referenceOutputs = preset?.subagent_swarm_enabled
|
|
41
65
|
? await runSubagentSwarm(adaptiveRefs, enrichedMessages, { temperature: refTemp, maxTokens, signal, cwd, runId }, callLlm, onProgress)
|
|
42
66
|
: await runReferencesParallel(
|
|
43
67
|
adaptiveRefs,
|
|
@@ -59,6 +83,7 @@ export async function stageCandidateRound1({
|
|
|
59
83
|
callLlm,
|
|
60
84
|
onProgress
|
|
61
85
|
)
|
|
86
|
+
}
|
|
62
87
|
|
|
63
88
|
if (signal?.aborted) {
|
|
64
89
|
await cleanMoaWorkspaces(cwd, { runId })
|
|
@@ -104,5 +129,20 @@ export async function stageCandidateRound1({
|
|
|
104
129
|
onProgress,
|
|
105
130
|
})
|
|
106
131
|
|
|
107
|
-
|
|
132
|
+
let adversarialTddResult = null
|
|
133
|
+
if (preset?.adversarial_tdd_enabled) {
|
|
134
|
+
adversarialTddResult = await runAdversarialTddLoop({
|
|
135
|
+
cwd,
|
|
136
|
+
runId,
|
|
137
|
+
userPrompt: typeof lastMsgText === "string" ? lastMsgText : "",
|
|
138
|
+
referenceOutputs,
|
|
139
|
+
preset,
|
|
140
|
+
callLlm,
|
|
141
|
+
execFn,
|
|
142
|
+
onProgress,
|
|
143
|
+
signal,
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { ok: true, referenceOutputs, successfulRefs, adversarialTddResult }
|
|
108
148
|
}
|
package/lib/pipeline/runner.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildAstCompositeMergeDirective } from '../moa-ast-merge.js'
|
|
1
2
|
import { runVisualMoaConsensus } from '../moa-visual-consensus.js'
|
|
2
3
|
import crypto from 'node:crypto'
|
|
3
4
|
import { isTestEnvironment } from '../history.js'
|
|
@@ -76,7 +77,7 @@ export async function runMoAPipeline({
|
|
|
76
77
|
})
|
|
77
78
|
return candStage.failurePayload
|
|
78
79
|
}
|
|
79
|
-
const { referenceOutputs, successfulRefs } = candStage
|
|
80
|
+
const { referenceOutputs, successfulRefs, adversarialTddResult } = candStage
|
|
80
81
|
|
|
81
82
|
for (const ref of referenceOutputs) {
|
|
82
83
|
emitEvent(ctx, MOA_EVENTS.CANDIDATE_COMPLETE, {
|
|
@@ -155,9 +156,13 @@ export async function runMoAPipeline({
|
|
|
155
156
|
if (!mjRes.ok) return mjRes.abortPayload
|
|
156
157
|
const multiJudgeResult = mjRes.multiJudgeResult
|
|
157
158
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
:
|
|
159
|
+
let compositeMergeDirective = null
|
|
160
|
+
if (preset?.ast_composite_merge_enabled && successfulRefs.length > 1) {
|
|
161
|
+
const astMerge = await buildAstCompositeMergeDirective(successfulRefs, { ctx, blind: Boolean(preset.blind_evaluation) })
|
|
162
|
+
compositeMergeDirective = astMerge.directive
|
|
163
|
+
} else if (preset?.composite_merge_enabled && successfulRefs.length > 1) {
|
|
164
|
+
compositeMergeDirective = buildCompositeBlockDirectives(successfulRefs, { blind: Boolean(preset.blind_evaluation) })
|
|
165
|
+
}
|
|
161
166
|
let visualResult = null
|
|
162
167
|
if (preset?.visual_moa_enabled) {
|
|
163
168
|
visualResult = await runVisualMoaConsensus({
|
|
@@ -171,7 +176,7 @@ export async function runMoAPipeline({
|
|
|
171
176
|
const rawFileDirective = (preset?.interactive_workbench_enabled)
|
|
172
177
|
? formatFileScoresDirective()
|
|
173
178
|
: null
|
|
174
|
-
const fileScoringDirective = [rawFileDirective, visualResult?.directive].filter(Boolean).join("\n\n") || null
|
|
179
|
+
const fileScoringDirective = [rawFileDirective, visualResult?.directive, adversarialTddResult?.directive].filter(Boolean).join("\n\n") || null
|
|
175
180
|
|
|
176
181
|
// Stage 7: Synthesis Phase
|
|
177
182
|
const synthRes = await stageSynthesis({
|