dsh-vibe-math 2.4.0 → 2.4.1

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.
@@ -1,5 +1,5 @@
1
- // Vibe Math V2 — host plugin implementing the NEW architecture spec
2
- // ("新架构-vibe-math-实现方案.md"): probability-driven scheduling over a
1
+ // Vibe Math V2 — host plugin implementing the classic-architecture spec
2
+ // ("vibe-math-v2/实现方案.md"): probability-driven scheduling over a
3
3
  // problem list (qs/qs.json) and a proposition knowledge base (Propos/),
4
4
  // multi-agent independent review → debate → consensus verification, with
5
5
  // checkpoint resume, manual intervention, and file/push progress reporting.
@@ -137,16 +137,10 @@ export function apply(ctx) {
137
137
 
138
138
  // ================= helpers =================
139
139
  function textBlock(t) { return { type: 'text', text: String(t) } }
140
- function now() { return Date.now() }
141
- function uuid() { const h = '0123456789abcdef'; let s = ''; for (let i = 0; i < 36; i++) { if (i === 8 || i === 13 || i === 18 || i === 23) s += '-'; else s += h[Math.floor(Math.random() * 16)] } return s }
142
- function shortId() { const h = '0123456789abcdef'; let s = ''; for (let i = 0; i < 8; i++) s += h[Math.floor(Math.random() * 16)]; return s }
143
- function clamp01(v) { const n = Number(v); if (!Number.isFinite(n)) return 0.5; return Math.max(0, Math.min(1, n)) }
144
140
  function workspaceRoot() { try { if (rootAgent && rootAgent.session && rootAgent.session.header && rootAgent.session.header.cwd) return rootAgent.session.header.cwd } catch (e) {} const sp = sandboxPolicyOf(); if (sp && sp.workspaceRoot) return sp.workspaceRoot; return '.' }
145
141
  function vibeRoot() { return (workspaceRoot() + '/VibeMath').replace(/\\/g, '/') }
146
142
  function projectRoot(slug) { return vibeRoot() + '/Projects/' + slug }
147
143
  function frameworkRoot() { return projectRoot(currentProject) }
148
- function slugify(s) { const t = String(s == null ? '' : s).trim().toLowerCase().replace(/[^a-z0-9_\-\u4e00-\u9fa5]+/g, '-').replace(/^-+|-+$/g, ''); return t || 'project' }
149
- function safeId(s) { return String(s == null ? 'anon' : s).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 80) || 'anon' }
150
144
  let warnedNoPolicy = false
151
145
  function warnNoPolicyOnce() { if (!warnedNoPolicy) { warnedNoPolicy = true; console.error('vibe-math-v2: sandboxPolicy unavailable; writes go out with no explicit policy') } }
152
146
  // Sandbox fence for our own writes. The `resolve({})` fallback is a last resort and is
@@ -155,33 +149,6 @@ export function apply(ctx) {
155
149
  // which is not necessarily this session's workspace — a silently different fence.
156
150
  function getPolicy() { const sp = sandboxPolicyOf(); if (!sp) { warnNoPolicyOnce(); return undefined } try { if (rootAgent && rootAgent.session) return sp.resolve({ session: rootAgent.session }) } catch (e) { warnNoPolicyOnce() } try { const p = sp.resolve({}); if (!warnedNoPolicy) { warnedNoPolicy = true; console.error('vibe-math-v2: falling back to sandboxPolicy.resolve({}) — the fence root is the host-configured workspace, not necessarily this session cwd') } return p } catch (e) { warnNoPolicyOnce() } return undefined }
157
151
  function makeSignal(ms) { return AbortSignal.timeout(ms || 30000) }
158
- function blocksToText(blocks) { if (!blocks) return ''; let out = ''; for (let i = 0; i < blocks.length; i++) { const b = blocks[i]; if (b && b.type === 'text' && typeof b.text === 'string') out += b.text + '\n' } return out.trim() }
159
- function parseJson(text) {
160
- if (typeof text !== 'string') return undefined
161
- const tryObj = function (s) { try { const v = JSON.parse(s); return (v && typeof v === 'object' && !Array.isArray(v)) ? v : undefined } catch (e) { return undefined } }
162
- const fenceRe = /```(?:json)?[ \t]*([\s\S]*?)```/gi
163
- let m
164
- while ((m = fenceRe.exec(text)) !== null) { const obj = tryObj(m[1].trim()); if (obj !== undefined) return obj }
165
- const whole = tryObj(text.trim()); if (whole !== undefined) return whole
166
- let best = undefined; let bestLen = -1
167
- for (let start = 0; start < text.length; start++) {
168
- if (text[start] !== '{') continue
169
- let depth = 0, inStr = false, esc = false, end = -1
170
- for (let i = start; i < text.length; i++) {
171
- const c = text[i]
172
- if (inStr) { if (esc) esc = false; else if (c === '\\') esc = true; else if (c === '"') inStr = false; continue }
173
- if (c === '"') { inStr = true; continue }
174
- if (c === '{') depth++
175
- else if (c === '}') { depth--; if (depth === 0) { end = i; break } }
176
- }
177
- if (end === -1) continue
178
- const obj = tryObj(text.slice(start, end + 1))
179
- if (obj !== undefined && (end - start + 1) > bestLen) { best = obj; bestLen = end - start + 1 }
180
- }
181
- return best
182
- }
183
- function safeJson(v, fb) { if (v == null || v === '') return fb; try { return JSON.parse(v) } catch (e) { return fb } }
184
- function stripJsonComments(text) { let out = ''; let inStr = false; let inLine = false; let inBlock = false; let esc = false; for (let i = 0; i < text.length; i++) { const c = text[i]; const n = text[i + 1]; if (inLine) { if (c === '\n') { inLine = false; out += c } continue } if (inBlock) { if (c === '*' && n === '/') { inBlock = false; i++ } continue } if (inStr) { out += c; if (esc) esc = false; else if (c === '\\') esc = true; else if (c === '"') inStr = false; continue } if (c === '"') { inStr = true; out += c; continue } if (c === '/' && n === '/') { inLine = true; i++; continue } if (c === '/' && n === '*') { inBlock = true; i++; continue } out += c } return out }
185
152
 
186
153
  // ================= parameter schema =================
187
154
  const PARAM_SCHEMA = [
@@ -996,7 +963,6 @@ export function apply(ctx) {
996
963
  L.push('')
997
964
  await writeText('Formal/TODO.md', L.join('\n'))
998
965
  }
999
- function fmtTime(ms) { try { return new Date(Number(ms) || now()).toISOString().replace('T', ' ').slice(0, 19) } catch (e) { return '' } }
1000
966
  async function rebuildLeanLibIndexes() {
1001
967
  // 扫描**不执行**工具链:每次问"有什么可复用"就跑一遍 lean 既慢又出人意料。
1002
968
  // 每个对象的运行结果存在对象记录里,显示在 Formal/Index.md。
@@ -1187,15 +1153,6 @@ export function apply(ctx) {
1187
1153
 
1188
1154
  // progress:结构化 JSON 对象(旧数据可能是 JSON 字符串,两者兼容解析)。
1189
1155
  // 注意:必须保证返回对象含 directions 数组(晋升/判断/子问题等 progress 可能只有来源/说明等字段)。
1190
- function parseProgress(q) {
1191
- const raw = (q && q.progress) || null
1192
- let p = null
1193
- if (raw && typeof raw === 'object' && !Array.isArray(raw)) p = raw
1194
- else p = safeJson(raw, null)
1195
- if (!p || typeof p !== 'object' || Array.isArray(p)) return { directions: [], experience: '' }
1196
- if (!Array.isArray(p.directions)) p.directions = []
1197
- return p
1198
- }
1199
1156
  async function saveProgress(qid, progObj) { const qs = await getQs(); const q = qs.find(function (x) { return x.id === qid }); if (!q) return; q.progress = progObj; await writeQs(qs) }
1200
1157
 
1201
1158
  // ================= data layer: Propos =================
@@ -1322,24 +1279,7 @@ export function apply(ctx) {
1322
1279
  * own rejection message, which lists every registered global tool, so this
1323
1280
  * never guesses. Returns undefined when nothing usable remains.
1324
1281
  */
1325
- function sanitizeToolFilter(filter, known) {
1326
- if (!filter || !(known instanceof Set) || known.size === 0) return filter
1327
- const out = {}
1328
- for (const key of ['allow', 'deny']) {
1329
- const list = filter[key]
1330
- if (!Array.isArray(list)) continue
1331
- const kept = list.filter(function (n) { return known.has(String(n).trim()) })
1332
- if (kept.length > 0) out[key] = kept
1333
- }
1334
- return (out.allow || out.deny) ? out : undefined
1335
- }
1336
1282
  /** The host names the offending tools and then lists the registered ones. */
1337
- function registeredToolsFromError(message) {
1338
- const m = /known global tools:\s*([^]*)$/.exec(String(message || ''))
1339
- if (!m) return undefined
1340
- const names = m[1].split(',').map(function (s) { return s.trim() }).filter(Boolean)
1341
- return names.length > 0 ? new Set(names) : undefined
1342
- }
1343
1283
  function buildToolFilter(role) { const allow = role === 'solver' ? params.solverToolAllow : role === 'verifier' ? params.verifierToolAllow : undefined; const deny = role === 'solver' ? params.solverToolDeny : role === 'verifier' ? params.verifierToolDeny : undefined; const net = role === 'solver' ? params.solverAllowNetwork : role === 'verifier' ? params.verifierAllowNetwork : undefined; const scr = role === 'solver' ? params.solverAllowScripts : role === 'verifier' ? params.verifierAllowScripts : undefined; let a = Array.isArray(allow) ? allow.slice() : []; let d = Array.isArray(deny) ? deny.slice() : []; if (net === false) d = d.concat(NETWORK_TOOLS); else if (net === true && a.length > 0) a = a.concat(NETWORK_TOOLS); if (scr === false) d = d.concat(SCRIPT_TOOLS); else if (scr === true && a.length > 0) a = a.concat(SCRIPT_TOOLS); const f = {}; if (a.length > 0) f.allow = a; if (d.length > 0) f.deny = d; return (f.allow || f.deny) ? f : undefined }
1344
1284
  async function spawnChild(label, promptText, meta) {
1345
1285
  const request = { prompt: [textBlock(promptText)], parent: rootAgent, agentOptions: childAgentOptions() }
@@ -2622,3 +2562,106 @@ export function apply(ctx) {
2622
2562
  // tick timer (registered once; ticks every running session at its own pace)
2623
2563
  ctx.effect(() => { const t = setInterval(function () { for (const s of sessions.values()) { if (s.getRunning() && !s.tickInFlight && s.tickDue() && s.scheduler.gate === null) s.scheduleTick() } }, 1000); return () => clearInterval(t) })
2624
2564
  }
2565
+
2566
+ // ---- test seam: pure, stateless helpers --------------------------------
2567
+ // These helpers were declared inside `apply()` and are now declared at module scope, so
2568
+ // `apply()` closes over exactly the same function objects this export hands out. The audit
2569
+ // suites therefore exercise the REAL implementations by importing this module, instead of
2570
+ // extracting source text and compiling function bodies through the Function constructor
2571
+ // (dynamic code execution, rejected by the plugin-catalog security scan as
2572
+ // DANGEROUS_DYNAMIC_EXECUTION).
2573
+ //
2574
+ // Contract: no member may touch `ctx`, session state or mutable module state. Most are pure;
2575
+ // three are deliberately non-deterministic (`uuid`/`shortId` use Math.random, `fmtTime` falls back
2576
+ // to the clock) and `parseProgress` normalises the object it is handed in place (pre-existing).
2577
+ // Nothing here is used by the plugin at runtime except through `apply()`, and behaviour is
2578
+ // byte-identical to the previous in-`apply` declarations.
2579
+ export const __testHelpers = {
2580
+ uuid,
2581
+ shortId,
2582
+ clamp01,
2583
+ slugify,
2584
+ safeId,
2585
+ blocksToText,
2586
+ parseJson,
2587
+ safeJson,
2588
+ stripJsonComments,
2589
+ fmtTime,
2590
+ parseProgress,
2591
+ sanitizeToolFilter,
2592
+ registeredToolsFromError,
2593
+ }
2594
+
2595
+ function now() { return Date.now() }
2596
+
2597
+ function uuid() { const h = '0123456789abcdef'; let s = ''; for (let i = 0; i < 36; i++) { if (i === 8 || i === 13 || i === 18 || i === 23) s += '-'; else s += h[Math.floor(Math.random() * 16)] } return s }
2598
+
2599
+ function shortId() { const h = '0123456789abcdef'; let s = ''; for (let i = 0; i < 8; i++) s += h[Math.floor(Math.random() * 16)]; return s }
2600
+
2601
+ function clamp01(v) { const n = Number(v); if (!Number.isFinite(n)) return 0.5; return Math.max(0, Math.min(1, n)) }
2602
+
2603
+ function slugify(s) { const t = String(s == null ? '' : s).trim().toLowerCase().replace(/[^a-z0-9_\-\u4e00-\u9fa5]+/g, '-').replace(/^-+|-+$/g, ''); return t || 'project' }
2604
+
2605
+ function safeId(s) { return String(s == null ? 'anon' : s).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 80) || 'anon' }
2606
+
2607
+ function blocksToText(blocks) { if (!blocks) return ''; let out = ''; for (let i = 0; i < blocks.length; i++) { const b = blocks[i]; if (b && b.type === 'text' && typeof b.text === 'string') out += b.text + '\n' } return out.trim() }
2608
+
2609
+ function parseJson(text) {
2610
+ if (typeof text !== 'string') return undefined
2611
+ const tryObj = function (s) { try { const v = JSON.parse(s); return (v && typeof v === 'object' && !Array.isArray(v)) ? v : undefined } catch (e) { return undefined } }
2612
+ const fenceRe = /```(?:json)?[ \t]*([\s\S]*?)```/gi
2613
+ let m
2614
+ while ((m = fenceRe.exec(text)) !== null) { const obj = tryObj(m[1].trim()); if (obj !== undefined) return obj }
2615
+ const whole = tryObj(text.trim()); if (whole !== undefined) return whole
2616
+ let best = undefined; let bestLen = -1
2617
+ for (let start = 0; start < text.length; start++) {
2618
+ if (text[start] !== '{') continue
2619
+ let depth = 0, inStr = false, esc = false, end = -1
2620
+ for (let i = start; i < text.length; i++) {
2621
+ const c = text[i]
2622
+ if (inStr) { if (esc) esc = false; else if (c === '\\') esc = true; else if (c === '"') inStr = false; continue }
2623
+ if (c === '"') { inStr = true; continue }
2624
+ if (c === '{') depth++
2625
+ else if (c === '}') { depth--; if (depth === 0) { end = i; break } }
2626
+ }
2627
+ if (end === -1) continue
2628
+ const obj = tryObj(text.slice(start, end + 1))
2629
+ if (obj !== undefined && (end - start + 1) > bestLen) { best = obj; bestLen = end - start + 1 }
2630
+ }
2631
+ return best
2632
+ }
2633
+
2634
+ function safeJson(v, fb) { if (v == null || v === '') return fb; try { return JSON.parse(v) } catch (e) { return fb } }
2635
+
2636
+ function stripJsonComments(text) { let out = ''; let inStr = false; let inLine = false; let inBlock = false; let esc = false; for (let i = 0; i < text.length; i++) { const c = text[i]; const n = text[i + 1]; if (inLine) { if (c === '\n') { inLine = false; out += c } continue } if (inBlock) { if (c === '*' && n === '/') { inBlock = false; i++ } continue } if (inStr) { out += c; if (esc) esc = false; else if (c === '\\') esc = true; else if (c === '"') inStr = false; continue } if (c === '"') { inStr = true; out += c; continue } if (c === '/' && n === '/') { inLine = true; i++; continue } if (c === '/' && n === '*') { inBlock = true; i++; continue } out += c } return out }
2637
+
2638
+ function fmtTime(ms) { try { return new Date(Number(ms) || now()).toISOString().replace('T', ' ').slice(0, 19) } catch (e) { return '' } }
2639
+
2640
+ function parseProgress(q) {
2641
+ const raw = (q && q.progress) || null
2642
+ let p = null
2643
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) p = raw
2644
+ else p = safeJson(raw, null)
2645
+ if (!p || typeof p !== 'object' || Array.isArray(p)) return { directions: [], experience: '' }
2646
+ if (!Array.isArray(p.directions)) p.directions = []
2647
+ return p
2648
+ }
2649
+
2650
+ function sanitizeToolFilter(filter, known) {
2651
+ if (!filter || !(known instanceof Set) || known.size === 0) return filter
2652
+ const out = {}
2653
+ for (const key of ['allow', 'deny']) {
2654
+ const list = filter[key]
2655
+ if (!Array.isArray(list)) continue
2656
+ const kept = list.filter(function (n) { return known.has(String(n).trim()) })
2657
+ if (kept.length > 0) out[key] = kept
2658
+ }
2659
+ return (out.allow || out.deny) ? out : undefined
2660
+ }
2661
+
2662
+ function registeredToolsFromError(message) {
2663
+ const m = /known global tools:\s*([^]*)$/.exec(String(message || ''))
2664
+ if (!m) return undefined
2665
+ const names = m[1].split(',').map(function (s) { return s.trim() }).filter(Boolean)
2666
+ return names.length > 0 ? new Set(names) : undefined
2667
+ }
@@ -197,17 +197,10 @@ export function apply(ctx) {
197
197
 
198
198
  // ================= helpers =================
199
199
  function textBlock(t) { return { type: 'text', text: String(t) } }
200
- function now() { return Date.now() }
201
- function uuid() { const h = '0123456789abcdef'; let s = ''; for (let i = 0; i < 36; i++) { if (i === 8 || i === 13 || i === 18 || i === 23) s += '-'; else s += h[Math.floor(Math.random() * 16)] } return s }
202
- function shortId() { const h = '0123456789abcdef'; let s = ''; for (let i = 0; i < 8; i++) s += h[Math.floor(Math.random() * 16)]; return s }
203
- function clamp01(v) { const n = Number(v); if (!Number.isFinite(n)) return 0.5; return Math.max(0, Math.min(1, n)) }
204
- function fmtTime(ts) { try { return new Date(ts || now()).toISOString().replace('T', ' ').slice(0, 19) } catch (e) { return String(ts || '') } }
205
200
  function workspaceRoot() { try { if (rootAgent && rootAgent.session && rootAgent.session.header && rootAgent.session.header.cwd) return rootAgent.session.header.cwd } catch (e) {} const sp = sandboxPolicyOf(); if (sp && sp.workspaceRoot) return sp.workspaceRoot; return '.' }
206
201
  function vibeRoot() { return (workspaceRoot() + '/VibeMath').replace(/\\/g, '/') }
207
202
  function projectRoot(slug) { return vibeRoot() + '/Projects/' + slug }
208
203
  function frameworkRoot() { return projectRoot(currentProject) }
209
- function slugify(s) { const t = String(s == null ? '' : s).trim().toLowerCase().replace(/[^a-z0-9_\-\u4e00-\u9fa5]+/g, '-').replace(/^-+|-+$/g, ''); return t || 'project' }
210
- function safeId(s) { return String(s == null ? 'anon' : s).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 80) || 'anon' }
211
204
  /**
212
205
  * 把模型/用户提供的对象 id 消毒成**单个安全文件名**。v4 已有等价的 idSafe(见
213
206
  * vibe-math-v4.js),v3 此前缺这一步:saveProblem/saveProposition/saveVerified 直接
@@ -215,14 +208,6 @@ export function apply(ctx) {
215
208
  * 这里替换路径分隔符与控制字符、折叠连续连字符,并剥掉首尾的点/连字符,
216
209
  * 保证结果永不为 `.` / `..`、也不含分隔符。
217
210
  */
218
- function idSafe(s) {
219
- const t = String(s == null ? '' : s).trim()
220
- .replace(/[\\/:*?"<>|\u0000-\u001f]+/g, '-')
221
- .replace(/-{2,}/g, '-')
222
- .replace(/^[.\-]+|[.\-]+$/g, '')
223
- .slice(0, 80)
224
- return t || 'id'
225
- }
226
211
  let warnedNoPolicy = false
227
212
  function warnNoPolicyOnce() { if (!warnedNoPolicy) { warnedNoPolicy = true; console.error('vibe-math-v3: sandboxPolicy unavailable; writes go out with no explicit policy') } }
228
213
  // Sandbox fence for our own writes. The `resolve({})` fallback is a last resort and is
@@ -231,33 +216,6 @@ export function apply(ctx) {
231
216
  // which is not necessarily this session's workspace — a silently different fence.
232
217
  function getPolicy() { const sp = sandboxPolicyOf(); if (!sp) { warnNoPolicyOnce(); return undefined } try { if (rootAgent && rootAgent.session) return sp.resolve({ session: rootAgent.session }) } catch (e) { warnNoPolicyOnce() } try { const p = sp.resolve({}); if (!warnedNoPolicy) { warnedNoPolicy = true; console.error('vibe-math-v3: falling back to sandboxPolicy.resolve({}) — the fence root is the host-configured workspace, not necessarily this session cwd') } return p } catch (e) { warnNoPolicyOnce() } return undefined }
233
218
  function makeSignal(ms) { return AbortSignal.timeout(ms || 30000) }
234
- function blocksToText(blocks) { if (!blocks) return ''; let out = ''; for (let i = 0; i < blocks.length; i++) { const b = blocks[i]; if (b && b.type === 'text' && typeof b.text === 'string') out += b.text + '\n' } return out.trim() }
235
- function parseJson(text) {
236
- if (typeof text !== 'string') return undefined
237
- const tryObj = function (s) { try { const v = JSON.parse(s); return (v && typeof v === 'object' && !Array.isArray(v)) ? v : undefined } catch (e) { return undefined } }
238
- const fenceRe = /```(?:json)?[ \t]*([\s\S]*?)```/gi
239
- let m
240
- while ((m = fenceRe.exec(text)) !== null) { const obj = tryObj(m[1].trim()); if (obj !== undefined) return obj }
241
- const whole = tryObj(text.trim()); if (whole !== undefined) return whole
242
- let best = undefined; let bestLen = -1
243
- for (let start = 0; start < text.length; start++) {
244
- if (text[start] !== '{') continue
245
- let depth = 0, inStr = false, esc = false, end = -1
246
- for (let i = start; i < text.length; i++) {
247
- const c = text[i]
248
- if (inStr) { if (esc) esc = false; else if (c === '\\') esc = true; else if (c === '"') inStr = false; continue }
249
- if (c === '"') { inStr = true; continue }
250
- if (c === '{') depth++
251
- else if (c === '}') { depth--; if (depth === 0) { end = i; break } }
252
- }
253
- if (end === -1) continue
254
- const obj = tryObj(text.slice(start, end + 1))
255
- if (obj !== undefined && (end - start + 1) > bestLen) { best = obj; bestLen = end - start + 1 }
256
- }
257
- return best
258
- }
259
- function safeJson(v, fb) { if (v == null || v === '') return fb; try { return JSON.parse(v) } catch (e) { return fb } }
260
- function stripJsonComments(text) { let out = ''; let inStr = false; let inLine = false; let inBlock = false; let esc = false; for (let i = 0; i < text.length; i++) { const c = text[i]; const n = text[i + 1]; if (inLine) { if (c === '\n') { inLine = false; out += c } continue } if (inBlock) { if (c === '*' && n === '/') { inBlock = false; i++ } continue } if (inStr) { out += c; if (esc) esc = false; else if (c === '\\') esc = true; else if (c === '"') inStr = false; continue } if (c === '"') { inStr = true; out += c; continue } if (c === '/' && n === '/') { inLine = true; i++; continue } if (c === '/' && n === '*') { inBlock = true; i++; continue } out += c } return out }
261
219
 
262
220
  // ================= parameter schema =================
263
221
  const PARAM_SCHEMA = [
@@ -466,75 +424,14 @@ export function apply(ctx) {
466
424
  // ================= md soft-spec helpers =================
467
425
  // 软规范:对象 md 头部锚点行(唯一强制部分)+ 正文自由叙述。调度器只解析
468
426
  // 头部锚点与条目标题行(### 解法/证明/证伪 N|标题|概率X|状态Y),从不解析正文散文。
469
- function splitHeader(text) {
470
- const idx = String(text).search(/\n## /)
471
- const head = idx === -1 ? String(text) : String(text).slice(0, idx)
472
- const body = idx === -1 ? '' : String(text).slice(idx + 1)
473
- return { head: head, body: body }
474
- }
475
- function parseAnchors(head) {
476
- const anchors = {}
477
- const re = /^-\s*([A-Za-z\u4e00-\u9fa5/]+)\s*:\s*(.*)$/gm
478
- let m
479
- while ((m = re.exec(head)) !== null) anchors[m[1].trim()] = m[2].trim()
480
- return anchors
481
- }
482
427
  function anchorLine(k, v) { return '- ' + k + ': ' + String(v == null ? '' : v) }
483
428
  // 解析条目标题行 + 其后正文,直到下一个 ### / ## 标题。返回 [{heading, text}]
484
- function parseEntries(body, kindRe) {
485
- const out = []
486
- const lines = String(body).split('\n')
487
- let cur = null
488
- for (let i = 0; i < lines.length; i++) {
489
- const line = lines[i]
490
- const m = kindRe.exec(line)
491
- if (m) {
492
- if (cur) out.push(cur)
493
- // 约定:m[1]=标题段,m[2]=概率段,m[3]=状态段(应用记录正则只有 m[1])
494
- cur = { heading: line.trim(), title: (m[1] || '').trim(), prob: m[2] !== undefined ? Number(m[2]) : undefined, status: (m[3] || '').trim(), text: [] }
495
- continue
496
- }
497
- if (/^#{2,3}\s/.test(line)) {
498
- if (cur) { out.push(cur); cur = null }
499
- continue
500
- }
501
- if (cur) cur.text.push(line)
502
- }
503
- if (cur) out.push(cur)
504
- for (const e of out) e.text = e.text.join('\n').trim()
505
- return out
506
- }
507
- function section(body, name) {
508
- const re = new RegExp('^##\\s+' + name + '\\s*$', 'm')
509
- const m = re.exec(String(body))
510
- if (!m) return ''
511
- const rest = String(body).slice(m.index + m[0].length)
512
- const end = rest.search(/\n##\s/)
513
- return (end === -1 ? rest : rest.slice(0, end)).trim()
514
- }
515
429
  /**
516
430
  * 按顺序切出正文里的所有 `## ` 顶层段。用于"无损往返":compose 只重新生成自己管理的
517
431
  * 那几段,其余段(例如 经验与教训、或代理自加的任何段)必须原样保留,否则每次调度器
518
432
  * 回写都会把代理写进卡片的内容抹掉(违反 实现方案.md「正文只追加,不覆盖」)。
519
433
  */
520
- function parseBodySections(body) {
521
- const text = String(body || '')
522
- const re = /^##\s+(.+?)\s*$/gm
523
- const found = []
524
- let m
525
- while ((m = re.exec(text)) !== null) found.push({ name: m[1].trim(), from: m.index, bodyFrom: m.index + m[0].length })
526
- for (let i = 0; i < found.length; i++) {
527
- const to = i + 1 < found.length ? found[i + 1].from : text.length
528
- found[i].text = text.slice(found[i].bodyFrom, to).trim()
529
- }
530
- return found
531
- }
532
434
  /** 未被 compose 接管的段:原样保留,避免回写时丢失。 */
533
- function extraBodySections(body, managedNames) {
534
- return parseBodySections(body)
535
- .filter(function (s) { return !managedNames.includes(s.name) })
536
- .map(function (s) { return { name: s.name, text: s.text } })
537
- }
538
435
  /** 把保留段追加到 compose 输出末尾(无则原样返回),保证正文只增不减。 */
539
436
  function withExtraSections(lines, extras) {
540
437
  if (!Array.isArray(extras) || extras.length === 0) return lines
@@ -691,34 +588,6 @@ export function apply(ctx) {
691
588
  * 必须把 问题/方向 取回来:compose 会按本对象的字段重写该标题行,若解析时丢成空串,
692
589
  * 每次回写都会把"用在哪"永久抹掉(实现方案.md 要求记录 问题/方向)。
693
590
  */
694
- function parseAppTitle(title) {
695
- const t = String(title || '')
696
- const mQ = /问题\s*(\S+)/.exec(t)
697
- const mD = /方向\s*(\S+)/.exec(t)
698
- // `at` = 第一段(时间戳)。取第一个 '|' 之前的部分。
699
- const at = t.split('|')[0].trim()
700
- return { at: at, 问题: mQ ? mQ[1] : '', 方向: mD ? mD[1] : '' }
701
- }
702
- function parseMethodMd(id, text) {
703
- const { head, body } = splitHeader(text)
704
- const a = parseAnchors(head)
705
- const apps = parseEntries(body, /^###\s*应用\s*\d+|(.*?)$/).map(function (e) {
706
- const t = parseAppTitle(e.title)
707
- return { at: t.at, 问题: t.问题, 方向: t.方向, text: e.text }
708
- })
709
- // 改进历史必须真正解析回来:此前硬编码 [] 导致每次 compose 都写成占位符,
710
- // 于是"下一次用到该方法"就把代理沉淀的改进历史静默销毁(违反「正文只追加,不覆盖」)。
711
- const improvements = parseEntries(body, /^###\s*v(\d+)\s*((.*?))\s*$/).map(function (e) {
712
- return { v: Number(e.title) || 0, 原因: e.status || '', text: e.text }
713
- })
714
- return {
715
- id: id, 标题: a['标题'] || id, 类型: a['类型'] || '方法', 状态: a['状态'] || '经验',
716
- 可信断言: safeJson(a['可信断言'], []), 上级体系: safeJson(a['上级体系'], []), 子方法: safeJson(a['子方法'], []), 相关: safeJson(a['相关'], []),
717
- 适用场景: a['适用场景'] || '', 核心内容: section(body, '核心内容'), 定义与记号: section(body, '定义与记号'),
718
- applications: apps, improvements: improvements, 来源: a['来源'] || '',
719
- extraSections: extraBodySections(body, ['核心内容', '定义与记号', '应用记录', '改进历史']),
720
- }
721
- }
722
591
 
723
592
  // ---- compose: Verified card ----
724
593
  function composeVerifiedMd(card) {
@@ -1096,24 +965,7 @@ export function apply(ctx) {
1096
965
  * own rejection message, which lists every registered global tool, so this
1097
966
  * never guesses. Returns undefined when nothing usable remains.
1098
967
  */
1099
- function sanitizeToolFilter(filter, known) {
1100
- if (!filter || !(known instanceof Set) || known.size === 0) return filter
1101
- const out = {}
1102
- for (const key of ['allow', 'deny']) {
1103
- const list = filter[key]
1104
- if (!Array.isArray(list)) continue
1105
- const kept = list.filter(function (n) { return known.has(String(n).trim()) })
1106
- if (kept.length > 0) out[key] = kept
1107
- }
1108
- return (out.allow || out.deny) ? out : undefined
1109
- }
1110
968
  /** The host names the offending tools and then lists the registered ones. */
1111
- function registeredToolsFromError(message) {
1112
- const m = /known global tools:\s*([^]*)$/.exec(String(message || ''))
1113
- if (!m) return undefined
1114
- const names = m[1].split(',').map(function (s) { return s.trim() }).filter(Boolean)
1115
- return names.length > 0 ? new Set(names) : undefined
1116
- }
1117
969
  function buildToolFilter(role) {
1118
970
  const allow = role === 'solver' ? params.solverToolAllow : role === 'verifier' ? params.verifierToolAllow : undefined
1119
971
  const deny = role === 'solver' ? params.solverToolDeny : role === 'verifier' ? params.verifierToolDeny : undefined
@@ -3799,3 +3651,210 @@ export function apply(ctx) {
3799
3651
  // tick timer (registered once; ticks every running session at its own pace)
3800
3652
  ctx.effect(() => { const t = setInterval(function () { for (const s of sessions.values()) { if (s.getRunning() && !s.tickInFlight && s.tickDue() && s.scheduler.gate === null) s.scheduleTick() } }, 1000); return () => clearInterval(t) })
3801
3653
  }
3654
+
3655
+ // ---- test seam: pure, stateless helpers --------------------------------
3656
+ // These helpers were declared inside `apply()` and are now declared at module scope, so
3657
+ // `apply()` closes over exactly the same function objects this export hands out. The audit
3658
+ // suites therefore exercise the REAL implementations by importing this module, instead of
3659
+ // extracting source text and compiling function bodies through the Function constructor
3660
+ // (dynamic code execution, rejected by the plugin-catalog security scan as
3661
+ // DANGEROUS_DYNAMIC_EXECUTION).
3662
+ //
3663
+ // Contract: no member may touch `ctx`, session state or mutable module state. Most are pure;
3664
+ // three are deliberately non-deterministic (`uuid`/`shortId` use Math.random, `fmtTime` falls back
3665
+ // to the clock) and `parseProgress` normalises the object it is handed in place (pre-existing).
3666
+ // Nothing here is used by the plugin at runtime except through `apply()`, and behaviour is
3667
+ // byte-identical to the previous in-`apply` declarations.
3668
+ export const __testHelpers = {
3669
+ uuid,
3670
+ shortId,
3671
+ clamp01,
3672
+ fmtTime,
3673
+ slugify,
3674
+ safeId,
3675
+ idSafe,
3676
+ blocksToText,
3677
+ parseJson,
3678
+ safeJson,
3679
+ stripJsonComments,
3680
+ splitHeader,
3681
+ parseAnchors,
3682
+ parseEntries,
3683
+ section,
3684
+ parseBodySections,
3685
+ extraBodySections,
3686
+ parseAppTitle,
3687
+ parseMethodMd,
3688
+ sanitizeToolFilter,
3689
+ registeredToolsFromError,
3690
+ }
3691
+
3692
+ function now() { return Date.now() }
3693
+
3694
+ function uuid() { const h = '0123456789abcdef'; let s = ''; for (let i = 0; i < 36; i++) { if (i === 8 || i === 13 || i === 18 || i === 23) s += '-'; else s += h[Math.floor(Math.random() * 16)] } return s }
3695
+
3696
+ function shortId() { const h = '0123456789abcdef'; let s = ''; for (let i = 0; i < 8; i++) s += h[Math.floor(Math.random() * 16)]; return s }
3697
+
3698
+ function clamp01(v) { const n = Number(v); if (!Number.isFinite(n)) return 0.5; return Math.max(0, Math.min(1, n)) }
3699
+
3700
+ function fmtTime(ts) { try { return new Date(ts || now()).toISOString().replace('T', ' ').slice(0, 19) } catch (e) { return String(ts || '') } }
3701
+
3702
+ function slugify(s) { const t = String(s == null ? '' : s).trim().toLowerCase().replace(/[^a-z0-9_\-\u4e00-\u9fa5]+/g, '-').replace(/^-+|-+$/g, ''); return t || 'project' }
3703
+
3704
+ function safeId(s) { return String(s == null ? 'anon' : s).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 80) || 'anon' }
3705
+
3706
+ function idSafe(s) {
3707
+ const t = String(s == null ? '' : s).trim()
3708
+ .replace(/[\\/:*?"<>|\u0000-\u001f]+/g, '-')
3709
+ .replace(/-{2,}/g, '-')
3710
+ .replace(/^[.\-]+|[.\-]+$/g, '')
3711
+ .slice(0, 80)
3712
+ return t || 'id'
3713
+ }
3714
+
3715
+ function blocksToText(blocks) { if (!blocks) return ''; let out = ''; for (let i = 0; i < blocks.length; i++) { const b = blocks[i]; if (b && b.type === 'text' && typeof b.text === 'string') out += b.text + '\n' } return out.trim() }
3716
+
3717
+ function parseJson(text) {
3718
+ if (typeof text !== 'string') return undefined
3719
+ const tryObj = function (s) { try { const v = JSON.parse(s); return (v && typeof v === 'object' && !Array.isArray(v)) ? v : undefined } catch (e) { return undefined } }
3720
+ const fenceRe = /```(?:json)?[ \t]*([\s\S]*?)```/gi
3721
+ let m
3722
+ while ((m = fenceRe.exec(text)) !== null) { const obj = tryObj(m[1].trim()); if (obj !== undefined) return obj }
3723
+ const whole = tryObj(text.trim()); if (whole !== undefined) return whole
3724
+ let best = undefined; let bestLen = -1
3725
+ for (let start = 0; start < text.length; start++) {
3726
+ if (text[start] !== '{') continue
3727
+ let depth = 0, inStr = false, esc = false, end = -1
3728
+ for (let i = start; i < text.length; i++) {
3729
+ const c = text[i]
3730
+ if (inStr) { if (esc) esc = false; else if (c === '\\') esc = true; else if (c === '"') inStr = false; continue }
3731
+ if (c === '"') { inStr = true; continue }
3732
+ if (c === '{') depth++
3733
+ else if (c === '}') { depth--; if (depth === 0) { end = i; break } }
3734
+ }
3735
+ if (end === -1) continue
3736
+ const obj = tryObj(text.slice(start, end + 1))
3737
+ if (obj !== undefined && (end - start + 1) > bestLen) { best = obj; bestLen = end - start + 1 }
3738
+ }
3739
+ return best
3740
+ }
3741
+
3742
+ function safeJson(v, fb) { if (v == null || v === '') return fb; try { return JSON.parse(v) } catch (e) { return fb } }
3743
+
3744
+ function stripJsonComments(text) { let out = ''; let inStr = false; let inLine = false; let inBlock = false; let esc = false; for (let i = 0; i < text.length; i++) { const c = text[i]; const n = text[i + 1]; if (inLine) { if (c === '\n') { inLine = false; out += c } continue } if (inBlock) { if (c === '*' && n === '/') { inBlock = false; i++ } continue } if (inStr) { out += c; if (esc) esc = false; else if (c === '\\') esc = true; else if (c === '"') inStr = false; continue } if (c === '"') { inStr = true; out += c; continue } if (c === '/' && n === '/') { inLine = true; i++; continue } if (c === '/' && n === '*') { inBlock = true; i++; continue } out += c } return out }
3745
+
3746
+ function splitHeader(text) {
3747
+ const idx = String(text).search(/\n## /)
3748
+ const head = idx === -1 ? String(text) : String(text).slice(0, idx)
3749
+ const body = idx === -1 ? '' : String(text).slice(idx + 1)
3750
+ return { head: head, body: body }
3751
+ }
3752
+
3753
+ function parseAnchors(head) {
3754
+ const anchors = {}
3755
+ const re = /^-\s*([A-Za-z\u4e00-\u9fa5/]+)\s*:\s*(.*)$/gm
3756
+ let m
3757
+ while ((m = re.exec(head)) !== null) anchors[m[1].trim()] = m[2].trim()
3758
+ return anchors
3759
+ }
3760
+
3761
+ function parseEntries(body, kindRe) {
3762
+ const out = []
3763
+ const lines = String(body).split('\n')
3764
+ let cur = null
3765
+ for (let i = 0; i < lines.length; i++) {
3766
+ const line = lines[i]
3767
+ const m = kindRe.exec(line)
3768
+ if (m) {
3769
+ if (cur) out.push(cur)
3770
+ // 约定:m[1]=标题段,m[2]=概率段,m[3]=状态段(应用记录正则只有 m[1])
3771
+ cur = { heading: line.trim(), title: (m[1] || '').trim(), prob: m[2] !== undefined ? Number(m[2]) : undefined, status: (m[3] || '').trim(), text: [] }
3772
+ continue
3773
+ }
3774
+ if (/^#{2,3}\s/.test(line)) {
3775
+ if (cur) { out.push(cur); cur = null }
3776
+ continue
3777
+ }
3778
+ if (cur) cur.text.push(line)
3779
+ }
3780
+ if (cur) out.push(cur)
3781
+ for (const e of out) e.text = e.text.join('\n').trim()
3782
+ return out
3783
+ }
3784
+
3785
+ function section(body, name) {
3786
+ const re = new RegExp('^##\\s+' + name + '\\s*$', 'm')
3787
+ const m = re.exec(String(body))
3788
+ if (!m) return ''
3789
+ const rest = String(body).slice(m.index + m[0].length)
3790
+ const end = rest.search(/\n##\s/)
3791
+ return (end === -1 ? rest : rest.slice(0, end)).trim()
3792
+ }
3793
+
3794
+ function parseBodySections(body) {
3795
+ const text = String(body || '')
3796
+ const re = /^##\s+(.+?)\s*$/gm
3797
+ const found = []
3798
+ let m
3799
+ while ((m = re.exec(text)) !== null) found.push({ name: m[1].trim(), from: m.index, bodyFrom: m.index + m[0].length })
3800
+ for (let i = 0; i < found.length; i++) {
3801
+ const to = i + 1 < found.length ? found[i + 1].from : text.length
3802
+ found[i].text = text.slice(found[i].bodyFrom, to).trim()
3803
+ }
3804
+ return found
3805
+ }
3806
+
3807
+ function extraBodySections(body, managedNames) {
3808
+ return parseBodySections(body)
3809
+ .filter(function (s) { return !managedNames.includes(s.name) })
3810
+ .map(function (s) { return { name: s.name, text: s.text } })
3811
+ }
3812
+
3813
+ function parseAppTitle(title) {
3814
+ const t = String(title || '')
3815
+ const mQ = /问题\s*(\S+)/.exec(t)
3816
+ const mD = /方向\s*(\S+)/.exec(t)
3817
+ // `at` = 第一段(时间戳)。取第一个 '|' 之前的部分。
3818
+ const at = t.split('|')[0].trim()
3819
+ return { at: at, 问题: mQ ? mQ[1] : '', 方向: mD ? mD[1] : '' }
3820
+ }
3821
+
3822
+ function parseMethodMd(id, text) {
3823
+ const { head, body } = splitHeader(text)
3824
+ const a = parseAnchors(head)
3825
+ const apps = parseEntries(body, /^###\s*应用\s*\d+|(.*?)$/).map(function (e) {
3826
+ const t = parseAppTitle(e.title)
3827
+ return { at: t.at, 问题: t.问题, 方向: t.方向, text: e.text }
3828
+ })
3829
+ // 改进历史必须真正解析回来:此前硬编码 [] 导致每次 compose 都写成占位符,
3830
+ // 于是"下一次用到该方法"就把代理沉淀的改进历史静默销毁(违反「正文只追加,不覆盖」)。
3831
+ const improvements = parseEntries(body, /^###\s*v(\d+)\s*((.*?))\s*$/).map(function (e) {
3832
+ return { v: Number(e.title) || 0, 原因: e.status || '', text: e.text }
3833
+ })
3834
+ return {
3835
+ id: id, 标题: a['标题'] || id, 类型: a['类型'] || '方法', 状态: a['状态'] || '经验',
3836
+ 可信断言: safeJson(a['可信断言'], []), 上级体系: safeJson(a['上级体系'], []), 子方法: safeJson(a['子方法'], []), 相关: safeJson(a['相关'], []),
3837
+ 适用场景: a['适用场景'] || '', 核心内容: section(body, '核心内容'), 定义与记号: section(body, '定义与记号'),
3838
+ applications: apps, improvements: improvements, 来源: a['来源'] || '',
3839
+ extraSections: extraBodySections(body, ['核心内容', '定义与记号', '应用记录', '改进历史']),
3840
+ }
3841
+ }
3842
+
3843
+ function sanitizeToolFilter(filter, known) {
3844
+ if (!filter || !(known instanceof Set) || known.size === 0) return filter
3845
+ const out = {}
3846
+ for (const key of ['allow', 'deny']) {
3847
+ const list = filter[key]
3848
+ if (!Array.isArray(list)) continue
3849
+ const kept = list.filter(function (n) { return known.has(String(n).trim()) })
3850
+ if (kept.length > 0) out[key] = kept
3851
+ }
3852
+ return (out.allow || out.deny) ? out : undefined
3853
+ }
3854
+
3855
+ function registeredToolsFromError(message) {
3856
+ const m = /known global tools:\s*([^]*)$/.exec(String(message || ''))
3857
+ if (!m) return undefined
3858
+ const names = m[1].split(',').map(function (s) { return s.trim() }).filter(Boolean)
3859
+ return names.length > 0 ? new Set(names) : undefined
3860
+ }
@@ -610,7 +610,7 @@ VibeMath/ # 工作区级
610
610
  | 4. 方法库 | 方法卡+体系层级、应用记录自动追加、`new_inventions` 上报、Method Keeper 提炼、经验条目可信约束、项目→全局晋升 | 上报→沉淀→体系化链路通;经验条目不被当定理;晋升走门 |
611
611
  | 5. 控制面 | 参数/工具/命令、人工干预门(计划审批/裁决/方法晋升)、汇报、提示词全量 | manual 门挂起与 decide 放行;push 汇报触发 |
612
612
  | 6. 多会话 | per-session 隔离 + 项目锁 + 进程纪元 | 双会话并行各跑各的项目;resume 0 误中断 |
613
- | 7. 文档与迁移 | README(v3 为主推)、可选 v2→v3 迁移工具(JSON 项目 → md 骨架) | 迁移后项目可被 v3 调度器索引与续跑 |
613
+ | 7. 文档与迁移 | README(v3 为经典)、可选 v2→v3 迁移工具(JSON 项目 → md 骨架) | 迁移后项目可被 v3 调度器索引与续跑 |
614
614
 
615
615
  ---
616
616
 
@@ -634,7 +634,7 @@ VibeMath/ # 工作区级
634
634
  3. **Verified 采用"调度器生成副本"**(Propos 保留历史,Verified 为定论只读版本),而非原位删除/移动。
635
635
  4. **规划代理输出 JSON**(机器接口),知识才用 md——调度器需要可校验的动作流,JSON 是最低成本的机器契约;md 的自由叙述留给知识对象。
636
636
  5. **软规范取折中**(锚点行强制 + 正文自由),而非完全零规范——否则调度器无法可靠索引;锚点仅 4~7 行,符合"不设较高严格规范"。
637
- 6. **v3 作为新 preset id** `vibe-math-v3` 并入现有 npm 包 `dsh-vibe-math`(与 v2/v4 同包多预设),版本发布策略沿用(README 标注 v2 与 v3 同级主推、按需选择;老的 `vibe-math-v1` 已于 v2.0.0 移除,本包仅含 v2/v3/v4)。
637
+ 6. **v3 作为新 preset id** `vibe-math-v3` 并入现有 npm 包 `dsh-vibe-math`(与 v2/v4 同包多预设),版本发布策略沿用(README 标注四者同级、v2 与 v3 为经典;老的 `vibe-math-v1` 已于 v2.0.0 移除,本包仅含 v2/v3/v4/v5)。
638
638
  7. **v2 审计发现的两个缺陷**(flat 高置信分歧误判 0.5、引用被证伪卡片)在 v3 默认修复(§8.2)。
639
639
  8. **方法库收录范围**:不限解题技巧——任何过程中发明的/经验性总结出的有价值理论体系、框架、工具、方法、思想、范式、技巧均可沉淀(§7.1);允许经验性条目(§7.4);条目可组成体系层级并跨项目持续扩充(§7.3)。
640
640