@goodandready/dsh-moa 0.2.12 → 0.2.14

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/docs/README.ru.md CHANGED
@@ -16,11 +16,21 @@
16
16
  </p>
17
17
 
18
18
  <p align="center">
19
- <a href="../README.md"><b>🇬🇧 English</b></a> •
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="../README.md"><b>🇬🇧 English</b></a> •
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
  ---
@@ -67,3 +67,7 @@
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.
72
+
73
+ - 2026-09-13 — Приведение локализации плагина в строгое соответствие со стандартами dhs-plugin-release-workflow и dsh-plugin-authoring (Gitea Issue #61): добавление полного китайского словаря (zh) в lib/client.js наряду с каноническим английским (en), удаление захардкоженных русских строк в серверной части (lib/moa-parser.js, lib/moa-runner.js), интернационализация эвристик в lib/file-workspace.js и lib/moa-prompts.js (en + zh), создание issue в goodandready/dsh-russian-lang для русификации.
@@ -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,19 @@ 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
+ 'proposers.role_general': 'General',
64
+ 'proposers.role_minimalist': 'Minimalist',
65
+ 'proposers.role_robustness': 'Robustness',
66
+ 'proposers.role_performance': 'Performance',
67
+ 'proposers.role_tester': 'Tester',
68
+ 'leaderboard.filter_all': 'All Presets',
69
+ 'leaderboard.export_csv': 'Export CSV',
70
+ 'leaderboard.export_json': 'Export JSON',
58
71
  'aggregator.blind_label': 'Blind Review (Anonymize Candidates for Judge)',
59
72
  'aggregator.blind_hint': 'When enabled: candidate model names and providers are hidden from the judge prompt to prevent family/brand bias.',
60
73
  'aggregator.timeout_label': 'Judge synthesis timeout (sec):',
@@ -102,6 +115,99 @@ window.__ModuleLoader__.load({
102
115
  'slash.desc': 'Run turn with Mixture of Agents ensemble synthesis (/moa [preset] <prompt>)',
103
116
  }
104
117
 
118
+ const zh = {
119
+ title: '智能体混合 (MoA)',
120
+ description: '多模型协同编排:通过 /moa <提示词> 进行候选模型并行提案与主裁判综合。',
121
+ 'header.title': '智能体混合 (MoA) 编排系统',
122
+ 'header.sub': '由多个候选提案模型并行生成方案,再由领先的聚合裁判模型进行终审评测与代码提拔。完成后自动切回您的会话模型。',
123
+ 'badge.online': 'MoA 已启用',
124
+ 'badge.offline': '主机不可达',
125
+ 'badge.preset': '预设:{name}',
126
+ 'badge.models': '{count} 个可用模型',
127
+ 'badge.candidates': '{count} 个提案模型',
128
+ 'presets.title': '📋 预设管理',
129
+ 'presets.desc': '选择当前活动预设或针对特定任务(如代码审查、数学推演、快速分流)创建自定义配置。',
130
+ 'presets.active_badge': '默认激活',
131
+ 'presets.set_default': '设为默认',
132
+ 'presets.new_btn': '+ 新建预设',
133
+ 'presets.del_btn': '删除',
134
+ 'presets.del_confirm': '确定要删除预设“{name}”吗?',
135
+ 'presets.create': '创建',
136
+ 'presets.cancel': '取消',
137
+ 'presets.name_placeholder': '新预设名称(例如:fast-audit)',
138
+ 'aggregator.title': '⚖️ 领先模型 / 裁判(综合与审计)',
139
+ 'aggregator.desc': '对比候选方案,消除差异,过滤幻觉,并综合生成最终生产级解决方案。',
140
+ 'aggregator.temp_label': '裁判采样温度 (Temperature)',
141
+ 'aggregator.temp_hint': '推荐:0.2 – 0.4,以确保严格的验证和确定性的代码综合。',
142
+ 'aggregator.criteria_label': '裁判评测标准(可选):',
143
+ 'aggregator.criteria_placeholder': '例如:优先考虑高性能、零外部依赖、完善的边界异常处理...',
144
+ 'aggregator.questions_label': '对于宽泛任务先提出澄清问题(需求问卷)',
145
+ 'aggregator.questions_hint': '启用时:对于简短或模糊的任务,裁判在生成代码前会先综合一份包含 2–4 个问题的选项清单。',
146
+ 'aggregator.curator_label': '策展人综合与主组装模型建议',
147
+ 'aggregator.curator_hint': '启用时:策展人会提炼每个候选方案的精华部分,应用反模式清单,并建议由哪个智能体模型组装最终方案。',
148
+ 'aggregator.stream_label': '实时流式传输裁判/策展人思考过程',
149
+ 'aggregator.stream_hint': '将裁判生成的思考过程实时推送到聊天框,实现零延迟首字响应。',
150
+ 'aggregator.peer_label': '决策评议会:候选模型交叉互评(第二轮 Consilium)',
151
+ 'aggregator.peer_hint': '在主裁判终审前,各候选模型互相审阅并改进彼此的方案代码。',
152
+ 'aggregator.override_label': '允许手动候选方案提拔操作 (Override)',
153
+ 'aggregator.override_hint': '在 .moa 目录中保留候选模型的工作区,允许通过聊天命令提拔任意候选模型的文件。',
154
+ 'proposers.role_label': '工程角色画像:',
155
+ 'proposers.role_general': '通用平衡 (General)',
156
+ 'proposers.role_minimalist': '极简标准库 (Minimalist)',
157
+ 'proposers.role_robustness': '防御鲁棒 (Robustness)',
158
+ 'proposers.role_performance': '极致性能 (Performance)',
159
+ 'proposers.role_tester': '测试驱动 (Tester)',
160
+ 'leaderboard.filter_all': '全部预设',
161
+ 'leaderboard.export_csv': '导出 CSV',
162
+ 'leaderboard.export_json': '导出 JSON',
163
+ 'aggregator.blind_label': '盲审模式(对裁判隐藏候选模型名称)',
164
+ 'aggregator.blind_hint': '启用时:从裁判提示词中匿名化候选模型的提供商和名称,杜绝品牌偏见。',
165
+ 'aggregator.timeout_label': '裁判综合超时时长(秒):',
166
+ 'proposers.timeout_label': '候选模型生成超时时长(秒):',
167
+ 'leaderboard.title': '🏆 模型胜率排行榜',
168
+ 'leaderboard.desc': '基于历史记录的 MoA 裁判评测判决数据统计指标。',
169
+ 'leaderboard.model': '模型',
170
+ 'leaderboard.runs': '运行次数',
171
+ 'leaderboard.wins': '胜出次数',
172
+ 'leaderboard.winrate': '胜率',
173
+ 'leaderboard.cost': '平均成本',
174
+ 'leaderboard.empty': '暂无历史运行记录。',
175
+ 'aggregator.fallbacks_title': '备用裁判链',
176
+ 'aggregator.fallbacks_desc': '当主裁判触发速率限制或服务不可用时,自动顺序调用的备用模型。',
177
+ 'aggregator.add_fallback_btn': '+ 添加备用裁判',
178
+ 'proposers.quorum_label': '法定人数快进(防拖尾模型机制)',
179
+ 'proposers.quorum_hint': '当 67% 以上候选模型完成时,开启缓冲定时器(默认 10 秒),不再死等卡顿的模型。',
180
+ 'proposers.grace_label': '缓冲等待时长(秒):',
181
+ 'proposers.title': '👥 候选提案模型 (Proposers)',
182
+ 'proposers.desc': '并行生成互相独立的解决方案,最大化思路多样性与解空间覆盖度。',
183
+ 'proposers.parallel_count': '并行提案模型数量',
184
+ 'proposers.candidate': '候选模型',
185
+ 'proposers.add_btn': '+ 添加候选模型',
186
+ 'proposers.temp_label': '候选模型采样温度 (Temperature)',
187
+ 'proposers.temp_hint': '推荐:0.6 – 0.8,鼓励创造性、发散性的独立探索。',
188
+ 'stats.title': '📈 MoA 数据分析与遥测',
189
+ 'stats.desc': '会话遥测、历史运行追踪及模型胜率统计。',
190
+ 'stats.total_runs': '总运行次数',
191
+ 'stats.avg_cost': '平均每次成本',
192
+ 'stats.active_models': '已配置模型数',
193
+ 'stats.preset_count': '预设总数',
194
+ 'picker.select': '选择模型…',
195
+ 'picker.search_placeholder': '按名称或提供商搜索模型…',
196
+ 'picker.found': '找到',
197
+ 'picker.of': '共',
198
+ 'picker.clear': '清除',
199
+ 'picker.none': '未找到匹配的模型。',
200
+ 'picker.custom': '使用自定义:',
201
+ 'actions.save': '保存配置',
202
+ 'actions.saving': '正在保存…',
203
+ 'actions.saved': '配置已成功保存!',
204
+ 'actions.retry': '重新加载配置',
205
+ 'badge.disabled': 'MoA 已禁用',
206
+ 'config.enabled_label': '启用 MoA(斜杠命令与轮次拦截路由)',
207
+ 'slash.desc': '使用智能体混合 (MoA) 协同综合运行此轮次 (/moa [预设] <提示词>)',
208
+ }
209
+
210
+
105
211
  function makeT(dict, fallback) {
106
212
  return function t(key, vars) {
107
213
  let val = (dict && dict[key]) || (fallback && fallback[key]) || key
@@ -461,6 +567,8 @@ window.__ModuleLoader__.load({
461
567
  hostStatus,
462
568
  stats,
463
569
  leaderboard = [],
570
+ leaderboardPresetFilter = 'all',
571
+ setLeaderboardPresetFilter = () => {},
464
572
  handleSave,
465
573
  saveStatus,
466
574
  reload,
@@ -877,6 +985,38 @@ window.__ModuleLoader__.load({
877
985
  },
878
986
  })
879
987
  ),
988
+ React.createElement(
989
+ 'label',
990
+ { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500, marginTop: 8 } },
991
+ React.createElement('input', {
992
+ type: 'checkbox',
993
+ checked: Boolean(currentPreset.peer_critique_enabled),
994
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, peer_critique_enabled: e.target.checked })),
995
+ style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
996
+ }),
997
+ t('aggregator.peer_label')
998
+ ),
999
+ React.createElement(
1000
+ 'div',
1001
+ { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
1002
+ t('aggregator.peer_hint')
1003
+ ),
1004
+ React.createElement(
1005
+ 'label',
1006
+ { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500, marginTop: 8 } },
1007
+ React.createElement('input', {
1008
+ type: 'checkbox',
1009
+ checked: Boolean(currentPreset.allow_candidate_override),
1010
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, allow_candidate_override: e.target.checked })),
1011
+ style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
1012
+ }),
1013
+ t('aggregator.override_label')
1014
+ ),
1015
+ React.createElement(
1016
+ 'div',
1017
+ { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
1018
+ t('aggregator.override_hint')
1019
+ ),
880
1020
  /* Fallback Judges Chain */
881
1021
  React.createElement(
882
1022
  'div',
@@ -1052,6 +1192,28 @@ window.__ModuleLoader__.load({
1052
1192
  t,
1053
1193
  })
1054
1194
  ),
1195
+ React.createElement(
1196
+ 'select',
1197
+ {
1198
+ className: 'moa-select',
1199
+ style: { width: 105, height: 32, fontSize: 12, padding: '0 6px' },
1200
+ value: ref.role_persona || 'general',
1201
+ title: t('proposers.role_label'),
1202
+ onChange: (e) => {
1203
+ const newRole = e.target.value
1204
+ updateCurrentPreset((p) => {
1205
+ const refs = [...(p.reference_models || [])]
1206
+ refs[idx] = { ...refs[idx], role_persona: newRole }
1207
+ return { ...p, reference_models: refs }
1208
+ })
1209
+ },
1210
+ },
1211
+ React.createElement('option', { value: 'general' }, t('proposers.role_general') || 'General'),
1212
+ React.createElement('option', { value: 'minimalist' }, t('proposers.role_minimalist') || 'Minimalist'),
1213
+ React.createElement('option', { value: 'robustness' }, t('proposers.role_robustness') || 'Robustness'),
1214
+ React.createElement('option', { value: 'performance' }, t('proposers.role_performance') || 'Performance'),
1215
+ React.createElement('option', { value: 'tester' }, t('proposers.role_tester') || 'Tester')
1216
+ ),
1055
1217
  currentRefs.length > 1 &&
1056
1218
  React.createElement(
1057
1219
  'button',
@@ -1139,7 +1301,46 @@ window.__ModuleLoader__.load({
1139
1301
  Array.isArray(leaderboard) && leaderboard.length > 0 && React.createElement(
1140
1302
  'div',
1141
1303
  { style: { marginTop: 12, display: 'flex', flexDirection: 'column', gap: 6 } },
1142
- React.createElement('div', { style: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, t('leaderboard.title')),
1304
+ React.createElement(
1305
+ 'div',
1306
+ { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' } },
1307
+ React.createElement('div', { style: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, t('leaderboard.title')),
1308
+ React.createElement(
1309
+ 'div',
1310
+ { style: { display: 'flex', alignItems: 'center', gap: 6 } },
1311
+ React.createElement(
1312
+ 'select',
1313
+ {
1314
+ className: 'moa-select',
1315
+ style: { height: 26, fontSize: 11, padding: '0 6px' },
1316
+ value: leaderboardPresetFilter,
1317
+ onChange: (e) => setLeaderboardPresetFilter(e.target.value),
1318
+ },
1319
+ React.createElement('option', { value: 'all' }, t('leaderboard.filter_all')),
1320
+ presets.map((p) => React.createElement('option', { key: p.name, value: p.name }, p.name))
1321
+ ),
1322
+ React.createElement(
1323
+ 'a',
1324
+ {
1325
+ href: `/dsh-moa/history/export?format=csv${leaderboardPresetFilter !== 'all' ? '&preset=' + encodeURIComponent(leaderboardPresetFilter) : ''}`,
1326
+ download: 'moa-history.csv',
1327
+ className: 'moa-btn',
1328
+ style: { height: 26, fontSize: 11, padding: '0 8px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center' },
1329
+ },
1330
+ t('leaderboard.export_csv') || 'CSV'
1331
+ ),
1332
+ React.createElement(
1333
+ 'a',
1334
+ {
1335
+ href: `/dsh-moa/history/export?format=json${leaderboardPresetFilter !== 'all' ? '&preset=' + encodeURIComponent(leaderboardPresetFilter) : ''}`,
1336
+ download: 'moa-history.json',
1337
+ className: 'moa-btn',
1338
+ style: { height: 26, fontSize: 11, padding: '0 8px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center' },
1339
+ },
1340
+ t('leaderboard.export_json') || 'JSON'
1341
+ )
1342
+ )
1343
+ ),
1143
1344
  React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginTop: -4 } }, t('leaderboard.desc')),
1144
1345
  React.createElement(
1145
1346
  'div',
@@ -1257,6 +1458,7 @@ window.__ModuleLoader__.load({
1257
1458
  const [hostStatus, setHostStatus] = React.useState('connecting')
1258
1459
  const [stats, setStats] = React.useState({ totalRuns: null, avgCostUsd: null })
1259
1460
  const [leaderboard, setLeaderboard] = React.useState([])
1461
+ const [leaderboardPresetFilter, setLeaderboardPresetFilter] = React.useState('all')
1260
1462
 
1261
1463
  React.useEffect(() => {
1262
1464
  if (!scope || !snapshot) return
@@ -1288,7 +1490,10 @@ window.__ModuleLoader__.load({
1288
1490
  setHostStatus('offline')
1289
1491
  })
1290
1492
 
1291
- fetch('/dsh-moa/leaderboard', { cache: 'no-store' })
1493
+ const lbUrl = leaderboardPresetFilter !== 'all'
1494
+ ? `/dsh-moa/leaderboard?preset=${encodeURIComponent(leaderboardPresetFilter)}`
1495
+ : '/dsh-moa/leaderboard'
1496
+ fetch(lbUrl, { cache: 'no-store' })
1292
1497
  .then((r) => r.json())
1293
1498
  .then((data) => {
1294
1499
  if (data && data.ok && Array.isArray(data.models)) {
@@ -1406,6 +1611,8 @@ window.__ModuleLoader__.load({
1406
1611
  hostStatus,
1407
1612
  stats,
1408
1613
  leaderboard,
1614
+ leaderboardPresetFilter,
1615
+ setLeaderboardPresetFilter,
1409
1616
  handleSave,
1410
1617
  saveStatus,
1411
1618
  reload,
@@ -1519,7 +1726,7 @@ window.__ModuleLoader__.load({
1519
1726
 
1520
1727
  if (typeof ctx.effect === 'function') {
1521
1728
  ctx.effect(() => {
1522
- const undo = [addLocale('en', en)]
1729
+ const undo = [addLocale('en', en), addLocale('zh', zh)]
1523
1730
  return () => {
1524
1731
  for (const off of undo) {
1525
1732
  if (typeof off === 'function') {
@@ -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
 
@@ -151,10 +152,10 @@ export function isRefinementTask(userPrompt = '', projectFiles = []) {
151
152
 
152
153
  // Greenfield creation phrases - ALWAYS fresh task
153
154
  const freshPhrases = [
154
- 'с нуля', 'from scratch', 'новый проект', 'новое приложение', 'new project', 'new app',
155
- 'создай проект', 'создай приложение', 'создай игру', 'создай сервис',
155
+ 'from scratch', 'new project', 'new app', 'create a new', 'build a new', 'generate a new',
156
+ '从头开始', '新建项目', '创建项目', '新应用', '新建应用',
157
+ 'с нуля', 'новый проект', 'новое приложение', 'создай проект', 'создай приложение', 'создай игру', 'создай сервис',
156
158
  'сделай проект', 'сделай приложение', 'сделай игру',
157
- 'create a new', 'build a new', 'generate a new',
158
159
  ]
159
160
  if (freshPhrases.some((phrase) => p.includes(phrase))) {
160
161
  return false
@@ -162,9 +163,11 @@ export function isRefinementTask(userPrompt = '', projectFiles = []) {
162
163
 
163
164
  // Modification keywords
164
165
  const modKeywords = [
165
- 'добавь', 'измени', 'поменяй', 'исправь', 'обнови', 'переделай', 'доработай', 'удали',
166
166
  'add', 'change', 'update', 'fix', 'modify', 'refactor', 'improve', 'enhance', 'adjust',
167
- 'style', 'css', 'темн', 'светл', 'color', 'цвет', 'кнопк', 'theme', 'bug', 'баг',
167
+ 'style', 'css', 'color', 'theme', 'bug',
168
+ '添加', '修改', '更新', '修复', '重构', '改进', '调整', '样式', '颜色', '主题',
169
+ 'добавь', 'измени', 'поменяй', 'исправь', 'обнови', 'переделай', 'доработай', 'удали',
170
+ 'темн', 'светл', 'цвет', 'кнопк', 'баг',
168
171
  ]
169
172
 
170
173
  return modKeywords.some((kw) => p.includes(kw))
@@ -275,3 +278,48 @@ async function getFilesRecursively(dir) {
275
278
  await scan(dir)
276
279
  return results
277
280
  }
281
+
282
+
283
+ /**
284
+ * Verifies the syntactic validity of generated code files.
285
+ * Uses Node.js in-memory vm.Script for JS (with ES module export/import transformation) and JSON.parse for JSON.
286
+ */
287
+ export function verifyFileSyntax(files = []) {
288
+ if (!Array.isArray(files)) return []
289
+ const warnings = []
290
+
291
+ for (const file of files) {
292
+ if (!file || typeof file.content !== 'string' || !file.relativePath) continue
293
+ const ext = path.extname(file.relativePath).toLowerCase()
294
+
295
+ if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
296
+ try {
297
+ const transformed = file.content
298
+ .replace(/^([ \t]*)export\s+default\s+/gm, '$1')
299
+ .replace(/^([ \t]*)export\s*\{[^}]*\}\s*;?/gm, '$1')
300
+ .replace(/^([ \t]*)export\s+(const|let|var|function|class|async\s+function)\s+/gm, '$1$2 ')
301
+ .replace(/^([ \t]*)import\s+[^;\n]+;?/gm, '$1')
302
+ .replace(/^([ \t]*)export\s+\*\s+from\s+[^;\n]+;?/gm, '$1')
303
+ new vm.Script(transformed)
304
+ } catch (err) {
305
+ warnings.push({
306
+ file: file.relativePath,
307
+ error: err?.message || String(err),
308
+ line: err?.lineNumber || 1,
309
+ })
310
+ }
311
+ } else if (ext === '.json') {
312
+ try {
313
+ JSON.parse(file.content)
314
+ } catch (err) {
315
+ warnings.push({
316
+ file: file.relativePath,
317
+ error: err?.message || String(err),
318
+ line: 1,
319
+ })
320
+ }
321
+ }
322
+ }
323
+
324
+ return warnings
325
+ }
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
+ }