dsh-vibe-math 2.3.16 → 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 +64 -61
- package/README.md +64 -61
- package/SECURITY.md +56 -0
- package/cordis.patch.yml +1123 -10
- package/docs/AUDIT-CHECKLIST.md +499 -384
- package/docs/COMPAT-AUDIT-ROUND2.md +3 -0
- package/docs/COMPAT-AUDIT-ROUND3.md +139 -0
- package/docs/generate_framework_diagram_v5.mjs +27 -27
- package/docs/release-notes/RELEASE-NOTES-2.4.0.md +172 -0
- package/docs/release-notes/RELEASE-NOTES-2.4.1.md +129 -0
- package/docs/test-timing.md +2 -1
- package/installer.js +89 -21
- package/package.json +36 -10
- package/preset-declaration.js +62 -0
- package/prompt-corpus-persona/persona-corpus.json +2 -2
- package/prompt-corpus-persona/persona-corpus.md +2 -2
- package/prompt-corpus-v5/prompt-corpus-v5.json +7 -7
- package/prompt-corpus-v5/prompt-corpus-v5.md +8 -129
- package/scripts/build-preset-rows.mjs +146 -0
- package/tests/audit-installer-compat.test.mjs +9 -3
- package/tests/audit-preset-rows.test.mjs +185 -0
- package/tests/audit-v5-integrity.mjs +49 -3
- package/tests/e2e-v5-round2.test.mjs +137 -67
- package/tests/formal-verify-v5.test.mjs +43 -35
- package/tests/prompt-v5-integrity.test.mjs +49 -47
- package/tests/selfdrive-v5.mjs +42 -55
- 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 +111 -64
- package/vibe-math-v3/vibe-math-v3.js +213 -150
- 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 +257 -34
- package/vibe-math-v5/agent.cordis.yml +8 -8
- package/vibe-math-v5/preset.yml +1 -1
- package/vibe-math-v5/vibe-math-v5.js +292 -129
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +56 -44
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +24 -22
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5-en.svg +14 -14
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5.svg +14 -14
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Vibe Math V2 — host plugin implementing the
|
|
2
|
-
// ("
|
|
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() }
|
|
@@ -2594,13 +2534,17 @@ export function apply(ctx) {
|
|
|
2594
2534
|
input: { hint: '[start|resume|pause|abort|status|report|mode <auto|manual>|setup|save|template [global|project]|add <id> <desc>|add-proposition <id> <概述>|list-propositions|project [list|new <name>|<name>]|decisions|agents]' },
|
|
2595
2535
|
handler: async function (invocation) {
|
|
2596
2536
|
const s = getSession(invocation && invocation.agent)
|
|
2597
|
-
if (!s) return { kind: '
|
|
2537
|
+
if (!s) return { kind: 'error', text: JSON.stringify({ ok: false, error: 'no vibe-math session for this agent' }) }
|
|
2598
2538
|
const line = String(invocation && invocation.rawInput ? invocation.rawInput : '').trim()
|
|
2599
2539
|
const parts = line.length > 0 ? line.split(/\s+/) : []
|
|
2600
2540
|
const cmd = parts[0] || ''
|
|
2601
2541
|
const rest = parts.slice(1)
|
|
2602
2542
|
const result = await s.dispatchVibeCommand(cmd, rest)
|
|
2603
|
-
|
|
2543
|
+
// A business failure (the dispatch result's own ok:false) is a FAILED command: the host's
|
|
2544
|
+
// CommandResult union distinguishes success from error, and returning 'success' made a rejected
|
|
2545
|
+
// invocation look identical to a successful one in the UI.
|
|
2546
|
+
const failed = result !== null && typeof result === 'object' && result.ok === false
|
|
2547
|
+
return { kind: failed ? 'error' : 'success', text: JSON.stringify(result, null, 2) }
|
|
2604
2548
|
},
|
|
2605
2549
|
}))
|
|
2606
2550
|
|
|
@@ -2618,3 +2562,106 @@ export function apply(ctx) {
|
|
|
2618
2562
|
// tick timer (registered once; ticks every running session at its own pace)
|
|
2619
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) })
|
|
2620
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
|
+
}
|