@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 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
+ }
@@ -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
- const referenceOutputs = preset?.subagent_swarm_enabled
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
- return { ok: true, referenceOutputs, successfulRefs }
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
  }
@@ -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
- const compositeMergeDirective = (preset?.composite_merge_enabled && successfulRefs.length > 1)
159
- ? buildCompositeBlockDirectives(successfulRefs, { blind: Boolean(preset.blind_evaluation) })
160
- : null
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({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.70",
3
+ "version": "0.2.71",
4
4
  "description": "Mixture of Agents (MoA) plugin for DeepSeek Harness with /moa slash command",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",