dsh-vibe-math 2.3.1 → 2.3.2
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/AUDIT-CHECKLIST.md +45 -0
- package/README.md +16 -6
- package/RELEASE-NOTES-2.3.2.md +145 -0
- package/audit-formal-sensitivity.mjs +11 -2
- package/audit-prompt-invariants.mjs +414 -0
- package/audit-spec-traceability.mjs +173 -0
- package/docs/formal-verification.md +33 -10
- package/docs/generate_framework_diagram_v5.mjs +2 -1
- package/docs/test-timing.md +32 -10
- package/formal-verify-v2.test.mjs +242 -2
- package/formal-verify-v3.test.mjs +176 -4
- package/formal-verify-v4.test.mjs +184 -5
- package/formal-verify-v5.test.mjs +91 -4
- package/installer.js +3 -1
- package/package.json +5 -2
- package/prompt-corpus-persona/persona-corpus.json +2 -2
- package/prompt-corpus-persona/persona-corpus.md +6 -2
- package/prompt-corpus-v2/formal-verify-v2.json +134 -44
- package/prompt-corpus-v2/formal-verify-v2.md +1033 -44
- package/prompt-corpus-v3/formal-verify-v3.json +200 -128
- package/prompt-corpus-v3/formal-verify-v3.md +948 -243
- package/prompt-corpus-v4/formal-verify-v4.json +8 -3
- package/prompt-corpus-v4/formal-verify-v4.md +38 -10
- package/prompt-corpus-v5/prompt-corpus-v5.json +175 -246
- package/prompt-corpus-v5/prompt-corpus-v5.md +341 -781
- package/prompt-v5-integrity.test.mjs +136 -22
- package/run-tests.mjs +30 -11
- package/vibe-math-v2/vibe-math-v2.js +149 -35
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +53 -5
- package/vibe-math-v3/vibe-math-v3.js +88 -23
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +7 -6
- package/vibe-math-v4/vibe-math-v4.js +103 -24
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +34 -11
- package/vibe-math-v5/agent.cordis.yml +6 -2
- package/vibe-math-v5/vibe-math-v5.js +56 -10
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +40 -13
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +16 -2
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5.svg +6 -5
|
@@ -46,9 +46,40 @@ const failures = []
|
|
|
46
46
|
const assert = (c, m) => {
|
|
47
47
|
if (c) { passed++; console.log(' ok - ' + m) } else { failed++; failures.push(m); console.error(' FAIL - ' + m) }
|
|
48
48
|
}
|
|
49
|
-
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
50
49
|
const section = (t) => console.log('\n[' + t + ']')
|
|
51
50
|
|
|
51
|
+
// ---------------------------------------------------------------
|
|
52
|
+
// VIRTUAL CLOCK
|
|
53
|
+
// The institute is driven by `ctx.timeout` + `Date.now()`. On the real clock, WHICH member a timer
|
|
54
|
+
// picks (heartbeat / meeting / digest) depends on load and on millisecond deltas, so the shipped
|
|
55
|
+
// corpus recorded different members on different runs and changed on every run — which makes it
|
|
56
|
+
// undiffable and hides real drift (AUDIT-CHECKLIST §2.4). A virtual clock fires every timer in a
|
|
57
|
+
// fixed order regardless of machine speed, and makes the suite faster because `sleep(n)` advances
|
|
58
|
+
// virtual time instead of waiting n real milliseconds.
|
|
59
|
+
let vnow = 1700000000000
|
|
60
|
+
let vtimerSeq = 0
|
|
61
|
+
const vtimers = new Map()
|
|
62
|
+
Date.now = () => vnow
|
|
63
|
+
function advanceClock(ms) {
|
|
64
|
+
const target = vnow + Math.max(0, Math.round(Number(ms) || 0))
|
|
65
|
+
for (;;) {
|
|
66
|
+
let next = null
|
|
67
|
+
for (const t of vtimers.values()) {
|
|
68
|
+
if (t.at > target) continue
|
|
69
|
+
if (!next || t.at < next.at || (t.at === next.at && t.seq < next.seq)) next = t
|
|
70
|
+
}
|
|
71
|
+
if (!next) break
|
|
72
|
+
vtimers.delete(next.id)
|
|
73
|
+
vnow = Math.max(vnow, next.at)
|
|
74
|
+
try { next.cb() } catch (e) { /* a throwing timer must not stop the clock */ }
|
|
75
|
+
}
|
|
76
|
+
vnow = target
|
|
77
|
+
}
|
|
78
|
+
const realSleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
79
|
+
// Advance the virtual clock, then give the real event loop a few turns so awaited work (fs writes,
|
|
80
|
+
// handler chains) can finish before the next assertion reads the state.
|
|
81
|
+
const sleep = async (ms) => { advanceClock(ms); for (let i = 0; i < 3; i++) await realSleep(1) }
|
|
82
|
+
|
|
52
83
|
// ---------------------------------------------------------------
|
|
53
84
|
// mock host
|
|
54
85
|
// ---------------------------------------------------------------
|
|
@@ -161,7 +192,13 @@ const ctx = {
|
|
|
161
192
|
on(e, fn) { (listeners[e] = listeners[e] || []).push(fn) },
|
|
162
193
|
effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d() } },
|
|
163
194
|
logger: { info() {}, warn() {}, error() {} },
|
|
164
|
-
|
|
195
|
+
// Virtualised: the plugin's timers fire when the suite advances the clock (see the virtual-clock
|
|
196
|
+
// block above), which is what makes timer-driven prompt selection reproducible.
|
|
197
|
+
timeout(cb, ms) {
|
|
198
|
+
const id = ++vtimerSeq
|
|
199
|
+
vtimers.set(id, { id, seq: id, at: vnow + Math.max(0, Math.round(Number(ms) || 0)), cb })
|
|
200
|
+
return () => { vtimers.delete(id) }
|
|
201
|
+
},
|
|
165
202
|
tools: { register(spec) { toolRegs.push(spec); return () => {} } },
|
|
166
203
|
commands: { register() { return () => {} } },
|
|
167
204
|
sessions: { async flush() { return true } },
|
|
@@ -403,10 +440,19 @@ const scrub = (s) => {
|
|
|
403
440
|
// Timestamps are part of the prompt a member reads, but not part of what a reviewer needs:
|
|
404
441
|
// normalise them too. Otherwise the shipped corpus changes on EVERY run — its headings carry
|
|
405
442
|
// `### YYYY-MM-DD hh:mm:ss|<member>` — and its diffs stop being meaningful.
|
|
406
|
-
|
|
443
|
+
// The VibeMath ROOT gets its own token (not `<WS>/VibeMath`), so v5's corpus can be diffed
|
|
444
|
+
// side by side with v2/v3/v4's, which render the same root as `<VIBEMATH>`.
|
|
445
|
+
return t.replace(/\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(?::\d{2})?/g, '<TIME>')
|
|
446
|
+
.replace(/[A-Za-z]:\/[^\s"'`)),。;:]*?[\\/]VibeMath/g, '<VIBEMATH>')
|
|
447
|
+
.replace(/\/VibeMath/g, '<VIBEMATH>')
|
|
448
|
+
.replace(re, '<WS>')
|
|
407
449
|
}
|
|
408
450
|
// Every Lean tool mention in agent-facing text must be the REGISTERED name.
|
|
409
|
-
|
|
451
|
+
// `Verified/Lean/` is a contract PATH, not a tool name, and on a case-insensitive reading it
|
|
452
|
+
// even contains the substring "lean_"; strip that exact path before the check (the same path
|
|
453
|
+
// is written by the plugin, so the prompt builders genuinely can emit it).
|
|
454
|
+
const noBareLeanTool = (t) => !/(^|[^a-z_])lean_(run|archive|lib)/.test(
|
|
455
|
+
String(t || '').replace(/Verified\/Lean\//g, ''))
|
|
410
456
|
function record(kind, owner, prompt, persona, extra) {
|
|
411
457
|
corpus.push({
|
|
412
458
|
kind, owner,
|
|
@@ -496,20 +542,38 @@ assert(normalWakes.some(w => w.owner === 'r-2' && w.prompt.indexOf('【研究所
|
|
|
496
542
|
'r-2 is woken with its inbox containing the DM framed from r-1')
|
|
497
543
|
assert(normalWakes.filter(w => w.owner === 'r-2').every(w => w.prompt.indexOf('【研究所·私信 from r-2】') === -1),
|
|
498
544
|
'r-2 never receives the DM framed as coming from itself')
|
|
499
|
-
// (b) the heartbeat must produce a CHECKPOINT prompt
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
545
|
+
// (b) the heartbeat must produce a CHECKPOINT prompt. Captured in its OWN single-member institute:
|
|
546
|
+
// the heartbeat wakes the most idle non-busy member, so with exactly one member the pick cannot
|
|
547
|
+
// depend on real `Date.now()` deltas under load — the earlier shared-root version recorded
|
|
548
|
+
// heartbeat prompts for different members on different runs, which made the SHIPPED corpus change
|
|
549
|
+
// between runs (AUDIT-CHECKLIST §2.4: corpora must be byte-diffable).
|
|
550
|
+
const RHB = makeRoot()
|
|
551
|
+
await callTool('vibe_v5_start', { problem: '心跳语料(单成员)', researcherCount: 1, academician: false }, RHB)
|
|
552
|
+
for (const sp of spawnsFor(RHB)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
|
|
553
|
+
await settleInstitute(RHB)
|
|
554
|
+
await callTool('vibe_v5_set', { activityTimeoutMs: 80, maxParallel: 6, chatDigestMax: 1 }, RHB)
|
|
555
|
+
let hbQueueIdx = -1
|
|
556
|
+
for (let i = 0; i < 15 && hbQueueIdx === -1; i++) {
|
|
557
|
+
await sleep(120); await settle()
|
|
558
|
+
// Inspect the QUEUE (never drains it): draining answers turns and changes who is idle next.
|
|
559
|
+
hbQueueIdx = wakes.findIndex(w => w.rootId === RHB.id && /【心跳检查/.test(w.prompt))
|
|
560
|
+
}
|
|
561
|
+
assert(hbQueueIdx !== -1, 'the heartbeat produced a checkpoint prompt')
|
|
562
|
+
if (hbQueueIdx !== -1) {
|
|
563
|
+
const hbWake = wakes.splice(hbQueueIdx, 1)[0]
|
|
564
|
+
const hbMember = memberOfChild(hbWake.childId)
|
|
565
|
+
assert(hbMember === 'r-1',
|
|
566
|
+
'★ the heartbeat picked the only member (r-1) — deterministic, not timing-dependent (got ' + hbMember + ')')
|
|
567
|
+
recordAndCheck('checkpoint', hbMember, hbWake.prompt)
|
|
507
568
|
// The heartbeat body may be preceded by a delivered inbox or the core-rules recap
|
|
508
569
|
// after a real compaction, so match anywhere rather than at offset 0.
|
|
509
|
-
assert(
|
|
510
|
-
assert(new RegExp('【心跳检查 —— (院士|常驻研究员|临时工) ' +
|
|
511
|
-
|
|
570
|
+
assert(hbWake.prompt.indexOf('【心跳检查 —— ') !== -1, hbMember + "'s heartbeat prompt names its own office and id")
|
|
571
|
+
assert(new RegExp('【心跳检查 —— (院士|常驻研究员|临时工) ' + hbMember + '】').test(hbWake.prompt),
|
|
572
|
+
hbMember + "'s heartbeat header carries its own kind and id")
|
|
573
|
+
fireEnd(hbWake.childId, { progress: hbMember + ':继续推进。', solved: false, contextPct: 20 })
|
|
574
|
+
await settle()
|
|
512
575
|
}
|
|
576
|
+
await endCase(RHB)
|
|
513
577
|
await endCase(RB)
|
|
514
578
|
|
|
515
579
|
// =============== CASE 3: interaction framing ====================================
|
|
@@ -970,6 +1034,7 @@ for (const w of vwB) recordAndCheck('lean-fidelity', memberOfChild(w.childId), w
|
|
|
970
1034
|
assert(/不要投 0/.test(txt), '★ it forbids expressing a faithfulness defect as 0 (= 命题为假)')
|
|
971
1035
|
assert(/decision:'defect'/.test(txt), 'it names the defect reply channel that withdraws the proof')
|
|
972
1036
|
assert(!/偏离 → 0/.test(txt), '★ the old "any deviation → 0" instruction is gone')
|
|
1037
|
+
assert(/本档没有门禁:请务必给一个严格介于 0 与 1 之间的弃权值/.test(txt), '★ the encourage branch says there is NO gate and that the abstention is what blocks a conclusion')
|
|
973
1038
|
assert(noBareLeanTool(txt), 'no abbreviated tool name appears in the fidelity prompt')
|
|
974
1039
|
}
|
|
975
1040
|
await drainWakes(10, RL2)
|
|
@@ -994,6 +1059,7 @@ for (const w of vwR) recordAndCheck('lean-require', memberOfChild(w.childId), w.
|
|
|
994
1059
|
assert(/本模式要求/.test(txt) && /formal-required/.test(txt), 'it states the conclusion gate and its reason code')
|
|
995
1060
|
assert(/vibe_v5_lean_archive/.test(txt) && /kind='blocked'/.test(txt), 'it names the full archive tool for the blocker route')
|
|
996
1061
|
assert(/宿主无 Lean 工具链/.test(txt), 'it also says what to do when the host has no Lean toolchain')
|
|
1062
|
+
assert(/LEAN_NOT_FOUND/.test(txt) && /NO_SUBPROCESS/.test(txt), '★ it names BOTH no-toolchain codes (a service-less host must not look like an unknown failure)')
|
|
997
1063
|
assert(noBareLeanTool(txt), 'no abbreviated tool name appears in the require-mode prompt')
|
|
998
1064
|
}
|
|
999
1065
|
await drainWakes(30, R_LEANREQ)
|
|
@@ -1027,6 +1093,45 @@ for (const w of delivered.filter(d => d.rootId === R_LEANDEF.id)) recordAndCheck
|
|
|
1027
1093
|
}
|
|
1028
1094
|
await endCase(R_LEANDEF)
|
|
1029
1095
|
|
|
1096
|
+
// (f) the three Lean tools' own agent-facing strings (hint / LEAN_NOT_FOUND message). A tool
|
|
1097
|
+
// `hint` is injected text by the contract's own wording ("工具自己返回的 hint 字段同样算注入文本"),
|
|
1098
|
+
// but it reached the corpus only through the prompts that happened to embed it. Capture it
|
|
1099
|
+
// directly so a reviewer can check the failure routes without reading the plugin source.
|
|
1100
|
+
const R_LEANHINT = makeRoot()
|
|
1101
|
+
await callTool('vibe_v5_start', { problem: 'Lean 工具提示语料测试', researcherCount: 2 }, R_LEANHINT)
|
|
1102
|
+
for (const sp of spawnsFor(R_LEANHINT)) { fireEnd(sp.childId, { progress: memberOfChild(sp.childId) + ':初始见解。', solved: false, contextPct: 10 }); await settle() }
|
|
1103
|
+
await settleInstitute(R_LEANHINT)
|
|
1104
|
+
await callTool('vibe_v5_set', { maxParallel: 8, formalVerify: 'encourage' }, R_LEANHINT)
|
|
1105
|
+
{
|
|
1106
|
+
const r1 = childAgent(childOf(R_LEANHINT, 'r-1'))
|
|
1107
|
+
// Labels are OURS (they name the call that produced the string); only the VALUES are
|
|
1108
|
+
// plugin text, so the bare-tool-name check runs on the values, never on the labels.
|
|
1109
|
+
const toolTexts = []
|
|
1110
|
+
await callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-lean-hint', content: 'theorem p_lean_hint : 1 + 1 = 2 := by decide\n' }, r1)
|
|
1111
|
+
const green = await callTool('vibe_v5_lean_run', { file: 'Formal/p-lean-hint.lean' }, r1)
|
|
1112
|
+
toolTexts.push(['vibe_v5_lean_run hint (green)', String(green.hint || '')])
|
|
1113
|
+
await callTool('vibe_v5_lean_archive', { kind: 'proof', target: 'p-lean-hint-red', content: 'theorem p_lean_hint_red : 1 = 2 := by sorry\n' }, r1)
|
|
1114
|
+
const red = await callTool('vibe_v5_lean_run', { file: 'Formal/p-lean-hint-red.lean' }, r1)
|
|
1115
|
+
toolTexts.push(['vibe_v5_lean_run hint (red)', String(red.hint || '')])
|
|
1116
|
+
const missing = await callTool('vibe_v5_lean_run', { file: 'Formal/no-such-file.lean' }, r1)
|
|
1117
|
+
toolTexts.push(['vibe_v5_lean_run on a missing file', String(missing.code || '') + '|' + String(missing.message || '')])
|
|
1118
|
+
const noName = await callTool('vibe_v5_lean_archive', { kind: 'def', content: 'def x := 1\n' }, r1)
|
|
1119
|
+
toolTexts.push(['vibe_v5_lean_archive without a name', String(noName.code || '') + '|' + String(noName.message || '')])
|
|
1120
|
+
const noNote = await callTool('vibe_v5_lean_archive', { kind: 'blocked', target: 'p-lean-hint' }, r1)
|
|
1121
|
+
toolTexts.push(['vibe_v5_lean_archive blocked without a note', String(noNote.code || '') + '|' + String(noNote.message || '')])
|
|
1122
|
+
const lib = await callTool('vibe_v5_lean_lib', {}, r1)
|
|
1123
|
+
toolTexts.push(['vibe_v5_lean_lib hint', String(lib.hint || '')])
|
|
1124
|
+
const values = toolTexts.map(([, v]) => v).join('\n')
|
|
1125
|
+
assert(/vibe_v5_lean_archive/.test(values), 'the tool hints name the REGISTERED archive tool (an abbreviated lean_archive is not callable)')
|
|
1126
|
+
assert(noBareLeanTool(values), 'no tool hint or failure message uses an abbreviated lean_* name')
|
|
1127
|
+
assert(/V5_NOT_FOUND/.test(values) && /必须写明原因/.test(values), 'the failure routes are explicit (missing file / missing note)')
|
|
1128
|
+
// NOT via recordAndCheck: these strings are tool output, not a round prompt, so they carry
|
|
1129
|
+
// no [状态] block and the identity sweep does not apply to them.
|
|
1130
|
+
record('lean-tool-hint', 'r-1', toolTexts.map(([k, v]) => k + ': ' + v).join('\n'))
|
|
1131
|
+
}
|
|
1132
|
+
await drainWakes(10, R_LEANHINT)
|
|
1133
|
+
await endCase(R_LEANHINT)
|
|
1134
|
+
|
|
1030
1135
|
// =============== PART: full-corpus sweep ========================================
|
|
1031
1136
|
section('13 full-corpus sweep over every prompt ever sent')
|
|
1032
1137
|
{
|
|
@@ -1044,7 +1149,8 @@ section('13 full-corpus sweep over every prompt ever sent')
|
|
|
1044
1149
|
for (const need of ['founding', 'founding-temp', 'founding-leaderless', 'resume', 'normal', 'checkpoint',
|
|
1045
1150
|
'verify', 'verify-debate', 'meeting', 'meeting-proposal', 'inbox-dm', 'inbox-voters', 'inbox-chat',
|
|
1046
1151
|
'inbox-office', 'inbox-assign', 'inbox-nudge', 'notice', 'notice-claim', 'after-failure',
|
|
1047
|
-
'lean-work', 'lean-verify', 'lean-fidelity', 'lean-require', 'lean-after-defect'
|
|
1152
|
+
'lean-work', 'lean-verify', 'lean-fidelity', 'lean-require', 'lean-after-defect',
|
|
1153
|
+
'lean-tool-hint']) {
|
|
1048
1154
|
assert(kinds.has(need), 'the corpus contains a ' + need + ' prompt')
|
|
1049
1155
|
}
|
|
1050
1156
|
assert(corpus.every(c => c.prompt && c.prompt.length > 200), 'no captured prompt is suspiciously short')
|
|
@@ -1081,7 +1187,7 @@ md.push('')
|
|
|
1081
1187
|
md.push('由 `prompt-v5-integrity.test.mjs` 在每次运行时重写。这里保存的是**框架真正发给每个')
|
|
1082
1188
|
md.push('成员的提示词原文**,用于人工复核提示词分配、成员代号与交互内容的正确性。')
|
|
1083
1189
|
md.push('')
|
|
1084
|
-
md.push('- 生成时刻的工作区路径被替换为 `<WS
|
|
1190
|
+
md.push('- 生成时刻的工作区路径被替换为 `<WS>`,VibeMath 根被替换为 `<VIBEMATH>`(与 v2/v3/v4 的语料一致,可并排 diff),因此内容是确定性的、可 diff 的。')
|
|
1085
1191
|
md.push('- `owner` 是这条提示词**实际发给的成员**;`kind` 是提示词类型。')
|
|
1086
1192
|
md.push('- 人设(charter/persona)按成员只完整打印一次,其余条目只记录字符数。')
|
|
1087
1193
|
md.push('- 这是提示词正确性的人工复核入口:任何“成员代号/职位/在册名单/交互署名”问题')
|
|
@@ -1091,8 +1197,16 @@ const seenPersona = new Set()
|
|
|
1091
1197
|
const order = ['founding', 'founding-temp', 'founding-leaderless', 'resume', 'normal', 'checkpoint',
|
|
1092
1198
|
'verify', 'verify-debate', 'meeting', 'meeting-proposal', 'inbox-dm', 'inbox-voters', 'inbox-chat',
|
|
1093
1199
|
'inbox-office', 'inbox-assign', 'inbox-nudge', 'notice', 'notice-claim', 'after-failure',
|
|
1094
|
-
'lean-work', 'lean-verify', 'lean-fidelity']
|
|
1095
|
-
|
|
1200
|
+
'lean-work', 'lean-verify', 'lean-fidelity', 'lean-require', 'lean-after-defect', 'lean-tool-hint']
|
|
1201
|
+
// The sort must be TOTAL, not just by kind: entries of the same kind were emitted in whatever order
|
|
1202
|
+
// the asynchronous drain produced them (two meeting prompts, two members' normal rounds), so the
|
|
1203
|
+
// shipped corpus still changed between runs even after the clock was virtualised. Sorting by
|
|
1204
|
+
// (kind, owner, prompt) makes the file a pure function of the recorded SET.
|
|
1205
|
+
const sorted = corpus.slice().sort((a, b) =>
|
|
1206
|
+
(order.indexOf(a.kind) - order.indexOf(b.kind)) ||
|
|
1207
|
+
(a.owner < b.owner ? -1 : a.owner > b.owner ? 1 : 0) ||
|
|
1208
|
+
(a.prompt < b.prompt ? -1 : a.prompt > b.prompt ? 1 : 0) ||
|
|
1209
|
+
(String(a.toolFilter || '') < String(b.toolFilter || '') ? -1 : 1))
|
|
1096
1210
|
for (let i = 0; i < sorted.length; i++) {
|
|
1097
1211
|
const c = sorted[i]
|
|
1098
1212
|
md.push('---')
|
|
@@ -1119,7 +1233,7 @@ for (let i = 0; i < sorted.length; i++) {
|
|
|
1119
1233
|
md.push('')
|
|
1120
1234
|
}
|
|
1121
1235
|
const byKind = {}
|
|
1122
|
-
for (const c of
|
|
1236
|
+
for (const c of sorted) byKind[c.kind] = (byKind[c.kind] || 0) + 1
|
|
1123
1237
|
md.push('---')
|
|
1124
1238
|
md.push('')
|
|
1125
1239
|
md.push('## 统计')
|
|
@@ -1132,8 +1246,8 @@ const mdPath = join(CORPUS_DIR, 'prompt-corpus-v5.md')
|
|
|
1132
1246
|
writeFileSync(mdPath, md.join('\n'), 'utf8')
|
|
1133
1247
|
writeFileSync(join(CORPUS_DIR, 'prompt-corpus-v5.json'), JSON.stringify({
|
|
1134
1248
|
note: 'Vibe Math V5 prompt/interaction corpus — generated by prompt-v5-integrity.test.mjs. <WS> = the run workspace.',
|
|
1135
|
-
counts: byKind, total:
|
|
1136
|
-
prompts:
|
|
1249
|
+
counts: byKind, total: sorted.length,
|
|
1250
|
+
prompts: sorted.map(c => ({
|
|
1137
1251
|
kind: c.kind, owner: c.owner, sentToLabel: c.sentToLabel,
|
|
1138
1252
|
charterChars: c.persona == null ? null : c.persona.length,
|
|
1139
1253
|
charter: c.persona, toolFilter: c.toolFilter, prompt: c.prompt,
|
package/run-tests.mjs
CHANGED
|
@@ -28,7 +28,17 @@ import { fileURLToPath } from 'node:url'
|
|
|
28
28
|
|
|
29
29
|
const HERE = fileURLToPath(new URL('./', import.meta.url))
|
|
30
30
|
const argv = process.argv.slice(2)
|
|
31
|
-
|
|
31
|
+
// Accept BOTH `--only=x` and `--only x` (the help text used the space form, which a value-taking
|
|
32
|
+
// flag() did not understand — the filter silently did nothing and every suite still ran).
|
|
33
|
+
const flag = (name) => {
|
|
34
|
+
const out = []
|
|
35
|
+
for (let i = 0; i < argv.length; i++) {
|
|
36
|
+
const a = argv[i]
|
|
37
|
+
if (a === '--' + name && i + 1 < argv.length && !argv[i + 1].startsWith('--')) out.push(argv[++i])
|
|
38
|
+
else if (a.startsWith('--' + name + '=')) out.push(a.split('=').slice(1).join('='))
|
|
39
|
+
}
|
|
40
|
+
return out
|
|
41
|
+
}
|
|
32
42
|
const has = (name) => argv.includes('--' + name)
|
|
33
43
|
const only = flag('only')
|
|
34
44
|
const exclude = flag('exclude')
|
|
@@ -62,16 +72,22 @@ async function worker(id) {
|
|
|
62
72
|
const r = await runSuite(suites[i])
|
|
63
73
|
const tail = String(r.out).trim().split('\n').filter(Boolean).slice(-1)[0] || ''
|
|
64
74
|
results[i] = r
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
75
|
+
if (!asJson) {
|
|
76
|
+
const mark = r.code === 0 ? 'PASS' : 'FAIL'
|
|
77
|
+
console.log(
|
|
78
|
+
mark + ' ' + r.file.padEnd(38) +
|
|
79
|
+
' exit=' + String(r.code).padStart(3) +
|
|
80
|
+
' ' + (r.ms / 1000).toFixed(1).padStart(6) + 's' +
|
|
81
|
+
(tail ? ' ' + tail.slice(0, 78) : '')
|
|
82
|
+
)
|
|
83
|
+
if (r.code !== 0) {
|
|
84
|
+
const lines = (r.out + '\n' + r.err).split('\n').filter(Boolean)
|
|
85
|
+
for (const l of lines.slice(-15)) console.log(' ' + l)
|
|
86
|
+
}
|
|
87
|
+
} else if (r.code !== 0) {
|
|
88
|
+
// In --json mode keep stdout machine-readable: the failure detail travels in the JSON.
|
|
73
89
|
const lines = (r.out + '\n' + r.err).split('\n').filter(Boolean)
|
|
74
|
-
|
|
90
|
+
r.tailDetail = lines.slice(-15).join('\n')
|
|
75
91
|
}
|
|
76
92
|
}
|
|
77
93
|
}
|
|
@@ -86,7 +102,10 @@ if (asJson) {
|
|
|
86
102
|
console.log(JSON.stringify({
|
|
87
103
|
concurrency, wallSeconds: Number(wall.toFixed(1)), sumSeconds: Number(sum.toFixed(1)),
|
|
88
104
|
pass: results.length - bad.length, fail: bad.length,
|
|
89
|
-
suites: results.map((r) => ({
|
|
105
|
+
suites: results.map((r) => ({
|
|
106
|
+
file: r.file, exit: r.code, seconds: Number((r.ms / 1000).toFixed(1)),
|
|
107
|
+
...(r.tailDetail ? { detail: r.tailDetail } : {}),
|
|
108
|
+
})),
|
|
90
109
|
}, null, 2))
|
|
91
110
|
} else {
|
|
92
111
|
console.log('')
|