@goodandready/dsh-moa 0.2.71 → 0.2.73
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 +11 -0
- package/README.ru.md +11 -0
- package/README.zh.md +11 -0
- package/lib/client.js +94 -0
- package/lib/events.js +5 -0
- package/lib/moa-adversarial-tdd.js +4 -1
- package/lib/moa-ast-merge.js +4 -1
- package/lib/moa-schema.js +23 -0
- package/lib/moa-swarm.js +13 -4
- package/lib/pipeline/candidates.js +16 -3
- package/lib/pipeline/context-pruner.js +323 -0
- package/lib/pipeline/context.js +11 -0
- package/lib/pipeline/early-consensus.js +324 -0
- package/lib/pipeline/runner.js +89 -6
- package/lib/pipeline/semantic-cache.js +348 -0
- package/lib/pipeline/syntax-filter.js +324 -0
- package/lib/routes/cache.js +42 -0
- package/lib/routes.js +3 -0
- package/lib/tui-settings.js +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-Token Local Pre-Judge Linter & Syntax Filter (#303)
|
|
3
|
+
* Inspects candidate proposals locally before LLM judge invocation.
|
|
4
|
+
* Disqualifies or penalizes candidates with fatal syntax errors without spending judge tokens.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import vm from 'node:vm'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extracts markdown fenced code blocks.
|
|
12
|
+
* @param {string} text
|
|
13
|
+
* @returns {Array<{ language: string, code: string }>}
|
|
14
|
+
*/
|
|
15
|
+
export function extractCodeBlocks(text = '') {
|
|
16
|
+
if (typeof text !== 'string') return []
|
|
17
|
+
const blocks = []
|
|
18
|
+
const regex = /(?:^|\n)```([a-zA-Z0-9_-]*)\r?\n([\s\S]*?)\r?\n```/g
|
|
19
|
+
let match
|
|
20
|
+
while ((match = regex.exec(text)) !== null) {
|
|
21
|
+
blocks.push({
|
|
22
|
+
language: (match[1] || '').trim().toLowerCase(),
|
|
23
|
+
code: match[2] || '',
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
return blocks
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Checks basic bracket and parenthesis balance.
|
|
31
|
+
* @param {string} code
|
|
32
|
+
* @returns {{ balanced: boolean, error?: string }}
|
|
33
|
+
*/
|
|
34
|
+
function checkBracketBalance(code = '') {
|
|
35
|
+
const stack = []
|
|
36
|
+
const pairs = { '(': ')', '{': '}', '[': ']' }
|
|
37
|
+
let inSingleQuote = false
|
|
38
|
+
let inDoubleQuote = false
|
|
39
|
+
let inTemplateLiteral = false
|
|
40
|
+
let inLineComment = false
|
|
41
|
+
let inBlockComment = false
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < code.length; i++) {
|
|
44
|
+
const ch = code[i]
|
|
45
|
+
const next = code[i + 1]
|
|
46
|
+
|
|
47
|
+
if (inLineComment) {
|
|
48
|
+
if (ch === '\n') inLineComment = false
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
if (inBlockComment) {
|
|
52
|
+
if (ch === '*' && next === '/') {
|
|
53
|
+
inBlockComment = false
|
|
54
|
+
i++
|
|
55
|
+
}
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
if (ch === '/' && next === '/' && !inSingleQuote && !inDoubleQuote && !inTemplateLiteral) {
|
|
59
|
+
inLineComment = true
|
|
60
|
+
i++
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
if (ch === '/' && next === '*' && !inSingleQuote && !inDoubleQuote && !inTemplateLiteral) {
|
|
64
|
+
inBlockComment = true
|
|
65
|
+
i++
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (ch === "'" && !inDoubleQuote && !inTemplateLiteral && code[i - 1] !== '\\') {
|
|
70
|
+
inSingleQuote = !inSingleQuote
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
if (ch === '"' && !inSingleQuote && !inTemplateLiteral && code[i - 1] !== '\\') {
|
|
74
|
+
inDoubleQuote = !inDoubleQuote
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
if (ch === '`' && !inSingleQuote && !inDoubleQuote && code[i - 1] !== '\\') {
|
|
78
|
+
inTemplateLiteral = !inTemplateLiteral
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (inSingleQuote || inDoubleQuote || inTemplateLiteral) continue
|
|
83
|
+
|
|
84
|
+
if (ch === '(' || ch === '{' || ch === '[') {
|
|
85
|
+
stack.push({ ch, pos: i })
|
|
86
|
+
} else if (ch === ')' || ch === '}' || ch === ']') {
|
|
87
|
+
if (stack.length === 0) {
|
|
88
|
+
return { balanced: false, error: `Unmatched closing bracket '${ch}' at index ${i}` }
|
|
89
|
+
}
|
|
90
|
+
const top = stack.pop()
|
|
91
|
+
if (pairs[top.ch] !== ch) {
|
|
92
|
+
return { balanced: false, error: `Mismatched bracket: expected '${pairs[top.ch]}', found '${ch}'` }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (stack.length > 0) {
|
|
98
|
+
const top = stack[stack.length - 1]
|
|
99
|
+
return { balanced: false, error: `Unclosed bracket '${top.ch}' opened at index ${top.pos}` }
|
|
100
|
+
}
|
|
101
|
+
return { balanced: true }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validates syntax of a single code block.
|
|
106
|
+
* @param {string} code
|
|
107
|
+
* @param {string} language
|
|
108
|
+
* @returns {{ valid: boolean, error: string | null }}
|
|
109
|
+
*/
|
|
110
|
+
export function validateCodeBlockSyntax(code = '', language = '') {
|
|
111
|
+
const lang = (language || '').toLowerCase().trim()
|
|
112
|
+
const trimmed = code.trim()
|
|
113
|
+
if (!trimmed) return { valid: true, error: null }
|
|
114
|
+
|
|
115
|
+
// 1. JSON validation
|
|
116
|
+
if (lang === 'json') {
|
|
117
|
+
try {
|
|
118
|
+
JSON.parse(trimmed)
|
|
119
|
+
return { valid: true, error: null }
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return { valid: false, error: `JSON Parse Error: ${err.message}` }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 2. JavaScript / TypeScript validation
|
|
126
|
+
if (['js', 'javascript', 'mjs', 'cjs', 'ts', 'typescript', 'jsx', 'tsx'].includes(lang)) {
|
|
127
|
+
// Check bracket balance
|
|
128
|
+
const bracketCheck = checkBracketBalance(trimmed)
|
|
129
|
+
if (!bracketCheck.balanced) {
|
|
130
|
+
return { valid: false, error: bracketCheck.error }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Try vm.Script for JS (stripping import/export and TypeScript type annotations)
|
|
134
|
+
try {
|
|
135
|
+
const sanitized = trimmed
|
|
136
|
+
.replace(/^([ \t]*)export\s+default\s+/gm, '$1')
|
|
137
|
+
.replace(/^([ \t]*)export\s*\{[^}]*\}\s*;?/gm, '$1')
|
|
138
|
+
.replace(/^([ \t]*)export\s+(const|let|var|function|class|async\s+function)\s+/gm, '$1$2 ')
|
|
139
|
+
.replace(/^([ \t]*)import\s+[^;\n]+;?/gm, '$1')
|
|
140
|
+
.replace(/^([ \t]*)export\s+\*\s+from\s+[^;\n]+;?/gm, '$1')
|
|
141
|
+
// Strip TS interface / type declarations
|
|
142
|
+
.replace(/^([ \t]*)interface\s+\w+(\s*<[^>]+>)?(\s+extends\s+[^{]+)?\s*\{[\s\S]*?\}/gm, '')
|
|
143
|
+
.replace(/^([ \t]*)type\s+\w+(\s*<[^>]+>)?\s*=[\s\S]*?;/gm, '')
|
|
144
|
+
|
|
145
|
+
// Wrap in async IIFE to allow top-level await and return statements
|
|
146
|
+
const wrapped = `(async function() {\n${sanitized}\n});`
|
|
147
|
+
new vm.Script(wrapped)
|
|
148
|
+
return { valid: true, error: null }
|
|
149
|
+
} catch (err) {
|
|
150
|
+
// In TS code, type annotations might trigger syntax error in raw vm.Script
|
|
151
|
+
if (['ts', 'typescript', 'tsx'].includes(lang)) {
|
|
152
|
+
// If bracket check passed, treat as valid TS fallback
|
|
153
|
+
return { valid: true, error: null }
|
|
154
|
+
}
|
|
155
|
+
return { valid: false, error: `JS Syntax Error: ${err.message}` }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 3. Python validation
|
|
160
|
+
if (['py', 'python'].includes(lang)) {
|
|
161
|
+
const bracketCheck = checkBracketBalance(trimmed)
|
|
162
|
+
if (!bracketCheck.balanced) {
|
|
163
|
+
return { valid: false, error: `Python syntax error: ${bracketCheck.error}` }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const lines = trimmed.split(/\r?\n/)
|
|
167
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
168
|
+
const line = lines[idx].trim()
|
|
169
|
+
if (!line || line.startsWith('#')) continue
|
|
170
|
+
// Lines starting with block keywords must end with ':'
|
|
171
|
+
const blockKeywords = /^(def\s+\w+|class\s+\w+|if\s+|elif\s+|else:|while\s+|for\s+|try:|except(\s+.*)?:|finally:|with\s+)/
|
|
172
|
+
if (blockKeywords.test(line)) {
|
|
173
|
+
// If line is incomplete or doesn't end with ':' (accounting for trailing comments)
|
|
174
|
+
const codePart = line.split('#')[0].trim()
|
|
175
|
+
if (!codePart.endsWith(':') && !codePart.endsWith('\\') && !codePart.endsWith('(')) {
|
|
176
|
+
return { valid: false, error: `Python Syntax Error at line ${idx + 1}: expected ':' at end of '${codePart}'` }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { valid: true, error: null }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Generic fallback: check balanced brackets
|
|
184
|
+
const bracketCheck = checkBracketBalance(trimmed)
|
|
185
|
+
if (!bracketCheck.balanced) {
|
|
186
|
+
return { valid: false, error: bracketCheck.error }
|
|
187
|
+
}
|
|
188
|
+
return { valid: true, error: null }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Validates all code blocks and files in a candidate proposal.
|
|
193
|
+
* @param {object} candidate
|
|
194
|
+
* @returns {{ valid: boolean, errors: string[], warnings: string[], blocksCount: number, validCount: number }}
|
|
195
|
+
*/
|
|
196
|
+
export function validateCandidateSyntax(candidate = {}) {
|
|
197
|
+
const errors = []
|
|
198
|
+
const warnings = []
|
|
199
|
+
let blocksCount = 0
|
|
200
|
+
let validCount = 0
|
|
201
|
+
|
|
202
|
+
// 1. Check markdown code blocks
|
|
203
|
+
if (typeof candidate.text === 'string') {
|
|
204
|
+
const blocks = extractCodeBlocks(candidate.text)
|
|
205
|
+
for (const b of blocks) {
|
|
206
|
+
blocksCount++
|
|
207
|
+
const res = validateCodeBlockSyntax(b.code, b.language)
|
|
208
|
+
if (res.valid) {
|
|
209
|
+
validCount++
|
|
210
|
+
} else {
|
|
211
|
+
errors.push(`[Block ${blocksCount} (${b.language || 'code'})]: ${res.error}`)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 2. Check candidate workspace files
|
|
217
|
+
if (Array.isArray(candidate.files)) {
|
|
218
|
+
for (const file of candidate.files) {
|
|
219
|
+
if (!file || typeof file.content !== 'string' || !file.relativePath) continue
|
|
220
|
+
blocksCount++
|
|
221
|
+
const ext = path.extname(file.relativePath).toLowerCase().replace('.', '')
|
|
222
|
+
const res = validateCodeBlockSyntax(file.content, ext)
|
|
223
|
+
if (res.valid) {
|
|
224
|
+
validCount++
|
|
225
|
+
} else {
|
|
226
|
+
errors.push(`[File ${file.relativePath}]: ${res.error}`)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const valid = errors.length === 0
|
|
232
|
+
return {
|
|
233
|
+
valid,
|
|
234
|
+
errors,
|
|
235
|
+
warnings,
|
|
236
|
+
blocksCount,
|
|
237
|
+
validCount,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Applies local zero-token syntax filter across all candidate outputs.
|
|
243
|
+
* @param {Array<object>} candidates
|
|
244
|
+
* @param {object} [options]
|
|
245
|
+
* @returns {{ candidates: Array<object>, validCandidates: Array<object>, invalidCandidates: Array<object>, filterApplied: boolean }}
|
|
246
|
+
*/
|
|
247
|
+
export function applySyntaxFilter(candidates = [], options = {}) {
|
|
248
|
+
if (!Array.isArray(candidates) || candidates.length === 0) {
|
|
249
|
+
return { candidates: [], validCandidates: [], invalidCandidates: [], filterApplied: false }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const enabled = options.enabled !== false
|
|
253
|
+
if (!enabled) {
|
|
254
|
+
return { candidates, validCandidates: candidates, invalidCandidates: [], filterApplied: false }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const validCandidates = []
|
|
258
|
+
const invalidCandidates = []
|
|
259
|
+
|
|
260
|
+
for (const cand of candidates) {
|
|
261
|
+
const syntax = validateCandidateSyntax(cand)
|
|
262
|
+
cand.syntaxValidation = syntax
|
|
263
|
+
|
|
264
|
+
if (syntax.valid) {
|
|
265
|
+
validCandidates.push(cand)
|
|
266
|
+
} else {
|
|
267
|
+
invalidCandidates.push(cand)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// If we have at least one valid candidate and some broken candidates,
|
|
272
|
+
// disqualify broken candidates from winning
|
|
273
|
+
if (validCandidates.length > 0 && invalidCandidates.length > 0) {
|
|
274
|
+
for (const invalid of invalidCandidates) {
|
|
275
|
+
invalid.disqualified = true
|
|
276
|
+
invalid.disqualificationReason = `Failed local pre-judge syntax filter (${invalid.syntaxValidation.errors.length} error(s))`
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
candidates,
|
|
282
|
+
validCandidates: validCandidates.length > 0 ? validCandidates : candidates,
|
|
283
|
+
invalidCandidates,
|
|
284
|
+
filterApplied: true,
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Formats an advisory syntax report directive for the judge.
|
|
290
|
+
* @param {Array<object>} candidates
|
|
291
|
+
* @param {object} [options]
|
|
292
|
+
* @param {boolean} [options.blind=false]
|
|
293
|
+
* @returns {string}
|
|
294
|
+
*/
|
|
295
|
+
export function formatSyntaxDirective(candidates = [], options = {}) {
|
|
296
|
+
if (!Array.isArray(candidates) || candidates.length === 0) return ''
|
|
297
|
+
const isBlind = Boolean(options.blind)
|
|
298
|
+
const lines = ['### Pre-Judge Local Syntax Gate Results (0 Tokens Spent):']
|
|
299
|
+
|
|
300
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
301
|
+
const cand = candidates[i]
|
|
302
|
+
const v = cand.syntaxValidation
|
|
303
|
+
const label = isBlind ? `Candidate ${cand.index ?? (i + 1)}` : (cand.label || `Candidate ${cand.index ?? (i + 1)}`)
|
|
304
|
+
if (!v) {
|
|
305
|
+
lines.push(`- **${label}**: Not evaluated`)
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (v.valid) {
|
|
310
|
+
lines.push(`- **${label}**: PASSED (${v.validCount}/${v.blocksCount} code block(s) syntactically valid)`)
|
|
311
|
+
} else {
|
|
312
|
+
lines.push(`- **${label}**: FAILED (${v.errors.length} syntax error(s)):`)
|
|
313
|
+
for (const err of v.errors.slice(0, 3)) {
|
|
314
|
+
lines.push(` * ${err}`)
|
|
315
|
+
}
|
|
316
|
+
if (v.errors.length > 3) {
|
|
317
|
+
lines.push(` * ...and ${v.errors.length - 3} more syntax error(s)`)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return lines.join('\n')
|
|
323
|
+
}
|
|
324
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic Cache Management Routes (#318)
|
|
3
|
+
* Exposes inspection and purge endpoints for locally cached candidate generations.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getSemanticCacheStats, clearSemanticCache } from '../pipeline/semantic-cache.js'
|
|
7
|
+
import { writeJson, readBody, assertAuthorizedRequest } from '../access.js'
|
|
8
|
+
import { isSafeWriteRequest } from '../updater.js'
|
|
9
|
+
|
|
10
|
+
export function registerCacheRoutes(ctx, { workspaceRoot }) {
|
|
11
|
+
ctx.effect(() => {
|
|
12
|
+
return ctx.webServer.register({
|
|
13
|
+
kind: 'exact',
|
|
14
|
+
path: '/dsh-moa/cache',
|
|
15
|
+
handler: async (req, res) => {
|
|
16
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
17
|
+
if (req.method === 'GET') {
|
|
18
|
+
const stats = getSemanticCacheStats({ cwd: workspaceRoot })
|
|
19
|
+
return writeJson(res, 200, { ok: true, stats })
|
|
20
|
+
}
|
|
21
|
+
if (req.method === 'POST') {
|
|
22
|
+
if (!isSafeWriteRequest(req)) {
|
|
23
|
+
return writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
|
|
24
|
+
}
|
|
25
|
+
let olderThanMs = null
|
|
26
|
+
try {
|
|
27
|
+
const raw = await readBody(req)
|
|
28
|
+
if (raw) {
|
|
29
|
+
const body = JSON.parse(raw)
|
|
30
|
+
if (typeof body.olderThanMs === 'number') olderThanMs = body.olderThanMs
|
|
31
|
+
}
|
|
32
|
+
} catch (_err) {
|
|
33
|
+
/* ignore invalid body for clear */
|
|
34
|
+
}
|
|
35
|
+
const result = clearSemanticCache({ cwd: workspaceRoot, olderThanMs })
|
|
36
|
+
return writeJson(res, 200, { ok: true, clearedCount: result.clearedCount })
|
|
37
|
+
}
|
|
38
|
+
return writeJson(res, 405, { ok: false, error: 'GET or POST required' })
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
}
|
package/lib/routes.js
CHANGED
|
@@ -7,15 +7,18 @@ import { registerPresetRoutes } from './routes/presets.js'
|
|
|
7
7
|
import { registerHistoryRoutes } from './routes/history.js'
|
|
8
8
|
import { registerWorkbenchRoutes } from './routes/workbench.js'
|
|
9
9
|
import { registerPipelineRoutes } from './routes/pipeline.js'
|
|
10
|
+
import { registerCacheRoutes } from './routes/cache.js'
|
|
10
11
|
|
|
11
12
|
export { registerPresetRoutes } from './routes/presets.js'
|
|
12
13
|
export { registerHistoryRoutes } from './routes/history.js'
|
|
13
14
|
export { registerWorkbenchRoutes } from './routes/workbench.js'
|
|
14
15
|
export { registerPipelineRoutes } from './routes/pipeline.js'
|
|
16
|
+
export { registerCacheRoutes } from './routes/cache.js'
|
|
15
17
|
|
|
16
18
|
export function registerMoaRoutes(ctx, deps) {
|
|
17
19
|
registerPresetRoutes(ctx, deps)
|
|
18
20
|
registerHistoryRoutes(ctx, deps)
|
|
19
21
|
registerWorkbenchRoutes(ctx, deps)
|
|
20
22
|
registerPipelineRoutes(ctx, deps)
|
|
23
|
+
registerCacheRoutes(ctx, deps)
|
|
21
24
|
}
|
package/lib/tui-settings.js
CHANGED
|
@@ -130,7 +130,7 @@ export function applyTuiSettings(ctx, { live }) {
|
|
|
130
130
|
const eff = typeof tuiCtx.effect === 'function' ? tuiCtx.effect : (typeof ctx?.effect === 'function' ? ctx.effect : null)
|
|
131
131
|
if (eff) {
|
|
132
132
|
eff(() => () => {
|
|
133
|
-
try { if (typeof dispose === 'function') dispose() } catch {}
|
|
133
|
+
try { if (typeof dispose === 'function') dispose() } catch (err) { /* best-effort dispose */ }
|
|
134
134
|
}, 'dsh-moa: tui settings')
|
|
135
135
|
}
|
|
136
136
|
} catch (err) {
|
|
@@ -144,3 +144,4 @@ export function applyTuiSettings(ctx, { live }) {
|
|
|
144
144
|
ctx.inject(['tuiSettingsSections'], registerWithService)
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
+
|