@goodandready/dsh-moa 0.2.69 → 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,101 @@ 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
+ ),
2116
+ /* Multimodal Visual MoA Consensus (#250) */
2117
+ React.createElement(
2118
+ "div",
2119
+ { 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 } },
2120
+ React.createElement(
2121
+ "label",
2122
+ { style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 13, fontWeight: 500 } },
2123
+ React.createElement("input", {
2124
+ type: "checkbox",
2125
+ checked: Boolean(currentPreset.visual_moa_enabled),
2126
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, visual_moa_enabled: e.target.checked })),
2127
+ style: { accentColor: "var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))", cursor: "pointer" }
2128
+ }),
2129
+ t("aggregator.visual_moa_title") || "\u{1F441}\uFE0F Multimodal Visual MoA Consensus"
2130
+ ),
2131
+ React.createElement(
2132
+ "div",
2133
+ { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary)", marginLeft: 22 } },
2134
+ t("aggregator.visual_moa_desc") || "Render UI candidates in Live Canvas and evaluate visual layout fidelity via Vision Bridge VLM."
2135
+ ),
2136
+ React.createElement(
2137
+ "div",
2138
+ { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary)", marginLeft: 22, marginTop: 2, display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" } },
2139
+ "\u{1F517} Companion Plugins:",
2140
+ React.createElement(
2141
+ "a",
2142
+ { href: "https://github.com/GooDAnDReaDY/dsh-vision-bridge", target: "_blank", rel: "noopener noreferrer", style: { color: "var(--dsw-alias-state-brand-primary)", textDecoration: "underline" } },
2143
+ "@goodandready/dsh-vision-bridge"
2144
+ ),
2145
+ "/",
2146
+ React.createElement(
2147
+ "a",
2148
+ { href: "https://github.com/GooDAnDReaDY/dsh-live-canvas", target: "_blank", rel: "noopener noreferrer", style: { color: "var(--dsw-alias-state-brand-primary)", textDecoration: "underline" } },
2149
+ "@goodandready/dsh-live-canvas"
2150
+ )
2151
+ )
2152
+ ),
2058
2153
  /* Quota-Aware Provider Balancing (#253) */
2059
2154
  React.createElement(
2060
2155
  "div",
@@ -2580,6 +2675,7 @@ window.__ModuleLoader__.load({
2580
2675
  React.createElement("th", { style: { padding: "6px 8px" } }, t("leaderboard.runs")),
2581
2676
  React.createElement("th", { style: { padding: "6px 8px" } }, t("leaderboard.wins")),
2582
2677
  React.createElement("th", { style: { padding: "6px 8px" } }, t("leaderboard.winrate")),
2678
+ React.createElement("th", { style: { padding: "6px 8px" } }, "Elo"),
2583
2679
  React.createElement("th", { style: { padding: "6px 10px" } }, t("leaderboard.cost"))
2584
2680
  )
2585
2681
  ),
@@ -2594,6 +2690,7 @@ window.__ModuleLoader__.load({
2594
2690
  React.createElement("td", { style: { padding: "6px 8px", color: "var(--dsw-alias-label-secondary)" } }, String(m.runs)),
2595
2691
  React.createElement("td", { style: { padding: "6px 8px", color: "var(--dsw-alias-state-success-primary)" } }, String(m.wins)),
2596
2692
  React.createElement("td", { style: { padding: "6px 8px", fontWeight: 600 } }, (m.winRate ?? 0) + "%"),
2693
+ React.createElement("td", { style: { padding: "6px 8px", fontWeight: 600, color: "var(--dsw-alias-state-brand-primary)" } }, String(m.elo ?? 1500)),
2597
2694
  React.createElement("td", { style: { padding: "6px 10px", color: "var(--dsw-alias-label-secondary)" } }, m.avgCostUsd > 0 ? "$" + m.avgCostUsd.toFixed(4) : "Free")
2598
2695
  )
2599
2696
  )
package/lib/history.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { classifyEloCategory, computeEloFromRuns, INITIAL_ELO } from './moa-elo.js'
1
2
  import { logger } from './logger.js'
2
3
  /**
3
4
  * MoA Run History & Analytics Module
@@ -355,6 +356,7 @@ export function getMoaLeaderboard(filePath = getDefaultHistoryFile(), options =
355
356
  _historyCache.totalCount = totalRuns
356
357
 
357
358
  const modelStats = new Map()
359
+ const parsedRuns = []
358
360
  let filteredRuns = 0
359
361
  for (const line of lines) {
360
362
  try {
@@ -362,6 +364,7 @@ export function getMoaLeaderboard(filePath = getDefaultHistoryFile(), options =
362
364
  if (!item) continue
363
365
  if (presetFilter && presetFilter !== 'all' && item.preset !== presetFilter) continue
364
366
  filteredRuns++
367
+ parsedRuns.push(item)
365
368
 
366
369
  const rawWinner = item.winnerModel ? String(item.winnerModel).trim() : null
367
370
  const cleanWinner = rawWinner ? rawWinner.replace(/\s*\[fallback for .*\]$/i, '').trim() : null
@@ -415,13 +418,25 @@ export function getMoaLeaderboard(filePath = getDefaultHistoryFile(), options =
415
418
  }
416
419
  }
417
420
 
421
+ const { globalRatings, categoryRatings } = computeEloFromRuns(parsedRuns)
418
422
  const models = Array.from(modelStats.values())
419
- .map((st) => ({
420
- ...st,
421
- winRate: st.runs > 0 ? Number(((st.wins / st.runs) * 100).toFixed(1)) : 0,
422
- avgTokens: st.runs > 0 ? Math.round(st.totalTokens / st.runs) : 0,
423
- avgCostUsd: st.runs > 0 ? Number((st.totalCostUsd / st.runs).toFixed(4)) : 0,
424
- }))
423
+ .map((st) => {
424
+ const elo = Math.round(globalRatings.get(st.modelKey) ?? INITIAL_ELO)
425
+ const categoryElo = {}
426
+ for (const [cat, cMap] of categoryRatings.entries()) {
427
+ if (cMap.has(st.modelKey)) {
428
+ categoryElo[cat] = Math.round(cMap.get(st.modelKey))
429
+ }
430
+ }
431
+ return {
432
+ ...st,
433
+ elo,
434
+ categoryElo,
435
+ winRate: st.runs > 0 ? Number(((st.wins / st.runs) * 100).toFixed(1)) : 0,
436
+ avgTokens: st.runs > 0 ? Math.round(st.totalTokens / st.runs) : 0,
437
+ avgCostUsd: st.runs > 0 ? Number((st.totalCostUsd / st.runs).toFixed(4)) : 0,
438
+ }
439
+ })
425
440
  .sort((a, b) => b.wins - a.wins || b.winRate - a.winRate)
426
441
 
427
442
  const result = {
@@ -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
+ }