@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 +97 -0
- package/lib/history.js +21 -6
- package/lib/moa-adversarial-tdd.js +219 -0
- package/lib/moa-ast-merge.js +230 -0
- package/lib/moa-elo.js +189 -0
- package/lib/moa-router.js +27 -9
- package/lib/moa-schema.js +12 -0
- package/lib/moa-speculative.js +195 -0
- package/lib/moa-visual-consensus.js +187 -0
- package/lib/moa-workbench.js +18 -0
- package/lib/pipeline/candidates.js +42 -2
- package/lib/pipeline/promotion.js +4 -2
- package/lib/pipeline/runner.js +23 -6
- package/package.json +1 -1
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
+
}
|