@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.
@@ -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
  }
@@ -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
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.71",
3
+ "version": "0.2.73",
4
4
  "description": "Mixture of Agents (MoA) plugin for DeepSeek Harness with /moa slash command",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",