@goodandready/dsh-moa 0.2.73 → 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.
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Dynamic Consensus Threshold & Multi-Judge Escalation (#300)
3
+ *
4
+ * Detects divergence among candidate outputs (Disagreement Metric).
5
+ * When candidate consensus is weak or contradictory, automatically escalates:
6
+ * - auto_upgrade: upgrades from lightweight/fast judge to flagship/senior judge
7
+ * - council_majority: convenes a 3-judge panel with majority voting
8
+ */
9
+
10
+ import { calculateSimilarity } from './early-consensus.js'
11
+
12
+ /**
13
+ * Calculates the Disagreement Metric (0.0 to 1.0) across candidate outputs.
14
+ * 0.0 means perfect consensus (identical solutions).
15
+ * 1.0 means total divergence (completely distinct solutions).
16
+ *
17
+ * @param {Array<object>} candidateOutputs
18
+ * @returns {number} Disagreement metric between 0.0 and 1.0
19
+ */
20
+ export function calculateDisagreementMetric(candidateOutputs = []) {
21
+ if (!Array.isArray(candidateOutputs) || candidateOutputs.length < 2) {
22
+ return 0.0
23
+ }
24
+
25
+ const validTexts = candidateOutputs
26
+ .filter((c) => c && typeof c.text === 'string' && c.text.trim())
27
+ .map((c) => c.text)
28
+
29
+ if (validTexts.length < 2) return 0.0
30
+
31
+ let totalSim = 0
32
+ let pairs = 0
33
+
34
+ for (let i = 0; i < validTexts.length; i++) {
35
+ for (let j = i + 1; j < validTexts.length; j++) {
36
+ totalSim += calculateSimilarity(validTexts[i], validTexts[j])
37
+ pairs++
38
+ }
39
+ }
40
+
41
+ if (pairs === 0) return 0.0
42
+ const avgSim = totalSim / pairs
43
+ const disagreement = Math.max(0.0, Math.min(1.0, 1.0 - avgSim))
44
+ return Number(disagreement.toFixed(4))
45
+ }
46
+
47
+ /**
48
+ * Evaluates whether candidate divergence warrants judge escalation.
49
+ *
50
+ * @param {object} params
51
+ * @param {Array<object>} params.candidateOutputs - Array of successful candidate results
52
+ * @param {object} params.primaryJudge - Primary configured judge slot
53
+ * @param {Array<object>} [params.fallbackJudges=[]] - Chain of backup judges
54
+ * @param {object} [params.escalationJudge] - Explicit flagship upgrade judge
55
+ * @param {string} [params.policy='single'] - 'single' | 'auto_upgrade' | 'council_majority'
56
+ * @param {number} [params.threshold=0.40] - Disagreement threshold to trigger escalation
57
+ * @returns {object} Escalation decision and effective judge(s)
58
+ */
59
+ export function evaluateJudgeEscalation({
60
+ candidateOutputs = [],
61
+ primaryJudge = { provider: '', model: '' },
62
+ fallbackJudges = [],
63
+ escalationJudge = null,
64
+ policy = 'single',
65
+ threshold = 0.40,
66
+ } = {}) {
67
+ const normPolicy = String(policy || 'single').toLowerCase()
68
+
69
+ if (normPolicy === 'single' || !Array.isArray(candidateOutputs) || candidateOutputs.length < 2) {
70
+ return {
71
+ escalated: false,
72
+ escalatedType: 'none',
73
+ judge: primaryJudge,
74
+ councilJudges: [primaryJudge],
75
+ disagreement: 0.0,
76
+ threshold,
77
+ reason: 'Standard single judge evaluation',
78
+ }
79
+ }
80
+
81
+ const disagreement = calculateDisagreementMetric(candidateOutputs)
82
+ const isTriggered = disagreement >= threshold
83
+
84
+ if (!isTriggered) {
85
+ return {
86
+ escalated: false,
87
+ escalatedType: 'none',
88
+ judge: primaryJudge,
89
+ councilJudges: [primaryJudge],
90
+ disagreement,
91
+ threshold,
92
+ reason: `Consensus adequate: disagreement ${(disagreement * 100).toFixed(1)}% < threshold ${(threshold * 100).toFixed(1)}%`,
93
+ }
94
+ }
95
+
96
+ if (normPolicy === 'auto_upgrade') {
97
+ // Pick designated escalationJudge, or the first available fallbackJudge
98
+ let targetJudge = null
99
+ if (escalationJudge && escalationJudge.model) {
100
+ targetJudge = escalationJudge
101
+ } else if (Array.isArray(fallbackJudges) && fallbackJudges.length > 0 && fallbackJudges[0]?.model) {
102
+ targetJudge = fallbackJudges[0]
103
+ } else {
104
+ targetJudge = primaryJudge
105
+ }
106
+
107
+ const upgraded = targetJudge.model !== primaryJudge.model || targetJudge.provider !== primaryJudge.provider
108
+ return {
109
+ escalated: upgraded,
110
+ escalatedType: upgraded ? 'auto_upgrade' : 'none',
111
+ judge: targetJudge,
112
+ originalJudge: primaryJudge,
113
+ councilJudges: [targetJudge],
114
+ disagreement,
115
+ threshold,
116
+ reason: upgraded
117
+ ? `High candidate divergence (${(disagreement * 100).toFixed(1)}% >= ${(threshold * 100).toFixed(1)}%): upgraded judge to ${targetJudge.provider ? targetJudge.provider + ':' : ''}${targetJudge.model}`
118
+ : `High divergence detected (${(disagreement * 100).toFixed(1)}%), but no senior fallback judge configured`,
119
+ }
120
+ }
121
+
122
+ if (normPolicy === 'council_majority') {
123
+ const council = [primaryJudge]
124
+ if (escalationJudge && escalationJudge.model && escalationJudge.model !== primaryJudge.model) {
125
+ council.push(escalationJudge)
126
+ }
127
+ if (Array.isArray(fallbackJudges)) {
128
+ for (const fb of fallbackJudges) {
129
+ if (council.length >= 3) break
130
+ if (fb && fb.model && !council.some((c) => c.model === fb.model && c.provider === fb.provider)) {
131
+ council.push(fb)
132
+ }
133
+ }
134
+ }
135
+
136
+ const hasMulti = council.length > 1
137
+ return {
138
+ escalated: hasMulti,
139
+ escalatedType: hasMulti ? 'council_majority' : 'none',
140
+ judge: council[0],
141
+ originalJudge: primaryJudge,
142
+ councilJudges: council,
143
+ disagreement,
144
+ threshold,
145
+ reason: hasMulti
146
+ ? `High candidate divergence (${(disagreement * 100).toFixed(1)}% >= ${(threshold * 100).toFixed(1)}%): assembled ${council.length}-judge council`
147
+ : `High divergence detected, but fewer than 2 judges available for council`,
148
+ }
149
+ }
150
+
151
+ return {
152
+ escalated: false,
153
+ escalatedType: 'none',
154
+ judge: primaryJudge,
155
+ councilJudges: [primaryJudge],
156
+ disagreement,
157
+ threshold,
158
+ reason: 'Standard single judge',
159
+ }
160
+ }
@@ -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
+ }
@@ -1,3 +1,5 @@
1
+ import { evaluateJudgeEscalation } from './escalation.js'
2
+ import { calculateSelfConsistencyAgreement } from './self-consistency.js'
1
3
  import { pruneCandidateEnsemble, formatPruningDirective } from './context-pruner.js'
2
4
  import { buildAstCompositeMergeDirective } from '../moa-ast-merge.js'
3
5
  import { runVisualMoaConsensus } from '../moa-visual-consensus.js'
@@ -106,6 +108,13 @@ export async function runMoAPipeline({
106
108
  })
107
109
  const syntaxDirective = syntaxResult.filterApplied ? formatSyntaxDirective(referenceOutputs, { blind: ctxStage.isBlindEvaluation }) : null
108
110
 
111
+ if (ctxStage.selfConsistencyExpanded && successfulRefs.length > 1) {
112
+ const scAgreement = calculateSelfConsistencyAgreement(successfulRefs)
113
+ if (typeof onProgress === 'function') {
114
+ onProgress(`🎯 *Self-Consistency Agreement: ${(scAgreement.agreementRatio * 100).toFixed(0)}% (${scAgreement.isStrongConsensus ? 'Strong consensus' : 'Divergent sampling'}) across ${scAgreement.sampleCount} samples*\n`)
115
+ }
116
+ }
117
+
109
118
  // Stage 3: Clarifying Questions Branch
110
119
  if (ctxStage.needsQuestions) {
111
120
  return await stageClarifyingQuestions({
@@ -190,11 +199,56 @@ export async function runMoAPipeline({
190
199
  return { error: new Error('Turn aborted') }
191
200
  }
192
201
 
202
+ // Pre-Stage 6: Dynamic Consensus Threshold & Multi-Judge Escalation (#300)
203
+ const escalationResult = evaluateJudgeEscalation({
204
+ candidateOutputs: syntaxResult.validCandidates.length > 0 ? syntaxResult.validCandidates : successfulRefs,
205
+ primaryJudge: ctxStage.primaryJudge,
206
+ fallbackJudges: ctxStage.fallbackJudges,
207
+ escalationJudge: ctxStage.escalationJudge,
208
+ policy: ctxStage.escalationPolicy,
209
+ threshold: ctxStage.escalationDisagreementThreshold,
210
+ })
211
+
212
+ let effectivePrimaryJudge = ctxStage.primaryJudge
213
+ let effectiveFallbackJudges = ctxStage.fallbackJudges
214
+ let effectiveJudgesChain = ctxStage.judgesChain
215
+ let effectivePreset = preset
216
+
217
+ if (escalationResult.escalated) {
218
+ emitEvent(ctx, MOA_EVENTS.JUDGE_ESCALATION, {
219
+ runId,
220
+ policy: ctxStage.escalationPolicy,
221
+ disagreement: escalationResult.disagreement,
222
+ threshold: escalationResult.threshold,
223
+ escalatedType: escalationResult.escalatedType,
224
+ originalJudge: escalationResult.originalJudge,
225
+ escalatedJudge: escalationResult.judge,
226
+ councilJudges: escalationResult.councilJudges,
227
+ })
228
+ if (typeof onProgress === 'function') {
229
+ onProgress(`⚖️ *Judge Escalation (${escalationResult.escalatedType}): ${escalationResult.reason}*\n`)
230
+ }
231
+
232
+ if (escalationResult.escalatedType === 'auto_upgrade') {
233
+ effectivePrimaryJudge = escalationResult.judge
234
+ effectiveFallbackJudges = ctxStage.fallbackJudges.filter(
235
+ (j) => !(j.provider === escalationResult.judge.provider && j.model === escalationResult.judge.model)
236
+ )
237
+ effectiveJudgesChain = [effectivePrimaryJudge, ...effectiveFallbackJudges]
238
+ } else if (escalationResult.escalatedType === 'council_majority') {
239
+ effectivePreset = {
240
+ ...preset,
241
+ multi_judge_enabled: true,
242
+ judge_models: escalationResult.councilJudges,
243
+ }
244
+ }
245
+ }
246
+
193
247
  // Stage 6: Multi-Judge Panel
194
248
  const mjRes = await stageMultiJudge({
195
- preset,
249
+ preset: effectivePreset,
196
250
  successfulRefs: syntaxResult.validCandidates.length > 0 ? syntaxResult.validCandidates : successfulRefs,
197
- referenceOutputs, primaryJudge: ctxStage.primaryJudge,
251
+ referenceOutputs, primaryJudge: effectivePrimaryJudge,
198
252
  userPrompt, judgeCriteria: ctxStage.judgeCriteria, prices, maxTokens: ctxStage.maxTokens,
199
253
  callLlm, signal, onProgress, cwd, runId, isRefinement: ctxStage.isRefinement,
200
254
  isFastMode: ctxStage.isFastMode, collectedCtx: ctxStage.collectedCtx, startTime,
@@ -253,7 +307,7 @@ export async function runMoAPipeline({
253
307
  }
254
308
  }
255
309
 
256
- 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
257
311
 
258
312
  // Stage 7: Synthesis Phase
259
313
  const synthRes = await stageSynthesis({
@@ -263,9 +317,9 @@ export async function runMoAPipeline({
263
317
  judgeCriteria: ctxStage.judgeCriteria,
264
318
  isCuratorSynthesis: ctxStage.isCuratorSynthesis, isBlindEvaluation: ctxStage.isBlindEvaluation,
265
319
  priorTurnBaseline: ctxStage.priorTurnBaseline, compositeMergeDirective, fileScoringDirective,
266
- multiJudgeResult, preset, prices, maxTokens: ctxStage.maxTokens, isFastMode: ctxStage.isFastMode,
267
- primaryJudge: ctxStage.primaryJudge, fallbackJudges: ctxStage.fallbackJudges,
268
- judgesChain: ctxStage.judgesChain, aggTemp: ctxStage.aggTemp,
320
+ multiJudgeResult, preset: effectivePreset, prices, maxTokens: ctxStage.maxTokens, isFastMode: ctxStage.isFastMode,
321
+ primaryJudge: effectivePrimaryJudge, fallbackJudges: effectiveFallbackJudges,
322
+ judgesChain: effectiveJudgesChain, aggTemp: ctxStage.aggTemp,
269
323
  aggTimeoutSec: ctxStage.aggTimeoutSec, isStreamAggregator: ctxStage.isStreamAggregator,
270
324
  onStreamDelta, signal, callLlm, onProgress, cwd, runId, isRefinement: ctxStage.isRefinement,
271
325
  collectedCtx: ctxStage.collectedCtx, startTime,
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Self-Consistency Temperature Sampling for Single Models (#301)
3
+ *
4
+ * Allows users with a single model slot (e.g., local Ollama/vLLM or a single API key)
5
+ * to benefit from Mixture of Agents ensemble diversity by generating parallel samples
6
+ * at varying temperatures and complementary system personas, followed by consistency voting.
7
+ */
8
+
9
+ import { ROLE_PERSONA_PROMPTS } from '../moa-prompts.js'
10
+
11
+ export const DEFAULT_SELF_CONSISTENCY_TEMPERATURES = Object.freeze([0.2, 0.6, 0.9])
12
+ export const DEFAULT_SELF_CONSISTENCY_PERSONAS = Object.freeze(['proposer', 'challenger', 'minimalist'])
13
+
14
+ /**
15
+ * Expands a single reference model into multiple parallel proposer slots
16
+ * with distinct temperatures and complementary reasoning personas.
17
+ *
18
+ * @param {Array<object>} referenceModels - Original reference model slots
19
+ * @param {object} [options]
20
+ * @param {boolean} [options.enabled=false] - Whether self-consistency expansion is enabled
21
+ * @param {number} [options.samples=3] - Number of candidate samples to generate (2-10)
22
+ * @param {Array<number>} [options.temperatures] - Array of temperatures for samples
23
+ * @param {Array<string>} [options.personas] - Array of persona role names
24
+ * @returns {{ references: Array<object>, isExpanded: boolean, sampleCount: number }}
25
+ */
26
+ export function expandSelfConsistencySlots(referenceModels = [], options = {}) {
27
+ const {
28
+ enabled = false,
29
+ samples = 3,
30
+ temperatures = DEFAULT_SELF_CONSISTENCY_TEMPERATURES,
31
+ personas = DEFAULT_SELF_CONSISTENCY_PERSONAS,
32
+ } = options
33
+
34
+ // Expansion applies only when explicitly enabled or requested
35
+ if (!enabled) {
36
+ return {
37
+ references: Array.isArray(referenceModels) ? referenceModels : [],
38
+ isExpanded: false,
39
+ sampleCount: Array.isArray(referenceModels) ? referenceModels.length : 0,
40
+ }
41
+ }
42
+
43
+ const rawList = Array.isArray(referenceModels) && referenceModels.length > 0
44
+ ? referenceModels
45
+ : [{ provider: '', model: '' }]
46
+
47
+ // Only expand when a single model slot is configured
48
+ if (rawList.length !== 1) {
49
+ return {
50
+ references: rawList,
51
+ isExpanded: false,
52
+ sampleCount: rawList.length,
53
+ }
54
+ }
55
+
56
+ const baseSlot = rawList[0] || {}
57
+ const count = Math.min(10, Math.max(2, Number(samples) || 3))
58
+ const expandedList = []
59
+
60
+ for (let i = 0; i < count; i++) {
61
+ const temp = Array.isArray(temperatures) && typeof temperatures[i] === 'number'
62
+ ? temperatures[i]
63
+ : (DEFAULT_SELF_CONSISTENCY_TEMPERATURES[i % DEFAULT_SELF_CONSISTENCY_TEMPERATURES.length] || 0.6)
64
+
65
+ const role = Array.isArray(personas) && personas[i]
66
+ ? personas[i]
67
+ : (DEFAULT_SELF_CONSISTENCY_PERSONAS[i % DEFAULT_SELF_CONSISTENCY_PERSONAS.length] || 'proposer')
68
+
69
+ expandedList.push({
70
+ ...baseSlot,
71
+ label: baseSlot.label
72
+ ? `${baseSlot.label} [T=${temp}, ${role}]`
73
+ : `Sample ${i + 1} (${baseSlot.model || 'model'}, T=${temp})`,
74
+ temperature: temp,
75
+ role_persona: role,
76
+ sampleIndex: i + 1,
77
+ isSelfConsistencySample: true,
78
+ })
79
+ }
80
+
81
+ return {
82
+ references: expandedList,
83
+ isExpanded: true,
84
+ sampleCount: expandedList.length,
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Calculates majority agreement and self-consistency metrics across candidate outputs.
90
+ *
91
+ * @param {Array<object>} candidateOutputs - Array of successful candidate results
92
+ * @returns {{ agreementRatio: number, isStrongConsensus: boolean, sampleCount: number }}
93
+ */
94
+ export function calculateSelfConsistencyAgreement(candidateOutputs = []) {
95
+ if (!Array.isArray(candidateOutputs) || candidateOutputs.length < 2) {
96
+ return {
97
+ agreementRatio: 1.0,
98
+ isStrongConsensus: true,
99
+ sampleCount: Array.isArray(candidateOutputs) ? candidateOutputs.length : 0,
100
+ }
101
+ }
102
+
103
+ const texts = candidateOutputs
104
+ .filter((c) => c && typeof c.text === 'string' && c.text.trim())
105
+ .map((c) => c.text.trim().toLowerCase().replace(/\s+/g, ' '))
106
+
107
+ if (texts.length < 2) {
108
+ return {
109
+ agreementRatio: 1.0,
110
+ isStrongConsensus: true,
111
+ sampleCount: texts.length,
112
+ }
113
+ }
114
+
115
+ // Count exact and near-match clusters
116
+ let maxClusterSize = 1
117
+ for (let i = 0; i < texts.length; i++) {
118
+ let clusterSize = 1
119
+ for (let j = 0; j < texts.length; j++) {
120
+ if (i === j) continue
121
+ // If texts are very similar (> 85% overlap)
122
+ if (texts[i] === texts[j] || (texts[i].length > 20 && texts[j].includes(texts[i].slice(0, 40)))) {
123
+ clusterSize++
124
+ }
125
+ }
126
+ if (clusterSize > maxClusterSize) maxClusterSize = clusterSize
127
+ }
128
+
129
+ const agreementRatio = Number((maxClusterSize / texts.length).toFixed(2))
130
+ return {
131
+ agreementRatio,
132
+ isStrongConsensus: agreementRatio >= 0.66,
133
+ sampleCount: texts.length,
134
+ }
135
+ }