@goodandready/dsh-moa 0.2.69 → 0.2.70

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,43 @@ window.__ModuleLoader__.load({
2055
2055
  "(" + (t("aggregator.workbench_companion_note") || "safe shadow Git checkpoints before file promotion") + ")"
2056
2056
  )
2057
2057
  ),
2058
+ /* Multimodal Visual MoA Consensus (#250) */
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.visual_moa_enabled),
2068
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, visual_moa_enabled: e.target.checked })),
2069
+ style: { accentColor: "var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))", cursor: "pointer" }
2070
+ }),
2071
+ t("aggregator.visual_moa_title") || "\u{1F441}\uFE0F Multimodal Visual MoA Consensus"
2072
+ ),
2073
+ React.createElement(
2074
+ "div",
2075
+ { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary)", marginLeft: 22 } },
2076
+ t("aggregator.visual_moa_desc") || "Render UI candidates in Live Canvas and evaluate visual layout fidelity via Vision Bridge VLM."
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, flexWrap: "wrap" } },
2081
+ "\u{1F517} Companion Plugins:",
2082
+ React.createElement(
2083
+ "a",
2084
+ { href: "https://github.com/GooDAnDReaDY/dsh-vision-bridge", target: "_blank", rel: "noopener noreferrer", style: { color: "var(--dsw-alias-state-brand-primary)", textDecoration: "underline" } },
2085
+ "@goodandready/dsh-vision-bridge"
2086
+ ),
2087
+ "/",
2088
+ React.createElement(
2089
+ "a",
2090
+ { href: "https://github.com/GooDAnDReaDY/dsh-live-canvas", target: "_blank", rel: "noopener noreferrer", style: { color: "var(--dsw-alias-state-brand-primary)", textDecoration: "underline" } },
2091
+ "@goodandready/dsh-live-canvas"
2092
+ )
2093
+ )
2094
+ ),
2058
2095
  /* Quota-Aware Provider Balancing (#253) */
2059
2096
  React.createElement(
2060
2097
  "div",
@@ -2580,6 +2617,7 @@ window.__ModuleLoader__.load({
2580
2617
  React.createElement("th", { style: { padding: "6px 8px" } }, t("leaderboard.runs")),
2581
2618
  React.createElement("th", { style: { padding: "6px 8px" } }, t("leaderboard.wins")),
2582
2619
  React.createElement("th", { style: { padding: "6px 8px" } }, t("leaderboard.winrate")),
2620
+ React.createElement("th", { style: { padding: "6px 8px" } }, "Elo"),
2583
2621
  React.createElement("th", { style: { padding: "6px 10px" } }, t("leaderboard.cost"))
2584
2622
  )
2585
2623
  ),
@@ -2594,6 +2632,7 @@ window.__ModuleLoader__.load({
2594
2632
  React.createElement("td", { style: { padding: "6px 8px", color: "var(--dsw-alias-label-secondary)" } }, String(m.runs)),
2595
2633
  React.createElement("td", { style: { padding: "6px 8px", color: "var(--dsw-alias-state-success-primary)" } }, String(m.wins)),
2596
2634
  React.createElement("td", { style: { padding: "6px 8px", fontWeight: 600 } }, (m.winRate ?? 0) + "%"),
2635
+ React.createElement("td", { style: { padding: "6px 8px", fontWeight: 600, color: "var(--dsw-alias-state-brand-primary)" } }, String(m.elo ?? 1500)),
2597
2636
  React.createElement("td", { style: { padding: "6px 10px", color: "var(--dsw-alias-label-secondary)" } }, m.avgCostUsd > 0 ? "$" + m.avgCostUsd.toFixed(4) : "Free")
2598
2637
  )
2599
2638
  )
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 = {
package/lib/moa-elo.js ADDED
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Empirical Elo Rating and Active Learning Leaderboard by Task Category (#249)
3
+ * Calculates pairwise Elo ratings (K-factor = 32, baseline = 1500) segmented by
4
+ * task category (frontend, backend, refactoring, testing, sql, general),
5
+ * and exposes category leaders to Smart Routing.
6
+ */
7
+
8
+ export const INITIAL_ELO = 1500
9
+ export const DEFAULT_K_FACTOR = 32
10
+
11
+ const CATEGORY_PATTERNS = [
12
+ { category: 'sql', regex: /\b(sql|postgres|sqlite|mysql|query|database|schema|migration|prisma|index|table)\b/i },
13
+ { category: 'frontend', regex: /\b(frontend|ui|css|html|react|vue|tailwind|component|button|layout|modal|svg|dom)\b/i },
14
+ { category: 'testing', regex: /\b(test|unit\s*test|e2e|vitest|jest|playwright|assert|coverage|mock|tdd)\b/i },
15
+ { category: 'refactoring', regex: /\b(refactor|clean|simplify|dedup|modular|rename|extract|decompose|yagni)\b/i },
16
+ { category: 'backend', regex: /\b(backend|api|endpoint|server|route|auth|jwt|middleware|express|http|rest|webhook|stream)\b/i },
17
+ ]
18
+
19
+ /**
20
+ * Classifies a prompt into one of the standard Elo leaderboard categories.
21
+ *
22
+ * @param {string} prompt
23
+ * @returns {'frontend' | 'backend' | 'refactoring' | 'testing' | 'sql' | 'general'}
24
+ */
25
+ export function classifyEloCategory(prompt = '') {
26
+ const text = String(prompt || '').trim()
27
+ if (!text) return 'general'
28
+ for (const rule of CATEGORY_PATTERNS) {
29
+ if (rule.regex.test(text)) return rule.category
30
+ }
31
+ return 'general'
32
+ }
33
+
34
+ /**
35
+ * Calculates expected win probability of player A against player B.
36
+ */
37
+ export function expectedEloScore(ratingA, ratingB) {
38
+ return 1 / (1 + Math.pow(10, (ratingB - ratingA) / 400))
39
+ }
40
+
41
+ /**
42
+ * Updates pairwise Elo ratings when winnerKey beats all other participantKeys in a round.
43
+ *
44
+ * @param {Map<string, number>} ratingMap
45
+ * @param {string} winnerKey
46
+ * @param {string[]} participantKeys
47
+ * @param {number} [kFactor=32]
48
+ */
49
+ export function applyRoundEloUpdate(ratingMap, winnerKey, participantKeys = [], kFactor = DEFAULT_K_FACTOR) {
50
+ if (!winnerKey || participantKeys.length < 2) return
51
+ const opponents = participantKeys.filter((k) => k && k !== winnerKey)
52
+ if (opponents.length === 0) return
53
+
54
+ // Normalize K-factor across the number of pairwise comparisons in the multi-candidate round
55
+ const effectiveK = kFactor / opponents.length
56
+ let winnerRating = ratingMap.get(winnerKey) ?? INITIAL_ELO
57
+ let winnerDelta = 0
58
+
59
+ for (const oppKey of opponents) {
60
+ const oppRating = ratingMap.get(oppKey) ?? INITIAL_ELO
61
+ const expWin = expectedEloScore(winnerRating, oppRating)
62
+ const expOpp = 1 - expWin
63
+
64
+ winnerDelta += effectiveK * (1 - expWin)
65
+ const nextOpp = oppRating + effectiveK * (0 - expOpp)
66
+ ratingMap.set(oppKey, nextOpp)
67
+ }
68
+
69
+ ratingMap.set(winnerKey, winnerRating + winnerDelta)
70
+ }
71
+
72
+ /**
73
+ * Computes global and per-category Elo ratings across a chronological list of MoA runs.
74
+ *
75
+ * @param {Array<object>} runs - Array of history run objects (oldest to newest)
76
+ * @param {number} [kFactor=32]
77
+ * @returns {{
78
+ * globalRatings: Map<string, number>,
79
+ * categoryRatings: Map<string, Map<string, number>>
80
+ * }}
81
+ */
82
+ export function computeEloFromRuns(runs = [], kFactor = DEFAULT_K_FACTOR) {
83
+ const globalRatings = new Map()
84
+ const categoryRatings = new Map()
85
+
86
+ for (const run of runs) {
87
+ if (!run || !Array.isArray(run.candidates) || run.candidates.length === 0) continue
88
+
89
+ const category = run.category || classifyEloCategory(run.prompt || '')
90
+ if (!categoryRatings.has(category)) {
91
+ categoryRatings.set(category, new Map())
92
+ }
93
+ const catMap = categoryRatings.get(category)
94
+
95
+ const participantKeys = []
96
+ let winnerKey = null
97
+
98
+ const rawWinner = run.winnerModel ? String(run.winnerModel).trim() : null
99
+ const cleanWinner = rawWinner ? rawWinner.replace(/\s*\[fallback for .*\]$/i, '').trim() : null
100
+ const normWinner = cleanWinner && cleanWinner !== 'none' && cleanWinner !== 'unknown'
101
+ ? cleanWinner.replace(/[\/:]/g, ':').toLowerCase()
102
+ : null
103
+
104
+ for (let idx = 0; idx < run.candidates.length; idx++) {
105
+ const cand = run.candidates[idx]
106
+ const key = cand.provider && cand.model ? `${cand.provider}:${cand.model}` : (cand.model || 'unknown')
107
+ participantKeys.push(key)
108
+ if (!globalRatings.has(key)) globalRatings.set(key, INITIAL_ELO)
109
+ if (!catMap.has(key)) catMap.set(key, INITIAL_ELO)
110
+
111
+ if (!winnerKey) {
112
+ if (normWinner) {
113
+ const normKey = key.replace(/[\/:]/g, ':').toLowerCase()
114
+ const normModel = String(cand.model || '').replace(/[\/:]/g, ':').toLowerCase()
115
+ if (normWinner === normKey || normWinner === normModel || normWinner.endsWith(`:${normModel}`)) {
116
+ winnerKey = key
117
+ }
118
+ }
119
+ if (!winnerKey && typeof run.winnerIndex === 'number' && run.winnerIndex > 0) {
120
+ if (cand.index === run.winnerIndex || idx === run.winnerIndex - 1) {
121
+ winnerKey = key
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ if (winnerKey && participantKeys.length >= 2) {
128
+ applyRoundEloUpdate(globalRatings, winnerKey, participantKeys, kFactor)
129
+ applyRoundEloUpdate(catMap, winnerKey, participantKeys, kFactor)
130
+ }
131
+ }
132
+
133
+ return { globalRatings, categoryRatings }
134
+ }
135
+
136
+ /**
137
+ * Selects the best preset for a prompt category based on historical Elo ratings of its reference models.
138
+ *
139
+ * @param {string} prompt
140
+ * @param {Array<object>} presets
141
+ * @param {object} leaderboard - Result from getMoaLeaderboard()
142
+ * @returns {{ presetName: string, category: string, avgElo: number } | null}
143
+ */
144
+ export function selectBestPresetByElo(prompt = '', presets = [], leaderboard = null) {
145
+ if (!leaderboard || !Array.isArray(leaderboard.models) || leaderboard.models.length === 0) return null
146
+ if (!Array.isArray(presets) || presets.length === 0) return null
147
+
148
+ const category = classifyEloCategory(prompt)
149
+ const modelMap = new Map()
150
+ for (const m of leaderboard.models) {
151
+ modelMap.set(m.modelKey, m)
152
+ }
153
+
154
+ let bestPreset = null
155
+ let bestElo = -Infinity
156
+
157
+ for (const preset of presets) {
158
+ if (preset.enabled === false) continue
159
+ const refs = Array.isArray(preset.reference_models) ? preset.reference_models : []
160
+ if (refs.length === 0) continue
161
+
162
+ let sumElo = 0
163
+ let matchedCount = 0
164
+ for (const ref of refs) {
165
+ const key = ref.provider && ref.model ? `${ref.provider}:${ref.model}` : ref.model
166
+ const st = modelMap.get(key)
167
+ if (st) {
168
+ const catElo = st.categoryElo?.[category] ?? st.elo ?? INITIAL_ELO
169
+ sumElo += catElo
170
+ matchedCount++
171
+ }
172
+ }
173
+
174
+ if (matchedCount > 0) {
175
+ const avgElo = sumElo / matchedCount
176
+ if (avgElo > bestElo && avgElo > INITIAL_ELO) {
177
+ bestElo = avgElo
178
+ bestPreset = preset.name
179
+ }
180
+ }
181
+ }
182
+
183
+ if (!bestPreset) return null
184
+ return {
185
+ presetName: bestPreset,
186
+ category,
187
+ avgElo: Math.round(bestElo),
188
+ }
189
+ }
package/lib/moa-router.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { selectBestPresetByElo } from './moa-elo.js'
1
2
  /**
2
3
  * Smart Preset Router for Mixture of Agents (MoA).
3
4
  * Supports heuristic keyword classification and optional LLM zero-shot routing.
@@ -32,15 +33,18 @@ export function classifyPromptIntent(prompt = '') {
32
33
  /**
33
34
  * Resolves the optimal preset for a given user prompt.
34
35
  */
35
- export async function resolvePresetForPrompt({
36
- prompt = '',
37
- presets = [],
38
- defaultPreset = 'default',
39
- routingModel = null,
40
- callLlm = null,
41
- enabled = false,
42
- timeoutMs = 2000,
43
- }) {
36
+ export async function resolvePresetForPrompt(arg1 = {}, arg2, arg3, arg4 = {}) {
37
+ const isPositional = typeof arg1 === "string"
38
+ const prompt = isPositional ? arg1 : (arg1.prompt || "")
39
+ const presets = isPositional ? (arg2 || []) : (arg1.presets || [])
40
+ const defaultPreset = isPositional ? (arg3 || "default") : (arg1.defaultPreset || "default")
41
+ const opts = isPositional ? (arg4 || {}) : arg1
42
+ const routingModel = opts.routingModel || null
43
+ const callLlm = opts.callLlm || null
44
+ const enabled = Boolean(opts.enabled ?? opts.smartRoutingEnabled ?? false)
45
+ const timeoutMs = opts.timeoutMs ?? 2000
46
+ const leaderboard = opts.leaderboard || null
47
+ const eloRankingEnabled = opts.eloRankingEnabled !== false
44
48
  const availableNames = (presets || []).map((p) => p.name).filter(Boolean)
45
49
  const safeDefault = availableNames.includes(defaultPreset) ? defaultPreset : (availableNames[0] || 'default')
46
50
 
@@ -87,6 +91,20 @@ Respond with ONLY the chosen preset name and nothing else.`
87
91
  }
88
92
  }
89
93
 
94
+ // 1.5. Active Learning Elo Leaderboard routing (#249)
95
+ if (leaderboard && eloRankingEnabled) {
96
+ const eloChoice = selectBestPresetByElo(prompt, presets, leaderboard)
97
+ if (eloChoice && availableNames.includes(eloChoice.presetName)) {
98
+ return {
99
+ presetName: eloChoice.presetName,
100
+ reason: "elo_active_learning",
101
+ category: eloChoice.category,
102
+ avgElo: eloChoice.avgElo,
103
+ isAutoRouted: true,
104
+ }
105
+ }
106
+ }
107
+
90
108
  // 2. Keyword heuristic classifier
91
109
  const heuristic = classifyPromptIntent(prompt)
92
110
  if (heuristic && availableNames.includes(heuristic)) {
package/lib/moa-schema.js CHANGED
@@ -70,6 +70,8 @@ export const PresetSchema = z.object({
70
70
  self_healing_enabled: z.boolean().default(false),
71
71
  self_healing_max_retries: z.number().default(2),
72
72
  self_healing_command: z.string().default("npm test"),
73
+ visual_moa_enabled: z.boolean().default(false),
74
+ elo_ranking_enabled: z.boolean().default(true),
73
75
  })
74
76
 
75
77
  export const Config = z.object({
@@ -86,6 +88,8 @@ export const Config = z.object({
86
88
  self_healing_enabled: z.boolean().default(false).volatile(),
87
89
  self_healing_max_retries: z.number().default(2).volatile(),
88
90
  self_healing_command: z.string().default("npm test").volatile(),
91
+ visual_moa_enabled: z.boolean().default(false).volatile(),
92
+ elo_ranking_enabled: z.boolean().default(true).volatile(),
89
93
  smart_routing_model: ModelSlotSchema.default({ provider: '', model: '' }).volatile(),
90
94
  prices: z.dict(PriceRow).default({}).volatile(),
91
95
  presets: z.array(PresetSchema).default([
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Multimodal Visual MoA Consensus via dsh-vision-bridge and dsh-live-canvas (#250)
3
+ * Renders UI/Frontend candidates (HTML, React, CSS, SVG) via dsh-live-canvas,
4
+ * captures headless screenshots, and evaluates layout/responsiveness via dsh-vision-bridge.
5
+ * Gracefully degrades to text evaluation when companion plugins are unavailable.
6
+ */
7
+
8
+ import { service } from './services.js'
9
+ import { logger } from './logger.js'
10
+ import { slotLabel } from './moa-prompts.js'
11
+
12
+ const UI_TASK_REGEX = /\b(html|css|svg|ui|frontend|react|vue|tailwind|layout|landing|page|component|button|modal|responsive|card|dashboard|widget)\b/i
13
+
14
+ /**
15
+ * Checks whether a prompt or candidate output involves UI/Frontend rendering.
16
+ */
17
+ export function isVisualUiTask(prompt = '', referenceOutputs = []) {
18
+ if (UI_TASK_REGEX.test(String(prompt || ''))) return true
19
+ for (const ref of referenceOutputs || []) {
20
+ const files = Array.isArray(ref?.files) ? ref.files : []
21
+ if (files.some((f) => /\.(html?|css|svg|jsx|tsx|vue)$/i.test(f.relativePath || f.path || ''))) {
22
+ return true
23
+ }
24
+ }
25
+ return false
26
+ }
27
+
28
+ /**
29
+ * Extracts renderable HTML/SVG preview markup from a candidate output.
30
+ */
31
+ export function extractRenderableMarkup(candidate = {}) {
32
+ const files = Array.isArray(candidate.files) ? candidate.files : []
33
+ const htmlFile = files.find((f) => /\.(html?|svg)$/i.test(f.relativePath || f.path || ''))
34
+ if (htmlFile?.content) {
35
+ return { title: htmlFile.relativePath || 'index.html', content: htmlFile.content }
36
+ }
37
+
38
+ const cssFile = files.find((f) => /\.css$/i.test(f.relativePath || f.path || ''))
39
+ const jsFile = files.find((f) => /\.(jsx?|tsx?)$/i.test(f.relativePath || f.path || ''))
40
+ if (cssFile || jsFile) {
41
+ const html = [
42
+ '<!DOCTYPE html><html><head><meta charset="utf-8">',
43
+ cssFile ? `<style>${cssFile.content}</style>` : '',
44
+ '</head><body><div id="app">UI Preview</div>',
45
+ '</body></html>',
46
+ ].join('\n')
47
+ return { title: 'synthesized-preview.html', content: html }
48
+ }
49
+
50
+ const text = String(candidate.text || '')
51
+ const fenceMatch = /```(?:html|svg)\s*([\s\S]*?)```/i.exec(text)
52
+ if (fenceMatch) {
53
+ return { title: 'inline-preview.html', content: fenceMatch[1].trim() }
54
+ }
55
+ return null
56
+ }
57
+
58
+ /**
59
+ * Evaluates candidates visually using dsh-live-canvas and dsh-vision-bridge.
60
+ * Returns { degraded: true } if companion services are absent so the caller falls back cleanly.
61
+ *
62
+ * @param {object} params
63
+ * @returns {Promise<{
64
+ * degraded: boolean,
65
+ * reason?: string,
66
+ * visualScores: Array<{ candidateIndex: number, label: string, visualScore: number, critique: string, previewUrl: string | null }>,
67
+ * directive: string
68
+ * }>}
69
+ */
70
+ export async function runVisualMoaConsensus({
71
+ ctx = null,
72
+ liveCanvas = null,
73
+ visionBridge = null,
74
+ userPrompt = '',
75
+ referenceOutputs = [],
76
+ onProgress = null,
77
+ }) {
78
+ const resolvedCanvas = liveCanvas || service(ctx, 'liveCanvas')
79
+ const resolvedVision = visionBridge || service(ctx, 'visionBridge')
80
+
81
+ if (!resolvedCanvas && !resolvedVision) {
82
+ return {
83
+ degraded: true,
84
+ reason: 'Companion plugins dsh-live-canvas and dsh-vision-bridge are not active; using standard text evaluation.',
85
+ visualScores: [],
86
+ directive: '',
87
+ }
88
+ }
89
+
90
+ if (typeof onProgress === 'function') {
91
+ onProgress('👁️ *Visual MoA Consensus: Rendering UI candidates in Live Canvas & evaluating via Vision Bridge...*\n')
92
+ }
93
+
94
+ const visualScores = []
95
+ for (let i = 0; i < referenceOutputs.length; i++) {
96
+ const ref = referenceOutputs[i]
97
+ if (!ref || ref.ok === false) continue
98
+ const idx = ref.index ?? (i + 1)
99
+ const label = ref.label || slotLabel(ref.slot || ref)
100
+ const markup = extractRenderableMarkup(ref)
101
+ if (!markup) continue
102
+
103
+ let previewUrl = null
104
+ let screenshot = null
105
+
106
+ if (resolvedCanvas) {
107
+ try {
108
+ if (typeof resolvedCanvas.createPreviewFromContent === 'function') {
109
+ const prev = await resolvedCanvas.createPreviewFromContent({
110
+ content: markup.content,
111
+ title: `Candidate #${idx} (${label}) - ${markup.title}`,
112
+ })
113
+ previewUrl = prev?.previewUrl || null
114
+ screenshot = prev?.screenshot || prev?.imageBase64 || null
115
+ }
116
+ if (!screenshot && typeof resolvedCanvas.captureScreenshot === 'function') {
117
+ screenshot = await resolvedCanvas.captureScreenshot({
118
+ content: markup.content,
119
+ previewUrl,
120
+ })
121
+ }
122
+ } catch (err) {
123
+ logger.warn(`[dsh-moa:visual] Live Canvas preview failed for candidate #${idx}:`, err?.message || err)
124
+ }
125
+ }
126
+
127
+ let visualScore = 7.5
128
+ let critique = 'Rendered cleanly in Live Canvas preview.'
129
+
130
+ if (resolvedVision) {
131
+ try {
132
+ const evalFn = resolvedVision.evaluateVisualLayout || resolvedVision.analyzeImage || resolvedVision.inspectUi
133
+ if (typeof evalFn === 'function') {
134
+ const vRes = await evalFn.call(resolvedVision, {
135
+ prompt: userPrompt,
136
+ candidateIndex: idx,
137
+ html: markup.content,
138
+ previewUrl,
139
+ screenshot,
140
+ })
141
+ if (vRes) {
142
+ if (Number.isFinite(Number(vRes.score))) {
143
+ visualScore = Math.max(1, Math.min(10, Number(vRes.score)))
144
+ }
145
+ if (vRes.critique || vRes.summary || vRes.feedback) {
146
+ critique = String(vRes.critique || vRes.summary || vRes.feedback).trim()
147
+ }
148
+ }
149
+ }
150
+ } catch (err) {
151
+ logger.warn(`[dsh-moa:visual] Vision Bridge evaluation failed for candidate #${idx}:`, err?.message || err)
152
+ }
153
+ }
154
+
155
+ visualScores.push({
156
+ candidateIndex: idx,
157
+ label,
158
+ visualScore: Number(visualScore.toFixed(1)),
159
+ critique,
160
+ previewUrl,
161
+ })
162
+ }
163
+
164
+ if (visualScores.length === 0) {
165
+ return {
166
+ degraded: true,
167
+ reason: 'No renderable HTML/CSS/SVG artifacts found in candidates.',
168
+ visualScores: [],
169
+ directive: '',
170
+ }
171
+ }
172
+
173
+ const lines = [
174
+ '### 👁️ Multimodal Visual MoA Consensus (`dsh-live-canvas` + `dsh-vision-bridge`):',
175
+ 'The following UI candidates were rendered and visually inspected for layout, responsiveness, and design fidelity:',
176
+ ...visualScores.map(
177
+ (v) => `- **Candidate #${v.candidateIndex} (${v.label})**: Visual Score **${v.visualScore}/10** — ${v.critique}${v.previewUrl ? ` ([Live Preview](${v.previewUrl}))` : ''}`
178
+ ),
179
+ 'Prioritize candidates with higher visual layout fidelity and zero rendering defects.',
180
+ ]
181
+
182
+ return {
183
+ degraded: false,
184
+ visualScores,
185
+ directive: lines.join('\n'),
186
+ }
187
+ }
@@ -39,6 +39,18 @@ export const COMPANION_PLUGINS = [
39
39
  url: 'https://github.com/GooDAnDReaDY/dsh-subscriptions',
40
40
  description: 'Subscription tier and quota tracking for LLM providers',
41
41
  },
42
+ {
43
+ id: 'dsh-vision-bridge',
44
+ name: 'DSH Vision Bridge',
45
+ url: 'https://github.com/GooDAnDReaDY/dsh-vision-bridge',
46
+ description: 'Multimodal VLM visual evaluation for UI/Frontend artifacts',
47
+ },
48
+ {
49
+ id: 'dsh-live-canvas',
50
+ name: 'DSH Live Canvas',
51
+ url: 'https://github.com/GooDAnDReaDY/dsh-live-canvas',
52
+ description: 'Isolated live HTML/CSS/SVG preview environment and screenshot capture',
53
+ },
42
54
  ]
43
55
 
44
56
  /**
@@ -1,3 +1,4 @@
1
+ import { classifyEloCategory } from '../moa-elo.js'
1
2
  import { runSelfHealingLoop } from '../moa-self-healing.js'
2
3
  import { logger } from '../logger.js'
3
4
  import { parseWinnerIndex, parseRecommendedAssembler } from '../moa-parser.js'
@@ -37,6 +38,7 @@ export async function stagePromotion({
37
38
  callLlm = null,
38
39
  onProgress = null,
39
40
  execFn = null,
41
+ visualResult = null,
40
42
  }) {
41
43
  const winningIndex = (multiJudgeResult?.consensus && multiJudgeResult?.winningCandidateIndex)
42
44
  ? multiJudgeResult.winningCandidateIndex
@@ -122,7 +124,7 @@ export async function stagePromotion({
122
124
 
123
125
  try {
124
126
  await recordMoaRunAsync({
125
- id: runId, cwd, prompt: userPrompt, preset: preset?.name || 'default', isRefinement,
127
+ id: runId, cwd, prompt: userPrompt, category: classifyEloCategory(userPrompt), preset: preset?.name || 'default', isRefinement,
126
128
  candidates: candidatesForHistory(referenceOutputs),
127
129
  aggregator: { provider: chosenJudge.provider, model: chosenJudge.model, usage: aggUsage, costUsd: aggUsage.costUsd },
128
130
  winnerIndex: winningIndex, winnerModel, promotedFiles, benchmarkReport, consensus: multiJudgeResult,
@@ -147,7 +149,7 @@ export async function stagePromotion({
147
149
  kind: 'synthesis', content: synthesizedText, aggregator: isFastMode ? 'Fast Mode (Direct)' : finalAggLabel,
148
150
  references: referenceOutputs, presetName: preset?.name || 'default', isRefinement, isFastMode, isCuratorSynthesis,
149
151
  recommendedAssembler, winningIndex, winnerModel, promotedFiles, runId, allowCandidateOverride,
150
- benchmarkReport, consensus: multiJudgeResult, fileScores, ...(selfHealingResult ? { selfHealing: selfHealingResult } : {}), ...(livePreview ? { liveCanvas: livePreview } : {}),
152
+ benchmarkReport, consensus: multiJudgeResult, fileScores, ...(selfHealingResult ? { selfHealing: selfHealingResult } : {}), ...(visualResult ? { visualConsensus: visualResult } : {}), ...(livePreview ? { liveCanvas: livePreview } : {}),
151
153
  usage: usageSummary, skippedFiles: collectedCtx?.skippedFiles || 0, skippedList: collectedCtx?.skippedList || [], durationMs,
152
154
  }
153
155
  }
@@ -1,3 +1,4 @@
1
+ import { runVisualMoaConsensus } from '../moa-visual-consensus.js'
1
2
  import crypto from 'node:crypto'
2
3
  import { isTestEnvironment } from '../history.js'
3
4
  import { cleanMoaWorkspaces } from '../workspace/fs.js'
@@ -157,9 +158,20 @@ export async function runMoAPipeline({
157
158
  const compositeMergeDirective = (preset?.composite_merge_enabled && successfulRefs.length > 1)
158
159
  ? buildCompositeBlockDirectives(successfulRefs, { blind: Boolean(preset.blind_evaluation) })
159
160
  : null
160
- const fileScoringDirective = (preset?.interactive_workbench_enabled)
161
+ let visualResult = null
162
+ if (preset?.visual_moa_enabled) {
163
+ visualResult = await runVisualMoaConsensus({
164
+ ctx,
165
+ liveCanvas,
166
+ userPrompt,
167
+ referenceOutputs,
168
+ onProgress,
169
+ })
170
+ }
171
+ const rawFileDirective = (preset?.interactive_workbench_enabled)
161
172
  ? formatFileScoresDirective()
162
173
  : null
174
+ const fileScoringDirective = [rawFileDirective, visualResult?.directive].filter(Boolean).join("\n\n") || null
163
175
 
164
176
  // Stage 7: Synthesis Phase
165
177
  const synthRes = await stageSynthesis({
@@ -189,7 +201,7 @@ export async function runMoAPipeline({
189
201
  promoteOpts, cwd, runId, signal, liveCanvas, startTime, chosenJudge, aggUsage,
190
202
  userPrompt, preset, isRefinement: ctxStage.isRefinement, isFastMode: ctxStage.isFastMode,
191
203
  effectiveReferences: ctxStage.effectiveReferences, referenceModels: ctxStage.referenceModels,
192
- historyFilePath, collectedCtx: ctxStage.collectedCtx, ctx, callLlm, onProgress, execFn,
204
+ historyFilePath, collectedCtx: ctxStage.collectedCtx, ctx, callLlm, onProgress, execFn, visualResult,
193
205
  })
194
206
  if (!promRes.ok) {
195
207
  if (promRes.abort) return { error: promRes.error }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.69",
3
+ "version": "0.2.70",
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",