@goodandready/dsh-moa 0.2.70 → 0.2.72

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 CHANGED
@@ -180,6 +180,12 @@ Seamlessly retains synthesized baseline code from prior turns while pruning inte
180
180
  ### 18. Graceful Degradation & Local Fallback Resilience
181
181
  When `local_fallback_enabled: true`, candidates that fail due to network outages, timeouts, or rate limits (429/500) automatically recover using configured local models (Ollama / MiniPC). Each fallback model operates with an independent execution budget and an isolated controller linked to the parent turn signal, preventing cloud timeouts from starving fallback models. Additionally, terminal errors and aborted streams are strictly classified, ensuring reliable error propagation and failover.
182
182
 
183
+ ### 19. Zero-Token Local Pre-Judge Syntax Filter
184
+ Validates markdown code blocks across all candidate outputs locally using JavaScript/TypeScript AST checking, JSON parsing, and bracket/quote balance verification before calling the Judge LLM. When malformed candidates are detected, they are automatically disqualified or annotated with structured syntax error reports, saving judge tokens and preventing syntax errors from propagating. Configured via `local_preflight_check: true` (enabled by default).
185
+
186
+ ### 20. Early Exit upon Semantic Consensus
187
+ Compares normalized candidate proposals using token-level Sørensen-Dice similarity. When models arrive at identical or near-identical solutions (meeting or exceeding `early_exit_threshold`, default: `0.95`), the expensive Judge synthesis phase is automatically bypassed. The consensus solution is promoted directly to the workspace, saving up to 100% of judge token costs while accelerating pipeline latency. Configured via `early_exit_consensus_enabled: true` and `early_exit_threshold: 0.95`.
188
+
183
189
  ### 6. Live Canvas 1-Click Preview (optional)
184
190
  If `@goodandready/dsh-live-canvas` is installed in the same profile, `dsh-moa` pushes the promoted HTML file to the Live Canvas REST contract (`POST /dsh-live-canvas/api/preview`, served by the same harness webServer) and appends a one-click preview link (`/dsh-live-canvas/sandbox/<id>`) to the answer. Without the plugin the step is skipped silently — no errors in the log, no dead links.
185
191
 
@@ -289,6 +295,9 @@ dsh-moa:
289
295
  | `presets[].quorum_enabled` | `boolean` | `false` | Straggler mitigation: proceed with synthesis once >= 60% candidates respond |
290
296
  | `presets[].grace_period_sec` | `number` | `10` | Grace period in seconds to wait for stragglers after quorum is reached |
291
297
  | `presets[].aggregator_fallbacks` | `array` | `[]` | Ordered fallback judge models tried if primary aggregator encounters transient errors |
298
+ | `presets[].local_preflight_check` | `boolean` | `true` | Zero-token local AST, JSON, and bracket balance verification for candidate code blocks before the Judge panel |
299
+ | `presets[].early_exit_consensus_enabled` | `boolean` | `false` | Bypass Judge evaluation and promote directly when candidate solutions reach high semantic consensus |
300
+ | `presets[].early_exit_threshold` | `number` | `0.95` | Token-level similarity threshold (0.50 to 1.00) required to trigger early exit consensus |
292
301
  | `presets[].blind_evaluation` | `boolean` | `false` | Anonymize candidate model names for the judge/curator to eliminate family/brand bias |
293
302
  | `presets[].reference_timeout_sec` | `number` | `60` | Per-candidate execution timeout in seconds |
294
303
  | `presets[].aggregator_timeout_sec` | `number` | `180` | Aggregator/judge synthesis timeout in seconds |
package/README.ru.md CHANGED
@@ -180,6 +180,12 @@ graph TD
180
180
  ### 18. Плавная деградация и локальный фоллбэк
181
181
  Опция `local_fallback_enabled` позволяет задать резервные локальные модели (Ollama, vLLM, MiniPC), к которым плагин бесшовно обращается при сбоях сетевых API, таймаутах или превышении лимитов rate limit (429/500). Каждая фоллбэк-модель запускается с независимым бюджетом времени и изолированным контроллером отмены, связанным с сигналом основного хода, предотвращая исчерпание времени ожидания из-за задержек облачных провайдеров. Стриминговые сбои и терминальные ошибки классифицируются типизированными исключениями с гарантированной защитой от позднего промоушна.
182
182
 
183
+ ### 19. Локальный синтаксический шлюз кандидатов без расхода токенов
184
+ Локально парсит блоки кода из Markdown-ответов кандидатов и проверяет их синтаксис (JavaScript, TypeScript, JSON, Python) и баланс скобок/кавычек до вызова дорогостоящей модели-судьи. Кандидаты с синтаксическими ошибками дисквалифицируются или помечаются структурированными аннотациями для судьи, полностью исключая лишний расход токенов на заведомо невалидный код. Включается настройкой `local_preflight_check: true` (включена по умолчанию).
185
+
186
+ ### 20. Ранний выход по семантическому консенсусу
187
+ Сравнивает нормализованные решения кандидатов по токенному сходству Сёренсена-Дайса. Если решения моделей совпадают или практически идентичны (достигнут порог `early_exit_threshold`, по умолчанию 0.95), стадия вызова судьи (Judge) пропускается. Согласованный вариант немедленно продвигается в проект, экономя до 100% токенов судейской коллегии и сокращая задержку ответа. Управляется параметрами `early_exit_consensus_enabled: true` и `early_exit_threshold: 0.95`.
188
+
183
189
  ### 6. Live Canvas 1-клик предпросмотр (опционально)
184
190
  Если в профиле установлен `@goodandready/dsh-live-canvas`, `dsh-moa` отправляет промоученный HTML-файл в REST-контракт Live Canvas (`POST /dsh-live-canvas/api/preview`, тот же webServer харнесса) и добавляет к ответу ссылку на предпросмотр (`/dsh-live-canvas/sandbox/<id>`). Без плагина шаг пропускается тихо — без ошибок в журнале и без битых ссылок.
185
191
 
@@ -289,6 +295,9 @@ dsh-moa:
289
295
  | `presets[].quorum_enabled` | `boolean` | `false` | Защита от зависших моделей (stragglers): запуск синтеза при ответе от >= 60% кандидатов |
290
296
  | `presets[].grace_period_sec` | `number` | `10` | Грейс-период (в секундах) ожидания оставшихся моделей после достижения кворума |
291
297
  | `presets[].aggregator_fallbacks` | `array` | `[]` | Список запасных моделей-судей при сбоях основной модели агрегатора |
298
+ | `presets[].local_preflight_check` | `boolean` | `true` | Локальная проверка AST, JSON и баланса скобок в блоках кода кандидатов без расхода токенов |
299
+ | `presets[].early_exit_consensus_enabled` | `boolean` | `false` | Пропуск этапа судьи и прямое продвижение при достижении семантического консенсуса кандидатов |
300
+ | `presets[].early_exit_threshold` | `number` | `0.95` | Порог токенного сходства (0.50–1.00) для срабатывания раннего выхода по консенсусу |
292
301
  | `presets[].blind_evaluation` | `boolean` | `false` | Обезличивание имен кандидатов («Candidate 1», «Candidate 2») для исключения предвзятости судьи |
293
302
  | `presets[].reference_timeout_sec` | `number` | `60` | Таймаут опроса каждого кандидата в секундах |
294
303
  | `presets[].aggregator_timeout_sec` | `number` | `180` | Таймаут синтеза решения судьей в секундах |
package/README.zh.md CHANGED
@@ -157,6 +157,12 @@ graph TD
157
157
  ### 18. 平滑降级与本地容灾恢复 (Local Fallback Resilience)
158
158
  当 `local_fallback_enabled: true` 时,因网络中断、超时或速率限制(429/500)而失败的候选模型会自动使用配置的本地模型(Ollama / MiniPC)恢复。每个后备模型都拥有独立的超时执行配额与关联主 turn signal 的隔离取消控制器,防止云端超时耗尽本地恢复时间。同时严格检测终止错误与流中断,确保安全容灾且杜绝已取消 turn 的延迟文件提升。
159
159
 
160
+ ### 19. 零 Token 本地预审语法网关 (Zero-Token Pre-Judge Syntax Filter)
161
+ 在调用裁判模型之前,在本地解析所有候选方案的 Markdown 代码块,进行 JavaScript/TypeScript AST 语法校验、JSON 解析以及括号/引号匹配检查。检测到语法损坏的候选将被自动过滤或附带结构化语法错误注解,无需消耗模型 Token 即可杜绝语法缺陷代码流入最终裁决。通过 `local_preflight_check: true` 配置(默认开启)。
162
+
163
+ ### 20. 语义共识提前退出机制 (Early Exit upon Semantic Consensus)
164
+ 基于 Token 级 Sørensen-Dice 相似度算法比对归一化后的候选方案。当多个候选模型给出高度一致的代码方案(达到或超过 `early_exit_threshold` 阈值,默认 0.95)时,自动跳过高耗费的裁判综合评审阶段,直接将共识方案提升至项目,在大幅缩短响应延迟的同时节省多达 100% 的裁判 Token 支出。通过 `early_exit_consensus_enabled: true` 与 `early_exit_threshold: 0.95` 配置。
165
+
160
166
  ### 6. Live Canvas 一键预览(可选)
161
167
  若同一 profile 中安装了 `@goodandready/dsh-live-canvas`,`dsh-moa` 会将提升后的 HTML 文件推送到 Live Canvas 的 REST 契约(`POST /dsh-live-canvas/api/preview`,由同一 harness webServer 提供服务),并在回答中附上一键预览链接(`/dsh-live-canvas/sandbox/<id>`)。未安装该插件时此步骤静默跳过——日志无报错,也不会出现死链接。
162
168
 
@@ -225,6 +231,9 @@ dsh-moa:
225
231
  | `presets[].reference_models` | `array` | `[...]` | 并行提案阶段并发调用的顾问模型列表 |
226
232
  | `presets[].aggregator` | `object` | `{...}` | 负责综合评审、代码审查与裁决胜出者的裁判模型 |
227
233
  | `presets[].ask_clarifying_questions` | `boolean` | `true` | 针对宽泛需求合成澄清问卷(预设级别开关) |
234
+ | `presets[].local_preflight_check` | `boolean` | `true` | 在裁判评审前本地验证候选代码块的 AST、JSON 与括号平衡(零 Token 消耗) |
235
+ | `presets[].early_exit_consensus_enabled` | `boolean` | `false` | 当候选方案达成高语义共识时跳过裁判评审并直接提升获胜方案 |
236
+ | `presets[].early_exit_threshold` | `number` | `0.95` | 触发语义共识提前退出的 Token 相似度阈值(0.50 至 1.00) |
228
237
  | `presets[].reference_temperature` / `.aggregator_temperature` | `number` | `0.6` / `0.4` | 顾问与裁判的采样温度 |
229
238
  | `presets[].max_tokens` | `number` | `4096` | 每次模型调用的最大输出 Token 数 |
230
239
  | `presets[].judge_criteria` | `string` | `""` | 传给裁判的可选附加评审准则 |
package/lib/client.js CHANGED
@@ -185,6 +185,10 @@ window.__ModuleLoader__.load({
185
185
  "aggregator.workbench_desc": "Judge scores each candidate file from 1 to 10 with pros/cons. Select desired files across candidates; top model harmonizes and glues them, followed by final judge audit.",
186
186
  "aggregator.workbench_companion": "Companion Plugin:",
187
187
  "aggregator.workbench_companion_note": "safe shadow Git checkpoints before file promotion",
188
+ "aggregator.preflight_syntax_title": "\u{1F50D} Pre-Judge Local Syntax Gate (Zero-Token Linter)",
189
+ "aggregator.preflight_syntax_desc": "Validate candidate code syntax locally before judge invocation to filter broken code without token costs.",
190
+ "aggregator.early_consensus_title": "\u26A1 Early Exit upon Semantic Consensus",
191
+ "aggregator.early_consensus_desc": "Bypass the expensive judge round when reference models converge on equivalent code solutions.",
188
192
  "workbench.title": "\u{1F6E0}\uFE0F Interactive Synthesis Workbench",
189
193
  "workbench.desc": "Inspect candidate file quality scores (1\u201310), mix and match files across proposals, harmonize with top-model glue, and run a final judge audit.",
190
194
  "workbench.select_run": "MoA Run:",
@@ -379,6 +383,10 @@ window.__ModuleLoader__.load({
379
383
  "aggregator.workbench_desc": "\u88C1\u5224\u4E3A\u6BCF\u4E2A\u5019\u9009\u6587\u4EF6\u6253\u5206 (1-10) \u5E76\u6807\u6CE8\u4F18\u7F3A\u70B9\u3002\u5728 UI \u4E2D\u81EA\u7531\u7EC4\u5408\u5404\u5019\u9009\u6587\u4EF6\uFF0C\u7531\u5F53\u8F6E\u6700\u9AD8\u5206\u6A21\u578B\u8FDB\u884C\u4EE3\u7801\u7C98\u5408\u7EDF\u4E00\uFF0C\u6700\u7EC8\u7531\u88C1\u5224\u590D\u5BA1\u3002",
380
384
  "aggregator.workbench_companion": "\u914D\u5957\u63D2\u4EF6\uFF1A",
381
385
  "aggregator.workbench_companion_note": "\u6587\u4EF6\u63D0\u5347\u524D\u5B89\u5168\u521B\u5EFA\u5F71\u5B50 Git \u5FEB\u7167",
386
+ "aggregator.preflight_syntax_title": "\u{1F50D} \u8BC4\u5BA1\u524D\u672C\u5730\u8BED\u6CD5\u68C0\u67E5\u95E8\u7981\uFF08\u96F6Token\u5F00\u9500\uFF09",
387
+ "aggregator.preflight_syntax_desc": "\u5728\u8C03\u7528\u88C1\u5224\u6A21\u578B\u524D\u5BF9\u5019\u9009\u4EE3\u7801\u8FDB\u884C\u672C\u5730\u8BED\u6CD5\u68C0\u67E5\uFF0C\u96F6Token\u5254\u9664\u8BED\u6CD5\u7834\u635F\u4EE3\u7801\u3002",
388
+ "aggregator.early_consensus_title": "\u26A1 \u8BED\u4E49\u5171\u8BC6\u5FEB\u901F\u76F4\u51FA\uFF08Early Exit\uFF09",
389
+ "aggregator.early_consensus_desc": "\u5F53\u591A\u5019\u9009\u6A21\u578B\u751F\u6210\u4E00\u81F4\u4EE3\u7801\uFF08\u76F8\u4F3C\u5EA6\u8FBE\u6807\uFF09\u65F6\uFF0C\u76F4\u63A5\u8FD4\u56DE\u5171\u8BC6\u89E3\uFF0C\u8DF3\u8FC7\u88C1\u5224\u9636\u6BB5\u8282\u7EA6Token\u4E0E\u8017\u65F6\u3002",
382
390
  "workbench.title": "\u{1F6E0}\uFE0F \u4EA4\u4E92\u5F0F\u5408\u6210\u5DE5\u4F5C\u53F0",
383
391
  "workbench.desc": "\u67E5\u770B\u5019\u9009\u6A21\u578B\u7684\u6587\u4EF6\u8D28\u91CF\u6253\u5206 (1\u201310)\uFF0C\u81EA\u7531\u6DF7\u642D\u7EC4\u5408\u76EE\u6807\u6587\u4EF6\uFF0C\u7531\u6700\u9AD8\u5206\u6A21\u578B\u7C98\u5408\u7EDF\u4E00\u5E76\u6267\u884C\u6700\u7EC8\u88C1\u5224\u590D\u5BA1\u3002",
384
392
  "workbench.select_run": "MoA \u8FD0\u884C\uFF1A",
@@ -2055,6 +2063,64 @@ window.__ModuleLoader__.load({
2055
2063
  "(" + (t("aggregator.workbench_companion_note") || "safe shadow Git checkpoints before file promotion") + ")"
2056
2064
  )
2057
2065
  ),
2066
+ /* AST-Level Composite Merge (#248) */
2067
+ React.createElement(
2068
+ "div",
2069
+ { 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 } },
2070
+ React.createElement(
2071
+ "label",
2072
+ { style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 13, fontWeight: 500 } },
2073
+ React.createElement("input", {
2074
+ type: "checkbox",
2075
+ checked: Boolean(currentPreset.ast_composite_merge_enabled),
2076
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, ast_composite_merge_enabled: e.target.checked })),
2077
+ style: { accentColor: "var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))", cursor: "pointer" }
2078
+ }),
2079
+ t("aggregator.ast_merge_title") || "\u{1F333} AST-Level Composite Merge (Semantic Tree Synthesis)"
2080
+ ),
2081
+ React.createElement(
2082
+ "div",
2083
+ { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary)", marginLeft: 22 } },
2084
+ t("aggregator.ast_merge_desc") || "Merge imports, type signatures, JSDoc, and function bodies at the AST declaration level without duplicate import collisions."
2085
+ ),
2086
+ React.createElement(
2087
+ "div",
2088
+ { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary)", marginLeft: 22, marginTop: 2, display: "flex", alignItems: "center", gap: 6 } },
2089
+ "\u{1F517} Companion Plugin:",
2090
+ React.createElement(
2091
+ "a",
2092
+ { href: "https://github.com/GooDAnDReaDY/dsh-context-lens", target: "_blank", rel: "noopener noreferrer", style: { color: "var(--dsw-alias-state-brand-primary)", textDecoration: "underline" } },
2093
+ "@goodandready/dsh-context-lens"
2094
+ )
2095
+ )
2096
+ ),
2097
+ /* Speculative MoA (#247) & Adversarial TDD Loop (#246) */
2098
+ React.createElement(
2099
+ "div",
2100
+ { 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 } },
2101
+ React.createElement(
2102
+ "label",
2103
+ { style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 13, fontWeight: 500 } },
2104
+ React.createElement("input", {
2105
+ type: "checkbox",
2106
+ checked: Boolean(currentPreset.speculative_moa_enabled),
2107
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, speculative_moa_enabled: e.target.checked })),
2108
+ style: { accentColor: "var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))", cursor: "pointer" }
2109
+ }),
2110
+ t("aggregator.speculative_moa_title") || "\u26A1 Speculative MoA (Fast Draft + Multi-Auditor Diffs)"
2111
+ ),
2112
+ React.createElement(
2113
+ "label",
2114
+ { style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 13, fontWeight: 500, marginTop: 4 } },
2115
+ React.createElement("input", {
2116
+ type: "checkbox",
2117
+ checked: Boolean(currentPreset.adversarial_tdd_enabled),
2118
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, adversarial_tdd_enabled: e.target.checked })),
2119
+ style: { accentColor: "var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))", cursor: "pointer" }
2120
+ }),
2121
+ t("aggregator.adversarial_tdd_title") || "\u{1F6E1}\uFE0F Adversarial TDD Loop (Auto-Generated Red-Team Unit Tests)"
2122
+ )
2123
+ ),
2058
2124
  /* Multimodal Visual MoA Consensus (#250) */
2059
2125
  React.createElement(
2060
2126
  "div",
@@ -2254,6 +2320,43 @@ window.__ModuleLoader__.load({
2254
2320
  )
2255
2321
  )
2256
2322
  ),
2323
+ /* Pre-Judge Local Syntax Gate (#303) & Early Semantic Consensus (#299) */
2324
+ React.createElement(
2325
+ "div",
2326
+ { 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 } },
2327
+ React.createElement(
2328
+ "label",
2329
+ { style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 13, fontWeight: 500 } },
2330
+ React.createElement("input", {
2331
+ type: "checkbox",
2332
+ checked: currentPreset.local_preflight_check !== false,
2333
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, local_preflight_check: e.target.checked })),
2334
+ style: { accentColor: "var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))", cursor: "pointer" }
2335
+ }),
2336
+ t("aggregator.preflight_syntax_title") || "\u{1F50D} Pre-Judge Local Syntax Gate (Zero-Token Linter)"
2337
+ ),
2338
+ React.createElement(
2339
+ "div",
2340
+ { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary)", marginLeft: 22 } },
2341
+ t("aggregator.preflight_syntax_desc") || "Validate candidate code syntax locally before judge invocation to filter broken code without token costs."
2342
+ ),
2343
+ React.createElement(
2344
+ "label",
2345
+ { style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 13, fontWeight: 500, marginTop: 4 } },
2346
+ React.createElement("input", {
2347
+ type: "checkbox",
2348
+ checked: Boolean(currentPreset.early_exit_consensus_enabled),
2349
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, early_exit_consensus_enabled: e.target.checked })),
2350
+ style: { accentColor: "var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))", cursor: "pointer" }
2351
+ }),
2352
+ t("aggregator.early_consensus_title") || "\u26A1 Early Exit upon Semantic Consensus"
2353
+ ),
2354
+ React.createElement(
2355
+ "div",
2356
+ { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary)", marginLeft: 22 } },
2357
+ t("aggregator.early_consensus_desc") || "Bypass the expensive judge round when reference models converge on equivalent code solutions."
2358
+ )
2359
+ ),
2257
2360
  /* Multi-Turn Context & Diff Pruning (#176) */
2258
2361
  React.createElement(
2259
2362
  "div",
package/lib/events.js CHANGED
@@ -5,6 +5,8 @@
5
5
  export const MOA_EVENTS = {
6
6
  RUN_START: 'moa/run-start',
7
7
  CANDIDATE_COMPLETE: 'moa/candidate-complete',
8
+ SYNTAX_VALIDATION: 'moa/syntax-validation',
9
+ EARLY_CONSENSUS: 'moa/early-consensus',
8
10
  CRITIQUE_COMPLETE: 'moa/critique-complete',
9
11
  JUDGE_VERDICT: 'moa/judge-verdict',
10
12
  RUN_FINISH: 'moa/run-finish',
@@ -28,3 +30,4 @@ export function emitEvent(ctx, eventName, payload) {
28
30
  /* best-effort event dispatch: listener exceptions must never abort pipeline */
29
31
  }
30
32
  }
33
+
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Adversarial TDD Loop with Auto-Generated Unit Test Gate (#246)
3
+ * Generates an autonomous unit test suite (covering edge cases, nullability, and security bounds)
4
+ * from the user prompt via a Red-Team / Test Engineer model, injects the generated test files
5
+ * into each candidate's isolated workspace, executes them via moa-test-gate.js, and formats
6
+ * an objective pass/fail verification report for the Judge panel.
7
+ */
8
+
9
+ import fs from 'node:fs/promises'
10
+ import fsSync from 'node:fs'
11
+ import path from 'node:path'
12
+ import { logger } from './logger.js'
13
+ import { slotLabel } from './moa-prompts.js'
14
+ import { extractFileBlocks } from './workspace/fs.js'
15
+ import { isSafeRelativePath, assertPathContained } from './workspace/security.js'
16
+ import { resolveCandidateDir, runCandidateTestGate } from './moa-test-gate.js'
17
+
18
+ export const RED_TEAM_TDD_SYSTEM_PROMPT = `You are an Adversarial Red-Team Test Engineer for a Mixture-of-Agents coding pipeline.
19
+ Given the user's coding prompt and any existing file signatures, generate a self-contained Node.js unit test file using \`node:test\` and \`node:assert/strict\`.
20
+ Rules:
21
+ 1. Output the test file inside a markdown fence with \`path=__moa_adversarial.test.mjs\`.
22
+ 2. Include edge-case assertions (empty inputs, null/undefined handling, boundary values, and security/injection guards).
23
+ 3. Ensure the test file is syntactically valid ES module code that exits with 0 when all assertions pass and non-zero on failure.`
24
+
25
+ /**
26
+ * Generates an adversarial unit test suite from the user prompt.
27
+ *
28
+ * @param {object} params
29
+ * @returns {Promise<{ testFiles: Array<{ relativePath: string, content: string }>, testCommand: string, generatorModel: string }>}
30
+ */
31
+ export async function generateAdversarialTestSuite({
32
+ userPrompt = '',
33
+ referenceOutputs = [],
34
+ redTeamSlot = null,
35
+ callLlm,
36
+ signal = null,
37
+ }) {
38
+ if (typeof callLlm !== 'function') {
39
+ return { testFiles: [], testCommand: '', generatorModel: 'none' }
40
+ }
41
+
42
+ // Pick a dedicated Red-Team / Test Engineer slot
43
+ const chosenSlot = (redTeamSlot?.provider && redTeamSlot?.model)
44
+ ? redTeamSlot
45
+ : (referenceOutputs.find((r) => r?.slot?.role_persona === 'tester')?.slot || referenceOutputs[0]?.slot || { provider: 'default', model: 'default' })
46
+
47
+ const sampleFiles = []
48
+ for (const ref of referenceOutputs) {
49
+ if (!ref?.ok || !Array.isArray(ref.files)) continue
50
+ for (const f of ref.files) {
51
+ const rel = f.relativePath || f.path
52
+ if (rel && !sampleFiles.includes(rel)) sampleFiles.push(rel)
53
+ }
54
+ }
55
+
56
+ const userMsg = [
57
+ `### User Task Specification:\n${userPrompt}`,
58
+ sampleFiles.length > 0 ? `\n### Target Candidate Files to Test:\n${sampleFiles.map((f) => `- \`${f}\``).join('\n')}` : '',
59
+ `\nGenerate the adversarial unit test file with \`\`\`js path=__moa_adversarial.test.mjs.`,
60
+ ].filter(Boolean).join('\n')
61
+
62
+ try {
63
+ const res = await callLlm({
64
+ provider: chosenSlot.provider,
65
+ model: chosenSlot.model,
66
+ messages: [
67
+ { role: 'system', content: RED_TEAM_TDD_SYSTEM_PROMPT },
68
+ { role: 'user', content: userMsg },
69
+ ],
70
+ temperature: 0.2,
71
+ maxTokens: 2048,
72
+ signal,
73
+ })
74
+
75
+ const rawText = typeof res === 'string' ? res : (res?.content || res?.text || '')
76
+ let files = extractFileBlocks(rawText).filter((f) =>
77
+ /\.(test|spec)\.(mjs|js|cjs|ts)$/i.test(f.relativePath || '') || (f.relativePath || '').includes('adversarial')
78
+ )
79
+
80
+ if (files.length === 0) {
81
+ const codeMatch = /```(?:js|javascript|mjs)\r?\n([\s\S]*?)```/i.exec(rawText)
82
+ if (codeMatch && codeMatch[1].trim()) {
83
+ files = [{ relativePath: '__moa_adversarial.test.mjs', content: codeMatch[1].trim() + '\n' }]
84
+ }
85
+ }
86
+
87
+ if (files.length === 0) {
88
+ return { testFiles: [], testCommand: '', generatorModel: slotLabel(chosenSlot) }
89
+ }
90
+
91
+ const primaryTestFile = files[0].relativePath
92
+ return {
93
+ testFiles: files,
94
+ testCommand: `node --test ${primaryTestFile}`,
95
+ generatorModel: slotLabel(chosenSlot),
96
+ }
97
+ } catch (err) {
98
+ logger.warn('[dsh-moa:adversarial-tdd] Failed to generate adversarial unit tests:', err?.message || err)
99
+ return { testFiles: [], testCommand: '', generatorModel: slotLabel(chosenSlot) }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Runs the Adversarial TDD Loop across all candidate workspaces and attaches results.
105
+ *
106
+ * @param {object} params
107
+ * @returns {Promise<{
108
+ * executed: boolean,
109
+ * generatorModel: string,
110
+ * testCommand: string,
111
+ * testFiles: string[],
112
+ * results: Array<{ candidateIndex: number, label: string, passed: boolean, exitCode: number, summary: string }>,
113
+ * directive: string
114
+ * }>}
115
+ */
116
+ export async function runAdversarialTddLoop({
117
+ cwd,
118
+ runId,
119
+ userPrompt = '',
120
+ referenceOutputs = [],
121
+ preset = {},
122
+ callLlm,
123
+ execFn = null,
124
+ onProgress = null,
125
+ signal = null,
126
+ }) {
127
+ const candidatesWithFiles = (referenceOutputs || []).filter((r) => r.ok && Array.isArray(r.files) && r.files.length > 0)
128
+ if (candidatesWithFiles.length === 0) {
129
+ return { executed: false, generatorModel: 'none', testCommand: '', testFiles: [], results: [], directive: '' }
130
+ }
131
+
132
+ if (typeof onProgress === 'function') {
133
+ onProgress('🛡️ *Adversarial TDD Loop: Generating Red-Team unit test suite from prompt...*\n')
134
+ }
135
+
136
+ const suite = await generateAdversarialTestSuite({
137
+ userPrompt,
138
+ referenceOutputs: candidatesWithFiles,
139
+ redTeamSlot: preset?.aggregator,
140
+ callLlm,
141
+ signal,
142
+ })
143
+
144
+ if (suite.testFiles.length === 0 || !suite.testCommand) {
145
+ return { executed: false, generatorModel: suite.generatorModel, testCommand: '', testFiles: [], results: [], directive: '' }
146
+ }
147
+
148
+ if (typeof onProgress === 'function') {
149
+ onProgress(`🧪 *Adversarial TDD Loop: Executing \`${suite.testCommand}\` against ${candidatesWithFiles.length} candidate workspace(s)...*\n`)
150
+ }
151
+
152
+ const timeoutMs = (typeof preset?.test_gate_timeout_sec === 'number' && preset.test_gate_timeout_sec > 0
153
+ ? preset.test_gate_timeout_sec
154
+ : 15) * 1000
155
+
156
+ const results = []
157
+ for (let i = 0; i < referenceOutputs.length; i++) {
158
+ const ref = referenceOutputs[i]
159
+ if (!ref.ok || !Array.isArray(ref.files) || ref.files.length === 0) continue
160
+ const candIdx = ref.index ?? (i + 1)
161
+ const candDir = resolveCandidateDir(cwd, candIdx, { runId })
162
+ if (!candDir || !fsSync.existsSync(candDir)) continue
163
+
164
+ // Materialize generated adversarial test files inside the candidate sandbox
165
+ for (const tf of suite.testFiles) {
166
+ if (!isSafeRelativePath(tf.relativePath)) continue
167
+ const dest = assertPathContained(candDir, tf.relativePath)
168
+ await fs.mkdir(path.dirname(dest), { recursive: true })
169
+ await fs.writeFile(dest, tf.content, 'utf8')
170
+ }
171
+
172
+ const gateRes = await runCandidateTestGate({
173
+ cwd,
174
+ candidateIndex: candIdx,
175
+ testCommand: suite.testCommand,
176
+ timeoutMs,
177
+ execFn,
178
+ runId,
179
+ })
180
+
181
+ // Remove temporary adversarial test files from candidate workspace so they don't get promoted unintentionally
182
+ for (const tf of suite.testFiles) {
183
+ if (!isSafeRelativePath(tf.relativePath)) continue
184
+ try {
185
+ const dest = assertPathContained(candDir, tf.relativePath)
186
+ await fs.rm(dest, { force: true })
187
+ } catch (err) {
188
+ logger.debug('[dsh-moa:tdd] cleanup test file error:', err?.message || err)
189
+ }
190
+ }
191
+
192
+ if (gateRes) {
193
+ ref.testResult = gateRes
194
+ results.push({
195
+ candidateIndex: candIdx,
196
+ label: ref.label || `Candidate ${candIdx}`,
197
+ passed: Boolean(gateRes.passed),
198
+ exitCode: gateRes.exitCode ?? (gateRes.passed ? 0 : 1),
199
+ summary: gateRes.summary || (gateRes.passed ? 'PASSED' : 'FAILED'),
200
+ })
201
+ }
202
+ }
203
+
204
+ const reportLines = [
205
+ `### 🛡️ Adversarial TDD Verification Report (Generated by Red-Team \`${suite.generatorModel}\`):`,
206
+ `Command executed in isolated sandbox: \`${suite.testCommand}\``,
207
+ ...results.map(
208
+ (r) => `- **Candidate #${r.candidateIndex} (${r.label})**: **${r.passed ? 'PASSED ✅' : `FAILED ❌ (exit ${r.exitCode})`}** — ${r.summary}`
209
+ ),
210
+ 'Strongly favor candidates that pass the adversarial Red-Team unit test suite.',
211
+ ]
212
+
213
+ return {
214
+ executed: results.length > 0,
215
+ generatorModel: suite.generatorModel,
216
+ testCommand: suite.testCommand,
217
+ testFiles: suite.testFiles.map((f) => f.relativePath),
218
+ results,
219
+ directive: results.length > 0 ? reportLines.join('\n') : '',
220
+ }
221
+ }
222
+