@goodandready/dsh-moa 0.2.74 → 0.2.75
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 +6 -0
- package/README.ru.md +6 -0
- package/README.zh.md +6 -0
- package/lib/client.js +53 -2
- package/lib/events.js +2 -0
- package/lib/moa-schema.js +4 -0
- package/lib/pipeline/context.js +31 -0
- package/lib/pipeline/developer-preferences.js +196 -0
- package/lib/pipeline/manifest-stack.js +287 -0
- package/lib/pipeline/runner.js +1 -1
- package/lib/routes/preferences.js +108 -0
- package/lib/routes.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -195,6 +195,12 @@ Dispatches `moa/judge-escalation` Cordis events and displays escalation decision
|
|
|
195
195
|
### 22. Self-Consistency Temperature Sampling for Single Models
|
|
196
196
|
Allows single-model configurations (such as a local Ollama/vLLM instance or a single API key) to leverage Mixture-of-Agents diversity. When enabled on a preset with 1 model slot, the runner expands it into $N$ parallel candidate streams with distinct temperatures (e.g. 0.2, 0.6, 0.9) and complementary personas (`proposer`, `challenger`, `minimalist`), calculates agreement ratio, and performs full MoA consensus synthesis. Dispatches `moa/self-consistency-expand` Cordis events. Configured via `self_consistency_enabled: true`, `self_consistency_samples: 3`, and `self_consistency_temperatures: [0.2, 0.6, 0.9]`.
|
|
197
197
|
|
|
198
|
+
### 23. Stack-Aware Dynamic Persona Evolution
|
|
199
|
+
Scans project manifests (`package.json`, `Cargo.toml`, `pyproject.toml`, `requirements.txt`, `go.mod`) to detect languages, frameworks, and libraries. Automatically specializes candidate proposers into domain-specific roles (e.g. React Optimization, TypeScript Safety, Accessibility, FastAPI Async) to maximize hypothesis diversity. Emits `moa/stack-detected` Cordis events. Configured via `stack_aware_personas_enabled: true`.
|
|
200
|
+
|
|
201
|
+
### 24. Self-Learning Developer Preferences Metaprompt
|
|
202
|
+
Maintains a persistent profile of developer coding principles and architectural preferences in `~/.dsh/moa/preferences.json`. Injects active principles into the Judge prompt to bias consensus toward team standards (strict typing, functional immutability, error boundaries). Manageable via `GET/POST/DELETE /dsh-moa/preferences` REST endpoints and preset toggle `developer_preferences_enabled: true`.
|
|
203
|
+
|
|
198
204
|
### 6. Live Canvas 1-Click Preview (optional)
|
|
199
205
|
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.
|
|
200
206
|
|
package/README.ru.md
CHANGED
|
@@ -195,6 +195,12 @@ graph TD
|
|
|
195
195
|
### 22. Самосогласованное температурное сэмплирование для одиночных моделей (Self-Consistency)
|
|
196
196
|
Позволяет пользователям с единственной моделью (локальная Ollama, vLLM или единственный API-ключ) получать преимущества ансамблевой валидации MoA. Автоматически разворачивает одиночный слот на $N$ параллельных запусков с различными температурами (`0.2, 0.6, 0.9`) и дополняющими друг друга инженерными персонами (`proposer`, `challenger`, `minimalist`), вычисляет коэффициент согласия (Self-Consistency Agreement) и синтезирует итоговый результат. Генерирует событие `moa/self-consistency-expand`.
|
|
197
197
|
|
|
198
|
+
### 23. Динамическая адаптация персон по манифестам проекта (Stack-Aware Personas)
|
|
199
|
+
Автоматически сканирует манифесты проекта (`package.json`, `Cargo.toml`, `pyproject.toml`, `requirements.txt`, `go.mod`) для определения стека технологий и библиотек. Специализирует роли кандидатов под конкретный стек (оптимизация рендера, строгая типизация, доступность, асинхронные интерфейсы), обеспечивая многоаспектный анализ задачи. Генерирует событие `moa/stack-detected`. Включается через `stack_aware_personas_enabled: true`.
|
|
200
|
+
|
|
201
|
+
### 24. Метапромпт предпочтений разработчика (Developer Preferences)
|
|
202
|
+
Хранит профиль архитектурных стандартов и стилевых предпочтений разработчика в `~/.dsh/moa/preferences.json`. Инжектирует активные принципы в контекст судьи, гарантируя соответствие выбранного решения вкусам команды (строгая типизация, иммутабельность, обработка ошибок). Доступен REST API (`GET/POST/DELETE /dsh-moa/preferences`) и опция пресета `developer_preferences_enabled: true`.
|
|
203
|
+
|
|
198
204
|
### 6. Live Canvas 1-клик предпросмотр (опционально)
|
|
199
205
|
Если в профиле установлен `@goodandready/dsh-live-canvas`, `dsh-moa` отправляет промоученный HTML-файл в REST-контракт Live Canvas (`POST /dsh-live-canvas/api/preview`, тот же webServer харнесса) и добавляет к ответу ссылку на предпросмотр (`/dsh-live-canvas/sandbox/<id>`). Без плагина шаг пропускается тихо — без ошибок в журнале и без битых ссылок.
|
|
200
206
|
|
package/README.zh.md
CHANGED
|
@@ -172,6 +172,12 @@ graph TD
|
|
|
172
172
|
### 22. 单模型自洽性温度采样 (Self-Consistency Temperature Sampling)
|
|
173
173
|
专为仅配置单模型(如本地 Ollama/vLLM 或单一 API Key)的场景设计。开启后自动将单槽位扩展为 $N$ 个并行采样(分别采用 0.2、0.6、0.9 等递增温度与 `proposer`、`challenger`、`minimalist` 互补提示词),评估自洽一致性比率并进行全流程 MoA 综合聚合,触发 `moa/self-consistency-expand` 事件。
|
|
174
174
|
|
|
175
|
+
### 23. 基于项目清单的感知角色演化 (Stack-Aware Personas)
|
|
176
|
+
自动扫描工作区清单 (`package.json`, `Cargo.toml`, `pyproject.toml`, `go.mod`) 识别编程语言、框架与核心依赖,动态为各候选槽位分派互补的领域专家角色(如 React 渲染优化、TypeScript 严格类型安全、可访问性审查、FastAPI 异步架构),触发 `moa/stack-detected` 事件。通过 `stack_aware_personas_enabled: true` 配置。
|
|
177
|
+
|
|
178
|
+
### 24. 自学习开发者代码偏好元提示词 (Developer Preferences)
|
|
179
|
+
在本地 `~/.dsh/moa/preferences.json` 持久化存储团队编码规范与架构偏好,将启用的规则动态注入裁判评审阶段,使综合方案贴合开发者个人习惯(严格类型、函数不可变性、防御性错误边界)。提供 `GET/POST/DELETE /dsh-moa/preferences` REST 端点,通过 `developer_preferences_enabled: true` 开启。
|
|
180
|
+
|
|
175
181
|
### 6. Live Canvas 一键预览(可选)
|
|
176
182
|
若同一 profile 中安装了 `@goodandready/dsh-live-canvas`,`dsh-moa` 会将提升后的 HTML 文件推送到 Live Canvas 的 REST 契约(`POST /dsh-live-canvas/api/preview`,由同一 harness webServer 提供服务),并在回答中附上一键预览链接(`/dsh-live-canvas/sandbox/<id>`)。未安装该插件时此步骤静默跳过——日志无报错,也不会出现死链接。
|
|
177
183
|
|
package/lib/client.js
CHANGED
|
@@ -224,7 +224,11 @@ window.__ModuleLoader__.load({
|
|
|
224
224
|
"aggregator.escalation_policy_upgrade": "Auto-Upgrade to Senior Judge on Divergence",
|
|
225
225
|
"aggregator.escalation_policy_council": "Convene 3-Judge Majority Council on Divergence",
|
|
226
226
|
"aggregator.escalation_threshold": "Divergence Threshold:",
|
|
227
|
-
"aggregator.escalation_judge_label": "Senior Judge:"
|
|
227
|
+
"aggregator.escalation_judge_label": "Senior Judge:",
|
|
228
|
+
"aggregator.stack_personas_title": "\u{1F4E6} Stack-Aware Dynamic Persona Evolution (Project Manifests)",
|
|
229
|
+
"aggregator.stack_personas_desc": "Scans project manifests (package.json, Cargo.toml, pyproject.toml, go.mod) and assigns specialized engineering roles to candidate slots.",
|
|
230
|
+
"aggregator.preferences_title": "\u{1F9E0} Developer Coding Principles & Preferences Metaprompt",
|
|
231
|
+
"aggregator.preferences_desc": "Injects learned developer preferences (strict typing, functional immutability, error boundaries) to guide Judge synthesis."
|
|
228
232
|
};
|
|
229
233
|
const zh = {
|
|
230
234
|
title: "\u667A\u80FD\u4F53\u6DF7\u5408 (MoA)",
|
|
@@ -439,7 +443,11 @@ window.__ModuleLoader__.load({
|
|
|
439
443
|
"aggregator.escalation_policy_upgrade": "\u5206\u6B67\u65F6\u81EA\u52A8\u5347\u7EA7\u81F3\u9AD8\u9636\u88C1\u5224",
|
|
440
444
|
"aggregator.escalation_policy_council": "\u5206\u6B67\u65F6\u7EC4\u5EFA 3 \u88C1\u5224\u591A\u6570\u51B3\u7B56\u5E2D",
|
|
441
445
|
"aggregator.escalation_threshold": "\u5206\u6B67\u9608\u503C:",
|
|
442
|
-
"aggregator.escalation_judge_label": "\u9AD8\u9636\u88C1\u5224\u6A21\u578B:"
|
|
446
|
+
"aggregator.escalation_judge_label": "\u9AD8\u9636\u88C1\u5224\u6A21\u578B:",
|
|
447
|
+
"aggregator.stack_personas_title": "\u{1F4E6} \u57FA\u4E8E\u9879\u76EE\u914D\u7F6E\u6E05\u5355\u7684\u611F\u77E5\u89D2\u8272\u6F14\u5316 (Stack-Aware Personas)",
|
|
448
|
+
"aggregator.stack_personas_desc": "\u81EA\u52A8\u626B\u63CF\u9879\u76EE\u6E05\u5355 (package.json, Cargo.toml, pyproject.toml, go.mod) \u5E76\u4E3A\u5404\u5019\u9009\u69FD\u4F4D\u6307\u6D3E\u4E13\u5C5E\u4E13\u4E1A\u89D2\u8272\u3002",
|
|
449
|
+
"aggregator.preferences_title": "\u{1F9E0} \u81EA\u5B66\u4E60\u5F00\u53D1\u8005\u4EE3\u7801\u504F\u597D\u5143\u63D0\u793A\u8BCD (Developer Preferences)",
|
|
450
|
+
"aggregator.preferences_desc": "\u5C06\u5F00\u53D1\u8005\u504F\u597D\uFF08\u4E25\u683C\u7C7B\u578B\u3001\u51FD\u6570\u4E0D\u53EF\u53D8\u6027\u3001\u9632\u5FA1\u6027\u9519\u8BEF\u5904\u7406\u7B49\uFF09\u6CE8\u5165\u88C1\u5224\u7EFC\u5408\u8BC4\u5BA1\u9636\u6BB5\u3002"
|
|
443
451
|
};
|
|
444
452
|
function makeT(dict, fallback) {
|
|
445
453
|
return function t(key, vars) {
|
|
@@ -2354,6 +2362,7 @@ window.__ModuleLoader__.load({
|
|
|
2354
2362
|
)
|
|
2355
2363
|
)
|
|
2356
2364
|
),
|
|
2365
|
+
// --- src/client/86-card-aggregator-ext2.js ---
|
|
2357
2366
|
/* Pre-Judge Local Syntax Gate (#303) & Early Semantic Consensus (#299) */
|
|
2358
2367
|
React.createElement(
|
|
2359
2368
|
"div",
|
|
@@ -2559,6 +2568,48 @@ window.__ModuleLoader__.load({
|
|
|
2559
2568
|
)
|
|
2560
2569
|
)
|
|
2561
2570
|
),
|
|
2571
|
+
/* Stack-Aware Dynamic Persona Evolution (#308) */
|
|
2572
|
+
React.createElement(
|
|
2573
|
+
"div",
|
|
2574
|
+
{ 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 } },
|
|
2575
|
+
React.createElement(
|
|
2576
|
+
"label",
|
|
2577
|
+
{ style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 13, fontWeight: 500 } },
|
|
2578
|
+
React.createElement("input", {
|
|
2579
|
+
type: "checkbox",
|
|
2580
|
+
checked: Boolean(currentPreset.stack_aware_personas_enabled),
|
|
2581
|
+
onChange: (e) => updateCurrentPreset((p) => ({ ...p, stack_aware_personas_enabled: e.target.checked })),
|
|
2582
|
+
style: { accentColor: "var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))", cursor: "pointer" }
|
|
2583
|
+
}),
|
|
2584
|
+
t("aggregator.stack_personas_title") || "\u{1F4E6} Stack-Aware Dynamic Persona Evolution (Project Manifests)"
|
|
2585
|
+
),
|
|
2586
|
+
React.createElement(
|
|
2587
|
+
"div",
|
|
2588
|
+
{ style: { fontSize: 12, color: "var(--dsw-alias-label-secondary)", marginLeft: 22 } },
|
|
2589
|
+
t("aggregator.stack_personas_desc") || "Scans project manifests (package.json, Cargo.toml, pyproject.toml, go.mod) and assigns specialized engineering roles to candidate slots."
|
|
2590
|
+
)
|
|
2591
|
+
),
|
|
2592
|
+
/* Self-Learning Developer Preferences Metaprompt (#309) */
|
|
2593
|
+
React.createElement(
|
|
2594
|
+
"div",
|
|
2595
|
+
{ 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 } },
|
|
2596
|
+
React.createElement(
|
|
2597
|
+
"label",
|
|
2598
|
+
{ style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 13, fontWeight: 500 } },
|
|
2599
|
+
React.createElement("input", {
|
|
2600
|
+
type: "checkbox",
|
|
2601
|
+
checked: Boolean(currentPreset.developer_preferences_enabled),
|
|
2602
|
+
onChange: (e) => updateCurrentPreset((p) => ({ ...p, developer_preferences_enabled: e.target.checked })),
|
|
2603
|
+
style: { accentColor: "var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))", cursor: "pointer" }
|
|
2604
|
+
}),
|
|
2605
|
+
t("aggregator.preferences_title") || "\u{1F9E0} Developer Coding Principles & Preferences Metaprompt"
|
|
2606
|
+
),
|
|
2607
|
+
React.createElement(
|
|
2608
|
+
"div",
|
|
2609
|
+
{ style: { fontSize: 12, color: "var(--dsw-alias-label-secondary)", marginLeft: 22 } },
|
|
2610
|
+
t("aggregator.preferences_desc") || "Injects learned developer preferences (strict typing, functional immutability, error boundaries) to guide Judge synthesis."
|
|
2611
|
+
)
|
|
2612
|
+
),
|
|
2562
2613
|
/* Fallback Judges Chain */
|
|
2563
2614
|
React.createElement(
|
|
2564
2615
|
"div",
|
package/lib/events.js
CHANGED
|
@@ -9,6 +9,8 @@ export const MOA_EVENTS = {
|
|
|
9
9
|
EARLY_CONSENSUS: 'moa/early-consensus',
|
|
10
10
|
JUDGE_ESCALATION: 'moa/judge-escalation',
|
|
11
11
|
SELF_CONSISTENCY_EXPAND: 'moa/self-consistency-expand',
|
|
12
|
+
STACK_DETECTED: 'moa/stack-detected',
|
|
13
|
+
PREFERENCES_APPLIED: 'moa/preferences-applied',
|
|
12
14
|
CRITIQUE_COMPLETE: 'moa/critique-complete',
|
|
13
15
|
JUDGE_VERDICT: 'moa/judge-verdict',
|
|
14
16
|
CONTEXT_PRUNING: 'moa/context-pruning',
|
package/lib/moa-schema.js
CHANGED
|
@@ -89,6 +89,8 @@ export const PresetSchema = z.object({
|
|
|
89
89
|
escalation_policy: z.string().default('single'),
|
|
90
90
|
escalation_disagreement_threshold: z.number().default(0.40),
|
|
91
91
|
escalation_judge: ModelSlotSchema.default({ provider: '', model: '' }),
|
|
92
|
+
stack_aware_personas_enabled: z.boolean().default(false),
|
|
93
|
+
developer_preferences_enabled: z.boolean().default(false),
|
|
92
94
|
})
|
|
93
95
|
|
|
94
96
|
export const Config = z.object({
|
|
@@ -124,6 +126,8 @@ export const Config = z.object({
|
|
|
124
126
|
escalation_policy: z.string().default('single').volatile(),
|
|
125
127
|
escalation_disagreement_threshold: z.number().default(0.40).volatile(),
|
|
126
128
|
escalation_judge: ModelSlotSchema.default({ provider: '', model: '' }).volatile(),
|
|
129
|
+
stack_aware_personas_enabled: z.boolean().default(false).volatile(),
|
|
130
|
+
developer_preferences_enabled: z.boolean().default(false).volatile(),
|
|
127
131
|
smart_routing_model: ModelSlotSchema.default({ provider: '', model: '' }).volatile(),
|
|
128
132
|
prices: z.dict(PriceRow).default({}).volatile(),
|
|
129
133
|
presets: z.array(PresetSchema).default([
|
package/lib/pipeline/context.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { detectProjectStack, assignStackAwarePersonas, formatStackAwareDirective } from './manifest-stack.js'
|
|
2
|
+
import { formatPreferencesDirective } from './developer-preferences.js'
|
|
1
3
|
import { expandSelfConsistencySlots } from './self-consistency.js'
|
|
2
4
|
import { emitEvent, MOA_EVENTS } from '../events.js'
|
|
3
5
|
import { balanceCandidateQuotas } from '../moa-quota-balancer.js'
|
|
@@ -81,6 +83,34 @@ export async function stageProjectContext({
|
|
|
81
83
|
const semanticCacheEnabled = Boolean(preset?.semantic_cache_enabled)
|
|
82
84
|
const semanticCacheTtlHours = typeof preset?.semantic_cache_ttl_hours === 'number' ? preset.semantic_cache_ttl_hours : 168
|
|
83
85
|
|
|
86
|
+
const stackInfo = detectProjectStack(cwd)
|
|
87
|
+
if (stackInfo.manifestFound) {
|
|
88
|
+
emitEvent(ctx, MOA_EVENTS.STACK_DETECTED, {
|
|
89
|
+
primaryLanguage: stackInfo.primaryLanguage,
|
|
90
|
+
frameworks: stackInfo.frameworks,
|
|
91
|
+
libraries: stackInfo.libraries,
|
|
92
|
+
stackSummary: stackInfo.stackSummary,
|
|
93
|
+
})
|
|
94
|
+
if (typeof onProgress === 'function' && stackInfo.stackSummary) {
|
|
95
|
+
onProgress(`📦 *Project Stack: Detected ${stackInfo.stackSummary}*\n`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const stackAwarePersonasEnabled = Boolean(preset?.stack_aware_personas_enabled)
|
|
100
|
+
if (stackAwarePersonasEnabled && !selfConsistencyExpanded) {
|
|
101
|
+
const stackRes = assignStackAwarePersonas(effectiveReferenceModels, stackInfo, { enabled: true })
|
|
102
|
+
if (stackRes.isSpecialized) {
|
|
103
|
+
effectiveReferenceModels = stackRes.references
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const developerPreferencesEnabled = Boolean(preset?.developer_preferences_enabled)
|
|
108
|
+
const developerPreferencesDirective = formatPreferencesDirective(null, { enabled: developerPreferencesEnabled })
|
|
109
|
+
if (developerPreferencesDirective) {
|
|
110
|
+
emitEvent(ctx, MOA_EVENTS.PREFERENCES_APPLIED, { enabled: true })
|
|
111
|
+
}
|
|
112
|
+
const stackAwareDirective = formatStackAwareDirective(stackInfo)
|
|
113
|
+
|
|
84
114
|
const collectedCtx = await collectProjectContext(cwd, 16000)
|
|
85
115
|
const isRefinement = Boolean(collectedCtx?.files?.length > 0 && isRefinementTask(userPrompt, collectedCtx.files))
|
|
86
116
|
let projectContext = ''
|
|
@@ -170,6 +200,7 @@ export async function stageProjectContext({
|
|
|
170
200
|
semanticCacheEnabled, semanticCacheTtlHours,
|
|
171
201
|
selfConsistencyEnabled, selfConsistencyExpanded, selfConsistencySamples, selfConsistencyTemperatures,
|
|
172
202
|
escalationPolicy, escalationDisagreementThreshold, escalationJudge,
|
|
203
|
+
stackInfo, stackAwareDirective, developerPreferencesDirective,
|
|
173
204
|
}
|
|
174
205
|
}
|
|
175
206
|
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-Learning Developer Preferences Metaprompt (#309)
|
|
3
|
+
*
|
|
4
|
+
* Tracks developer coding principles, architectural standards, and preferred idioms
|
|
5
|
+
* in a persistent local store (~/.dsh/moa/preferences.json).
|
|
6
|
+
* Injects a concise "Developer Coding Principles" directive into the Judge
|
|
7
|
+
* synthesis phase (and candidate prompts) to bias consensus toward the engineer's tastes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'node:fs'
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
import os from 'node:os'
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_PREFERENCES = Object.freeze([
|
|
15
|
+
{
|
|
16
|
+
id: 'strict-typing',
|
|
17
|
+
title: 'Strict Types & Explicit Contracts',
|
|
18
|
+
rule: "Never use loose 'any' types or unchecked type casts. Explicitly define interfaces, return types, and input validation schemas.",
|
|
19
|
+
category: 'typing',
|
|
20
|
+
enabled: true,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'functional-purity',
|
|
24
|
+
title: 'Functional Immutability',
|
|
25
|
+
rule: 'Prefer pure functions, immutable data transforms, and explicit return values over hidden global state mutations.',
|
|
26
|
+
category: 'architecture',
|
|
27
|
+
enabled: true,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'defensive-errors',
|
|
31
|
+
title: 'Defensive Error Handling',
|
|
32
|
+
rule: 'Never swallow exceptions silently. Use structured error types, descriptive messages, and clean fallback strategies.',
|
|
33
|
+
category: 'reliability',
|
|
34
|
+
enabled: true,
|
|
35
|
+
},
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolves the persistent preferences file path.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} [customPath]
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
export function getPreferencesFilePath(customPath = null) {
|
|
45
|
+
if (customPath && typeof customPath === 'string' && customPath.trim()) {
|
|
46
|
+
return path.resolve(customPath.trim())
|
|
47
|
+
}
|
|
48
|
+
return path.join(os.homedir(), '.dsh', 'moa', 'preferences.json')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Loads developer preferences from disk with default fallback.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} [customPath]
|
|
55
|
+
* @returns {Array<object>}
|
|
56
|
+
*/
|
|
57
|
+
export function loadDeveloperPreferences(customPath = null) {
|
|
58
|
+
const filePath = getPreferencesFilePath(customPath)
|
|
59
|
+
if (!fs.existsSync(filePath)) {
|
|
60
|
+
// Seed default preferences file
|
|
61
|
+
try {
|
|
62
|
+
const dir = path.dirname(filePath)
|
|
63
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
64
|
+
fs.writeFileSync(filePath, JSON.stringify(DEFAULT_PREFERENCES, null, 2), 'utf8')
|
|
65
|
+
} catch {
|
|
66
|
+
// safe fallback if filesystem is read-only
|
|
67
|
+
}
|
|
68
|
+
return [...DEFAULT_PREFERENCES]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const raw = fs.readFileSync(filePath, 'utf8')
|
|
73
|
+
const parsed = JSON.parse(raw)
|
|
74
|
+
if (Array.isArray(parsed)) {
|
|
75
|
+
return parsed.filter((p) => p && typeof p === 'object' && p.id && p.rule)
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// safe fallback
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return [...DEFAULT_PREFERENCES]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Atomically saves developer preferences to disk.
|
|
86
|
+
*
|
|
87
|
+
* @param {Array<object>} preferences
|
|
88
|
+
* @param {string} [customPath]
|
|
89
|
+
* @returns {boolean}
|
|
90
|
+
*/
|
|
91
|
+
export function saveDeveloperPreferences(preferences, customPath = null) {
|
|
92
|
+
if (!Array.isArray(preferences)) return false
|
|
93
|
+
const filePath = getPreferencesFilePath(customPath)
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const dir = path.dirname(filePath)
|
|
97
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
98
|
+
const tmp = `${filePath}.tmp.${Date.now()}`
|
|
99
|
+
fs.writeFileSync(tmp, JSON.stringify(preferences, null, 2), 'utf8')
|
|
100
|
+
fs.renameSync(tmp, filePath)
|
|
101
|
+
return true
|
|
102
|
+
} catch {
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Adds a new developer preference rule.
|
|
109
|
+
*
|
|
110
|
+
* @param {object} item - { title, rule, category, enabled }
|
|
111
|
+
* @param {string} [customPath]
|
|
112
|
+
* @returns {object|null}
|
|
113
|
+
*/
|
|
114
|
+
export function addDeveloperPreference(item = {}, customPath = null) {
|
|
115
|
+
if (!item || !item.rule || typeof item.rule !== 'string') return null
|
|
116
|
+
const current = loadDeveloperPreferences(customPath)
|
|
117
|
+
|
|
118
|
+
const id = item.id || `pref-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`
|
|
119
|
+
const newPref = {
|
|
120
|
+
id,
|
|
121
|
+
title: (item.title && String(item.title).trim()) || 'Custom Coding Standard',
|
|
122
|
+
rule: String(item.rule).trim(),
|
|
123
|
+
category: (item.category && String(item.category).trim()) || 'general',
|
|
124
|
+
enabled: item.enabled !== false,
|
|
125
|
+
createdAt: new Date().toISOString(),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
current.push(newPref)
|
|
129
|
+
saveDeveloperPreferences(current, customPath)
|
|
130
|
+
return newPref
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Updates an existing developer preference.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} id
|
|
137
|
+
* @param {object} updates
|
|
138
|
+
* @param {string} [customPath]
|
|
139
|
+
* @returns {object|null}
|
|
140
|
+
*/
|
|
141
|
+
export function updateDeveloperPreference(id, updates = {}, customPath = null) {
|
|
142
|
+
if (!id) return null
|
|
143
|
+
const current = loadDeveloperPreferences(customPath)
|
|
144
|
+
const idx = current.findIndex((p) => p.id === id)
|
|
145
|
+
if (idx === -1) return null
|
|
146
|
+
|
|
147
|
+
const target = current[idx]
|
|
148
|
+
if (typeof updates.title === 'string') target.title = updates.title.trim()
|
|
149
|
+
if (typeof updates.rule === 'string') target.rule = updates.rule.trim()
|
|
150
|
+
if (typeof updates.category === 'string') target.category = updates.category.trim()
|
|
151
|
+
if (typeof updates.enabled === 'boolean') target.enabled = updates.enabled
|
|
152
|
+
|
|
153
|
+
saveDeveloperPreferences(current, customPath)
|
|
154
|
+
return target
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Deletes a developer preference by ID.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} id
|
|
161
|
+
* @param {string} [customPath]
|
|
162
|
+
* @returns {boolean}
|
|
163
|
+
*/
|
|
164
|
+
export function deleteDeveloperPreference(id, customPath = null) {
|
|
165
|
+
if (!id) return false
|
|
166
|
+
const current = loadDeveloperPreferences(customPath)
|
|
167
|
+
const filtered = current.filter((p) => p.id !== id)
|
|
168
|
+
if (filtered.length === current.length) return false
|
|
169
|
+
|
|
170
|
+
return saveDeveloperPreferences(filtered, customPath)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Formats active developer preferences into an injected prompt directive.
|
|
175
|
+
*
|
|
176
|
+
* @param {Array<object>} [preferences] - Array of preferences (loads from disk if omitted)
|
|
177
|
+
* @param {object} [options]
|
|
178
|
+
* @param {boolean} [options.enabled=true]
|
|
179
|
+
* @returns {string|null}
|
|
180
|
+
*/
|
|
181
|
+
export function formatPreferencesDirective(preferences = null, options = {}) {
|
|
182
|
+
const { enabled = true, customPath = null } = options
|
|
183
|
+
if (!enabled) return null
|
|
184
|
+
|
|
185
|
+
const list = Array.isArray(preferences) ? preferences : loadDeveloperPreferences(customPath)
|
|
186
|
+
const active = list.filter((p) => p && p.enabled !== false && p.rule)
|
|
187
|
+
|
|
188
|
+
if (active.length === 0) return null
|
|
189
|
+
|
|
190
|
+
const lines = active.map((p) => `- [${p.title}]: ${p.rule}`)
|
|
191
|
+
|
|
192
|
+
return `### Developer Coding Principles & Preferred Conventions
|
|
193
|
+
The developer has established the following coding standards for this project:
|
|
194
|
+
${lines.join('\n')}
|
|
195
|
+
Evaluate all proposals against these principles and favor implementations adhering to them.`
|
|
196
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stack-Aware Dynamic Persona Evolution from Project Manifests (#308)
|
|
3
|
+
*
|
|
4
|
+
* Scans project manifests (package.json, Cargo.toml, pyproject.toml, requirements.txt, go.mod)
|
|
5
|
+
* in the workspace root to infer primary programming languages, frameworks, ORMs,
|
|
6
|
+
* and test suites. Evolves candidate proposers into specialized domain experts
|
|
7
|
+
* (e.g. React Optimization, TypeScript Strict Safety, Accessibility, FastAPI Async)
|
|
8
|
+
* to maximize architectural hypothesis diversity.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Scans workspace root manifests to detect project languages and dependencies.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} cwd - Workspace root directory
|
|
18
|
+
* @returns {object} Detected stack information
|
|
19
|
+
*/
|
|
20
|
+
export function detectProjectStack(cwd = process.cwd()) {
|
|
21
|
+
const stack = {
|
|
22
|
+
primaryLanguage: 'unknown',
|
|
23
|
+
frameworks: [],
|
|
24
|
+
libraries: [],
|
|
25
|
+
manifestFound: false,
|
|
26
|
+
stackSummary: '',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!cwd || typeof cwd !== 'string') return stack
|
|
30
|
+
|
|
31
|
+
// 1. Node / JavaScript / TypeScript (package.json)
|
|
32
|
+
const pkgPath = path.join(cwd, 'package.json')
|
|
33
|
+
if (fs.existsSync(pkgPath)) {
|
|
34
|
+
try {
|
|
35
|
+
const raw = fs.readFileSync(pkgPath, 'utf8')
|
|
36
|
+
const pkg = JSON.parse(raw)
|
|
37
|
+
stack.manifestFound = true
|
|
38
|
+
stack.primaryLanguage = 'javascript'
|
|
39
|
+
|
|
40
|
+
const allDeps = {
|
|
41
|
+
...(pkg.dependencies || {}),
|
|
42
|
+
...(pkg.devDependencies || {}),
|
|
43
|
+
...(pkg.peerDependencies || {}),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (allDeps.typescript || fs.existsSync(path.join(cwd, 'tsconfig.json'))) {
|
|
47
|
+
stack.primaryLanguage = 'typescript'
|
|
48
|
+
stack.libraries.push('typescript')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Frameworks
|
|
52
|
+
if (allDeps.next) stack.frameworks.push('next.js')
|
|
53
|
+
else if (allDeps.react) stack.frameworks.push('react')
|
|
54
|
+
|
|
55
|
+
if (allDeps.nuxt) stack.frameworks.push('nuxt')
|
|
56
|
+
else if (allDeps.vue) stack.frameworks.push('vue')
|
|
57
|
+
|
|
58
|
+
if (allDeps['@sveltejs/kit']) stack.frameworks.push('sveltekit')
|
|
59
|
+
else if (allDeps.svelte) stack.frameworks.push('svelte')
|
|
60
|
+
|
|
61
|
+
if (allDeps['@angular/core']) stack.frameworks.push('angular')
|
|
62
|
+
if (allDeps['@nestjs/core']) stack.frameworks.push('nestjs')
|
|
63
|
+
if (allDeps.fastify) stack.frameworks.push('fastify')
|
|
64
|
+
if (allDeps.express) stack.frameworks.push('express')
|
|
65
|
+
|
|
66
|
+
// Styling & UI
|
|
67
|
+
if (allDeps.tailwindcss) stack.libraries.push('tailwindcss')
|
|
68
|
+
|
|
69
|
+
// ORM & Data
|
|
70
|
+
if (allDeps.prisma || allDeps['@prisma/client']) stack.libraries.push('prisma')
|
|
71
|
+
if (allDeps['drizzle-orm']) stack.libraries.push('drizzle')
|
|
72
|
+
if (allDeps.typeorm) stack.libraries.push('typeorm')
|
|
73
|
+
if (allDeps.mongoose) stack.libraries.push('mongoose')
|
|
74
|
+
|
|
75
|
+
// Validation
|
|
76
|
+
if (allDeps.zod) stack.libraries.push('zod')
|
|
77
|
+
if (allDeps.valibot) stack.libraries.push('valibot')
|
|
78
|
+
|
|
79
|
+
// Testing
|
|
80
|
+
if (allDeps.vitest) stack.libraries.push('vitest')
|
|
81
|
+
if (allDeps.jest) stack.libraries.push('jest')
|
|
82
|
+
if (allDeps.playwright || allDeps['@playwright/test']) stack.libraries.push('playwright')
|
|
83
|
+
} catch {
|
|
84
|
+
// safe fallback on corrupted package.json
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2. Rust (Cargo.toml)
|
|
89
|
+
const cargoPath = path.join(cwd, 'Cargo.toml')
|
|
90
|
+
if (fs.existsSync(cargoPath)) {
|
|
91
|
+
try {
|
|
92
|
+
const content = fs.readFileSync(cargoPath, 'utf8')
|
|
93
|
+
stack.manifestFound = true
|
|
94
|
+
stack.primaryLanguage = 'rust'
|
|
95
|
+
|
|
96
|
+
if (content.includes('axum')) stack.frameworks.push('axum')
|
|
97
|
+
if (content.includes('actix-web')) stack.frameworks.push('actix-web')
|
|
98
|
+
if (content.includes('tokio')) stack.libraries.push('tokio')
|
|
99
|
+
if (content.includes('serde')) stack.libraries.push('serde')
|
|
100
|
+
if (content.includes('sqlx')) stack.libraries.push('sqlx')
|
|
101
|
+
if (content.includes('diesel')) stack.libraries.push('diesel')
|
|
102
|
+
} catch {
|
|
103
|
+
// safe fallback
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 3. Python (pyproject.toml, requirements.txt)
|
|
108
|
+
const pyprojectPath = path.join(cwd, 'pyproject.toml')
|
|
109
|
+
const reqPath = path.join(cwd, 'requirements.txt')
|
|
110
|
+
if (fs.existsSync(pyprojectPath) || fs.existsSync(reqPath)) {
|
|
111
|
+
try {
|
|
112
|
+
const pyContent = (fs.existsSync(pyprojectPath) ? fs.readFileSync(pyprojectPath, 'utf8') : '') +
|
|
113
|
+
'\n' + (fs.existsSync(reqPath) ? fs.readFileSync(reqPath, 'utf8') : '')
|
|
114
|
+
stack.manifestFound = true
|
|
115
|
+
if (stack.primaryLanguage === 'unknown') stack.primaryLanguage = 'python'
|
|
116
|
+
|
|
117
|
+
if (pyContent.includes('fastapi')) stack.frameworks.push('fastapi')
|
|
118
|
+
if (pyContent.includes('django')) stack.frameworks.push('django')
|
|
119
|
+
if (pyContent.includes('flask')) stack.frameworks.push('flask')
|
|
120
|
+
if (pyContent.includes('pydantic')) stack.libraries.push('pydantic')
|
|
121
|
+
if (pyContent.includes('sqlalchemy')) stack.libraries.push('sqlalchemy')
|
|
122
|
+
if (pyContent.includes('pytest')) stack.libraries.push('pytest')
|
|
123
|
+
if (pyContent.includes('torch')) stack.libraries.push('pytorch')
|
|
124
|
+
if (pyContent.includes('pandas')) stack.libraries.push('pandas')
|
|
125
|
+
} catch {
|
|
126
|
+
// safe fallback
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 4. Go (go.mod)
|
|
131
|
+
const goModPath = path.join(cwd, 'go.mod')
|
|
132
|
+
if (fs.existsSync(goModPath)) {
|
|
133
|
+
try {
|
|
134
|
+
const goContent = fs.readFileSync(goModPath, 'utf8')
|
|
135
|
+
stack.manifestFound = true
|
|
136
|
+
if (stack.primaryLanguage === 'unknown') stack.primaryLanguage = 'go'
|
|
137
|
+
|
|
138
|
+
if (goContent.includes('gin-gonic/gin')) stack.frameworks.push('gin')
|
|
139
|
+
if (goContent.includes('gofiber/fiber')) stack.frameworks.push('fiber')
|
|
140
|
+
if (goContent.includes('labstack/echo')) stack.frameworks.push('echo')
|
|
141
|
+
if (goContent.includes('gorm.io/gorm')) stack.libraries.push('gorm')
|
|
142
|
+
if (goContent.includes('stretchr/testify')) stack.libraries.push('testify')
|
|
143
|
+
} catch {
|
|
144
|
+
// safe fallback
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Build summary string
|
|
149
|
+
const parts = []
|
|
150
|
+
if (stack.primaryLanguage !== 'unknown') parts.push(stack.primaryLanguage)
|
|
151
|
+
if (stack.frameworks.length > 0) parts.push(`framework: ${stack.frameworks.join(', ')}`)
|
|
152
|
+
if (stack.libraries.length > 0) parts.push(`libs: ${stack.libraries.slice(0, 4).join(', ')}`)
|
|
153
|
+
stack.stackSummary = parts.join(' | ')
|
|
154
|
+
|
|
155
|
+
return stack
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Returns specialized domain personas based on detected stack.
|
|
160
|
+
*
|
|
161
|
+
* @param {object} stackInfo
|
|
162
|
+
* @returns {Array<string>} Array of specialized persona role descriptions
|
|
163
|
+
*/
|
|
164
|
+
export function getStackPersonaSpecialties(stackInfo = {}) {
|
|
165
|
+
const lang = stackInfo.primaryLanguage || 'unknown'
|
|
166
|
+
const frameworks = Array.isArray(stackInfo.frameworks) ? stackInfo.frameworks : []
|
|
167
|
+
const libs = Array.isArray(stackInfo.libraries) ? stackInfo.libraries : []
|
|
168
|
+
|
|
169
|
+
// React / Next.js / Frontend
|
|
170
|
+
if (frameworks.includes('react') || frameworks.includes('next.js') || frameworks.includes('vue') || frameworks.includes('svelte')) {
|
|
171
|
+
return [
|
|
172
|
+
'Component Architecture, Hooks & Re-render Optimization Specialist',
|
|
173
|
+
libs.includes('typescript') || lang === 'typescript'
|
|
174
|
+
? 'TypeScript Strict Typing, Generic Invariants & Defensive Schema Expert'
|
|
175
|
+
: 'State Management, Data Flow & Edge-Case Specialist',
|
|
176
|
+
'Accessibility (a11y), Semantic UI & Modern CSS Performance Reviewer',
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Python / FastAPI / Django
|
|
181
|
+
if (lang === 'python') {
|
|
182
|
+
return [
|
|
183
|
+
frameworks.includes('fastapi')
|
|
184
|
+
? 'FastAPI Async Concurrency & High-Throughput REST Architect'
|
|
185
|
+
: 'Pythonic Clean Architecture & Idiomatic Object Design Specialist',
|
|
186
|
+
libs.includes('pydantic')
|
|
187
|
+
? 'Pydantic V2 Schema Validation & Type-Safe Domain Modeler'
|
|
188
|
+
: 'Defensive Data Integrity & Error Boundary Engineer',
|
|
189
|
+
libs.includes('pytest')
|
|
190
|
+
? 'Pytest Fixtures, Mocking & Comprehensive Edge-Case Test Engineer'
|
|
191
|
+
: 'Performance Profiling & Memory Leak Auditor',
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Rust
|
|
196
|
+
if (lang === 'rust') {
|
|
197
|
+
return [
|
|
198
|
+
'Rust Zero-Cost Abstractions & Concurrency Architect',
|
|
199
|
+
'Rust Memory Safety, Error Handling & Lifetime Specialist',
|
|
200
|
+
'Rust Idiomatic API & Performance Specialist',
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Go
|
|
205
|
+
if (lang === 'go') {
|
|
206
|
+
return [
|
|
207
|
+
'Go Idiomatic Concurrency & Channel Synchronization Architect',
|
|
208
|
+
'Go Interface Design, Error Wrapping & Zero-Allocation Specialist',
|
|
209
|
+
'Go Production Reliability & Table-Driven Unit Testing Specialist',
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// General TypeScript / Node
|
|
214
|
+
if (lang === 'typescript') {
|
|
215
|
+
return [
|
|
216
|
+
'TypeScript Strict Type Safety & Clean Architecture Specialist',
|
|
217
|
+
'Async Flow, Micro-Optimization & Error Boundary Engineer',
|
|
218
|
+
'Maintainability, Modularity & Test Coverage Auditor',
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Universal Default
|
|
223
|
+
return [
|
|
224
|
+
'System Architecture & Core Correctness Engineer',
|
|
225
|
+
'Edge-Case Analysis & Defensive Error Handling Specialist',
|
|
226
|
+
'Clean Code, Maintainability & Documentation Specialist',
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Assigns stack-aware specialized personas to candidate proposer slots.
|
|
232
|
+
*
|
|
233
|
+
* @param {Array<object>} references - Proposer slots
|
|
234
|
+
* @param {object} stackInfo - Output of detectProjectStack
|
|
235
|
+
* @param {object} [options]
|
|
236
|
+
* @param {boolean} [options.enabled=true] - Whether stack-aware personas are enabled
|
|
237
|
+
* @returns {{ references: Array<object>, isSpecialized: boolean, stackSummary: string }}
|
|
238
|
+
*/
|
|
239
|
+
export function assignStackAwarePersonas(references = [], stackInfo = {}, options = {}) {
|
|
240
|
+
const { enabled = true } = options
|
|
241
|
+
|
|
242
|
+
if (!enabled || !Array.isArray(references) || references.length === 0) {
|
|
243
|
+
return {
|
|
244
|
+
references: Array.isArray(references) ? references : [],
|
|
245
|
+
isSpecialized: false,
|
|
246
|
+
stackSummary: stackInfo.stackSummary || '',
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const specialties = getStackPersonaSpecialties(stackInfo)
|
|
251
|
+
const evolved = references.map((slot, idx) => {
|
|
252
|
+
// If user already specified a custom non-default persona, preserve it
|
|
253
|
+
const hasCustom = slot.role_persona && slot.role_persona !== 'general' && slot.role_persona !== 'proposer'
|
|
254
|
+
if (hasCustom) {
|
|
255
|
+
return { ...slot }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const specialty = specialties[idx % specialties.length]
|
|
259
|
+
return {
|
|
260
|
+
...slot,
|
|
261
|
+
role_persona: specialty,
|
|
262
|
+
stackSpecialization: specialty,
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
references: evolved,
|
|
268
|
+
isSpecialized: Boolean(stackInfo.manifestFound),
|
|
269
|
+
stackSummary: stackInfo.stackSummary || '',
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Formats a directive summarizing the project stack for injection into judge or synthesis prompts.
|
|
275
|
+
*
|
|
276
|
+
* @param {object} stackInfo
|
|
277
|
+
* @returns {string|null}
|
|
278
|
+
*/
|
|
279
|
+
export function formatStackAwareDirective(stackInfo = {}) {
|
|
280
|
+
if (!stackInfo || !stackInfo.manifestFound || !stackInfo.stackSummary) {
|
|
281
|
+
return null
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return `### Project Stack & Architecture Context
|
|
285
|
+
- Detected Environment: ${stackInfo.stackSummary}
|
|
286
|
+
- Verification Requirement: All proposals must adhere to the conventions and idiomatic idioms of this technology stack.`
|
|
287
|
+
}
|
package/lib/pipeline/runner.js
CHANGED
|
@@ -307,7 +307,7 @@ export async function runMoAPipeline({
|
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
-
const fileScoringDirective = [rawFileDirective, visualResult?.directive, adversarialTddResult?.directive, syntaxDirective, pruningDirective].filter(Boolean).join("\n\n") || null
|
|
310
|
+
const fileScoringDirective = [rawFileDirective, visualResult?.directive, adversarialTddResult?.directive, syntaxDirective, pruningDirective, ctxStage.stackAwareDirective, ctxStage.developerPreferencesDirective].filter(Boolean).join("\n\n") || null
|
|
311
311
|
|
|
312
312
|
// Stage 7: Synthesis Phase
|
|
313
313
|
const synthRes = await stageSynthesis({
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Developer Preferences Management Routes (#309)
|
|
3
|
+
* Exposes endpoints to inspect, add, toggle, and delete developer coding preferences.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
loadDeveloperPreferences,
|
|
8
|
+
addDeveloperPreference,
|
|
9
|
+
updateDeveloperPreference,
|
|
10
|
+
deleteDeveloperPreference,
|
|
11
|
+
} from '../pipeline/developer-preferences.js'
|
|
12
|
+
import { writeJson, readBody, assertAuthorizedRequest } from '../access.js'
|
|
13
|
+
import { isSafeWriteRequest } from '../updater.js'
|
|
14
|
+
|
|
15
|
+
export function registerPreferencesRoutes(ctx) {
|
|
16
|
+
// 1. Exact route: /dsh-moa/preferences
|
|
17
|
+
ctx.effect(() => {
|
|
18
|
+
return ctx.webServer.register({
|
|
19
|
+
kind: 'exact',
|
|
20
|
+
path: '/dsh-moa/preferences',
|
|
21
|
+
handler: async (req, res) => {
|
|
22
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
23
|
+
|
|
24
|
+
if (req.method === 'GET') {
|
|
25
|
+
const preferences = loadDeveloperPreferences()
|
|
26
|
+
return writeJson(res, 200, { ok: true, preferences })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (req.method === 'POST') {
|
|
30
|
+
if (!isSafeWriteRequest(req)) {
|
|
31
|
+
return writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let body = {}
|
|
35
|
+
try {
|
|
36
|
+
const raw = await readBody(req)
|
|
37
|
+
if (raw) body = JSON.parse(raw)
|
|
38
|
+
} catch (_err) {
|
|
39
|
+
return writeJson(res, 400, { ok: false, error: 'Malformed JSON payload' })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (body.action === 'delete' && body.id) {
|
|
43
|
+
const deleted = deleteDeveloperPreference(body.id)
|
|
44
|
+
return writeJson(res, 200, { ok: true, deleted, id: body.id })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (body.action === 'update' && body.id) {
|
|
48
|
+
const updated = updateDeveloperPreference(body.id, body.updates || body)
|
|
49
|
+
if (!updated) return writeJson(res, 404, { ok: false, error: 'Preference not found' })
|
|
50
|
+
return writeJson(res, 200, { ok: true, preference: updated })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const created = addDeveloperPreference(body)
|
|
54
|
+
if (!created) {
|
|
55
|
+
return writeJson(res, 400, { ok: false, error: 'Missing required rule field' })
|
|
56
|
+
}
|
|
57
|
+
return writeJson(res, 201, { ok: true, preference: created })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return writeJson(res, 405, { ok: false, error: 'GET or POST required' })
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// 2. Prefix route: /dsh-moa/preferences/ for REST ID operations
|
|
66
|
+
ctx.effect(() => {
|
|
67
|
+
return ctx.webServer.register({
|
|
68
|
+
kind: 'prefix',
|
|
69
|
+
path: '/dsh-moa/preferences/',
|
|
70
|
+
handler: async (req, res) => {
|
|
71
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
72
|
+
|
|
73
|
+
const url = new URL(req.url, 'http://127.0.0.1')
|
|
74
|
+
const id = url.pathname.replace(/^\/dsh-moa\/preferences\//, '').trim()
|
|
75
|
+
|
|
76
|
+
if (!id) {
|
|
77
|
+
return writeJson(res, 400, { ok: false, error: 'Missing preference ID in URL path' })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (req.method === 'DELETE') {
|
|
81
|
+
if (!isSafeWriteRequest(req)) {
|
|
82
|
+
return writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
|
|
83
|
+
}
|
|
84
|
+
const deleted = deleteDeveloperPreference(id)
|
|
85
|
+
return writeJson(res, 200, { ok: true, deleted, id })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (req.method === 'PATCH' || req.method === 'POST') {
|
|
89
|
+
if (!isSafeWriteRequest(req)) {
|
|
90
|
+
return writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
|
|
91
|
+
}
|
|
92
|
+
let body = {}
|
|
93
|
+
try {
|
|
94
|
+
const raw = await readBody(req)
|
|
95
|
+
if (raw) body = JSON.parse(raw)
|
|
96
|
+
} catch {
|
|
97
|
+
return writeJson(res, 400, { ok: false, error: 'Malformed JSON payload' })
|
|
98
|
+
}
|
|
99
|
+
const updated = updateDeveloperPreference(id, body)
|
|
100
|
+
if (!updated) return writeJson(res, 404, { ok: false, error: 'Preference not found' })
|
|
101
|
+
return writeJson(res, 200, { ok: true, preference: updated })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return writeJson(res, 405, { ok: false, error: 'DELETE or PATCH required' })
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
}
|
package/lib/routes.js
CHANGED
|
@@ -8,12 +8,14 @@ import { registerHistoryRoutes } from './routes/history.js'
|
|
|
8
8
|
import { registerWorkbenchRoutes } from './routes/workbench.js'
|
|
9
9
|
import { registerPipelineRoutes } from './routes/pipeline.js'
|
|
10
10
|
import { registerCacheRoutes } from './routes/cache.js'
|
|
11
|
+
import { registerPreferencesRoutes } from './routes/preferences.js'
|
|
11
12
|
|
|
12
13
|
export { registerPresetRoutes } from './routes/presets.js'
|
|
13
14
|
export { registerHistoryRoutes } from './routes/history.js'
|
|
14
15
|
export { registerWorkbenchRoutes } from './routes/workbench.js'
|
|
15
16
|
export { registerPipelineRoutes } from './routes/pipeline.js'
|
|
16
17
|
export { registerCacheRoutes } from './routes/cache.js'
|
|
18
|
+
export { registerPreferencesRoutes } from './routes/preferences.js'
|
|
17
19
|
|
|
18
20
|
export function registerMoaRoutes(ctx, deps) {
|
|
19
21
|
registerPresetRoutes(ctx, deps)
|
|
@@ -21,4 +23,5 @@ export function registerMoaRoutes(ctx, deps) {
|
|
|
21
23
|
registerWorkbenchRoutes(ctx, deps)
|
|
22
24
|
registerPipelineRoutes(ctx, deps)
|
|
23
25
|
registerCacheRoutes(ctx, deps)
|
|
26
|
+
registerPreferencesRoutes(ctx, deps)
|
|
24
27
|
}
|