@goodandready/dsh-moa 0.2.12 → 0.2.13
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 +48 -2
- package/README.ru.md +287 -0
- package/README.zh.md +243 -0
- package/docs/README.ru.md +47 -1
- package/docs/README.zh.md +11 -1
- package/docs/design/DESIGN.md +2 -0
- package/docs/plans/59-power-pack-plan.md +73 -0
- package/lib/client.js +111 -2
- package/lib/file-workspace.js +46 -0
- package/lib/history.js +56 -2
- package/lib/index.js +86 -2
- package/lib/moa-parser.js +24 -0
- package/lib/moa-prompts.js +70 -6
- package/lib/moa-runner.js +63 -52
- package/package.json +1 -1
package/docs/README.ru.md
CHANGED
|
@@ -16,11 +16,21 @@
|
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
|
-
<a href="
|
|
19
|
+
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
20
20
|
<a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
21
21
|
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
22
22
|
</p>
|
|
23
23
|
|
|
24
|
+
<table align="center">
|
|
25
|
+
<tr>
|
|
26
|
+
<td align="center">
|
|
27
|
+
⭐ <strong>Если вам нравится этот плагин, поставьте ему звезду на GitHub</strong> — это покажет мне, что плагин вам полезен, и будет мотивировать меня развивать его дальше.
|
|
28
|
+
<br><br>
|
|
29
|
+
🐛 <strong>Если вы нашли баг или хотите предложить новый функционал</strong>, создайте issue на GitHub на любом языке — я рассмотрю ваше предложение и реализую полезные идеи в одной из следующих версий плагина.
|
|
30
|
+
</td>
|
|
31
|
+
</tr>
|
|
32
|
+
</table>
|
|
33
|
+
|
|
24
34
|
</div>
|
|
25
35
|
|
|
26
36
|
---
|
|
@@ -143,8 +153,44 @@ dsh plugin --profile web add @goodandready/dsh-moa
|
|
|
143
153
|
|
|
144
154
|
---
|
|
145
155
|
|
|
156
|
+
## ⚡ 10 встроенных пресетов и инженерные персоны
|
|
157
|
+
|
|
158
|
+
В версии v0.2.13 добавлены 10 специализированных пресетов под реальные задачи разработки:
|
|
159
|
+
|
|
160
|
+
| Имя пресета | Назначение | Судья по умолчанию | Рецензия (Round 2) | Слепая оценка |
|
|
161
|
+
| :--- | :--- | :--- | :---: | :---: |
|
|
162
|
+
| `default` | Сбалансированная генерация | `codex:gpt-5.6-sol` | Опционально | Выкл |
|
|
163
|
+
| `code-review` | Глубокое взаимное ревью кода | `codex:gpt-5.6-sol` | Вкл | Вкл |
|
|
164
|
+
| `fast-audit` | Сверхбыстрый экспресс-аудит (Fast Mode) | `codex:gpt-5.6-sol` | Выкл | Выкл |
|
|
165
|
+
| `deep-architect` | Проектирование распределенных систем | `codex:gpt-5.6-sol` | Вкл | Выкл |
|
|
166
|
+
| `bug-hunter` | Поиск скрытых багов и граничных случаев | `codex:gpt-5.6-sol` | Вкл | Выкл |
|
|
167
|
+
| `refactor-cleanup` | Чистка легаси и удаление оверинжиниринга | `codex:gpt-5.6-sol` | Выкл | Выкл |
|
|
168
|
+
| `frontend-ui` | Качественные адаптивные веб-интерфейсы | `codex:gpt-5.6-sol` | Выкл | Выкл |
|
|
169
|
+
| `security-audit` | Анализ уязвимостей и санитайзинг | `codex:gpt-5.6-sol` | Вкл | Вкл |
|
|
170
|
+
| `math-logic` | Алгоритмы, математическая логика и доказательства | `codex:gpt-5.6-sol` | Вкл | Выкл |
|
|
171
|
+
| `creative-brainstorm`| Нестандартные идеи и дивергентный поиск | `codex:gpt-5.6-sol` | Выкл | Выкл |
|
|
172
|
+
|
|
173
|
+
### Инженерные персоны кандидатов (`role_persona`)
|
|
174
|
+
Задавайте кандидатам четкие роли для максимального разнообразия подходов:
|
|
175
|
+
- **`minimalist` (Ponytail)**: решение на стандартной библиотеке, 0 лишних зависимостей, минимальный код.
|
|
176
|
+
- **`robustness`**: защитное программирование, валидация границ, идемпотентность, устойчивость к сбоям.
|
|
177
|
+
- **`performance`**: минимальная алгоритмическая сложность, экономия памяти, zero-copy.
|
|
178
|
+
- **`tester`**: test-driven архитектура, 100% покрытие веток, четкие ассерты.
|
|
179
|
+
- **`general`**: сбалансированный подход общего назначения.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## 🤝 Консилиум Раунд 2, Синтаксический гейт и Override
|
|
184
|
+
|
|
185
|
+
- **Консилиум (Раунд 2)**: флаг `peer_critique_enabled: true` включает этап взаимного ревью, в котором кандидаты изучают предложения оппонентов и дорабатывают код перед финальным судейством.
|
|
186
|
+
- **Синтаксический гейт**: мгновенная валидация JS/JSON через `node:vm` и `JSON.parse`. Если в коде кандидата есть опечатка, он помечается `[⚠️ Syntax Warning]`, а судья получает жесткую директиву: *если архитектура кандидата превосходит конкурентов, исправить синтаксис в финальном решении и присудить ему победу*.
|
|
187
|
+
- **Пользовательский выбор кандидата (Override)**: флаг `allow_candidate_override: true` сохраняет папки `.moa/candidate-N/`. Любой кандидат может быть повышен в проект командой `/moa promote <runId> <candidateIndex>` или кнопкой в карточке настроек.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
146
191
|
## ⚙️ Конфигурация (`settings.yaml`)
|
|
147
192
|
|
|
193
|
+
|
|
148
194
|
Настройка пресетов и пайплайнов моделей доступна в `settings.yaml` или через интерфейс (Настройки → Плагины → Mixture of Agents):
|
|
149
195
|
|
|
150
196
|
```yaml
|
package/docs/README.zh.md
CHANGED
|
@@ -16,11 +16,21 @@
|
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
|
-
<a href="
|
|
19
|
+
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
20
20
|
<a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
21
21
|
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
22
22
|
</p>
|
|
23
23
|
|
|
24
|
+
<table align="center">
|
|
25
|
+
<tr>
|
|
26
|
+
<td align="center">
|
|
27
|
+
⭐ <strong>如果您喜欢这个插件,请在 GitHub 上为它点亮 Star</strong> — 这能让我知道插件对您有用,并鼓励我继续开发和维护它。
|
|
28
|
+
<br><br>
|
|
29
|
+
🐛 <strong>如果您发现 Bug 或希望增加功能</strong>,请使用任意语言在 GitHub 上提交 Issue — 我会评估您的建议,并在后续版本中实现有价值的改进。
|
|
30
|
+
</td>
|
|
31
|
+
</tr>
|
|
32
|
+
</table>
|
|
33
|
+
|
|
24
34
|
</div>
|
|
25
35
|
|
|
26
36
|
---
|
package/docs/design/DESIGN.md
CHANGED
|
@@ -67,3 +67,5 @@
|
|
|
67
67
|
- 2026-09-12 — Реализация кураторского синтеза (`curator_synthesis`), строгой рубрики антипаттернов (`ANTIPATTERNS_RUBRIC`), живого потокового стриминга куратора (`stream_aggregator`), кворума кандидатов с льготным периодом (`quorum_enabled`, `grace_period_sec`), авторетрая транзиентных ошибок (`callWithTransientRetry`) и цепочки запасных судей (`aggregator_fallbacks`). Все опции конфигурируются в пресетах с сохранением 100% обратной совместимости.
|
|
68
68
|
|
|
69
69
|
- 2026-09-12 (вечер) — Декомпозиция lib/moa-runner.js на специализированные модули (`lib/moa-prompts.js`, `lib/moa-parser.js`, `lib/moa-runner.js`) с соблюдением канонического лимита <= 800 строк. Реализация автоматической ротации истории при превышении 10 МБ (`rotateHistoryFileIfNeeded`) и неблокирующего асинхронного сохранения (`recordMoaRunAsync`). Настраиваемые таймауты кандидатов и агрегатора (`reference_timeout_sec`, `aggregator_timeout_sec`).
|
|
70
|
+
|
|
71
|
+
- 2026-09-13 — Расширение функционала MoA (Gitea Issue #59): интерактивный селектор альтернативного кандидата в чате (User Candidate Override: allow_candidate_override), пакет из 10 специализированных пресетов с ролями кандидатов (role_persona), локальный гейт синтаксической проверки (Syntax Pre-check Gate) с директивой автоисправления синтаксиса судьёй, режим взаимного рецензирования («Консилиум» / Peer Critique Round 2: peer_critique_enabled), фильтрация лидерборда по пресетам и выгрузка телеметрии в CSV/JSON.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Plan 59: MoA Power Pack v0.2.13
|
|
2
|
+
|
|
3
|
+
## Objectives
|
|
4
|
+
Implement the 5 approved feature expansions for `@goodandready/dsh-moa`:
|
|
5
|
+
1. User Candidate Override (`allow_candidate_override: boolean`).
|
|
6
|
+
2. 10 specialized built-in presets + candidate role personas (`role_persona`).
|
|
7
|
+
3. Syntax Pre-check Gate with Judge Auto-Fix Directive.
|
|
8
|
+
4. Consilium / Peer Critique Round 2 (`peer_critique_enabled: boolean`).
|
|
9
|
+
5. Analytics filtering by preset and CSV/JSON export.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Technical Specifications
|
|
14
|
+
|
|
15
|
+
### 1. User Candidate Override
|
|
16
|
+
- **Schema**: `PresetSchema` field `allow_candidate_override: z.boolean().default(false)`.
|
|
17
|
+
- **Backend**:
|
|
18
|
+
- `writeCandidateWorkspace` stores candidates in `.moa-workspaces/<runId>/candidate-<index>`.
|
|
19
|
+
- Add API route `POST /dsh-moa/promote` with `{ runId, candidateIndex }` that promotes specified candidate files to the project root.
|
|
20
|
+
- In `formatMoAResponse`, if enabled, render interactive action hints:
|
|
21
|
+
`🔄 Alternative Candidate Override: /moa promote <runId> <candidateIndex>`
|
|
22
|
+
- **Client**: Add toggle in Section 1 / Section 3 of `lib/client.js`.
|
|
23
|
+
|
|
24
|
+
### 2. 10 Built-in Presets & Candidate Role Personas
|
|
25
|
+
- **Presets**:
|
|
26
|
+
1. `default`: General MoA ensemble.
|
|
27
|
+
2. `code-review`: Zero regressions, backward compatibility, edge cases.
|
|
28
|
+
3. `fast-audit`: Quorum mitigation, low latency.
|
|
29
|
+
4. `deep-architect`: System architecture, API contracts, curator synthesis.
|
|
30
|
+
5. `bug-hunter`: Concurrency, race conditions, memory leaks, null safety.
|
|
31
|
+
6. `refactor-cleanup`: Ponytail minimalism, stdlib first, zero external dependencies.
|
|
32
|
+
7. `frontend-ui`: Ergonomics, DSH design tokens, responsive layout, no AI slop.
|
|
33
|
+
8. `security-audit`: OWASP Top 10, input sanitization, safe error handling.
|
|
34
|
+
9. `math-logic`: Algorithmic correctness, asymptotic proofs.
|
|
35
|
+
10. `creative-brainstorm`: High divergence, alternative architectures.
|
|
36
|
+
- **Candidate Roles (`role_persona`)**:
|
|
37
|
+
- `minimalist`: Ponytail style, stdlib only, fewest lines.
|
|
38
|
+
- `robustness`: Paranoid validation, edge cases, defensive typing.
|
|
39
|
+
- `performance`: Memory efficiency, asymptotic complexity.
|
|
40
|
+
- `tester`: TDD, unit test coverage, mock isolation.
|
|
41
|
+
- `general`: Standard expert proposer.
|
|
42
|
+
|
|
43
|
+
### 3. Syntax Pre-check Gate with Judge Auto-fix Directive
|
|
44
|
+
- **Checker**:
|
|
45
|
+
- JS/MJS/CJS: `new vm.Script(code)` from built-in `node:vm` (instant, memory-safe).
|
|
46
|
+
- JSON: `JSON.parse(code)`.
|
|
47
|
+
- Python: `python3 -m py_compile`.
|
|
48
|
+
- **Judge Directive**:
|
|
49
|
+
- If a candidate has a syntax warning, inform the judge:
|
|
50
|
+
`CRITICAL JUDGE DIRECTIVE: If Candidate X has the best overall architecture or solution despite this syntax error, DO NOT reject them! You MUST correct the syntax error directly during final code synthesis and declare Candidate X the winner.`
|
|
51
|
+
|
|
52
|
+
### 4. Consilium / Peer Critique Round 2
|
|
53
|
+
- **Schema**: `peer_critique_enabled: z.boolean().default(false)`.
|
|
54
|
+
- **Pipeline**:
|
|
55
|
+
- If enabled and `candidates.length >= 2`, run Round 2:
|
|
56
|
+
- Pass anonymized candidate proposals to each proposer.
|
|
57
|
+
- Collect refined solutions with peer critique.
|
|
58
|
+
- Supply improved proposals to Judge/Curator.
|
|
59
|
+
|
|
60
|
+
### 5. Analytics Filtering & CSV/JSON Export
|
|
61
|
+
- **Leaderboard Filter**: `getMoaLeaderboard(historyFilePath, limit, filterPreset)`.
|
|
62
|
+
- **Export Endpoints**:
|
|
63
|
+
- `GET /dsh-moa/leaderboard?preset=<name>&format=json|csv`.
|
|
64
|
+
- **Client UI**:
|
|
65
|
+
- Dropdown filter by preset in Section 4.
|
|
66
|
+
- `Export CSV` / `Export JSON` action buttons.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Verification
|
|
71
|
+
- Unit tests for all 5 features in `test/moa-power-pack.test.mjs`.
|
|
72
|
+
- Total test suite passing with 0 regressions.
|
|
73
|
+
- Live deployment to `/home/vadim/.dsh/profiles/web` on MiniPC and endpoint verification.
|
package/lib/client.js
CHANGED
|
@@ -55,6 +55,14 @@ window.__ModuleLoader__.load({
|
|
|
55
55
|
'aggregator.curator_hint': 'When enabled: the curator highlights the finest components of each candidate, applies the antipatterns rubric, and advises which agent model should assemble the solution.',
|
|
56
56
|
'aggregator.stream_label': 'Live Stream Curator/Judge Thinking',
|
|
57
57
|
'aggregator.stream_hint': 'Streams aggregator tokens directly to chat in real time for zero-latency initial response.',
|
|
58
|
+
'aggregator.peer_label': 'Consilium: Peer Critique (Round 2)',
|
|
59
|
+
'aggregator.peer_hint': 'Candidates critique each other\'s solutions and refine their code before final judge synthesis.',
|
|
60
|
+
'aggregator.override_label': 'Allow Candidate Override Actions',
|
|
61
|
+
'aggregator.override_hint': 'Keeps candidate workspaces in .moa to let you apply any candidate files via chat command.',
|
|
62
|
+
'proposers.role_label': 'Persona:',
|
|
63
|
+
'leaderboard.filter_all': 'All Presets',
|
|
64
|
+
'leaderboard.export_csv': 'Export CSV',
|
|
65
|
+
'leaderboard.export_json': 'Export JSON',
|
|
58
66
|
'aggregator.blind_label': 'Blind Review (Anonymize Candidates for Judge)',
|
|
59
67
|
'aggregator.blind_hint': 'When enabled: candidate model names and providers are hidden from the judge prompt to prevent family/brand bias.',
|
|
60
68
|
'aggregator.timeout_label': 'Judge synthesis timeout (sec):',
|
|
@@ -461,6 +469,8 @@ window.__ModuleLoader__.load({
|
|
|
461
469
|
hostStatus,
|
|
462
470
|
stats,
|
|
463
471
|
leaderboard = [],
|
|
472
|
+
leaderboardPresetFilter = 'all',
|
|
473
|
+
setLeaderboardPresetFilter = () => {},
|
|
464
474
|
handleSave,
|
|
465
475
|
saveStatus,
|
|
466
476
|
reload,
|
|
@@ -877,6 +887,38 @@ window.__ModuleLoader__.load({
|
|
|
877
887
|
},
|
|
878
888
|
})
|
|
879
889
|
),
|
|
890
|
+
React.createElement(
|
|
891
|
+
'label',
|
|
892
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500, marginTop: 8 } },
|
|
893
|
+
React.createElement('input', {
|
|
894
|
+
type: 'checkbox',
|
|
895
|
+
checked: Boolean(currentPreset.peer_critique_enabled),
|
|
896
|
+
onChange: (e) => updateCurrentPreset((p) => ({ ...p, peer_critique_enabled: e.target.checked })),
|
|
897
|
+
style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
|
|
898
|
+
}),
|
|
899
|
+
t('aggregator.peer_label')
|
|
900
|
+
),
|
|
901
|
+
React.createElement(
|
|
902
|
+
'div',
|
|
903
|
+
{ style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
|
|
904
|
+
t('aggregator.peer_hint')
|
|
905
|
+
),
|
|
906
|
+
React.createElement(
|
|
907
|
+
'label',
|
|
908
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500, marginTop: 8 } },
|
|
909
|
+
React.createElement('input', {
|
|
910
|
+
type: 'checkbox',
|
|
911
|
+
checked: Boolean(currentPreset.allow_candidate_override),
|
|
912
|
+
onChange: (e) => updateCurrentPreset((p) => ({ ...p, allow_candidate_override: e.target.checked })),
|
|
913
|
+
style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
|
|
914
|
+
}),
|
|
915
|
+
t('aggregator.override_label')
|
|
916
|
+
),
|
|
917
|
+
React.createElement(
|
|
918
|
+
'div',
|
|
919
|
+
{ style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
|
|
920
|
+
t('aggregator.override_hint')
|
|
921
|
+
),
|
|
880
922
|
/* Fallback Judges Chain */
|
|
881
923
|
React.createElement(
|
|
882
924
|
'div',
|
|
@@ -1052,6 +1094,28 @@ window.__ModuleLoader__.load({
|
|
|
1052
1094
|
t,
|
|
1053
1095
|
})
|
|
1054
1096
|
),
|
|
1097
|
+
React.createElement(
|
|
1098
|
+
'select',
|
|
1099
|
+
{
|
|
1100
|
+
className: 'moa-select',
|
|
1101
|
+
style: { width: 105, height: 32, fontSize: 12, padding: '0 6px' },
|
|
1102
|
+
value: ref.role_persona || 'general',
|
|
1103
|
+
title: t('proposers.role_label'),
|
|
1104
|
+
onChange: (e) => {
|
|
1105
|
+
const newRole = e.target.value
|
|
1106
|
+
updateCurrentPreset((p) => {
|
|
1107
|
+
const refs = [...(p.reference_models || [])]
|
|
1108
|
+
refs[idx] = { ...refs[idx], role_persona: newRole }
|
|
1109
|
+
return { ...p, reference_models: refs }
|
|
1110
|
+
})
|
|
1111
|
+
},
|
|
1112
|
+
},
|
|
1113
|
+
React.createElement('option', { value: 'general' }, 'General'),
|
|
1114
|
+
React.createElement('option', { value: 'minimalist' }, 'Minimalist'),
|
|
1115
|
+
React.createElement('option', { value: 'robustness' }, 'Robustness'),
|
|
1116
|
+
React.createElement('option', { value: 'performance' }, 'Performance'),
|
|
1117
|
+
React.createElement('option', { value: 'tester' }, 'Tester')
|
|
1118
|
+
),
|
|
1055
1119
|
currentRefs.length > 1 &&
|
|
1056
1120
|
React.createElement(
|
|
1057
1121
|
'button',
|
|
@@ -1139,7 +1203,46 @@ window.__ModuleLoader__.load({
|
|
|
1139
1203
|
Array.isArray(leaderboard) && leaderboard.length > 0 && React.createElement(
|
|
1140
1204
|
'div',
|
|
1141
1205
|
{ style: { marginTop: 12, display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
1142
|
-
React.createElement(
|
|
1206
|
+
React.createElement(
|
|
1207
|
+
'div',
|
|
1208
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' } },
|
|
1209
|
+
React.createElement('div', { style: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, t('leaderboard.title')),
|
|
1210
|
+
React.createElement(
|
|
1211
|
+
'div',
|
|
1212
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 6 } },
|
|
1213
|
+
React.createElement(
|
|
1214
|
+
'select',
|
|
1215
|
+
{
|
|
1216
|
+
className: 'moa-select',
|
|
1217
|
+
style: { height: 26, fontSize: 11, padding: '0 6px' },
|
|
1218
|
+
value: leaderboardPresetFilter,
|
|
1219
|
+
onChange: (e) => setLeaderboardPresetFilter(e.target.value),
|
|
1220
|
+
},
|
|
1221
|
+
React.createElement('option', { value: 'all' }, t('leaderboard.filter_all')),
|
|
1222
|
+
presets.map((p) => React.createElement('option', { key: p.name, value: p.name }, p.name))
|
|
1223
|
+
),
|
|
1224
|
+
React.createElement(
|
|
1225
|
+
'a',
|
|
1226
|
+
{
|
|
1227
|
+
href: `/dsh-moa/history/export?format=csv${leaderboardPresetFilter !== 'all' ? '&preset=' + encodeURIComponent(leaderboardPresetFilter) : ''}`,
|
|
1228
|
+
download: 'moa-history.csv',
|
|
1229
|
+
className: 'moa-btn',
|
|
1230
|
+
style: { height: 26, fontSize: 11, padding: '0 8px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center' },
|
|
1231
|
+
},
|
|
1232
|
+
'CSV'
|
|
1233
|
+
),
|
|
1234
|
+
React.createElement(
|
|
1235
|
+
'a',
|
|
1236
|
+
{
|
|
1237
|
+
href: `/dsh-moa/history/export?format=json${leaderboardPresetFilter !== 'all' ? '&preset=' + encodeURIComponent(leaderboardPresetFilter) : ''}`,
|
|
1238
|
+
download: 'moa-history.json',
|
|
1239
|
+
className: 'moa-btn',
|
|
1240
|
+
style: { height: 26, fontSize: 11, padding: '0 8px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center' },
|
|
1241
|
+
},
|
|
1242
|
+
'JSON'
|
|
1243
|
+
)
|
|
1244
|
+
)
|
|
1245
|
+
),
|
|
1143
1246
|
React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginTop: -4 } }, t('leaderboard.desc')),
|
|
1144
1247
|
React.createElement(
|
|
1145
1248
|
'div',
|
|
@@ -1257,6 +1360,7 @@ window.__ModuleLoader__.load({
|
|
|
1257
1360
|
const [hostStatus, setHostStatus] = React.useState('connecting')
|
|
1258
1361
|
const [stats, setStats] = React.useState({ totalRuns: null, avgCostUsd: null })
|
|
1259
1362
|
const [leaderboard, setLeaderboard] = React.useState([])
|
|
1363
|
+
const [leaderboardPresetFilter, setLeaderboardPresetFilter] = React.useState('all')
|
|
1260
1364
|
|
|
1261
1365
|
React.useEffect(() => {
|
|
1262
1366
|
if (!scope || !snapshot) return
|
|
@@ -1288,7 +1392,10 @@ window.__ModuleLoader__.load({
|
|
|
1288
1392
|
setHostStatus('offline')
|
|
1289
1393
|
})
|
|
1290
1394
|
|
|
1291
|
-
|
|
1395
|
+
const lbUrl = leaderboardPresetFilter !== 'all'
|
|
1396
|
+
? `/dsh-moa/leaderboard?preset=${encodeURIComponent(leaderboardPresetFilter)}`
|
|
1397
|
+
: '/dsh-moa/leaderboard'
|
|
1398
|
+
fetch(lbUrl, { cache: 'no-store' })
|
|
1292
1399
|
.then((r) => r.json())
|
|
1293
1400
|
.then((data) => {
|
|
1294
1401
|
if (data && data.ok && Array.isArray(data.models)) {
|
|
@@ -1406,6 +1513,8 @@ window.__ModuleLoader__.load({
|
|
|
1406
1513
|
hostStatus,
|
|
1407
1514
|
stats,
|
|
1408
1515
|
leaderboard,
|
|
1516
|
+
leaderboardPresetFilter,
|
|
1517
|
+
setLeaderboardPresetFilter,
|
|
1409
1518
|
handleSave,
|
|
1410
1519
|
saveStatus,
|
|
1411
1520
|
reload,
|
package/lib/file-workspace.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import vm from 'node:vm'
|
|
1
2
|
import fs from 'node:fs/promises'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
|
|
@@ -275,3 +276,48 @@ async function getFilesRecursively(dir) {
|
|
|
275
276
|
await scan(dir)
|
|
276
277
|
return results
|
|
277
278
|
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Verifies the syntactic validity of generated code files.
|
|
283
|
+
* Uses Node.js in-memory vm.Script for JS (with ES module export/import transformation) and JSON.parse for JSON.
|
|
284
|
+
*/
|
|
285
|
+
export function verifyFileSyntax(files = []) {
|
|
286
|
+
if (!Array.isArray(files)) return []
|
|
287
|
+
const warnings = []
|
|
288
|
+
|
|
289
|
+
for (const file of files) {
|
|
290
|
+
if (!file || typeof file.content !== 'string' || !file.relativePath) continue
|
|
291
|
+
const ext = path.extname(file.relativePath).toLowerCase()
|
|
292
|
+
|
|
293
|
+
if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
|
|
294
|
+
try {
|
|
295
|
+
const transformed = file.content
|
|
296
|
+
.replace(/^([ \t]*)export\s+default\s+/gm, '$1')
|
|
297
|
+
.replace(/^([ \t]*)export\s*\{[^}]*\}\s*;?/gm, '$1')
|
|
298
|
+
.replace(/^([ \t]*)export\s+(const|let|var|function|class|async\s+function)\s+/gm, '$1$2 ')
|
|
299
|
+
.replace(/^([ \t]*)import\s+[^;\n]+;?/gm, '$1')
|
|
300
|
+
.replace(/^([ \t]*)export\s+\*\s+from\s+[^;\n]+;?/gm, '$1')
|
|
301
|
+
new vm.Script(transformed)
|
|
302
|
+
} catch (err) {
|
|
303
|
+
warnings.push({
|
|
304
|
+
file: file.relativePath,
|
|
305
|
+
error: err?.message || String(err),
|
|
306
|
+
line: err?.lineNumber || 1,
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
} else if (ext === '.json') {
|
|
310
|
+
try {
|
|
311
|
+
JSON.parse(file.content)
|
|
312
|
+
} catch (err) {
|
|
313
|
+
warnings.push({
|
|
314
|
+
file: file.relativePath,
|
|
315
|
+
error: err?.message || String(err),
|
|
316
|
+
line: 1,
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return warnings
|
|
323
|
+
}
|
package/lib/history.js
CHANGED
|
@@ -159,7 +159,7 @@ export function getMoaHistory(limit = 20, offset = 0, filePath = DEFAULT_HISTORY
|
|
|
159
159
|
/**
|
|
160
160
|
* Calculates win-rate leaderboard and model performance statistics across history.
|
|
161
161
|
*/
|
|
162
|
-
export function getMoaLeaderboard(filePath = DEFAULT_HISTORY_FILE) {
|
|
162
|
+
export function getMoaLeaderboard(filePath = DEFAULT_HISTORY_FILE, options = {}) {
|
|
163
163
|
try {
|
|
164
164
|
if (!fs.existsSync(filePath)) {
|
|
165
165
|
return { totalRuns: 0, models: [] }
|
|
@@ -170,11 +170,15 @@ export function getMoaLeaderboard(filePath = DEFAULT_HISTORY_FILE) {
|
|
|
170
170
|
const totalRuns = lines.length
|
|
171
171
|
|
|
172
172
|
const modelStats = new Map()
|
|
173
|
+
const presetFilter = typeof options === 'string' ? options : (options?.presetFilter || options?.preset || null)
|
|
173
174
|
|
|
175
|
+
let filteredRuns = 0
|
|
174
176
|
for (const line of lines) {
|
|
175
177
|
try {
|
|
176
178
|
const item = JSON.parse(line)
|
|
177
179
|
if (!item) continue
|
|
180
|
+
if (presetFilter && presetFilter !== 'all' && item.preset !== presetFilter) continue
|
|
181
|
+
filteredRuns++
|
|
178
182
|
|
|
179
183
|
const winner = item.winnerModel ? String(item.winnerModel).trim() : null
|
|
180
184
|
|
|
@@ -218,7 +222,7 @@ export function getMoaLeaderboard(filePath = DEFAULT_HISTORY_FILE) {
|
|
|
218
222
|
}))
|
|
219
223
|
.sort((a, b) => b.wins - a.wins || b.winRate - a.winRate)
|
|
220
224
|
|
|
221
|
-
return { totalRuns, models }
|
|
225
|
+
return { totalRuns: presetFilter && presetFilter !== 'all' ? filteredRuns : totalRuns, models, preset: presetFilter }
|
|
222
226
|
} catch (err) {
|
|
223
227
|
console.warn('[dsh-moa] Failed to calculate leaderboard:', err)
|
|
224
228
|
return { totalRuns: 0, models: [] }
|
|
@@ -244,3 +248,53 @@ export function getMoaRunById(runId, filePath = DEFAULT_HISTORY_FILE) {
|
|
|
244
248
|
return null
|
|
245
249
|
}
|
|
246
250
|
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Exports history data in JSON or CSV format with optional preset filtering.
|
|
254
|
+
*/
|
|
255
|
+
export function exportMoaHistory(filePath = DEFAULT_HISTORY_FILE, options = {}) {
|
|
256
|
+
try {
|
|
257
|
+
if (!fs.existsSync(filePath)) {
|
|
258
|
+
return options?.format === 'csv' ? 'id,timestamp,preset,winnerModel,totalTokens,totalCostUsd,durationMs\n' : '[]'
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const raw = fs.readFileSync(filePath, 'utf8')
|
|
262
|
+
const lines = raw.trim().split('\n').filter(Boolean)
|
|
263
|
+
const presetFilter = typeof options === 'string' ? options : (options?.presetFilter || options?.preset || null)
|
|
264
|
+
const format = options?.format === 'csv' ? 'csv' : 'json'
|
|
265
|
+
|
|
266
|
+
const records = []
|
|
267
|
+
for (const line of lines) {
|
|
268
|
+
try {
|
|
269
|
+
const item = JSON.parse(line)
|
|
270
|
+
if (!item) continue
|
|
271
|
+
if (presetFilter && presetFilter !== 'all' && item.preset !== presetFilter) continue
|
|
272
|
+
records.push(item)
|
|
273
|
+
} catch {}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (format === 'csv') {
|
|
277
|
+
const header = 'id,timestamp,preset,winnerModel,totalTokens,totalCostUsd,durationMs,isRefinement,isFastMode'
|
|
278
|
+
const rows = records.map((r) => {
|
|
279
|
+
const escapeCsv = (val) => `"${String(val ?? '').replace(/"/g, '""')}"`
|
|
280
|
+
return [
|
|
281
|
+
escapeCsv(r.id),
|
|
282
|
+
escapeCsv(r.timestamp),
|
|
283
|
+
escapeCsv(r.preset),
|
|
284
|
+
escapeCsv(r.winnerModel),
|
|
285
|
+
Number(r.totalTokens || 0),
|
|
286
|
+
Number(r.totalCostUsd || 0).toFixed(4),
|
|
287
|
+
Number(r.durationMs || 0),
|
|
288
|
+
Boolean(r.isRefinement),
|
|
289
|
+
Boolean(r.isFastMode),
|
|
290
|
+
].join(',')
|
|
291
|
+
})
|
|
292
|
+
return [header, ...rows].join('\n')
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return JSON.stringify(records, null, 2)
|
|
296
|
+
} catch (err) {
|
|
297
|
+
console.warn('[dsh-moa] Failed to export history:', err)
|
|
298
|
+
return options?.format === 'csv' ? '' : '[]'
|
|
299
|
+
}
|
|
300
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -23,7 +23,9 @@ import {
|
|
|
23
23
|
getMoaHistory,
|
|
24
24
|
getMoaLeaderboard,
|
|
25
25
|
getMoaRunById,
|
|
26
|
+
exportMoaHistory,
|
|
26
27
|
} from './history.js'
|
|
28
|
+
import { promoteCandidateWorkspace } from './file-workspace.js'
|
|
27
29
|
|
|
28
30
|
import { createLiveCanvasClient } from './live-canvas.js'
|
|
29
31
|
|
|
@@ -41,6 +43,7 @@ export const PriceRow = z.object({
|
|
|
41
43
|
export const ModelSlotSchema = z.object({
|
|
42
44
|
provider: z.string().default('opencode-go'),
|
|
43
45
|
model: z.string().default('deepseek-v4-flash'),
|
|
46
|
+
role_persona: z.string().default(''),
|
|
44
47
|
})
|
|
45
48
|
|
|
46
49
|
export const PresetSchema = z.object({
|
|
@@ -59,6 +62,8 @@ export const PresetSchema = z.object({
|
|
|
59
62
|
reference_timeout_sec: z.number().default(60),
|
|
60
63
|
aggregator_timeout_sec: z.number().default(180),
|
|
61
64
|
blind_evaluation: z.boolean().default(false),
|
|
65
|
+
peer_critique_enabled: z.boolean().default(false),
|
|
66
|
+
allow_candidate_override: z.boolean().default(false),
|
|
62
67
|
max_tokens: z.number().default(4096),
|
|
63
68
|
judge_criteria: z.string().default(''),
|
|
64
69
|
})
|
|
@@ -421,7 +426,7 @@ export function apply(ctx, config) {
|
|
|
421
426
|
})
|
|
422
427
|
}, 'dsh-moa: history route')
|
|
423
428
|
|
|
424
|
-
// Route: /dsh-moa/leaderboard
|
|
429
|
+
// Route: /dsh-moa/leaderboard
|
|
425
430
|
ctx.effect(() => {
|
|
426
431
|
return ctx.webServer.register({
|
|
427
432
|
kind: 'exact',
|
|
@@ -432,7 +437,21 @@ export function apply(ctx, config) {
|
|
|
432
437
|
return
|
|
433
438
|
}
|
|
434
439
|
try {
|
|
435
|
-
const
|
|
440
|
+
const url = new URL(req.url || '', 'http://127.0.0.1')
|
|
441
|
+
const preset = url.searchParams.get('preset') || null
|
|
442
|
+
const format = url.searchParams.get('format') || 'json'
|
|
443
|
+
|
|
444
|
+
if (format === 'csv') {
|
|
445
|
+
const csv = exportMoaHistory(undefined, { format: 'csv', preset })
|
|
446
|
+
res.writeHead(200, {
|
|
447
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
448
|
+
'Content-Disposition': 'attachment; filename="moa-leaderboard.csv"',
|
|
449
|
+
})
|
|
450
|
+
res.end(csv)
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const data = getMoaLeaderboard(undefined, { preset })
|
|
436
455
|
writeJson(res, 200, { ok: true, ...data })
|
|
437
456
|
} catch (err) {
|
|
438
457
|
writeJson(res, 500, { ok: false, error: err?.message || String(err) })
|
|
@@ -441,6 +460,71 @@ export function apply(ctx, config) {
|
|
|
441
460
|
})
|
|
442
461
|
}, 'dsh-moa: leaderboard route')
|
|
443
462
|
|
|
463
|
+
// Route: /dsh-moa/promote (Candidate Override Action)
|
|
464
|
+
ctx.effect(() => {
|
|
465
|
+
return ctx.webServer.register({
|
|
466
|
+
kind: 'exact',
|
|
467
|
+
path: '/dsh-moa/promote',
|
|
468
|
+
handler: async (req, res) => {
|
|
469
|
+
if (req.method !== 'POST') {
|
|
470
|
+
writeJson(res, 405, { ok: false, error: 'POST required' })
|
|
471
|
+
return
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
const raw = await readBody(req)
|
|
475
|
+
const body = JSON.parse(raw || '{}')
|
|
476
|
+
const candidateIndex = parseInt(body?.candidateIndex, 10)
|
|
477
|
+
if (isNaN(candidateIndex) || candidateIndex < 1) {
|
|
478
|
+
writeJson(res, 400, { ok: false, error: 'Valid candidateIndex required (>= 1)' })
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
const cwd = body?.cwd || process.cwd()
|
|
482
|
+
const promotedFiles = await promoteCandidateWorkspace(cwd, candidateIndex, { keepMoa: true })
|
|
483
|
+
writeJson(res, 200, { ok: true, candidateIndex, promotedFiles })
|
|
484
|
+
} catch (err) {
|
|
485
|
+
writeJson(res, 500, { ok: false, error: err?.message || String(err) })
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
})
|
|
489
|
+
}, 'dsh-moa: promote candidate route')
|
|
490
|
+
|
|
491
|
+
// Route: /dsh-moa/history/export
|
|
492
|
+
ctx.effect(() => {
|
|
493
|
+
return ctx.webServer.register({
|
|
494
|
+
kind: 'exact',
|
|
495
|
+
path: '/dsh-moa/history/export',
|
|
496
|
+
handler: async (req, res) => {
|
|
497
|
+
if (req.method !== 'GET') {
|
|
498
|
+
writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
const url = new URL(req.url || '', 'http://127.0.0.1')
|
|
503
|
+
const preset = url.searchParams.get('preset') || null
|
|
504
|
+
const format = url.searchParams.get('format') || 'json'
|
|
505
|
+
const data = exportMoaHistory(undefined, { format, preset })
|
|
506
|
+
|
|
507
|
+
if (format === 'csv') {
|
|
508
|
+
res.writeHead(200, {
|
|
509
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
510
|
+
'Content-Disposition': 'attachment; filename="moa-history.csv"',
|
|
511
|
+
})
|
|
512
|
+
res.end(data)
|
|
513
|
+
return
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
res.writeHead(200, {
|
|
517
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
518
|
+
'Content-Disposition': 'attachment; filename="moa-history.json"',
|
|
519
|
+
})
|
|
520
|
+
res.end(data)
|
|
521
|
+
} catch (err) {
|
|
522
|
+
writeJson(res, 500, { ok: false, error: err?.message || String(err) })
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
})
|
|
526
|
+
}, 'dsh-moa: history export route')
|
|
527
|
+
|
|
444
528
|
// Route: /dsh-moa/runs/<id> (run replay by id)
|
|
445
529
|
ctx.effect(() => {
|
|
446
530
|
return ctx.webServer.register({
|
package/lib/moa-parser.js
CHANGED
|
@@ -45,6 +45,17 @@ export function parseMoACommand(text, presets = []) {
|
|
|
45
45
|
return null
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Check for promote subcommand: /moa promote <runId> <candidateIndex> or /moa promote <candidateIndex>
|
|
49
|
+
const promoteMatch = /^\/moa\s+promote\s+([a-zA-Z0-9_\-]+)(?:\s+(\d+))?/i.exec(text.trim())
|
|
50
|
+
if (promoteMatch) {
|
|
51
|
+
const hasTwoArgs = Boolean(promoteMatch[2])
|
|
52
|
+
return {
|
|
53
|
+
isPromote: true,
|
|
54
|
+
runId: hasTwoArgs ? promoteMatch[1] : null,
|
|
55
|
+
candidateIndex: hasTwoArgs ? parseInt(promoteMatch[2], 10) : parseInt(promoteMatch[1], 10),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
48
59
|
const remainder = text.slice(4).trim()
|
|
49
60
|
if (!remainder) {
|
|
50
61
|
return {
|
|
@@ -177,5 +188,18 @@ export function formatMoAResponse({ moaResult, presetName }) {
|
|
|
177
188
|
})
|
|
178
189
|
}
|
|
179
190
|
|
|
191
|
+
if (moaResult?.allowCandidateOverride && refs.length > 1) {
|
|
192
|
+
const runId = moaResult?.runId || ''
|
|
193
|
+
parts.push('### 🔄 Candidate Override Actions')
|
|
194
|
+
parts.push('To apply an alternative candidate\'s files instead of the judge\'s pick:')
|
|
195
|
+
refs.forEach((ref, idx) => {
|
|
196
|
+
const isWinner = (idx + 1) === moaResult.winningIndex
|
|
197
|
+
const tag = isWinner ? ' *(Current Judge Pick)*' : ''
|
|
198
|
+
const promoteCmd = runId ? `/moa promote ${runId} ${idx + 1}` : `/moa promote ${idx + 1}`
|
|
199
|
+
parts.push(`- \`${promoteCmd}\` — Candidate ${idx + 1} (${ref.label})${tag}`)
|
|
200
|
+
})
|
|
201
|
+
parts.push('')
|
|
202
|
+
}
|
|
203
|
+
|
|
180
204
|
return parts.join('\n')
|
|
181
205
|
}
|