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.
- package/README.en.md +15 -15
- package/README.md +16 -16
- package/SECURITY.md +56 -0
- package/cordis.patch.yml +5 -5
- package/docs/AUDIT-CHECKLIST.md +499 -429
- package/docs/release-notes/RELEASE-NOTES-2.4.1.md +129 -0
- package/package.json +5 -3
- package/prompt-corpus-persona/persona-corpus.json +2 -2
- package/prompt-corpus-persona/persona-corpus.md +2 -2
- package/vibe-math-v2/agent.cordis.yml +5 -5
- package/vibe-math-v2/preset.yml +1 -1
- package/vibe-math-v2/vibe-math-v2.js +105 -62
- package/vibe-math-v3/vibe-math-v3.js +207 -148
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +2 -2
- package/vibe-math-v4/vibe-math-v4.js +83 -38
- package/vibe-math-v5/vibe-math-v5.js +94 -55
|
@@ -109,17 +109,9 @@ export function apply(ctx) {
|
|
|
109
109
|
const activityLogCap = 200
|
|
110
110
|
|
|
111
111
|
// ---- utils ----
|
|
112
|
-
function now(){ return Date.now() }
|
|
113
|
-
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 }
|
|
114
|
-
function shortId(){ const h='0123456789abcdef'; let s=''; for(let i=0;i<8;i++) s+=h[Math.floor(Math.random()*16)]; return s }
|
|
115
|
-
function clamp01(v){ const n=Number(v); if(!Number.isFinite(n)) return 0.5; return Math.max(0,Math.min(1,n)) }
|
|
116
|
-
function fmtTime(ts){ try { return new Date(ts||now()).toISOString().replace('T',' ').slice(0,19) } catch(e){ return String(ts||'') } }
|
|
117
|
-
function cl(x){ return clamp01(Number(x)) }
|
|
118
112
|
// contextPct is a PERCENT (0-100); never clamp to 0-1 or the compactThreshold
|
|
119
113
|
// comparison (e.g. 66) becomes `1.0 >= 66` and never fires.
|
|
120
|
-
function clPct(x){ const n=Number(x); if(!Number.isFinite(n)) return 0; return Math.max(0,Math.min(100,n)) }
|
|
121
114
|
function textBlock(t){ return { type:'text', text:String(t) } }
|
|
122
|
-
function blocksToText(b){ if(!b) return ''; let out=''; for(const x of b){ if(x&&x.type==='text'&&typeof x.text==='string') out+=x.text+'\n' } return out.trim() }
|
|
123
115
|
function logActivity(event,detail){ activityLog.push({at:now(),event,detail:String(detail||'')}); if(activityLog.length>activityLogCap) activityLog.shift() }
|
|
124
116
|
function logDecision(kind,detail){ decisions.push({at:now(),kind,detail:String(detail||'')}) }
|
|
125
117
|
// Record that the project made real progress (new artifact, meeting, verify, task, or a
|
|
@@ -135,7 +127,6 @@ export function apply(ctx) {
|
|
|
135
127
|
// (misconfigured via vibe_v4_set) would otherwise make recoverStallMs negative → every meeting/
|
|
136
128
|
// verify watchdog fires INSTANTLY (abandoning all consensus) and A-fill's idle window would never
|
|
137
129
|
// elapse (waking everyone every pass). Guard every duration read with this.
|
|
138
|
-
function posMs(v,def){ const n=Number(v); return (Number.isFinite(n)&&n>0)?n:(def||120000) }
|
|
139
130
|
function recoverStallMs(){ return posMs(params.activityTimeoutMs,120000) * 2 }
|
|
140
131
|
function pickProvider(){ try { const n=subagents.list?subagents.list():[]; if(n.indexOf('spawn')!==-1) return 'spawn'; if(n.indexOf('fork')!==-1) return 'fork' } catch(e){} return 'spawn' }
|
|
141
132
|
// Per-resident model/provider inheritance: when params.provider / params.model are set,
|
|
@@ -155,16 +146,11 @@ export function apply(ctx) {
|
|
|
155
146
|
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 '.' }
|
|
156
147
|
function vibeRoot(){ return (workspaceRoot()+'/VibeMath').replace(/\\/g,'/') }
|
|
157
148
|
function frameworkRoot(){ return vibeRoot()+'/Projects/'+currentProject }
|
|
158
|
-
function slugify(s){ const t=String(s==null?'':s).trim().toLowerCase().replace(/[^a-z0-9_\-\u4e00-\u9fa5]+/g,'-').replace(/^-+|-+$/g,''); return t||'project' }
|
|
159
149
|
// Object ids (verify targets, recorded cards) become FILE NAMES and DIRECTORY PATHS
|
|
160
150
|
// (Verified/命题/<id>.md, Shared/debates/<id>.md, Propos/<r>/<id>.md, source-card scans).
|
|
161
151
|
// A hostile/sloppy id containing path separators ('../../x') or Windows-forbidden chars would
|
|
162
152
|
// escape the project tree. Keep every harmless character (incl. Chinese) and replace only
|
|
163
153
|
// separators/control chars; strip leading/trailing dots/dashes so the name is never '.'/'..'.
|
|
164
|
-
function idSafe(s){
|
|
165
|
-
const t=String(s==null?'':s).trim().replace(/[\\/:*?"<>|\u0000-\u001f]+/g,'-').replace(/-{2,}/g,'-').replace(/^[.\-]+|[.\-]+$/g,'')
|
|
166
|
-
return t||'id'
|
|
167
|
-
}
|
|
168
154
|
let warnedNoPolicy = false
|
|
169
155
|
function warnNoPolicyOnce(){ if(!warnedNoPolicy){ warnedNoPolicy=true; console.error('vibe-math-v4: sandboxPolicy unavailable; writes go out with no explicit policy') } }
|
|
170
156
|
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-v4: 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 }
|
|
@@ -273,13 +259,6 @@ export function apply(ctx) {
|
|
|
273
259
|
async function writeTextAbs(path,content){ try { const t=await fs.resolve(path); await fs.writeText(t,content,undefined,undefined,getPolicy()); return true } catch(e){ return false } }
|
|
274
260
|
async function readCurrentProject(){ try { const t=await readTextAbs(vibeRoot()+'/.current'); if(t) return String(t).trim() } catch(e){} return currentProject }
|
|
275
261
|
async function writeCurrentProject(){ try { await writeTextAbs(vibeRoot()+'/.current', currentProject) } catch(e){} }
|
|
276
|
-
function tryJson(s){ try { return JSON.parse(s) } catch(e){ return undefined } }
|
|
277
|
-
function parseReply(text){
|
|
278
|
-
let obj; const fence=/```(?:json)?[ \t]*([\s\S]*?)```/gi; let m
|
|
279
|
-
while((m=fence.exec(text))!==null){ const o=tryJson(m[1].trim()); if(o&&typeof o==='object'&&!Array.isArray(o)) obj=o }
|
|
280
|
-
if(!obj){ const w=tryJson(text.trim()); if(w&&typeof w==='object'&&!Array.isArray(w)) obj=w }
|
|
281
|
-
return obj||{}
|
|
282
|
-
}
|
|
283
262
|
|
|
284
263
|
// ================= Lean formal verification ==============================
|
|
285
264
|
// Contract: docs/formal-verification.md (shared by v2/v3/v4/v5).
|
|
@@ -1622,23 +1601,6 @@ export function apply(ctx) {
|
|
|
1622
1601
|
// fail with no operator-facing message. The host's own rejection lists every registered tool, so
|
|
1623
1602
|
// the retry below never guesses. FAIL CLOSED: if nothing survives the filter we rethrow instead of
|
|
1624
1603
|
// spawning WITHOUT one (that would grant exactly what the operator denied).
|
|
1625
|
-
function registeredToolsFromError(message){
|
|
1626
|
-
const m = /known global tools:\s*([^]*)$/.exec(String(message || ''))
|
|
1627
|
-
if(!m) return undefined
|
|
1628
|
-
const names = m[1].split(',').map(function(s){ return s.trim() }).filter(Boolean)
|
|
1629
|
-
return names.length > 0 ? new Set(names) : undefined
|
|
1630
|
-
}
|
|
1631
|
-
function sanitizeToolFilter(filter, known){
|
|
1632
|
-
if(!filter || !(known instanceof Set) || known.size === 0) return filter
|
|
1633
|
-
const out = {}
|
|
1634
|
-
for(const key of ['allow','deny']){
|
|
1635
|
-
const list = filter[key]
|
|
1636
|
-
if(!Array.isArray(list)) continue
|
|
1637
|
-
const kept = list.filter(function(n){ return known.has(String(n).trim()) })
|
|
1638
|
-
if(kept.length > 0) out[key] = kept
|
|
1639
|
-
}
|
|
1640
|
-
return (out.allow || out.deny) ? out : undefined
|
|
1641
|
-
}
|
|
1642
1604
|
async function startWithToolFilter(toolFilter, makeSpec){
|
|
1643
1605
|
try {
|
|
1644
1606
|
return await subagents.startContinuable(makeSpec(toolFilter))
|
|
@@ -2259,3 +2221,86 @@ export function apply(ctx) {
|
|
|
2259
2221
|
.finally(()=>{ if(s.forgetAgent) s.forgetAgent(info.id) })
|
|
2260
2222
|
})
|
|
2261
2223
|
}
|
|
2224
|
+
|
|
2225
|
+
// ---- test seam: pure, stateless helpers --------------------------------
|
|
2226
|
+
// These helpers were declared inside `apply()` and are now declared at module scope, so
|
|
2227
|
+
// `apply()` closes over exactly the same function objects this export hands out. The audit
|
|
2228
|
+
// suites therefore exercise the REAL implementations by importing this module, instead of
|
|
2229
|
+
// extracting source text and compiling function bodies through the Function constructor
|
|
2230
|
+
// (dynamic code execution, rejected by the plugin-catalog security scan as
|
|
2231
|
+
// DANGEROUS_DYNAMIC_EXECUTION).
|
|
2232
|
+
//
|
|
2233
|
+
// Contract: no member may touch `ctx`, session state or mutable module state. Most are pure;
|
|
2234
|
+
// three are deliberately non-deterministic (`uuid`/`shortId` use Math.random, `fmtTime` falls back
|
|
2235
|
+
// to the clock) and `parseProgress` normalises the object it is handed in place (pre-existing).
|
|
2236
|
+
// Nothing here is used by the plugin at runtime except through `apply()`, and behaviour is
|
|
2237
|
+
// byte-identical to the previous in-`apply` declarations.
|
|
2238
|
+
export const __testHelpers = {
|
|
2239
|
+
uuid,
|
|
2240
|
+
shortId,
|
|
2241
|
+
clamp01,
|
|
2242
|
+
fmtTime,
|
|
2243
|
+
cl,
|
|
2244
|
+
clPct,
|
|
2245
|
+
blocksToText,
|
|
2246
|
+
posMs,
|
|
2247
|
+
slugify,
|
|
2248
|
+
idSafe,
|
|
2249
|
+
tryJson,
|
|
2250
|
+
parseReply,
|
|
2251
|
+
sanitizeToolFilter,
|
|
2252
|
+
registeredToolsFromError,
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
function now(){ return Date.now() }
|
|
2256
|
+
|
|
2257
|
+
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 }
|
|
2258
|
+
|
|
2259
|
+
function shortId(){ const h='0123456789abcdef'; let s=''; for(let i=0;i<8;i++) s+=h[Math.floor(Math.random()*16)]; return s }
|
|
2260
|
+
|
|
2261
|
+
function clamp01(v){ const n=Number(v); if(!Number.isFinite(n)) return 0.5; return Math.max(0,Math.min(1,n)) }
|
|
2262
|
+
|
|
2263
|
+
function fmtTime(ts){ try { return new Date(ts||now()).toISOString().replace('T',' ').slice(0,19) } catch(e){ return String(ts||'') } }
|
|
2264
|
+
|
|
2265
|
+
function cl(x){ return clamp01(Number(x)) }
|
|
2266
|
+
|
|
2267
|
+
function clPct(x){ const n=Number(x); if(!Number.isFinite(n)) return 0; return Math.max(0,Math.min(100,n)) }
|
|
2268
|
+
|
|
2269
|
+
function blocksToText(b){ if(!b) return ''; let out=''; for(const x of b){ if(x&&x.type==='text'&&typeof x.text==='string') out+=x.text+'\n' } return out.trim() }
|
|
2270
|
+
|
|
2271
|
+
function posMs(v,def){ const n=Number(v); return (Number.isFinite(n)&&n>0)?n:(def||120000) }
|
|
2272
|
+
|
|
2273
|
+
function slugify(s){ const t=String(s==null?'':s).trim().toLowerCase().replace(/[^a-z0-9_\-\u4e00-\u9fa5]+/g,'-').replace(/^-+|-+$/g,''); return t||'project' }
|
|
2274
|
+
|
|
2275
|
+
function idSafe(s){
|
|
2276
|
+
const t=String(s==null?'':s).trim().replace(/[\\/:*?"<>|\u0000-\u001f]+/g,'-').replace(/-{2,}/g,'-').replace(/^[.\-]+|[.\-]+$/g,'')
|
|
2277
|
+
return t||'id'
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
function tryJson(s){ try { return JSON.parse(s) } catch(e){ return undefined } }
|
|
2281
|
+
|
|
2282
|
+
function parseReply(text){
|
|
2283
|
+
let obj; const fence=/```(?:json)?[ \t]*([\s\S]*?)```/gi; let m
|
|
2284
|
+
while((m=fence.exec(text))!==null){ const o=tryJson(m[1].trim()); if(o&&typeof o==='object'&&!Array.isArray(o)) obj=o }
|
|
2285
|
+
if(!obj){ const w=tryJson(text.trim()); if(w&&typeof w==='object'&&!Array.isArray(w)) obj=w }
|
|
2286
|
+
return obj||{}
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
function sanitizeToolFilter(filter, known){
|
|
2290
|
+
if(!filter || !(known instanceof Set) || known.size === 0) return filter
|
|
2291
|
+
const out = {}
|
|
2292
|
+
for(const key of ['allow','deny']){
|
|
2293
|
+
const list = filter[key]
|
|
2294
|
+
if(!Array.isArray(list)) continue
|
|
2295
|
+
const kept = list.filter(function(n){ return known.has(String(n).trim()) })
|
|
2296
|
+
if(kept.length > 0) out[key] = kept
|
|
2297
|
+
}
|
|
2298
|
+
return (out.allow || out.deny) ? out : undefined
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
function registeredToolsFromError(message){
|
|
2302
|
+
const m = /known global tools:\s*([^]*)$/.exec(String(message || ''))
|
|
2303
|
+
if(!m) return undefined
|
|
2304
|
+
const names = m[1].split(',').map(function(s){ return s.trim() }).filter(Boolean)
|
|
2305
|
+
return names.length > 0 ? new Set(names) : undefined
|
|
2306
|
+
}
|
|
@@ -129,19 +129,13 @@ export function apply(ctx) {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
// ---- utils -------------------------------------------------------------
|
|
132
|
-
const now = () => Date.now()
|
|
133
132
|
function hex(n) { let s = ''; for (let i = 0; i < n; i++) s += '0123456789abcdef'[Math.floor(Math.random() * 16)]; return s }
|
|
134
133
|
const shortId = () => hex(8)
|
|
135
|
-
function clamp01(v) { const n = Number(v); if (!Number.isFinite(n)) return 0.5; return Math.max(0, Math.min(1, n)) }
|
|
136
134
|
// contextPct is a PERCENT (0-100); never clamp to 0-1 or the compactThreshold
|
|
137
135
|
// comparison (e.g. 66) becomes `1.0 >= 66` and never fires (v4 §17 defect).
|
|
138
|
-
function clPct(x) { const n = Number(x); if (!Number.isFinite(n)) return 0; return Math.max(0, Math.min(100, n)) }
|
|
139
136
|
// Positive duration with a safe fallback: a NEGATIVE/NaN duration parameter must
|
|
140
137
|
// never make a watchdog fire instantly or an idle window never elapse (v4 §30-T41).
|
|
141
|
-
function posMs(v, def) { const n = Number(v); return (Number.isFinite(n) && n > 0) ? n : (def || 120000) }
|
|
142
138
|
const textBlock = (t) => ({ type: 'text', text: String(t) })
|
|
143
|
-
function blocksToText(b) { if (!b) return ''; let o = ''; for (const x of b) { if (x && x.type === 'text' && typeof x.text === 'string') o += x.text + '\n' } return o }
|
|
144
|
-
function fmtTime(ts) { try { return new Date(ts || now()).toISOString().replace('T', ' ').slice(0, 19) } catch (e) { return String(ts || '') } }
|
|
145
139
|
function makeSignal(ms) { try { return AbortSignal.timeout(posMs(ms, 30000)) } catch (e) { return undefined } }
|
|
146
140
|
|
|
147
141
|
// Object ids (verify targets, card ids, member ids) become FILE NAMES and DIRECTORY
|
|
@@ -149,10 +143,6 @@ export function apply(ctx) {
|
|
|
149
143
|
// characters would escape the project tree. Keep every harmless character (incl.
|
|
150
144
|
// Chinese) and replace only separators/control chars; strip leading/trailing dots
|
|
151
145
|
// and dashes so the name is never '.' or '..' (v4 §30-T39).
|
|
152
|
-
function idSafe(s) {
|
|
153
|
-
const t = String(s == null ? '' : s).trim().replace(/[\\/:*?"<>|\u0000-\u001f]+/g, '-').replace(/-{2,}/g, '-').replace(/^[.\-]+|[.\-]+$/g, '')
|
|
154
|
-
return t
|
|
155
|
-
}
|
|
156
146
|
// Advisory write-scope normalisation, ported from DSH agent-teams: backslashes to
|
|
157
147
|
// '/', strip a leading './' and trailing '/', reject empty/absolute/drive-letter/
|
|
158
148
|
// '..'-segment scopes.
|
|
@@ -3476,30 +3466,6 @@ export function apply(ctx) {
|
|
|
3476
3466
|
}
|
|
3477
3467
|
|
|
3478
3468
|
// ---- reply parsing -----------------------------------------------------
|
|
3479
|
-
function tryJson(s) { try { return JSON.parse(s) } catch (e) { return undefined } }
|
|
3480
|
-
function parseReply(text) {
|
|
3481
|
-
let obj
|
|
3482
|
-
const fence = /```(?:json)?[ \t]*([\s\S]*?)```/gi
|
|
3483
|
-
let m
|
|
3484
|
-
while ((m = fence.exec(text)) !== null) {
|
|
3485
|
-
const o = tryJson(String(m[1]).trim())
|
|
3486
|
-
if (o && typeof o === 'object' && !Array.isArray(o)) obj = o
|
|
3487
|
-
}
|
|
3488
|
-
if (!obj) {
|
|
3489
|
-
const w = tryJson(String(text || '').trim())
|
|
3490
|
-
if (w && typeof w === 'object' && !Array.isArray(w)) obj = w
|
|
3491
|
-
}
|
|
3492
|
-
if (!obj) {
|
|
3493
|
-
// Last resort: the outermost {...} span (models sometimes wrap prose around it).
|
|
3494
|
-
const t = String(text || '')
|
|
3495
|
-
const i = t.indexOf('{'), j = t.lastIndexOf('}')
|
|
3496
|
-
if (i !== -1 && j > i) {
|
|
3497
|
-
const o = tryJson(t.slice(i, j + 1))
|
|
3498
|
-
if (o && typeof o === 'object' && !Array.isArray(o)) obj = o
|
|
3499
|
-
}
|
|
3500
|
-
}
|
|
3501
|
-
return obj || {}
|
|
3502
|
-
}
|
|
3503
3469
|
function normVerdictNumber(v) {
|
|
3504
3470
|
// verdict is a PURE 0-1 probability. Models often send a quoted number, and a
|
|
3505
3471
|
// quoted "0.9" used to fall through to a 0.5 default and be silently recorded
|
|
@@ -3704,23 +3670,6 @@ export function apply(ctx) {
|
|
|
3704
3670
|
// Same guard as v2/v3/v4: `tools.restrict` throws for an unregistered name and the throw escapes
|
|
3705
3671
|
// child creation, so a stale `vibe_v5_set{toolAllow|toolDeny|tempToolAllow|tempToolDeny}` would
|
|
3706
3672
|
// make every member spawn fail silently. The host names the registered tools in its rejection.
|
|
3707
|
-
function registeredToolsFromError(message){
|
|
3708
|
-
const m = /known global tools:\s*([^]*)$/.exec(String(message || ''))
|
|
3709
|
-
if(!m) return undefined
|
|
3710
|
-
const names = m[1].split(',').map(function(s){ return s.trim() }).filter(Boolean)
|
|
3711
|
-
return names.length > 0 ? new Set(names) : undefined
|
|
3712
|
-
}
|
|
3713
|
-
function sanitizeToolFilter(filter, known){
|
|
3714
|
-
if(!filter || !(known instanceof Set) || known.size === 0) return filter
|
|
3715
|
-
const out = {}
|
|
3716
|
-
for(const key of ['allow','deny']){
|
|
3717
|
-
const list = filter[key]
|
|
3718
|
-
if(!Array.isArray(list)) continue
|
|
3719
|
-
const kept = list.filter(function(n){ return known.has(String(n).trim()) })
|
|
3720
|
-
if(kept.length > 0) out[key] = kept
|
|
3721
|
-
}
|
|
3722
|
-
return (out.allow || out.deny) ? out : undefined
|
|
3723
|
-
}
|
|
3724
3673
|
async function startWithToolFilter(toolFilter, makeSpec){
|
|
3725
3674
|
try {
|
|
3726
3675
|
return await subagents.startContinuable(makeSpec(toolFilter))
|
|
@@ -3904,10 +3853,6 @@ export function apply(ctx) {
|
|
|
3904
3853
|
'',
|
|
3905
3854
|
].join('\n'))
|
|
3906
3855
|
}
|
|
3907
|
-
function slugify(s) {
|
|
3908
|
-
const t = String(s == null ? '' : s).trim().toLowerCase().replace(/[^a-z0-9_\-\u4e00-\u9fa5]+/g, '-').replace(/^-+|-+$/g, '')
|
|
3909
|
-
return t || ''
|
|
3910
|
-
}
|
|
3911
3856
|
async function doStart(args) {
|
|
3912
3857
|
const a = args || {}
|
|
3913
3858
|
if (running && !autoDone) return { ok: false, code: 'V5_INSTITUTE_STATE', message: 'already running' }
|
|
@@ -4486,3 +4431,97 @@ export function apply(ctx) {
|
|
|
4486
4431
|
.finally(() => s.forgetAgent(info.id))
|
|
4487
4432
|
})
|
|
4488
4433
|
}
|
|
4434
|
+
|
|
4435
|
+
const now = () => Date.now()
|
|
4436
|
+
|
|
4437
|
+
// ---- test seam: pure, stateless helpers --------------------------------
|
|
4438
|
+
// These helpers were declared inside `apply()` and are now declared at module scope, so
|
|
4439
|
+
// `apply()` closes over exactly the same function objects this export hands out. The audit
|
|
4440
|
+
// suites therefore exercise the REAL implementations by importing this module, instead of
|
|
4441
|
+
// extracting source text and compiling function bodies through the Function constructor
|
|
4442
|
+
// (dynamic code execution, rejected by the plugin-catalog security scan as
|
|
4443
|
+
// DANGEROUS_DYNAMIC_EXECUTION).
|
|
4444
|
+
//
|
|
4445
|
+
// Contract: no member may touch `ctx`, session state or mutable module state. Most are pure;
|
|
4446
|
+
// three are deliberately non-deterministic (`uuid`/`shortId` use Math.random, `fmtTime` falls back
|
|
4447
|
+
// to the clock) and `parseProgress` normalises the object it is handed in place (pre-existing).
|
|
4448
|
+
// Nothing here is used by the plugin at runtime except through `apply()`, and behaviour is
|
|
4449
|
+
// byte-identical to the previous in-`apply` declarations.
|
|
4450
|
+
export const __testHelpers = {
|
|
4451
|
+
clamp01,
|
|
4452
|
+
clPct,
|
|
4453
|
+
posMs,
|
|
4454
|
+
blocksToText,
|
|
4455
|
+
fmtTime,
|
|
4456
|
+
idSafe,
|
|
4457
|
+
slugify,
|
|
4458
|
+
tryJson,
|
|
4459
|
+
parseReply,
|
|
4460
|
+
sanitizeToolFilter,
|
|
4461
|
+
registeredToolsFromError,
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4464
|
+
function clamp01(v) { const n = Number(v); if (!Number.isFinite(n)) return 0.5; return Math.max(0, Math.min(1, n)) }
|
|
4465
|
+
|
|
4466
|
+
function clPct(x) { const n = Number(x); if (!Number.isFinite(n)) return 0; return Math.max(0, Math.min(100, n)) }
|
|
4467
|
+
|
|
4468
|
+
function posMs(v, def) { const n = Number(v); return (Number.isFinite(n) && n > 0) ? n : (def || 120000) }
|
|
4469
|
+
|
|
4470
|
+
function blocksToText(b) { if (!b) return ''; let o = ''; for (const x of b) { if (x && x.type === 'text' && typeof x.text === 'string') o += x.text + '\n' } return o }
|
|
4471
|
+
|
|
4472
|
+
function fmtTime(ts) { try { return new Date(ts || now()).toISOString().replace('T', ' ').slice(0, 19) } catch (e) { return String(ts || '') } }
|
|
4473
|
+
|
|
4474
|
+
function idSafe(s) {
|
|
4475
|
+
const t = String(s == null ? '' : s).trim().replace(/[\\/:*?"<>|\u0000-\u001f]+/g, '-').replace(/-{2,}/g, '-').replace(/^[.\-]+|[.\-]+$/g, '')
|
|
4476
|
+
return t
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
function slugify(s) {
|
|
4480
|
+
const t = String(s == null ? '' : s).trim().toLowerCase().replace(/[^a-z0-9_\-\u4e00-\u9fa5]+/g, '-').replace(/^-+|-+$/g, '')
|
|
4481
|
+
return t || ''
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4484
|
+
function tryJson(s) { try { return JSON.parse(s) } catch (e) { return undefined } }
|
|
4485
|
+
|
|
4486
|
+
function parseReply(text) {
|
|
4487
|
+
let obj
|
|
4488
|
+
const fence = /```(?:json)?[ \t]*([\s\S]*?)```/gi
|
|
4489
|
+
let m
|
|
4490
|
+
while ((m = fence.exec(text)) !== null) {
|
|
4491
|
+
const o = tryJson(String(m[1]).trim())
|
|
4492
|
+
if (o && typeof o === 'object' && !Array.isArray(o)) obj = o
|
|
4493
|
+
}
|
|
4494
|
+
if (!obj) {
|
|
4495
|
+
const w = tryJson(String(text || '').trim())
|
|
4496
|
+
if (w && typeof w === 'object' && !Array.isArray(w)) obj = w
|
|
4497
|
+
}
|
|
4498
|
+
if (!obj) {
|
|
4499
|
+
// Last resort: the outermost {...} span (models sometimes wrap prose around it).
|
|
4500
|
+
const t = String(text || '')
|
|
4501
|
+
const i = t.indexOf('{'), j = t.lastIndexOf('}')
|
|
4502
|
+
if (i !== -1 && j > i) {
|
|
4503
|
+
const o = tryJson(t.slice(i, j + 1))
|
|
4504
|
+
if (o && typeof o === 'object' && !Array.isArray(o)) obj = o
|
|
4505
|
+
}
|
|
4506
|
+
}
|
|
4507
|
+
return obj || {}
|
|
4508
|
+
}
|
|
4509
|
+
|
|
4510
|
+
function sanitizeToolFilter(filter, known){
|
|
4511
|
+
if(!filter || !(known instanceof Set) || known.size === 0) return filter
|
|
4512
|
+
const out = {}
|
|
4513
|
+
for(const key of ['allow','deny']){
|
|
4514
|
+
const list = filter[key]
|
|
4515
|
+
if(!Array.isArray(list)) continue
|
|
4516
|
+
const kept = list.filter(function(n){ return known.has(String(n).trim()) })
|
|
4517
|
+
if(kept.length > 0) out[key] = kept
|
|
4518
|
+
}
|
|
4519
|
+
return (out.allow || out.deny) ? out : undefined
|
|
4520
|
+
}
|
|
4521
|
+
|
|
4522
|
+
function registeredToolsFromError(message){
|
|
4523
|
+
const m = /known global tools:\s*([^]*)$/.exec(String(message || ''))
|
|
4524
|
+
if(!m) return undefined
|
|
4525
|
+
const names = m[1].split(',').map(function(s){ return s.trim() }).filter(Boolean)
|
|
4526
|
+
return names.length > 0 ? new Set(names) : undefined
|
|
4527
|
+
}
|