dsh-vibe-math 2.4.1 → 2.5.0
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 +14 -14
- package/README.md +15 -14
- package/cordis.patch.yml +33 -10
- package/docs/AUDIT-CHECKLIST.md +25 -6
- package/docs/COMPAT-AUDIT-ROUND3.md +16 -1
- package/docs/release-notes/RELEASE-NOTES-2.5.0.md +173 -0
- package/docs/test-timing.md +73 -40
- package/installer.js +225 -45
- package/package.json +3 -2
- package/preset-declaration.js +38 -7
- package/prompt-corpus-persona/persona-corpus.json +9 -9
- package/prompt-corpus-persona/persona-corpus.md +67 -21
- package/prompt-corpus-v2/formal-verify-v2.json +116 -92
- package/prompt-corpus-v2/formal-verify-v2.md +240 -93
- package/prompt-corpus-v3/formal-verify-v3.json +81 -81
- package/prompt-corpus-v3/formal-verify-v3.md +288 -1111
- package/prompt-corpus-v4/formal-verify-v4.json +38 -8
- package/prompt-corpus-v4/formal-verify-v4.md +157 -25
- package/prompt-corpus-v5/prompt-corpus-v5.json +17 -17
- package/prompt-corpus-v5/prompt-corpus-v5.md +32 -26
- package/tests/audit-installer-compat.test.mjs +70 -4
- package/tests/audit-installer-policy.test.mjs +229 -4
- package/tests/audit-persona-sensitivity.mjs +11 -4
- package/tests/audit-persona-surface.test.mjs +5 -1
- package/tests/audit-v5-sensitivity.mjs +47 -6
- package/tests/e2e-v5-round2.test.mjs +354 -0
- package/tests/formal-verify-v2.test.mjs +54 -2
- package/tests/formal-verify-v3.test.mjs +63 -2
- package/tests/formal-verify-v4.test.mjs +120 -4
- package/tests/run-tests.mjs +104 -23
- package/vibe-math-v2/agent.cordis.yml +6 -0
- package/vibe-math-v2/vibe-math-v2.js +503 -99
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +20 -4
- package/vibe-math-v3/agent.cordis.yml +26 -10
- package/vibe-math-v3/vibe-math-v3.js +555 -175
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +3 -3
- package/vibe-math-v4/agent.cordis.yml +26 -6
- package/vibe-math-v4/vibe-math-v4.js +404 -51
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +91 -9
- package/vibe-math-v5/agent.cordis.yml +8 -4
- package/vibe-math-v5/vibe-math-v5.js +513 -105
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +73 -40
|
@@ -72,11 +72,43 @@ const corpus = []
|
|
|
72
72
|
// same class of bug is recorded in AUDIT-CHECKLIST §2.4 as a real accident): the random plan id in
|
|
73
73
|
// the label, epoch-millisecond `at` timestamps, child ids, and the free-slot count. They are run
|
|
74
74
|
// metadata, not the text under review, so they are normalised to placeholders.
|
|
75
|
-
|
|
75
|
+
//
|
|
76
|
+
// The brief is not only run metadata, it is a LIVE SNAPSHOT of mutable scheduler state
|
|
77
|
+
// (`buildBrief()` reads `agentRegistry`, `activityLog`, the verify candidates and the free-slot
|
|
78
|
+
// count at the instant the planner is spawned). This suite drives the plugin from OUTSIDE, and the
|
|
79
|
+
// plugin's own scheduler keeps running concurrently: `vibe_math_start` fires `scheduleTick()`
|
|
80
|
+
// without awaiting it, and the plugin's 1s tick timer calls `scheduleTick()` on its own schedule, so
|
|
81
|
+
// a background tick can register the explorer (or append its activity event, or close a verify)
|
|
82
|
+
// BETWEEN the forced planner's snapshot and the next one. Two runs then legitimately disagree about
|
|
83
|
+
// `active_agents` (the explorer is in flight or it is not), about `recent_events` (the last-eight
|
|
84
|
+
// window and its order), and about every other instant-dependent field of the brief.
|
|
85
|
+
//
|
|
86
|
+
// A byte-stable corpus therefore cannot carry that snapshot's values — the disagreement is a
|
|
87
|
+
// property of the INSTANT, not of the prompt text a human must review (this is exactly the "语料
|
|
88
|
+
// 每次运行都变" accident §2.4 records). What the corpus carries is the brief's CONTRACT: the same
|
|
89
|
+
// keys in the same order, with the instant-dependent values canonically placeholdered and the
|
|
90
|
+
// stable configuration scalars kept. The prompt TEXT under review — the persona, the ACTION
|
|
91
|
+
// VOCABULARY, and the HARD RULES that name `brief.problems[].running_solver_dirs`,
|
|
92
|
+
// `brief.active_agents` and `brief.free_slots` — is untouched.
|
|
93
|
+
const SNAPSHOT_KEYS = ['problems', 'verify_candidates', 'active_agents', 'methods', 'pending_inventions', 'last_plan', 'recent_events']
|
|
94
|
+
/** Replace every instant-dependent collection of the planner's state brief with a canonical
|
|
95
|
+
* placeholder, keeping the key set, the key order and the configuration scalars. Only called on a
|
|
96
|
+
* valid-JSON brief (`buildBrief` is serialised with JSON.stringify(brief, null, 2)); anything that
|
|
97
|
+
* does not match the exact planner framing is returned unchanged, so the suite can never silently
|
|
98
|
+
* mangle an unrelated prompt. */
|
|
99
|
+
const canonBrief = (s) => {
|
|
100
|
+
const m = s.match(/(CURRENT STATE BRIEF \(JSON\):\n)(\{[\s\S]*?\n\})(\n\nACTION VOCABULARY)/)
|
|
101
|
+
if (!m) return s
|
|
102
|
+
let brief
|
|
103
|
+
try { brief = JSON.parse(m[2]) } catch (e) { return s }
|
|
104
|
+
for (const k of SNAPSHOT_KEYS) if (Object.prototype.hasOwnProperty.call(brief, k)) brief[k] = '<' + k.toUpperCase() + '>'
|
|
105
|
+
return s.slice(0, m.index) + m[1] + JSON.stringify(brief, null, 2) + m[3] + s.slice(m.index + m[0].length)
|
|
106
|
+
}
|
|
107
|
+
const scrub = (s) => canonBrief(String(s == null ? '' : s)
|
|
76
108
|
.split(VIBE).join('<VIBEMATH>')
|
|
77
109
|
.split(VIBE.replace(/\\/g, '/')).join('<VIBEMATH>')
|
|
78
110
|
.split(WS).join('<WS>')
|
|
79
|
-
.split(WS.replace(/\\/g, '/')).join('<WS>')
|
|
111
|
+
.split(WS.replace(/\\/g, '/')).join('<WS>'))
|
|
80
112
|
.replace(/plan-[0-9a-f]{8}/g, 'plan-<ID>')
|
|
81
113
|
.replace(/("at"\s*:\s*)\d{10,16}/g, '$1"<TIME>"')
|
|
82
114
|
.replace(/("childId"\s*:\s*")c\d+(")/g, '$1<CHILD>$2')
|
|
@@ -1246,6 +1278,35 @@ section('10 the captured prompt corpus is written for human review')
|
|
|
1246
1278
|
assert(volatile.length === 0, '★ no captured prompt/label keeps volatile run metadata (random plan id / epoch timestamps / child ids / slot count) — the corpus is byte-deterministic: ' + volatile.map((b) => b.label).join(','))
|
|
1247
1279
|
assert(corpus.some((c) => /"free_slots": <SLOTS>/.test(c.prompt)) && corpus.some((c) => /plan-<ID>/.test(c.label)),
|
|
1248
1280
|
'★ and the normalisation actually fired (a planner brief and its plan id were captured)')
|
|
1281
|
+
// The brief's instant-dependent snapshot is placeholdered, but its CONTRACT (key set + order) and
|
|
1282
|
+
// the stable configuration scalars stay, and the instruction text that references the brief is
|
|
1283
|
+
// untouched — so the human reviewer still sees the real planner prompt, not a blank.
|
|
1284
|
+
const briefEntries = corpus.filter((c) => /CURRENT STATE BRIEF \(JSON\):/.test(c.prompt))
|
|
1285
|
+
assert(briefEntries.length >= 3 && briefEntries.every((c) => SNAPSHOT_KEYS.every((k) => c.prompt.indexOf('"' + k + '": "<' + k.toUpperCase() + '>"') !== -1)),
|
|
1286
|
+
'★ every captured planner brief keeps all ' + SNAPSHOT_KEYS.length + ' contract keys with canonically placeholdered snapshot values (' + briefEntries.length + ' briefs)')
|
|
1287
|
+
assert(briefEntries.every((c) => /"horizon": \d+/.test(c.prompt) && /"free_slots": <SLOTS>/.test(c.prompt) && /"maxParallelThreshold": \d+/.test(c.prompt)),
|
|
1288
|
+
'★ the brief keeps its real configuration scalars (horizon / free_slots / maxParallelThreshold), not just placeholders')
|
|
1289
|
+
assert(briefEntries.every((c) => /brief\.problems\[\]\.running_solver_dirs and brief\.active_agents/.test(c.prompt) && /ACTION VOCABULARY/.test(c.prompt)),
|
|
1290
|
+
'★ and the planner INSTRUCTION text under review (ACTION VOCABULARY + the HARD RULES naming brief fields) is verbatim')
|
|
1291
|
+
{
|
|
1292
|
+
// Direct witness of the race the normaliser exists for: the two shapes a background tick can
|
|
1293
|
+
// produce (explorer registered before vs. after the forced planner read the brief, activity log
|
|
1294
|
+
// one entry longer, free slots one lower) must normalise to ONE byte-identical text. Without
|
|
1295
|
+
// this the corpus could only be checked by hoping the race fires.
|
|
1296
|
+
const briefText = (agents, events, slots, horizon) => 'PERSONA\nCURRENT STATE BRIEF (JSON):\n' + JSON.stringify({
|
|
1297
|
+
at: 1759200000000, horizon: horizon, free_slots: slots, maxParallelThreshold: 64,
|
|
1298
|
+
problems: [{ id: 'qE', 状态: '求解中', 方向数: agents.length, 活跃方向: agents.length ? ['d1'] : [], running_solver_dirs: [], 最高存活率: agents.length ? 0.7 : null, 解法数: 0 }],
|
|
1299
|
+
verify_candidates: agents.length ? [{ rId: 'r-qE', kind: 'problem', target: 'qE', prob: 0.5, priority: 1 }] : [],
|
|
1300
|
+
active_agents: agents, methods: [], pending_inventions: 0, last_plan: null, recent_events: events,
|
|
1301
|
+
}, null, 2) + '\n\nACTION VOCABULARY:\n- x'
|
|
1302
|
+
const withExplorer = briefText([{ childId: 'c7', role: 'explorer', target: 'qE', direction: '', round: '' }],
|
|
1303
|
+
[{ at: 1759200000001, event: 'start', detail: 'scheduler started' }, { at: 1759200000002, event: 'explorer', detail: 'problem qE explorer spawned (plan)' }], 63, 5)
|
|
1304
|
+
const withoutExplorer = briefText([], [{ at: 1759200000009, event: 'plan', detail: 'planner plan-abcdef12 called with 1 problem(s)' }], 64, 5)
|
|
1305
|
+
assert(scrub(withExplorer) === scrub(withoutExplorer),
|
|
1306
|
+
'★ the planner-brief normaliser maps the two racing snapshots (explorer present/absent, different activity order and free slots) to ONE byte-identical text')
|
|
1307
|
+
assert(scrub(withExplorer) !== withExplorer && /"active_agents": "<ACTIVE_AGENTS>"/.test(scrub(withExplorer)),
|
|
1308
|
+
'★ and it really rewrote the snapshot (the test above cannot pass just because nothing was normalised)')
|
|
1309
|
+
}
|
|
1249
1310
|
}
|
|
1250
1311
|
|
|
1251
1312
|
console.log('')
|
|
@@ -965,6 +965,11 @@ const K = await establish()
|
|
|
965
965
|
const scrub = (s) => String(s == null ? '' : s)
|
|
966
966
|
.split(vibe).join('<VIBEMATH>').split(vibe.replace(/\\/g, '/')).join('<VIBEMATH>')
|
|
967
967
|
.split(K.WS).join('<WS>').split(K.WS.replace(/\\/g, '/')).join('<WS>')
|
|
968
|
+
// `propose_task` mints `t-<8 hex>` from Math.random (vibe-math-v4.js:1317 + shortId :2582).
|
|
969
|
+
// The task-frame entry embeds that id, so without this the SHIPPED corpus changed on every
|
|
970
|
+
// run (AUDIT-CHECKLIST §2.4: two consecutive runs must hash identically) and `git status`
|
|
971
|
+
// always showed prompt-corpus-v4/* as modified. Normalise it like the machine paths.
|
|
972
|
+
.replace(/\bt-[0-9a-f]{8}\b/g, '<TASKID>')
|
|
968
973
|
const corpus = []
|
|
969
974
|
const add = (kind, label, prompt) => corpus.push({ kind, label, prompt: scrub(prompt) })
|
|
970
975
|
// (a) off: a TRUE no-op must be visible in the corpus, not merely asserted
|
|
@@ -1008,6 +1013,91 @@ const K = await establish()
|
|
|
1008
1013
|
// (h) the tool hints are injected text too (docs §6 hard requirement 1)
|
|
1009
1014
|
add('hint', 'lean_run hint (green)', (await K.callTool('vibe_v4_lean_run', { file: 'Formal/p-corpus-passed.lean' }, k1)).hint)
|
|
1010
1015
|
add('hint', 'lean_lib hint', (await K.callTool('vibe_v4_lean_lib', {}, k1)).hint)
|
|
1016
|
+
// (i) ── THE NON-FORMAL SURFACES (audit M6/F15) ─────────────────────────────────────────────
|
|
1017
|
+
// The corpus used to cover ONLY Lean/formal variants, so the meeting prompt — this preset's ONLY
|
|
1018
|
+
// group-chat / task-allocation / stop-vote carrier — and every message frame had NO reviewable
|
|
1019
|
+
// artefact at all: a defect in `meetingPrompt` (e.g. its stop-vote contract) could not be read by
|
|
1020
|
+
// a human reviewer. These entries close that gap. `st` is passed explicitly so the capture does
|
|
1021
|
+
// not depend on the scheduler's state, and both the meeting's two perspectives (first speaker with
|
|
1022
|
+
// no prior speech / a later speaker seeing a relayed contribution) are captured.
|
|
1023
|
+
{
|
|
1024
|
+
const st = {
|
|
1025
|
+
id: 'mt-corpus', agenda: '分工与是否需要验证', type: 'general', targetId: null, round: 0, asked: [], order: ['r-1', 'r-2'],
|
|
1026
|
+
inputs: { 'r-1': { input: '我建议先验证 p-corpus,并认领引理 A 的整理。', voteSolved: false, propose_verify: 'p-corpus', propose_task: '整理引理 A', task_desc: '把已有引理写成可复用形式', claim_task: null } },
|
|
1027
|
+
transcript: [], at: Date.now(), lastInputAt: Date.now(),
|
|
1028
|
+
}
|
|
1029
|
+
add('meeting', 'meeting/first speaker (no prior speech)', await K.prompts('meeting', 'r-2', { meeting: { agenda: st.agenda, type: st.type, inputs: {}, order: st.order } }))
|
|
1030
|
+
add('meeting', 'meeting/later speaker (others relayed)', await K.prompts('meeting', 'r-2', { meeting: st }))
|
|
1031
|
+
}
|
|
1032
|
+
// (i) ── the private-message frame ──────────────────────────────────────────────────────────────
|
|
1033
|
+
// A resident reads a message in TWO shapes, and both must be reviewable:
|
|
1034
|
+
// · `[MESSAGE from <sender>] <content>` — the frame `deliverNextMailbox` prepends when it wakes an
|
|
1035
|
+
// idle recipient with a QUEUED message (captured here from a REAL delivery, not re-typed);
|
|
1036
|
+
// · `[<sender>] <content>` — the inbox line every prompt's "New items:" block renders.
|
|
1037
|
+
// r-1 is put in flight first (a message to an IDLE resident is delivered immediately as its own
|
|
1038
|
+
// wake instead of queueing), so the second message is queued and the prompt shows the inbox line.
|
|
1039
|
+
{
|
|
1040
|
+
const before = K.followups.length
|
|
1041
|
+
await K.callTool('vibe_v4_message', { to: 'r-1', content: '先做一轮常规工作。' })
|
|
1042
|
+
for (let i = 0; i < 200 && K.followups.length === before; i++) await sleep(10)
|
|
1043
|
+
const inFlight = K.followups[K.followups.length - 1]
|
|
1044
|
+
assert(!!inFlight, 'corpus: r-1 is in flight so the next message is QUEUED (mailbox frame prerequisite)')
|
|
1045
|
+
await K.callTool('vibe_v4_message', { to: 'r-1', content: '请优先核对引理 B 的假设条件。' })
|
|
1046
|
+
add('work', 'normal + queued inbox line ([<sender>] content)', await K.prompts('normal', 'r-1'))
|
|
1047
|
+
K.fireEnd(inFlight.childId, { summary: '推进。', solved: false, input: '我对引理 B 的假设有异议:需要 n≥1。', contextPct: 20 })
|
|
1048
|
+
await sleep(60)
|
|
1049
|
+
}
|
|
1050
|
+
// the REAL delivery frames. A message that arrives while its recipient is idle is delivered as a
|
|
1051
|
+
// wake whose body is `<ordinary round>\n\n[MESSAGE from <sender>]\n<content>`, and a message that
|
|
1052
|
+
// was relayed by a teammate (`relayToGroup` posts it with the sender's own id and the `[群聊]`
|
|
1053
|
+
// prefix — the exact same `postMessage` path the human messenger uses) renders as `[群聊] …`.
|
|
1054
|
+
// Each frame is captured from a REAL delivery on its own host, so the corpus text is generated by
|
|
1055
|
+
// the code under review rather than re-typed here.
|
|
1056
|
+
const deliveredFrame = async (content, sender) => {
|
|
1057
|
+
const L = await establish()
|
|
1058
|
+
await L.callTool('vibe_v4_set', { activityTimeoutMs: 600000 }) // keep the heartbeat out of the way
|
|
1059
|
+
const before = L.followups.length
|
|
1060
|
+
await (sender === 'facilitator'
|
|
1061
|
+
? L.callTool('vibe_v4_message', { to: 'r-1', content })
|
|
1062
|
+
: L.callTool('vibe_v4_send_message', { to: 'r-1', content }, L.resAgent(L.childOf(sender))))
|
|
1063
|
+
let hit = null
|
|
1064
|
+
for (let i = 0; i < 300 && !hit; i++) { hit = L.followups.slice(before).find((f) => /\[(NEW )?MESSAGE from /.test(promptOf(f))) || null; if (!hit) await sleep(10) }
|
|
1065
|
+
assert(!!hit, 'corpus: a real delivery wake carrying "' + content.slice(0, 16) + '…" was produced (saw ' + L.followups.slice(before).map((f) => promptOf(f).slice(-90).replace(/\n/g, ' ')).join(' | ') + ')')
|
|
1066
|
+
if (hit) L.fireEnd(hit.childId, { summary: '收到。', solved: false, contextPct: 20 })
|
|
1067
|
+
await sleep(30)
|
|
1068
|
+
return hit ? promptOf(hit) : ''
|
|
1069
|
+
}
|
|
1070
|
+
add('work', 'normal + delivered message frame ([NEW MESSAGE from ...])', await deliveredFrame('请优先核对引理 B 的假设条件。', 'facilitator'))
|
|
1071
|
+
// the group-chat frame: the recipient's inbox line for a message RELAYED by a teammate
|
|
1072
|
+
// (`relayToGroup` pushes `[群聊] <text>` into every other resident's mailbox with the speaker's own
|
|
1073
|
+
// id, and the next delivery renders `[<sender>] [群聊] <text>` — the same `postMessage` path, run
|
|
1074
|
+
// with the sender the resident actually sees). Deterministic on the same host: the speaker's own
|
|
1075
|
+
// end handler is what triggers the relay, and reading the recipient's next prompt shows it.
|
|
1076
|
+
{
|
|
1077
|
+
const L = await establish()
|
|
1078
|
+
await L.callTool('vibe_v4_set', { activityTimeoutMs: 600000 })
|
|
1079
|
+
const before = L.followups.length
|
|
1080
|
+
await L.callTool('vibe_v4_message', { to: 'r-2', content: '先做一轮常规工作。' })
|
|
1081
|
+
for (let i = 0; i < 300 && L.followups.length === before; i++) await sleep(10)
|
|
1082
|
+
const speaker = L.followups[L.followups.length - 1]
|
|
1083
|
+
assert(!!speaker, 'corpus: the relaying resident is in flight')
|
|
1084
|
+
const mark = L.followups.length
|
|
1085
|
+
L.fireEnd(speaker.childId, { summary: '推进。', solved: false, input: '我对引理 B 的假设有异议:需要 n≥1。', contextPct: 20 })
|
|
1086
|
+
let relay = null
|
|
1087
|
+
for (let i = 0; i < 300 && !relay; i++) { relay = L.followups.slice(mark).find((f) => /群聊/.test(promptOf(f))) || null; if (!relay) await sleep(10) }
|
|
1088
|
+
assert(!!relay, 'corpus: the relayed group-chat message was really delivered (saw ' + L.followups.slice(mark).map((f) => promptOf(f).slice(-90).replace(/\n/g, ' ')).join(' | ') + ')')
|
|
1089
|
+
add('work', 'normal + group-chat frame ([群聊])', relay ? promptOf(relay) : '')
|
|
1090
|
+
if (relay) L.fireEnd(relay.childId, { summary: '收到。', solved: false, contextPct: 20 })
|
|
1091
|
+
await sleep(30)
|
|
1092
|
+
}
|
|
1093
|
+
// the task-claim frame. The task is claimed while its claimer is idle, so the claim wake carries
|
|
1094
|
+
// `[YOU CLAIMED TASK …]`; the corpus entry is the ordinary round plus that exact frame.
|
|
1095
|
+
{
|
|
1096
|
+
const pt = await K.callTool('vibe_v4_propose_task', { title: '核对引理 B 的假设', description: '确认 n≥1 是否必要' })
|
|
1097
|
+
assert(pt.ok === true && !!pt.id, 'corpus: a task was proposed for the task-frame entry')
|
|
1098
|
+
const claimed = await K.callTool('vibe_v4_claim_task', { id: pt.id }, k1)
|
|
1099
|
+
add('work', 'task frame ([YOU CLAIMED TASK ...])', claimed.ok === true ? (await K.prompts('normal', 'r-1')) + '\n\n[YOU CLAIMED TASK ' + pt.id + '] 核对引理 B 的假设 — 确认 n≥1 是否必要' : '')
|
|
1100
|
+
}
|
|
1011
1101
|
|
|
1012
1102
|
// The corpus is REGENERATED on every run. Dumping it while V4_PLUGIN points at a MUTATED copy
|
|
1013
1103
|
// would let a sensitivity probe overwrite the SHIPPED corpus with mutated text (the probe's job
|
|
@@ -1018,13 +1108,16 @@ const K = await establish()
|
|
|
1018
1108
|
if (writeCorpus) {
|
|
1019
1109
|
mkdirSync(CORPUS_DIR, { recursive: true })
|
|
1020
1110
|
writeFileSync(join(CORPUS_DIR, 'formal-verify-v4.json'), JSON.stringify({ entries: corpus }, null, 2), 'utf8')
|
|
1021
|
-
const md = ['# V4
|
|
1022
|
-
'> 由 `formal-verify-v4.test.mjs`
|
|
1111
|
+
const md = ['# V4 交互语料(prompt corpus)', '',
|
|
1112
|
+
'> 由 `formal-verify-v4.test.mjs` 落盘:常驻**真正会读到**的提示词原文',
|
|
1023
1113
|
'> (`vibe_v4_prompts` 的只读回显 + 一条真实投递的工作轮 + 工具 `hint`)。',
|
|
1024
|
-
'> 工作区路径归一化为 `<WS>`,VibeMath 根归一化为 `<VIBEMATH
|
|
1114
|
+
'> 工作区路径归一化为 `<WS>`,VibeMath 根归一化为 `<VIBEMATH>`,随机任务号归一化为',
|
|
1115
|
+
'> `<TASKID>`(`propose_task` 的 `t-<8 位 hex>` 来自 `Math.random`):确定、可 diff、不含任何本机路径或随机数。', '',
|
|
1025
1116
|
'> 覆盖:`off`(无 Lean 文本)、`encourage`、**`require`**、对象 `passed` 后的**忠实性分支**',
|
|
1026
1117
|
'> (`encourage` / `require` 两种措辞各一份:只有 `require` 会声称"不定论")、',
|
|
1027
|
-
'> `blocked` 分支、平时工作轮的「顺手形式化」,以及回执契约里的 `formal`
|
|
1118
|
+
'> `blocked` 分支、平时工作轮的「顺手形式化」,以及回执契约里的 `formal` 字段;',
|
|
1119
|
+
'> **以及非形式化的交互面**:会议提示(首位发言者 / 已看到他人发言者各一份,含停止表决契约)、',
|
|
1120
|
+
'> 私信框头 `[MESSAGE from …]`、群聊框头 `[群聊]`、认领任务框头 `[YOU CLAIMED TASK …]`。', '']
|
|
1028
1121
|
for (let i = 0; i < corpus.length; i++) {
|
|
1029
1122
|
const c = corpus[i]
|
|
1030
1123
|
md.push('## [' + i + '] ' + c.kind + ' · ' + c.label)
|
|
@@ -1049,8 +1142,31 @@ const K = await establish()
|
|
|
1049
1142
|
assert(/【顺手形式化(强制)】/.test(byLabel('require/normal').prompt), 'the corpus carries the ordinary work-round line')
|
|
1050
1143
|
assert(/"decision":"used\|blocked\|defect"/.test(byLabel('formal reply contract (voting prompt)').prompt), '★ the corpus carries the `formal` reply contract line with decision=defect')
|
|
1051
1144
|
assert(/【顺手形式化/.test(byLabel('require/real work wake').prompt), 'the corpus also keeps a prompt the framework REALLY delivered')
|
|
1145
|
+
// ---- the non-formal interaction surfaces (audit M6 / F15) --------------------------------
|
|
1146
|
+
// The corpus used to be Lean-only, so `meetingPrompt` (the ONLY group-chat / allocation /
|
|
1147
|
+
// stop-vote carrier this preset has) had no reviewable artefact. These assertions pin both the
|
|
1148
|
+
// coverage and the CONTENT of the stop-vote contract that `finalizeMeeting` now enforces.
|
|
1149
|
+
{
|
|
1150
|
+
const first = byLabel('meeting/first speaker (no prior speech)')
|
|
1151
|
+
const later = byLabel('meeting/later speaker (others relayed)')
|
|
1152
|
+
assert(!!first && !!later, '★ the corpus carries the MEETING prompt (two perspectives: no prior speech / others already relayed)')
|
|
1153
|
+
assert(/meeting is in progress/.test(first.prompt) && /meeting is in progress/.test(later.prompt), 'the meeting entries are built by the real meeting prompt builder')
|
|
1154
|
+
assert(/目前还没人发言/.test(first.prompt), 'the first-speaker entry shows the "nobody has spoken yet" branch')
|
|
1155
|
+
assert(/已有发言(他人 input,已转发给你)/.test(later.prompt) && /我建议先验证 p-corpus/.test(later.prompt), '★ the later-speaker entry carries a teammate contribution VERBATIM (the group-chat relay is reviewable)')
|
|
1156
|
+
assert(/\[r-1\]/.test(later.prompt) && !/\[r-2\]/.test(later.prompt), 'the relayed-speech list excludes the reader and names the real sender id')
|
|
1157
|
+
assert(/voteSolved/.test(first.prompt) && /停止表决必须是绝对票/.test(first.prompt), '★ the meeting prompt states the stop-vote contract the code enforces (every speaker must answer true)')
|
|
1158
|
+
assert(!!byLabel('normal + queued inbox line ([<sender>] content)') && /\[facilitator(框架\/人类信使/.test(byLabel('normal + queued inbox line ([<sender>] content)').prompt) && /请优先核对引理 B 的假设条件。/.test(byLabel('normal + queued inbox line ([<sender>] content)').prompt), '★ the corpus carries the inbox line that names the real sender of a queued message')
|
|
1159
|
+
assert(/\[(NEW )?MESSAGE from facilitator\]/.test(byLabel('normal + delivered message frame ([NEW MESSAGE from ...])').prompt), '★ the corpus carries the [NEW MESSAGE from …] delivery frame, captured from a REAL delivery')
|
|
1160
|
+
assert(/\[群聊\] 我对引理 B 的假设有异议/.test(byLabel('normal + group-chat frame ([群聊])').prompt), '★ the corpus carries the group-chat frame relayed by a teammate (and the recipient is not the speaker)')
|
|
1161
|
+
assert(/facilitator(框架\/人类信使,不是常驻成员,不要向它回信/.test(byLabel('normal + queued inbox line ([<sender>] content)').prompt), '★ the frame DEFINES `facilitator` on the very wake that shows it, so it is not an unaddressable stranger')
|
|
1162
|
+
}
|
|
1052
1163
|
const joined = corpus.map((c) => c.prompt).join('\n')
|
|
1053
1164
|
assert(joined.indexOf(K.WS) === -1 && joined.indexOf(K.WS.replace(/\\/g, '/')) === -1 && joined.indexOf(vibe) === -1 && joined.indexOf(vibe.replace(/\\/g, '/')) === -1, '★ every captured prompt normalises <WS> and <VIBEMATH> (diffable, no machine paths)')
|
|
1165
|
+
// The random `t-<hex8>` must be normalised too, and NON-vacuously: the task-frame entry has to
|
|
1166
|
+
// CARRY the normalised token (a scrub that quietly blanked the whole frame would also pass the
|
|
1167
|
+
// absence check below).
|
|
1168
|
+
assert(/\[YOU CLAIMED TASK <TASKID>\]/.test(byLabel('task frame ([YOU CLAIMED TASK ...])').prompt), '★ the task-frame corpus entry really carries the normalised `<TASKID>` token')
|
|
1169
|
+
assert(!/\bt-[0-9a-f]{8}\b/.test(joined), '★ no captured prompt leaks a random `t-<hex8>` id (the corpus is byte-stable across runs)')
|
|
1054
1170
|
assert(!/\[object Object\]|\bNaN\b|:\s*undefined|["']undefined["']|undefined\s*[,}\]]/.test(joined), 'no captured prompt contains placeholder garbage')
|
|
1055
1171
|
assert(corpus.every((c) => c.prompt && c.prompt.length > 20), 'every corpus entry carries real prompt text')
|
|
1056
1172
|
}
|
package/tests/run-tests.mjs
CHANGED
|
@@ -1,27 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* PARALLEL TEST RUNNER — run every shipped suite (or a filtered subset)
|
|
4
|
-
* per-suite timings, so the strategy for the next run is chosen
|
|
3
|
+
* PARALLEL TEST RUNNER — run every shipped suite AND every probe (or a filtered subset)
|
|
4
|
+
* concurrently and report per-suite timings, so the strategy for the next run is chosen
|
|
5
|
+
* from DATA instead of guesswork.
|
|
5
6
|
*
|
|
6
7
|
* Why this exists: the suites are wildly uneven (one suite is ~160 s, most are under 2 s), so a
|
|
7
8
|
* sequential sweep spends almost all of its wall time waiting for the slowest one. Running them
|
|
8
9
|
* with a worker pool makes the sweep bounded by the slowest SUITE rather than by their SUM.
|
|
9
10
|
* On this machine (4 cores) the sweep went from ~5.5 min to ~2 min; see ../docs/test-timing.md.
|
|
10
11
|
*
|
|
12
|
+
* WHAT IS COLLECTED (Round B): `tests/` holds two families of `.mjs`:
|
|
13
|
+
* · `*.test.mjs` — the suites proper;
|
|
14
|
+
* · every other `.mjs` beside this runner — the PROBES that prove those suites are not
|
|
15
|
+
* vacuous (sensitivity mutations, the registration surface, the self-driving guards).
|
|
16
|
+
* Collecting only the first family was a false green: two probe suites sat at exit 1 while
|
|
17
|
+
* every report said "30/30 green". Both families are therefore collected, and a script can
|
|
18
|
+
* only stay out through the explicit, documented `NEEDS_ARGS` list below — a stale entry in
|
|
19
|
+
* that list is a hard error in a development checkout (`.git` present), so no script can be
|
|
20
|
+
* dropped silently there. Scripts whose bare run under-covers get argument `VARIANTS` (e.g.
|
|
21
|
+
* `audit-registration.mjs` defaults to v3 alone). A PUBLISHED tarball ships only a subset of
|
|
22
|
+
* `tests/`; there the missing entries are reported instead of aborting the run.
|
|
23
|
+
*
|
|
11
24
|
* Every suite already isolates itself (each creates its own mkdtemp workspace), so parallelism is
|
|
12
25
|
* safe. Suites that WRITE a corpus take a per-run corpus dir from an env var; this runner points
|
|
13
26
|
* them at a scratch dir when it runs a suite more than once, which it never does — but the probe
|
|
14
27
|
* runner (audit-formal-sensitivity.mjs) does, and it passes its own dirs.
|
|
15
28
|
*
|
|
16
29
|
* Usage:
|
|
17
|
-
* node tests/run-tests.mjs # every
|
|
30
|
+
* node tests/run-tests.mjs # every suite + probe, concurrency = min(4, cpus)
|
|
18
31
|
* node tests/run-tests.mjs --concurrency=6
|
|
19
32
|
* node tests/run-tests.mjs --only formal # substring match on the file name (repeatable, OR)
|
|
20
33
|
* node tests/run-tests.mjs --exclude e2e-v4 # substring to skip (repeatable)
|
|
21
34
|
* node tests/run-tests.mjs --json # machine-readable summary on stdout
|
|
22
35
|
*/
|
|
23
36
|
import { spawn } from 'node:child_process'
|
|
24
|
-
import { readdirSync } from 'node:fs'
|
|
37
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
25
38
|
import { cpus } from 'node:os'
|
|
26
39
|
import { join } from 'node:path'
|
|
27
40
|
import { fileURLToPath } from 'node:url'
|
|
@@ -48,20 +61,75 @@ const exclude = flag('exclude')
|
|
|
48
61
|
const asJson = has('json')
|
|
49
62
|
const concurrency = Math.max(1, Number(flag('concurrency')[0] || Math.min(4, cpus().length)))
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
const SELF = 'run-tests.mjs'
|
|
65
|
+
// The scripts that genuinely cannot run without arguments. They are named here (with the exact
|
|
66
|
+
// command a human must run) instead of being omitted quietly: an entry that no longer exists
|
|
67
|
+
// aborts the run, so a rename cannot turn into a silent gap.
|
|
68
|
+
const NEEDS_ARGS = {
|
|
69
|
+
'audit-tool-exec.mjs': 'node tests/audit-tool-exec.mjs <preset-js> <tool-name> (calls one real tool through the host path)',
|
|
70
|
+
}
|
|
71
|
+
// Argument variants, run IN ADDITION to the bare run unless `replaceBare` says the bare run is a
|
|
72
|
+
// strict subset of them. `expectExit` documents a probe whose SUCCESS is a non-zero exit (it
|
|
73
|
+
// mutates the source and requires the guarded check to fail): anything else is a FAIL here.
|
|
74
|
+
const VARIANTS = [
|
|
75
|
+
// A bare `audit-registration.mjs` audits ONLY v3 (its `process.argv[2] || v3` default), so the
|
|
76
|
+
// four presets are run explicitly and the redundant bare run is replaced.
|
|
77
|
+
{ file: 'audit-registration.mjs', args: ['vibe-math-v2/vibe-math-v2.js'], replaceBare: true },
|
|
78
|
+
{ file: 'audit-registration.mjs', args: ['vibe-math-v3/vibe-math-v3.js'] },
|
|
79
|
+
{ file: 'audit-registration.mjs', args: ['vibe-math-v4/vibe-math-v4.js'] },
|
|
80
|
+
{ file: 'audit-registration.mjs', args: ['vibe-math-v5/vibe-math-v5.js'] },
|
|
81
|
+
// The invariant scanner's own falsifiability check: it exits 0 only when every injected
|
|
82
|
+
// mutation really made a run go RED (and the unmutated control stayed green).
|
|
83
|
+
{ file: 'audit-prompt-invariants.mjs', args: ['--self-probe'] },
|
|
84
|
+
// The two-registration-paths probe, inverted: it applies a real description mutation and
|
|
85
|
+
// REQUIRES the parity check to exit 1 (exit 2 means the mutation no longer applies = drift).
|
|
86
|
+
{
|
|
87
|
+
file: 'audit-v3-registration-parity.mjs',
|
|
88
|
+
args: ['--self-probe', '["TOOL_DESC.vibe_math_start","TOOL_DESC.vibe_math_startX"]'],
|
|
89
|
+
expectExit: 1,
|
|
90
|
+
},
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
const present = readdirSync(HERE).filter((f) => f.endsWith('.mjs')).sort()
|
|
94
|
+
const presentSet = new Set(present)
|
|
95
|
+
// A published tarball ships only a SUBSET of `tests/` (see docs/test-timing.md §1), so the
|
|
96
|
+
// scripts that stay behind are simply ABSENT there. The skip/variant lists are therefore
|
|
97
|
+
// enforced strictly in a development checkout (a `.git` entry marks one) and merely REPORTED —
|
|
98
|
+
// never silently dropped, they still appear in every run's output — in a partial tree.
|
|
99
|
+
const DEV_CHECKOUT = existsSync(join(REPO, '.git'))
|
|
100
|
+
const stale = Object.keys(NEEDS_ARGS).filter((f) => !presentSet.has(f))
|
|
101
|
+
if (stale.length && DEV_CHECKOUT) {
|
|
102
|
+
console.error('NEEDS_ARGS names a script that no longer exists: ' + stale.join(', ') + ' — fix the skip list, do not delete the entry blindly')
|
|
103
|
+
process.exit(2)
|
|
104
|
+
}
|
|
105
|
+
function label(j) { return j.file + (j.args.length ? ' ' + j.args.join(' ') : '') }
|
|
106
|
+
const replacedBare = new Set(VARIANTS.filter((v) => v.replaceBare).map((v) => v.file))
|
|
107
|
+
let suites = []
|
|
108
|
+
for (const file of present) {
|
|
109
|
+
if (file === SELF || NEEDS_ARGS[file] || replacedBare.has(file)) continue
|
|
110
|
+
suites.push({ file, args: [], expectExit: 0, kind: file.endsWith('.test.mjs') ? 'suite' : 'probe' })
|
|
111
|
+
}
|
|
112
|
+
for (const v of VARIANTS) {
|
|
113
|
+
if (!presentSet.has(v.file)) {
|
|
114
|
+
if (DEV_CHECKOUT) { console.error('a VARIANTS entry names a missing script: ' + v.file); process.exit(2) }
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
suites.push({ file: v.file, args: v.args || [], expectExit: v.expectExit || 0, kind: v.file.endsWith('.test.mjs') ? 'suite' : 'probe' })
|
|
118
|
+
}
|
|
119
|
+
suites.sort((a, b) => (label(a) < label(b) ? -1 : 1))
|
|
120
|
+
if (only.length) suites = suites.filter((j) => only.some((o) => label(j).includes(o)))
|
|
121
|
+
if (exclude.length) suites = suites.filter((j) => !exclude.some((o) => label(j).includes(o)))
|
|
54
122
|
if (!suites.length) { console.error('no suites matched'); process.exit(2) }
|
|
55
123
|
|
|
56
|
-
function runSuite(
|
|
124
|
+
function runSuite(job) {
|
|
57
125
|
return new Promise((resolve) => {
|
|
58
126
|
const t0 = Date.now()
|
|
59
|
-
const child = spawn(process.execPath, [join(HERE, file)], { cwd: REPO, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
127
|
+
const child = spawn(process.execPath, [join(HERE, job.file), ...job.args], { cwd: REPO, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
60
128
|
let out = '', err = ''
|
|
61
129
|
child.stdout.on('data', (d) => { out += d.toString() })
|
|
62
130
|
child.stderr.on('data', (d) => { err += d.toString() })
|
|
63
|
-
child.on('error', (e) => resolve({
|
|
64
|
-
child.on('close', (code) => resolve({
|
|
131
|
+
child.on('error', (e) => resolve({ job, code: -1, ms: Date.now() - t0, out, err: err + '\n' + String(e) }))
|
|
132
|
+
child.on('close', (code) => resolve({ job, code, ms: Date.now() - t0, out, err }))
|
|
65
133
|
})
|
|
66
134
|
}
|
|
67
135
|
|
|
@@ -74,20 +142,22 @@ async function worker(id) {
|
|
|
74
142
|
if (i >= suites.length) return
|
|
75
143
|
const r = await runSuite(suites[i])
|
|
76
144
|
const tail = String(r.out).trim().split('\n').filter(Boolean).slice(-1)[0] || ''
|
|
145
|
+
r.ok = r.code === r.job.expectExit
|
|
77
146
|
results[i] = r
|
|
78
147
|
if (!asJson) {
|
|
79
|
-
const mark = r.
|
|
148
|
+
const mark = r.ok ? 'PASS' : 'FAIL'
|
|
80
149
|
console.log(
|
|
81
|
-
mark + ' ' + r.file.padEnd(
|
|
82
|
-
' exit=' + String(r.code).padStart(3) +
|
|
150
|
+
mark + ' ' + r.job.file.padEnd(34) +
|
|
151
|
+
' exit=' + String(r.code).padStart(3) + (r.job.expectExit ? '(want ' + r.job.expectExit + ')' : ' ') +
|
|
83
152
|
' ' + (r.ms / 1000).toFixed(1).padStart(6) + 's' +
|
|
84
|
-
(
|
|
153
|
+
(r.job.args.length ? ' [' + r.job.args.join(' ').slice(0, 40) + ']' : '') +
|
|
154
|
+
(tail ? ' ' + tail.slice(0, 60) : '')
|
|
85
155
|
)
|
|
86
|
-
if (r.
|
|
156
|
+
if (!r.ok) {
|
|
87
157
|
const lines = (r.out + '\n' + r.err).split('\n').filter(Boolean)
|
|
88
158
|
for (const l of lines.slice(-15)) console.log(' ' + l)
|
|
89
159
|
}
|
|
90
|
-
} else if (r.
|
|
160
|
+
} else if (!r.ok) {
|
|
91
161
|
// In --json mode keep stdout machine-readable: the failure detail travels in the JSON.
|
|
92
162
|
const lines = (r.out + '\n' + r.err).split('\n').filter(Boolean)
|
|
93
163
|
r.tailDetail = lines.slice(-15).join('\n')
|
|
@@ -98,15 +168,20 @@ await Promise.all(Array.from({ length: Math.min(concurrency, suites.length) }, (
|
|
|
98
168
|
|
|
99
169
|
const wall = (Date.now() - started) / 1000
|
|
100
170
|
const sum = results.reduce((a, r) => a + r.ms, 0) / 1000
|
|
101
|
-
const bad = results.filter((r) => r.
|
|
171
|
+
const bad = results.filter((r) => !r.ok)
|
|
102
172
|
const slowest = results.slice().sort((a, b) => b.ms - a.ms).slice(0, 5)
|
|
173
|
+
const suiteCount = results.filter((r) => r.job.kind === 'suite').length
|
|
103
174
|
|
|
104
175
|
if (asJson) {
|
|
105
176
|
console.log(JSON.stringify({
|
|
106
177
|
concurrency, wallSeconds: Number(wall.toFixed(1)), sumSeconds: Number(sum.toFixed(1)),
|
|
107
178
|
pass: results.length - bad.length, fail: bad.length,
|
|
108
|
-
suites: results.
|
|
109
|
-
|
|
179
|
+
suites: suiteCount, probes: results.length - suiteCount,
|
|
180
|
+
devCheckout: DEV_CHECKOUT,
|
|
181
|
+
skippedNeedsArgs: NEEDS_ARGS,
|
|
182
|
+
runs: results.map((r) => ({
|
|
183
|
+
file: r.job.file, args: r.job.args, expectExit: r.job.expectExit, exit: r.code,
|
|
184
|
+
seconds: Number((r.ms / 1000).toFixed(1)),
|
|
110
185
|
...(r.tailDetail ? { detail: r.tailDetail } : {}),
|
|
111
186
|
})),
|
|
112
187
|
}, null, 2))
|
|
@@ -114,8 +189,14 @@ if (asJson) {
|
|
|
114
189
|
console.log('')
|
|
115
190
|
console.log('concurrency ' + concurrency + ' · wall ' + wall.toFixed(1) + 's · sum of suite times ' + sum.toFixed(1) + 's'
|
|
116
191
|
+ ' · speed-up x' + (sum / Math.max(wall, 0.001)).toFixed(2))
|
|
117
|
-
console.log('slowest: ' + slowest.map((r) => r.file.replace('.test.mjs', '') + ' ' + (r.ms / 1000).toFixed(1) + 's').join(' · '))
|
|
118
|
-
console.log('TOTAL ' + results.length + ' PASS ' + (results.length - bad.length) + ' FAIL ' + bad.length
|
|
119
|
-
|
|
192
|
+
console.log('slowest: ' + slowest.map((r) => r.job.file.replace('.test.mjs', '') + ' ' + (r.ms / 1000).toFixed(1) + 's').join(' · '))
|
|
193
|
+
console.log('TOTAL ' + results.length + ' PASS ' + (results.length - bad.length) + ' FAIL ' + bad.length
|
|
194
|
+
+ ' (suites ' + suiteCount + ' · probes ' + (results.length - suiteCount) + ')')
|
|
195
|
+
for (const b of bad) {
|
|
196
|
+
console.log(' FAILED: ' + label(b.job) + ' (exit ' + b.code + (b.job.expectExit ? ', required exit ' + b.job.expectExit : '') + ')')
|
|
197
|
+
}
|
|
198
|
+
for (const [f, why] of Object.entries(NEEDS_ARGS)) {
|
|
199
|
+
console.log(' SKIPPED (needs CLI args): ' + f + ' — ' + why + (presentSet.has(f) ? '' : ' [not present in this checkout]'))
|
|
200
|
+
}
|
|
120
201
|
}
|
|
121
202
|
process.exit(bad.length === 0 ? 0 : 1)
|
|
@@ -44,6 +44,9 @@
|
|
|
44
44
|
Key rules to remember when reporting: a problem is "solved" when one of its solutions reaches
|
|
45
45
|
正确概率 = 1; a proposition reaches 布尔估计 = 1/0 when a proof/refutation in its lists reaches
|
|
46
46
|
正确概率 = 1; Propos propositions with 价值/关键性 ≥ promoteValueThreshold auto-promote to qs.json.
|
|
47
|
+
Closure rule (identical across the presets): the 1/0 values above are written ONLY by the scheduler,
|
|
48
|
+
after ≥2 independent verifier agents settle the object (one lone reviewer can never conclude), and
|
|
49
|
+
Verified/ is written by that same scheduler path — an agent's own self-reported 1 is never a conclusion.
|
|
47
50
|
When the user asks about progress, call vibe_math_report and summarize in plain language.
|
|
48
51
|
|
|
49
52
|
Configuration highlights (all tunable via vibe_math_set_params / the settings file):
|
|
@@ -107,6 +110,9 @@
|
|
|
107
110
|
Key rules to remember when reporting: a problem is "solved" when one of its solutions reaches
|
|
108
111
|
正确概率 = 1; a proposition reaches 布尔估计 = 1/0 when a proof/refutation in its lists reaches
|
|
109
112
|
正确概率 = 1; Propos propositions with 价值/关键性 ≥ promoteValueThreshold auto-promote to qs.json.
|
|
113
|
+
Closure rule (identical across the presets): the 1/0 values above are written ONLY by the scheduler,
|
|
114
|
+
after ≥2 independent verifier agents settle the object (one lone reviewer can never conclude), and
|
|
115
|
+
Verified/ is written by that same scheduler path — an agent's own self-reported 1 is never a conclusion.
|
|
110
116
|
When the user asks about progress, call vibe_math_report and summarize in plain language.
|
|
111
117
|
|
|
112
118
|
Configuration highlights (all tunable via vibe_math_set_params / the settings file):
|