@goodandready/dsh-moa 0.2.40 → 0.2.42
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/README.md +7 -0
- package/README.ru.md +7 -0
- package/README.zh.md +11 -0
- package/lib/client.js +407 -98
- package/lib/history.js +169 -35
- package/lib/index.js +21 -3
- package/lib/live-canvas.js +39 -6
- package/lib/moa-multi-judge.js +151 -59
- package/lib/moa-report.js +41 -20
- package/lib/moa-runner.js +11 -6
- package/lib/moa-stream.js +57 -1
- package/lib/pricing.js +10 -6
- package/lib/routes.js +3 -2
- package/lib/updater.js +23 -0
- package/package.json +1 -1
package/lib/history.js
CHANGED
|
@@ -41,7 +41,6 @@ export function getDefaultHistoryFile() {
|
|
|
41
41
|
return path.join(getDefaultHistoryDir(), 'moa-history.jsonl')
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
45
44
|
/** Max history file size before rotation (10 MB) */
|
|
46
45
|
export const MAX_HISTORY_BYTES = 10 * 1024 * 1024
|
|
47
46
|
|
|
@@ -77,6 +76,7 @@ export function rotateHistoryFileIfNeeded(filePath = getDefaultHistoryFile(), ma
|
|
|
77
76
|
|
|
78
77
|
/**
|
|
79
78
|
* Prepares a normalized history record entry.
|
|
79
|
+
* Preserves candidate persona, temperature, fallback, testResult, and slot details (#169).
|
|
80
80
|
*/
|
|
81
81
|
function normalizeRunRecord(record) {
|
|
82
82
|
return {
|
|
@@ -86,19 +86,33 @@ function normalizeRunRecord(record) {
|
|
|
86
86
|
preset: record.preset || 'default',
|
|
87
87
|
isRefinement: Boolean(record.isRefinement),
|
|
88
88
|
isFastMode: Boolean(record.isFastMode),
|
|
89
|
+
cwd: record.cwd ? path.resolve(record.cwd) : undefined,
|
|
89
90
|
candidates: Array.isArray(record.candidates)
|
|
90
|
-
? record.candidates.map((c) => ({
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
? record.candidates.map((c, idx) => ({
|
|
92
|
+
index: typeof c.index === 'number' ? c.index : (idx + 1),
|
|
93
|
+
provider: c.provider || c.slot?.provider || '',
|
|
94
|
+
model: c.model || c.slot?.model || '',
|
|
95
|
+
slot: c.slot ? { provider: c.slot.provider, model: c.slot.model } : (c.provider && c.model ? { provider: c.provider, model: c.model } : undefined),
|
|
96
|
+
role: c.role || c.role_persona || 'general',
|
|
97
|
+
temperature: typeof c.temperature === 'number' ? c.temperature : undefined,
|
|
98
|
+
wasFallback: Boolean(c.wasFallback || c.was_fallback),
|
|
99
|
+
originalModel: c.originalModel || c.original_model || null,
|
|
93
100
|
filesCount: Array.isArray(c.files) ? c.files.length : (c.filesCount || 0),
|
|
101
|
+
files: Array.isArray(c.files) ? c.files : undefined,
|
|
94
102
|
usage: c.usage || { inputTokens: 0, outputTokens: 0 },
|
|
95
103
|
costUsd: Number(c.costUsd || 0),
|
|
104
|
+
testResult: c.testResult ? {
|
|
105
|
+
passed: Boolean(c.testResult.passed),
|
|
106
|
+
exitCode: c.testResult.exitCode ?? 0,
|
|
107
|
+
summary: c.testResult.summary || '',
|
|
108
|
+
durationMs: c.testResult.durationMs ?? 0,
|
|
109
|
+
} : null,
|
|
96
110
|
}))
|
|
97
111
|
: [],
|
|
98
112
|
aggregator: record.aggregator
|
|
99
113
|
? {
|
|
100
|
-
provider: record.aggregator.provider || '',
|
|
101
|
-
model: record.aggregator.model || '',
|
|
114
|
+
provider: record.aggregator.provider || record.aggregator.slot?.provider || '',
|
|
115
|
+
model: record.aggregator.model || record.aggregator.slot?.model || '',
|
|
102
116
|
usage: record.aggregator.usage || { inputTokens: 0, outputTokens: 0 },
|
|
103
117
|
costUsd: Number(record.aggregator.costUsd || 0),
|
|
104
118
|
}
|
|
@@ -162,6 +176,7 @@ export async function recordMoaRunAsync(record, filePath = getDefaultHistoryFile
|
|
|
162
176
|
/**
|
|
163
177
|
* Reads lines from the end of a file in reverse order in chunks.
|
|
164
178
|
* Avoids loading multi-megabyte JSONL files into V8 heap strings.
|
|
179
|
+
* Buffer-safe: never splits multi-byte UTF-8 sequences at chunk boundaries (#184).
|
|
165
180
|
*/
|
|
166
181
|
export function readTailLinesSync(filePath, countNeeded = 50) {
|
|
167
182
|
const CHUNK_SIZE = 64 * 1024
|
|
@@ -169,7 +184,7 @@ export function readTailLinesSync(filePath, countNeeded = 50) {
|
|
|
169
184
|
try {
|
|
170
185
|
const stat = fs.fstatSync(fd)
|
|
171
186
|
let pos = stat.size
|
|
172
|
-
let
|
|
187
|
+
let remainderBuf = Buffer.alloc(0)
|
|
173
188
|
const collectedLines = []
|
|
174
189
|
|
|
175
190
|
while (pos > 0 && collectedLines.length < countNeeded) {
|
|
@@ -177,29 +192,107 @@ export function readTailLinesSync(filePath, countNeeded = 50) {
|
|
|
177
192
|
pos -= bytesToRead
|
|
178
193
|
const buf = Buffer.alloc(bytesToRead)
|
|
179
194
|
fs.readSync(fd, buf, 0, bytesToRead, pos)
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
195
|
+
const totalBuf = remainderBuf.length > 0 ? Buffer.concat([buf, remainderBuf]) : buf
|
|
196
|
+
|
|
197
|
+
let endIdx = totalBuf.length
|
|
198
|
+
for (let i = totalBuf.length - 1; i >= 0; i--) {
|
|
199
|
+
if (totalBuf[i] === 0x0A) {
|
|
200
|
+
if (i + 1 < endIdx) {
|
|
201
|
+
const lineBuf = totalBuf.subarray(i + 1, endIdx)
|
|
202
|
+
const lineStr = lineBuf.toString('utf8').trim()
|
|
203
|
+
if (lineStr) {
|
|
204
|
+
collectedLines.push(lineStr)
|
|
205
|
+
if (collectedLines.length >= countNeeded) break
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
endIdx = i
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (collectedLines.length >= countNeeded) {
|
|
213
|
+
remainderBuf = Buffer.alloc(0)
|
|
214
|
+
break
|
|
186
215
|
}
|
|
187
216
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
217
|
+
if (pos > 0) {
|
|
218
|
+
remainderBuf = totalBuf.subarray(0, endIdx)
|
|
219
|
+
} else {
|
|
220
|
+
if (endIdx > 0) {
|
|
221
|
+
const firstLineStr = totalBuf.subarray(0, endIdx).toString('utf8').trim()
|
|
222
|
+
if (firstLineStr && collectedLines.length < countNeeded) {
|
|
223
|
+
collectedLines.push(firstLineStr)
|
|
224
|
+
}
|
|
193
225
|
}
|
|
226
|
+
remainderBuf = Buffer.alloc(0)
|
|
194
227
|
}
|
|
195
228
|
}
|
|
196
|
-
|
|
197
|
-
|
|
229
|
+
|
|
230
|
+
if (remainderBuf.length > 0 && collectedLines.length < countNeeded) {
|
|
231
|
+
const lineStr = remainderBuf.toString('utf8').trim()
|
|
232
|
+
if (lineStr) collectedLines.push(lineStr)
|
|
198
233
|
}
|
|
234
|
+
|
|
199
235
|
return collectedLines
|
|
200
236
|
} finally {
|
|
201
|
-
fs.closeSync(fd)
|
|
237
|
+
try { fs.closeSync(fd) } catch { /* best effort */ }
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Counts lines in a file by scanning for 0x0A newline bytes in chunks.
|
|
243
|
+
* Fast, memory-efficient, and caches result in _historyCache (#183).
|
|
244
|
+
*/
|
|
245
|
+
export function countFileLinesSync(filePath, stat) {
|
|
246
|
+
if (
|
|
247
|
+
_historyCache &&
|
|
248
|
+
_historyCache.filePath === filePath &&
|
|
249
|
+
_historyCache.mtimeMs === stat.mtimeMs &&
|
|
250
|
+
_historyCache.size === stat.size &&
|
|
251
|
+
typeof _historyCache.totalCount === 'number'
|
|
252
|
+
) {
|
|
253
|
+
return _historyCache.totalCount
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let count = 0
|
|
257
|
+
const fd = fs.openSync(filePath, 'r')
|
|
258
|
+
try {
|
|
259
|
+
const CHUNK_SIZE = 64 * 1024
|
|
260
|
+
const buf = Buffer.alloc(CHUNK_SIZE)
|
|
261
|
+
let pos = 0
|
|
262
|
+
let lastByte = 0
|
|
263
|
+
while (pos < stat.size) {
|
|
264
|
+
const bytesToRead = Math.min(CHUNK_SIZE, stat.size - pos)
|
|
265
|
+
fs.readSync(fd, buf, 0, bytesToRead, pos)
|
|
266
|
+
for (let i = 0; i < bytesToRead; i++) {
|
|
267
|
+
if (buf[i] === 0x0A) count++
|
|
268
|
+
}
|
|
269
|
+
lastByte = buf[bytesToRead - 1]
|
|
270
|
+
pos += bytesToRead
|
|
271
|
+
}
|
|
272
|
+
if (stat.size > 0 && lastByte !== 0x0A) {
|
|
273
|
+
count++
|
|
274
|
+
}
|
|
275
|
+
} finally {
|
|
276
|
+
try { fs.closeSync(fd) } catch { /* best effort */ }
|
|
202
277
|
}
|
|
278
|
+
|
|
279
|
+
if (
|
|
280
|
+
!_historyCache ||
|
|
281
|
+
_historyCache.filePath !== filePath ||
|
|
282
|
+
_historyCache.mtimeMs !== stat.mtimeMs ||
|
|
283
|
+
_historyCache.size !== stat.size
|
|
284
|
+
) {
|
|
285
|
+
_historyCache = {
|
|
286
|
+
filePath,
|
|
287
|
+
mtimeMs: stat.mtimeMs,
|
|
288
|
+
size: stat.size,
|
|
289
|
+
leaderboards: new Map(),
|
|
290
|
+
totalCount: count,
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
_historyCache.totalCount = count
|
|
294
|
+
}
|
|
295
|
+
return count
|
|
203
296
|
}
|
|
204
297
|
|
|
205
298
|
/**
|
|
@@ -220,6 +313,23 @@ export function getMoaHistory(limit = 20, offset = 0, filePath = getDefaultHisto
|
|
|
220
313
|
const lines = raw.trim().split('\n').filter(Boolean)
|
|
221
314
|
const total = lines.length
|
|
222
315
|
|
|
316
|
+
if (
|
|
317
|
+
!_historyCache ||
|
|
318
|
+
_historyCache.filePath !== filePath ||
|
|
319
|
+
_historyCache.mtimeMs !== stat.mtimeMs ||
|
|
320
|
+
_historyCache.size !== stat.size
|
|
321
|
+
) {
|
|
322
|
+
_historyCache = {
|
|
323
|
+
filePath,
|
|
324
|
+
mtimeMs: stat.mtimeMs,
|
|
325
|
+
size: stat.size,
|
|
326
|
+
leaderboards: new Map(),
|
|
327
|
+
totalCount: total,
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
_historyCache.totalCount = total
|
|
331
|
+
}
|
|
332
|
+
|
|
223
333
|
const sliced = lines
|
|
224
334
|
.slice(Math.max(0, total - offset - limit), total - offset)
|
|
225
335
|
.reverse()
|
|
@@ -250,7 +360,7 @@ export function getMoaHistory(limit = 20, offset = 0, filePath = getDefaultHisto
|
|
|
250
360
|
|
|
251
361
|
let total = _historyCache?.totalCount
|
|
252
362
|
if (typeof total !== 'number') {
|
|
253
|
-
total = tailLines.length
|
|
363
|
+
total = tailLines.length < needed ? tailLines.length : countFileLinesSync(filePath, stat)
|
|
254
364
|
}
|
|
255
365
|
|
|
256
366
|
return { total, runs }
|
|
@@ -263,6 +373,7 @@ export function getMoaHistory(limit = 20, offset = 0, filePath = getDefaultHisto
|
|
|
263
373
|
/**
|
|
264
374
|
* Calculates win-rate leaderboard and model performance statistics across history.
|
|
265
375
|
* Employs in-memory caching keyed by file mtime and size for O(1) repeated queries.
|
|
376
|
+
* Robust matching for aliases, fallbacks, and winnerIndex (#185).
|
|
266
377
|
*/
|
|
267
378
|
export function getMoaLeaderboard(filePath = getDefaultHistoryFile(), options = {}) {
|
|
268
379
|
try {
|
|
@@ -307,10 +418,15 @@ export function getMoaLeaderboard(filePath = getDefaultHistoryFile(), options =
|
|
|
307
418
|
if (presetFilter && presetFilter !== 'all' && item.preset !== presetFilter) continue
|
|
308
419
|
filteredRuns++
|
|
309
420
|
|
|
310
|
-
const
|
|
421
|
+
const rawWinner = item.winnerModel ? String(item.winnerModel).trim() : null
|
|
422
|
+
const cleanWinner = rawWinner ? rawWinner.replace(/\s*\[fallback for .*\]$/i, '').trim() : null
|
|
423
|
+
const normWinner = cleanWinner && cleanWinner !== 'none' && cleanWinner !== 'unknown'
|
|
424
|
+
? cleanWinner.replace(/[\/:]/g, ':').toLowerCase()
|
|
425
|
+
: null
|
|
311
426
|
|
|
312
427
|
if (Array.isArray(item.candidates)) {
|
|
313
|
-
for (
|
|
428
|
+
for (let candIdx = 0; candIdx < item.candidates.length; candIdx++) {
|
|
429
|
+
const cand = item.candidates[candIdx]
|
|
314
430
|
const key = cand.provider && cand.model ? `${cand.provider}:${cand.model}` : (cand.model || 'unknown')
|
|
315
431
|
if (!modelStats.has(key)) {
|
|
316
432
|
modelStats.set(key, {
|
|
@@ -326,12 +442,25 @@ export function getMoaLeaderboard(filePath = getDefaultHistoryFile(), options =
|
|
|
326
442
|
const st = modelStats.get(key)
|
|
327
443
|
st.runs += 1
|
|
328
444
|
|
|
329
|
-
const inTok = cand.usage?.inputTokens
|
|
330
|
-
const outTok = cand.usage?.outputTokens
|
|
445
|
+
const inTok = cand.usage?.inputTokens ?? cand.usage?.totalInputTokens ?? cand.usage?.prompt_tokens ?? 0
|
|
446
|
+
const outTok = cand.usage?.outputTokens ?? cand.usage?.totalOutputTokens ?? cand.usage?.completion_tokens ?? 0
|
|
331
447
|
st.totalTokens += inTok + outTok
|
|
332
448
|
st.totalCostUsd += Number(cand.costUsd || 0)
|
|
333
449
|
|
|
334
|
-
|
|
450
|
+
let isWin = false
|
|
451
|
+
if (normWinner) {
|
|
452
|
+
const normKey = key.replace(/[\/:]/g, ':').toLowerCase()
|
|
453
|
+
const normModel = String(cand.model || '').replace(/[\/:]/g, ':').toLowerCase()
|
|
454
|
+
if (normWinner === normKey || normWinner === normModel || normWinner.endsWith(`:${normModel}`)) {
|
|
455
|
+
isWin = true
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (!isWin && typeof item.winnerIndex === 'number' && item.winnerIndex > 0) {
|
|
459
|
+
if (cand.index === item.winnerIndex || candIdx === item.winnerIndex - 1) {
|
|
460
|
+
isWin = true
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (isWin) {
|
|
335
464
|
st.wins += 1
|
|
336
465
|
}
|
|
337
466
|
}
|
|
@@ -433,22 +562,27 @@ export function exportMoaHistory(filePath = getDefaultHistoryFile(), options = {
|
|
|
433
562
|
}
|
|
434
563
|
}
|
|
435
564
|
|
|
565
|
+
/**
|
|
566
|
+
* Formats candidate outputs for history storage (#169).
|
|
567
|
+
*/
|
|
436
568
|
export function candidatesForHistory(referenceOutputs) {
|
|
437
|
-
return (referenceOutputs || []).map((r) => ({
|
|
438
|
-
|
|
439
|
-
|
|
569
|
+
return (referenceOutputs || []).map((r, i) => ({
|
|
570
|
+
index: r.index ?? (i + 1),
|
|
571
|
+
provider: r.slot?.provider || r.provider || "",
|
|
572
|
+
model: r.slot?.model || r.model || "",
|
|
573
|
+
slot: r.slot ? { provider: r.slot.provider, model: r.slot.model } : undefined,
|
|
440
574
|
role: r.role_persona || r.slot?.role_persona || r.slot?.role || "general",
|
|
441
575
|
temperature: r.temperature,
|
|
442
576
|
wasFallback: Boolean(r.was_fallback),
|
|
443
577
|
originalModel: r.original_model || null,
|
|
444
|
-
files: (r.files || []).map((f) => f.relativePath),
|
|
578
|
+
files: (r.files || []).map((f) => f.relativePath || f),
|
|
445
579
|
usage: r.usage || { inputTokens: 0, outputTokens: 0 },
|
|
446
580
|
costUsd: r.costUsd || 0,
|
|
447
581
|
testResult: r.testResult ? {
|
|
448
|
-
passed: r.testResult.passed,
|
|
449
|
-
exitCode: r.testResult.exitCode,
|
|
450
|
-
summary: r.testResult.summary,
|
|
451
|
-
durationMs: r.testResult.durationMs,
|
|
582
|
+
passed: Boolean(r.testResult.passed),
|
|
583
|
+
exitCode: r.testResult.exitCode ?? 0,
|
|
584
|
+
summary: r.testResult.summary || '',
|
|
585
|
+
durationMs: r.testResult.durationMs ?? 0,
|
|
452
586
|
} : null,
|
|
453
587
|
}))
|
|
454
588
|
}
|
package/lib/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { createLlmCaller } from './moa-llm.js'
|
|
|
12
12
|
import {
|
|
13
13
|
parseMoACommand,
|
|
14
14
|
streamMoATurn,
|
|
15
|
+
streamPromoteTurn,
|
|
15
16
|
} from './moa-runner.js'
|
|
16
17
|
import { registerMoaRoutes } from './routes.js'
|
|
17
18
|
import { resolvePresetForPrompt } from './moa-router.js'
|
|
@@ -174,6 +175,20 @@ export function apply(ctx, config) {
|
|
|
174
175
|
if (cfg.enabled !== false) {
|
|
175
176
|
const effectiveDefault = cfg.default_preset || 'default'
|
|
176
177
|
const parsed = parseMoACommand(userText, cfg.presets || [], effectiveDefault) || { prompt: userText, presetName: effectiveDefault, isExplicit: false }
|
|
178
|
+
if (parsed.isPromote) {
|
|
179
|
+
const sessCwd = event.cwd || event.session?.cwd || (event.session?.path ? path.dirname(event.session.path) : undefined) || process.cwd()
|
|
180
|
+
const turnContext = {
|
|
181
|
+
isPromoteTurn: true,
|
|
182
|
+
runId: parsed.runId,
|
|
183
|
+
candidateIndex: parsed.candidateIndex,
|
|
184
|
+
cwd: sessCwd,
|
|
185
|
+
webServerPort: ctx.webServer?.port,
|
|
186
|
+
timeMachineEngine: ctx.reflect?.get?.('timeMachineEngine', false),
|
|
187
|
+
}
|
|
188
|
+
if (event.signal) moaPendingTurns.set(event.signal, turnContext)
|
|
189
|
+
if (event.session?.id) setSessionTurn(event.session.id, turnContext)
|
|
190
|
+
return next()
|
|
191
|
+
}
|
|
177
192
|
const isExplicitPreset = Boolean(parsed.isExplicit || userText.includes('--preset') || userText.includes('-p '))
|
|
178
193
|
if (!isExplicitPreset && (cfg.smart_routing_enabled || cfg.presets?.some((p) => p.smart_routing_enabled))) {
|
|
179
194
|
try {
|
|
@@ -234,11 +249,11 @@ export function apply(ctx, config) {
|
|
|
234
249
|
const turnContext = (payload.signal ? moaPendingTurns.get(payload.signal) : null)
|
|
235
250
|
|| (payload.session?.id ? getSessionTurn(payload.session.id) : null)
|
|
236
251
|
|
|
237
|
-
if (turnContext) {
|
|
252
|
+
if (turnContext && !turnContext.isPromoteTurn) {
|
|
238
253
|
return {
|
|
239
254
|
...resolved,
|
|
240
|
-
provider: turnContext.aggregator.provider,
|
|
241
|
-
model: turnContext.aggregator.model,
|
|
255
|
+
provider: turnContext.aggregator?.provider || resolved.provider,
|
|
256
|
+
model: turnContext.aggregator?.model || resolved.model,
|
|
242
257
|
}
|
|
243
258
|
}
|
|
244
259
|
return resolved
|
|
@@ -254,6 +269,9 @@ export function apply(ctx, config) {
|
|
|
254
269
|
if (options.signal) moaPendingTurns.delete(options.signal)
|
|
255
270
|
if (options.session?.id) consumeSessionTurn(options.session.id)
|
|
256
271
|
|
|
272
|
+
if (turnContext.isPromoteTurn) {
|
|
273
|
+
return streamPromoteTurn(turnContext, options)
|
|
274
|
+
}
|
|
257
275
|
return streamMoATurn(turnContext, options)
|
|
258
276
|
}
|
|
259
277
|
|
package/lib/live-canvas.js
CHANGED
|
@@ -1,17 +1,50 @@
|
|
|
1
1
|
import path from "node:path"
|
|
2
|
+
import fs from "node:fs/promises"
|
|
2
3
|
|
|
3
|
-
export async function createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs) {
|
|
4
|
+
export async function createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs, options = {}) {
|
|
4
5
|
if (!liveCanvas || typeof liveCanvas.createPreviewFromContent !== "function") return null
|
|
5
6
|
const htmlRel = (promotedFiles || []).find((f) => f.endsWith(".html") || f.endsWith(".htm"))
|
|
6
7
|
if (!htmlRel) return null
|
|
8
|
+
|
|
7
9
|
let content = null
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
|
|
11
|
+
// 1. Try reading directly from disk in cwd (the promoted winner or curator synthesis)
|
|
12
|
+
if (cwd) {
|
|
13
|
+
try {
|
|
14
|
+
const fullPath = path.resolve(cwd, htmlRel)
|
|
15
|
+
content = await fs.readFile(fullPath, "utf8")
|
|
16
|
+
} catch {
|
|
17
|
+
content = null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 2. Fallback to synthesized files / winning files
|
|
22
|
+
if (!content) {
|
|
23
|
+
const winningFiles = options.winningFiles || options.synthesizedFiles
|
|
24
|
+
if (Array.isArray(winningFiles)) {
|
|
25
|
+
const block = winningFiles.find((f) => f.relativePath === htmlRel)
|
|
26
|
+
if (block?.content) content = block.content
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 3. Fallback to winning candidate by index
|
|
31
|
+
if (!content && typeof options.winningIndex === 'number' && referenceOutputs) {
|
|
32
|
+
const winnerRef = referenceOutputs[options.winningIndex - 1]
|
|
33
|
+
const block = (winnerRef?.files || []).find((f) => f.relativePath === htmlRel)
|
|
34
|
+
if (block?.content) content = block.content
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 4. Fallback to searching referenceOutputs
|
|
38
|
+
if (!content) {
|
|
39
|
+
for (const r of referenceOutputs || []) {
|
|
40
|
+
const block = (r?.files || []).find((f) => f.relativePath === htmlRel)
|
|
41
|
+
if (block?.content) {
|
|
42
|
+
content = block.content
|
|
43
|
+
break
|
|
44
|
+
}
|
|
13
45
|
}
|
|
14
46
|
}
|
|
47
|
+
|
|
15
48
|
if (!content) return null
|
|
16
49
|
try {
|
|
17
50
|
return await liveCanvas.createPreviewFromContent({ content, title: htmlRel, filePath: path.join(cwd, htmlRel) })
|