dsh-vibe-math 2.7.1 → 2.7.3
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 +682 -690
- package/README.md +19 -27
- package/cordis.patch.yml +112 -16
- package/docs/AUDIT-CHECKLIST.md +744 -518
- package/docs/COMPAT-AUDIT-ROUND2.md +35 -9
- package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
- package/docs/final-paper.md +172 -172
- package/docs/formal-verification.md +588 -588
- package/docs/math-computation.md +194 -163
- package/docs/parameter-schema.md +37 -0
- package/docs/release-notes/README.en.md +49 -0
- package/docs/release-notes/README.md +48 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +72 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.2.md +72 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
- package/docs/release-notes/TEMPLATE.md +126 -0
- package/docs/status-report-fields.md +167 -0
- package/docs/test-timing.md +157 -154
- package/installer.js +27 -17
- package/package.json +55 -3
- package/prompt-corpus-persona/persona-corpus.json +9 -9
- package/prompt-corpus-persona/persona-corpus.md +225 -33
- package/prompt-corpus-v2/formal-verify-v2.json +305 -113
- package/prompt-corpus-v2/formal-verify-v2.md +2399 -279
- package/prompt-corpus-v3/formal-verify-v3.json +134 -110
- package/prompt-corpus-v3/formal-verify-v3.md +409 -141
- package/prompt-corpus-v4/formal-verify-v4.json +18 -18
- package/prompt-corpus-v4/formal-verify-v4.md +49 -44
- package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
- package/prompt-corpus-v5/prompt-corpus-v5.md +513 -259
- package/scripts/update-doc-counts.mjs +71 -0
- package/tests/audit-artifact-docs.mjs +72 -0
- package/tests/audit-artifact-docs.mutants.mjs +58 -0
- package/tests/audit-installer-assertions.mutants.mjs +63 -0
- package/tests/audit-installer-compat.test.mjs +52 -4
- package/tests/audit-installer-policy.test.mjs +41 -8
- package/tests/audit-math-computation-contract.mjs +107 -9
- package/tests/audit-math-computation-contract.mutants.mjs +106 -0
- package/tests/audit-math-computation-parity.mjs +164 -3
- package/tests/audit-math-computation-sensitivity.mjs +6 -4
- package/tests/audit-package-membership.mjs +57 -0
- package/tests/audit-package-membership.mutants.mjs +28 -0
- package/tests/audit-participant-set-parity.mjs +160 -0
- package/tests/audit-path-discipline.mjs +83 -0
- package/tests/audit-path-discipline.mutants.mjs +51 -0
- package/tests/audit-persona-sensitivity.mjs +14 -0
- package/tests/audit-persona-surface.mutants.mjs +69 -0
- package/tests/audit-persona-surface.test.mjs +63 -22
- package/tests/audit-prompt-duplication.mjs +143 -0
- package/tests/audit-prompt-duplication.mutants.mjs +50 -0
- package/tests/audit-prompt-invariants.mjs +43 -5
- package/tests/audit-readme-bilingual.test.mjs +152 -0
- package/tests/audit-readme-counts.mjs +95 -0
- package/tests/audit-readme-counts.mutants.mjs +52 -0
- package/tests/audit-registration.mjs +78 -0
- package/tests/audit-status-report-fields.mjs +172 -0
- package/tests/audit-status-report-fields.mutants.mjs +64 -0
- package/tests/audit-v3-registration-parity.mjs +156 -0
- package/tests/audit-v5-integrity.mjs +300 -13
- package/tests/audit-v5-lean-abstention.mjs +392 -0
- package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
- package/tests/audit-v5-prompt-duplication.mjs +88 -0
- package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
- package/tests/audit-v5-sensitivity.mjs +15 -8
- package/tests/e2e-identity-a6.mutants.mjs +65 -0
- package/tests/e2e-v5-round2.test.mjs +566 -12
- package/tests/formal-verify-v2.test.mjs +91 -5
- package/tests/formal-verify-v3.mutants.mjs +117 -0
- package/tests/formal-verify-v3.test.mjs +85 -6
- package/tests/formal-verify-v4.mutants.mjs +152 -0
- package/tests/formal-verify-v4.test.mjs +628 -9
- package/tests/formal-verify-v5.test.mjs +68 -9
- package/tests/helpers/doc-counts-format.mjs +41 -0
- package/tests/helpers/math-computation-fake-seam.mjs +4 -0
- package/tests/math-computation-a1.mutants.mjs +72 -0
- package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
- package/tests/math-computation-discovery.mutants.mjs +76 -0
- package/tests/math-computation-shared.test.mjs +396 -2
- package/tests/math-computation-v2.test.mjs +767 -0
- package/tests/math-computation-v3.test.mjs +759 -0
- package/tests/math-computation-v4.mutants.mjs +133 -0
- package/tests/math-computation-v4.test.mjs +739 -0
- package/tests/prompt-v5-integrity.test.mjs +13 -2
- package/tests/run-tests.mjs +139 -9
- package/tests/run-tests.mutants.mjs +70 -0
- package/tests/selfdrive-v5.mjs +53 -1
- package/tests/selfdrive-v5.mutants.mjs +132 -0
- package/tests/v2-fix-probes.mutants.mjs +100 -0
- package/tests/v2-fix-probes.test.mjs +1039 -0
- package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
- package/tests/v3-fix-probes.mutants.mjs +95 -0
- package/tests/v3-fix-probes.test.mjs +891 -0
- package/tests/v5-institute-fixes.mutants.mjs +190 -0
- package/vibe-math-v2/agent.cordis.yml +62 -6
- package/vibe-math-v2/math-computation.js +229 -29
- package/vibe-math-v2/math-engines.js +12 -12
- package/vibe-math-v2/vibe-math-v2.js +458 -119
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +5 -1
- package/vibe-math-v3/agent.cordis.yml +72 -6
- package/vibe-math-v3/math-computation.js +229 -29
- package/vibe-math-v3/math-engines.js +12 -12
- package/vibe-math-v3/vibe-math-v3.js +482 -139
- package/vibe-math-v4/agent.cordis.yml +44 -8
- package/vibe-math-v4/math-computation.js +229 -29
- package/vibe-math-v4/math-engines.js +12 -12
- package/vibe-math-v4/vibe-math-v4.js +245 -52
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +2 -0
- package/vibe-math-v5/agent.cordis.yml +46 -12
- package/vibe-math-v5/math-computation.js +229 -29
- package/vibe-math-v5/math-engines.js +12 -12
- package/vibe-math-v5/vibe-math-v5.js +1307 -243
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1655
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
|
@@ -1166,16 +1166,27 @@ section('13 full-corpus sweep over every prompt ever sent')
|
|
|
1166
1166
|
// A single prompt must not deliver the same message twice. The inbox used to be
|
|
1167
1167
|
// prepended AND re-emitted from the [状态] block, so a member read every new message
|
|
1168
1168
|
// twice in one prompt.
|
|
1169
|
+
// A3 (assertion-quality review): this used to end with a vacuous `assert(true, …)` and to
|
|
1170
|
+
// `break` after the FIRST duplicate frame, so (a) a property nobody checked was attributed to an
|
|
1171
|
+
// assertion that asserted nothing, and (b) only one duplicate per prompt was ever reported.
|
|
1172
|
+
// The loop now COLLECTS every duplicate frame and reports the count, and the summary assertion
|
|
1173
|
+
// states exactly what was checked.
|
|
1174
|
+
const duplicateFrames = []
|
|
1175
|
+
let inboxOffenders = 0
|
|
1169
1176
|
for (const c of corpus) {
|
|
1170
1177
|
const bodies = c.prompt.match(/【[^】]*】[^\n]{20,}/g) || []
|
|
1171
1178
|
for (const frame of new Set(bodies)) {
|
|
1172
1179
|
const n = bodies.filter(b => b === frame).length
|
|
1173
|
-
if (n > 1) {
|
|
1180
|
+
if (n > 1) duplicateFrames.push({ kind: c.kind, owner: c.owner, n, frame: frame.slice(0, 60) })
|
|
1174
1181
|
}
|
|
1175
1182
|
const inboxHeads = (c.prompt.match(/\[新到的消息/g) || []).length
|
|
1183
|
+
if (inboxHeads > 1) inboxOffenders += 1
|
|
1176
1184
|
assert(inboxHeads <= 1, c.kind + '/' + c.owner + ': at most one inbox section per prompt (found ' + inboxHeads + ')')
|
|
1177
1185
|
}
|
|
1178
|
-
assert(
|
|
1186
|
+
assert(duplicateFrames.length === 0,
|
|
1187
|
+
'no prompt delivers the same framed message twice (duplicate frames found: ' + duplicateFrames.length
|
|
1188
|
+
+ (duplicateFrames.length ? ' — ' + duplicateFrames.slice(0, 5).map(d => d.kind + '/' + d.owner + ' ' + d.n + '× ' + d.frame).join(' | ') : '')
|
|
1189
|
+
+ '); prompts with two inbox sections: ' + inboxOffenders)
|
|
1179
1190
|
// The identity claim inside a prompt must agree with the persona shipped alongside it.
|
|
1180
1191
|
for (const c of corpus) {
|
|
1181
1192
|
if (!c.persona) continue
|
package/tests/run-tests.mjs
CHANGED
|
@@ -62,6 +62,115 @@ const asJson = has('json')
|
|
|
62
62
|
const concurrency = Math.max(1, Number(flag('concurrency')[0] || Math.min(4, cpus().length)))
|
|
63
63
|
|
|
64
64
|
const SELF = 'run-tests.mjs'
|
|
65
|
+
// Per-suite HARD timeout (GATE_SUITE_TIMEOUT_MS overrides, ms). It bounds an intermittent hang so the
|
|
66
|
+
// gate always terminates and a hang is reported - never mistaken for "still running". 180 s comes from
|
|
67
|
+
// measurements: whole-gate wall ~200 s, slowest honest suite 135-144 s (audit-math-computation-sensitivity),
|
|
68
|
+
// so 180 s bounds a hang without killing an honest slow suite. Do NOT raise it silently to "fix" reds.
|
|
69
|
+
const SUITE_TIMEOUT_MS = Math.max(1000, Number(process.env.GATE_SUITE_TIMEOUT_MS || 180000))
|
|
70
|
+
|
|
71
|
+
// NAMED per-suite overrides for suites that are HONEST but slow (measured, not guessed). Raising a
|
|
72
|
+
// limit is a documented act: update the numbers here AND say so in the commit message - never silently.
|
|
73
|
+
// v2-fix-probes.mutants.mjs: ~43.1 s per family x 10 families ~= 430 s measured (inner child cap 120 s)
|
|
74
|
+
// v3-fix-probes.mutants.mjs: ~23.5 s per family x 11 families ~= 260 s measured (inner child cap 120 s)
|
|
75
|
+
// Both report hangs=[] with their own 120 s child cap, i.e. they are slow, not hung - so the 180 s
|
|
76
|
+
// default would misreport them. Everything else keeps the 180 s default.
|
|
77
|
+
const TIMEOUT_OVERRIDES = {
|
|
78
|
+
'v2-fix-probes.mutants.mjs': 900000,
|
|
79
|
+
'v3-fix-probes.mutants.mjs': 900000,
|
|
80
|
+
// v5-institute-fixes.mutants.mjs: MEASURED 201.9 s wall (hangs=[], skipped=[], ALL MUTANTS RED
|
|
81
|
+
// AS REQUIRED) - i.e. OVER the 180 s default, so it gets the same 900 s as the v2/v3 families.
|
|
82
|
+
// History worth keeping: a writer CLAIMED this family fitted its override when it did not, and the
|
|
83
|
+
// gate caught it as "FAILED: v5-institute-fixes.mutants.mjs [probe] (TIMEOUT after 180s)" - the
|
|
84
|
+
// named-timeout mechanism doing its job. For contrast, formal-verify-v4.mutants.mjs MEASURED
|
|
85
|
+
// 70.9 s wall, i.e. ample headroom, so it needs no override.
|
|
86
|
+
'v5-institute-fixes.mutants.mjs': 900000,
|
|
87
|
+
}
|
|
88
|
+
/** One place decides a job limit: explicit job value, then the named override, then the default. */
|
|
89
|
+
function jobLimit(job) { return job.timeoutMs || TIMEOUT_OVERRIDES[job.file] || SUITE_TIMEOUT_MS }
|
|
90
|
+
/**
|
|
91
|
+
* DIAGNOSABILITY (protocol: every red must name an assertion): a failing suite's assertion NAMES are
|
|
92
|
+
* what a reader needs, and they must appear under the FAILED line - not only in the suite's own last
|
|
93
|
+
* stdout line. Extract the failing-assertion lines (this repo prints ` - <name>` or ` FAIL - <name>`,
|
|
94
|
+
* and suites that abort print `FAILURES:`/`Error:` markers), and fall back to the raw tail when a suite
|
|
95
|
+
* names nothing.
|
|
96
|
+
*/
|
|
97
|
+
/**
|
|
98
|
+
* A reporting line's OWN prefix decides whether it names a failure. Anchored at the start (after the
|
|
99
|
+
* reporter's indent) so a token appearing INSIDE a passing line (e.g. ` ok - FAIL - ★★★ message`) can
|
|
100
|
+
* never be extracted as a failure — that false positive made a green run look red.
|
|
101
|
+
* own prefix: `FAIL`, `FAIL -`/`FAIL:`, `FAILURES:`; assertion bullets `- <name>` / `* <name>`; `✗`/`✘`.
|
|
102
|
+
* line-anchored abort markers: `SyntaxError:`, `TypeError:`, `ReferenceError:`, `Error:`.
|
|
103
|
+
* `ok`-prefixed lines are excluded explicitly, so a passing bullet is never counted.
|
|
104
|
+
*/
|
|
105
|
+
function namedFailureLines(text) {
|
|
106
|
+
const out = []
|
|
107
|
+
for (const raw of String(text || '').split(/\r?\n/)) {
|
|
108
|
+
const l = raw.replace(/\s+$/, '')
|
|
109
|
+
if (!l.trim()) continue
|
|
110
|
+
if (/^\s*ok\b/.test(l)) continue // a PASSING line, whatever it contains
|
|
111
|
+
if (/^\s*(?:FAIL\b|FAILURES:|✗|✘)/.test(l)) { out.push(l); continue }
|
|
112
|
+
if (/^\s*(?:-|\*)\s+\S/.test(l)) { out.push(l); continue }
|
|
113
|
+
if (/^\s*(?:SyntaxError|TypeError|ReferenceError|Error):/.test(l)) { out.push(l); continue }
|
|
114
|
+
}
|
|
115
|
+
return out
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function failureDetail(r) {
|
|
119
|
+
const lines = (String(r.out || '') + '\n' + String(r.err || '')).split('\n').map((l) => l.replace(/\s+$/, '')).filter(Boolean)
|
|
120
|
+
const named = namedFailureLines(String(r.out || '') + '\n' + String(r.err || ''))
|
|
121
|
+
const chosen = (named.length ? named : lines).slice(-40)
|
|
122
|
+
return chosen.length ? chosen : ['(no captured output)']
|
|
123
|
+
}
|
|
124
|
+
// `--self-check`: prove the diagnostics above actually surface a NAME. Runs one synthetic failing child
|
|
125
|
+
// through the same extractor and asserts the extracted detail contains its assertion name; the mutant
|
|
126
|
+
// that strips `failureDetail` makes this red.
|
|
127
|
+
/** ONE place builds the failure line, so the reporter and the self-check cannot diverge. */
|
|
128
|
+
function failedLine(b) {
|
|
129
|
+
const kind = '[' + b.job.kind + ']'
|
|
130
|
+
const why = b.timedOut ? 'TIMEOUT after ' + Math.round(jobLimit(b.job) / 1000) + 's'
|
|
131
|
+
: 'exit ' + b.code + (b.job.expectExit ? ', required exit ' + b.job.expectExit : '')
|
|
132
|
+
return ' FAILED: ' + label(b.job) + ' ' + kind + ' (' + why + ')'
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (process.argv.includes('--self-check')) {
|
|
136
|
+
const { spawnSync } = await import('node:child_process')
|
|
137
|
+
const synth = spawnSync(process.execPath, ['-e', "console.error(' - synthetic assertion name XYZ'); process.exit(1)"], { encoding: 'utf8' })
|
|
138
|
+
const detail = failureDetail({ out: synth.stdout, err: synth.stderr }).join('\n')
|
|
139
|
+
const okSelf = detail.indexOf('synthetic assertion name XYZ') !== -1
|
|
140
|
+
console.log((okSelf ? 'SELF-CHECK PASS' : 'SELF-CHECK FAIL') + ': a failing child yields a NAMED assertion line (' + detail.trim().slice(0, 80) + ')')
|
|
141
|
+
// FALSE-POSITIVE direction: a PASSING line whose message literally contains `FAIL - ` must extract
|
|
142
|
+
// NOTHING (the v2/v3 owner saw a green run look red this way).
|
|
143
|
+
const leaked = namedFailureLines(' ok - FAIL - ★★★ [F2cap] the assertion message mentions FAIL - inside it\n')
|
|
144
|
+
const okPassing = leaked.length === 0
|
|
145
|
+
console.log((okPassing ? 'SELF-CHECK PASS' : 'SELF-CHECK FAIL') + ': a passing line containing "FAIL - " is NOT extracted as a failure (' + JSON.stringify(leaked) + ')')
|
|
146
|
+
// FALSE-NEGATIVE direction stays closed: a genuine FAIL line and a line-anchored abort marker extract.
|
|
147
|
+
const okReal = namedFailureLines(' FAIL - ★★★ real failure\n').length === 1
|
|
148
|
+
const okAbort = namedFailureLines('TypeError: boom\n').length === 1
|
|
149
|
+
console.log((okReal && okAbort ? 'SELF-CHECK PASS' : 'SELF-CHECK FAIL') + ': a genuine FAIL line and an abort marker are still extracted')
|
|
150
|
+
// TIMEOUT case on the REAL path: a synthetic job that sleeps past a 1 s per-job limit, run through
|
|
151
|
+
// the SAME runSuite/reporting path the gate uses. A string-only check could pass with a half-armed
|
|
152
|
+
// timer; this one cannot, because it reads the run own timedOut/exit and the line the reporter builds.
|
|
153
|
+
const slow = await runSuite({ file: '(synthetic-sleeper)', args: [], expectExit: 0, kind: 'probe', eval: 'setTimeout(() => {}, 5000)', timeoutMs: 1000 })
|
|
154
|
+
const killedByRunner = slow.timedOut === true && slow.code !== 0
|
|
155
|
+
const line = failedLine(slow)
|
|
156
|
+
const namedTimeout = /FAILED: .*\[probe\] \(TIMEOUT after 1s\)/.test(line)
|
|
157
|
+
console.log((killedByRunner ? 'SELF-CHECK PASS' : 'SELF-CHECK FAIL') + ': the RUNNER kills a sleeping job at its per-job limit (timedOut=true, exit=' + slow.code + ')')
|
|
158
|
+
console.log((namedTimeout ? 'SELF-CHECK PASS' : 'SELF-CHECK FAIL') + ': the failure line names the timeout: ' + line.trim())
|
|
159
|
+
// OVERRIDE case (real path): the named override must actually EXTEND the limit. A job that sleeps
|
|
160
|
+
// 1.5 s would be killed by 1 s, so it must survive under the override and report its own seconds.
|
|
161
|
+
const NAMED_OVERRIDES = ['v2-fix-probes.mutants.mjs', 'v3-fix-probes.mutants.mjs', 'v5-institute-fixes.mutants.mjs']
|
|
162
|
+
const overrideOk = NAMED_OVERRIDES.every((f) => jobLimit({ file: f }) === 900000)
|
|
163
|
+
&& jobLimit({ file: 'anything-else.mjs' }) === SUITE_TIMEOUT_MS
|
|
164
|
+
&& jobLimit({ file: 'formal-verify-v4.mutants.mjs' }) === SUITE_TIMEOUT_MS // measured 70.9 s: no override needed
|
|
165
|
+
const survived = await runSuite({ file: '(synthetic-ok)', args: [], expectExit: 0, kind: 'probe', eval: 'setTimeout(() => {}, 1500)', timeoutMs: 900000 })
|
|
166
|
+
const extended = survived.timedOut === false && survived.code === 0
|
|
167
|
+
const overrideLine = failedLine({ job: { file: 'v2-fix-probes.mutants.mjs', args: [], kind: 'probe', expectExit: 0 }, code: null, timedOut: true })
|
|
168
|
+
const namesOverride = /TIMEOUT after 900s/.test(overrideLine)
|
|
169
|
+
console.log((overrideOk ? 'SELF-CHECK PASS' : 'SELF-CHECK FAIL') + ': a named override is resolved for its suite and nothing else')
|
|
170
|
+
console.log((extended ? 'SELF-CHECK PASS' : 'SELF-CHECK FAIL') + ': a job that would die at 1 s SURVIVES under the override (real runSuite)')
|
|
171
|
+
console.log((namesOverride ? 'SELF-CHECK PASS' : 'SELF-CHECK FAIL') + ': a timed-out override reports its own limit (' + overrideLine.trim() + ')')
|
|
172
|
+
process.exit(okSelf && okPassing && okReal && okAbort && killedByRunner && namedTimeout && overrideOk && extended && namesOverride ? 0 : 1)
|
|
173
|
+
}
|
|
65
174
|
// The scripts that genuinely cannot run without arguments. They are named here (with the exact
|
|
66
175
|
// command a human must run) instead of being omitted quietly: an entry that no longer exists
|
|
67
176
|
// aborts the run, so a rename cannot turn into a silent gap.
|
|
@@ -81,6 +190,12 @@ const VARIANTS = [
|
|
|
81
190
|
// The invariant scanner's own falsifiability check: it exits 0 only when every injected
|
|
82
191
|
// mutation really made a run go RED (and the unmutated control stayed green).
|
|
83
192
|
{ file: 'audit-prompt-invariants.mjs', args: ['--self-probe'] },
|
|
193
|
+
// The participant-set proof, shipped: the guard's own predicates against broken strings.
|
|
194
|
+
{ file: 'audit-participant-set-parity.mjs', args: ['--self-probe'] },
|
|
195
|
+
// §7/§8 predicates, shipped: same functions fed broken strings.
|
|
196
|
+
{ file: 'audit-math-computation-parity.mjs', args: ['--self-probe'] },
|
|
197
|
+
// §30 verify-provenance predicates (case 3), shipped.
|
|
198
|
+
{ file: 'math-computation-shared.test.mjs', args: ['--self-probe'] },
|
|
84
199
|
// The two-registration-paths probe, inverted: it applies a real description mutation and
|
|
85
200
|
// REQUIRES the parity check to exit 1 (exit 2 means the mutation no longer applies = drift).
|
|
86
201
|
{
|
|
@@ -114,22 +229,38 @@ for (const v of VARIANTS) {
|
|
|
114
229
|
if (DEV_CHECKOUT) { console.error('a VARIANTS entry names a missing script: ' + v.file); process.exit(2) }
|
|
115
230
|
continue
|
|
116
231
|
}
|
|
117
|
-
|
|
232
|
+
// A VARIANT is an EXTRA instrumented run of the SAME script (argument variants), never an additional
|
|
233
|
+
// suite: the counting unit for `suites` is "the suites proper", one per script. Classifying by file name
|
|
234
|
+
// here would inflate `suites` whenever a suite gains a `--self-probe` job (R18: it did, 44 -> 45).
|
|
235
|
+
suites.push({ file: v.file, args: v.args || [], expectExit: v.expectExit || 0, kind: 'probe' })
|
|
118
236
|
}
|
|
119
237
|
suites.sort((a, b) => (label(a) < label(b) ? -1 : 1))
|
|
120
238
|
if (only.length) suites = suites.filter((j) => only.some((o) => label(j).includes(o)))
|
|
121
239
|
if (exclude.length) suites = suites.filter((j) => !exclude.some((o) => label(j).includes(o)))
|
|
122
240
|
if (!suites.length) { console.error('no suites matched'); process.exit(2) }
|
|
123
241
|
|
|
242
|
+
// --counts: print the DERIVED suite/probe totals (the same job list the gate runs) as JSON, then
|
|
243
|
+
// exit. Docs quote these numbers, so they must be derived and checked rather than typed by hand.
|
|
244
|
+
if (process.argv.includes('--counts')) {
|
|
245
|
+
const suiteN = suites.filter((j) => j.kind === 'suite').length
|
|
246
|
+
console.log(JSON.stringify({ total: suites.length, suites: suiteN, probes: suites.length - suiteN }))
|
|
247
|
+
process.exit(0)
|
|
248
|
+
}
|
|
249
|
+
|
|
124
250
|
function runSuite(job) {
|
|
125
251
|
return new Promise((resolve) => {
|
|
126
252
|
const t0 = Date.now()
|
|
127
|
-
const
|
|
253
|
+
const argv = job.eval ? ['-e', job.eval] : [join(HERE, job.file), ...job.args]
|
|
254
|
+
const limitMs = jobLimit(job)
|
|
255
|
+
const child = spawn(process.execPath, argv, { cwd: REPO, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
256
|
+
let timedOut = false
|
|
257
|
+
const killer = setTimeout(() => { timedOut = true; try { child.kill('SIGKILL') } catch (e) { /* already gone */ } }, limitMs)
|
|
258
|
+
const clearKiller = () => clearTimeout(killer)
|
|
128
259
|
let out = '', err = ''
|
|
129
260
|
child.stdout.on('data', (d) => { out += d.toString() })
|
|
130
261
|
child.stderr.on('data', (d) => { err += d.toString() })
|
|
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 }))
|
|
262
|
+
child.on('error', (e) => { clearKiller(); resolve({ job, code: -1, ms: Date.now() - t0, out, err: err + '\n' + String(e), timedOut }) })
|
|
263
|
+
child.on('close', (code) => { clearKiller(); resolve({ job, code, ms: Date.now() - t0, out, err, timedOut }) })
|
|
133
264
|
})
|
|
134
265
|
}
|
|
135
266
|
|
|
@@ -154,13 +285,11 @@ async function worker(id) {
|
|
|
154
285
|
(tail ? ' ' + tail.slice(0, 60) : '')
|
|
155
286
|
)
|
|
156
287
|
if (!r.ok) {
|
|
157
|
-
const
|
|
158
|
-
for (const l of lines.slice(-15)) console.log(' ' + l)
|
|
288
|
+
for (const l of failureDetail(r)) console.log(' ' + l)
|
|
159
289
|
}
|
|
160
290
|
} else if (!r.ok) {
|
|
161
291
|
// In --json mode keep stdout machine-readable: the failure detail travels in the JSON.
|
|
162
|
-
|
|
163
|
-
r.tailDetail = lines.slice(-15).join('\n')
|
|
292
|
+
r.tailDetail = failureDetail(r).join('\n')
|
|
164
293
|
}
|
|
165
294
|
}
|
|
166
295
|
}
|
|
@@ -193,7 +322,8 @@ if (asJson) {
|
|
|
193
322
|
console.log('TOTAL ' + results.length + ' PASS ' + (results.length - bad.length) + ' FAIL ' + bad.length
|
|
194
323
|
+ ' (suites ' + suiteCount + ' · probes ' + (results.length - suiteCount) + ')')
|
|
195
324
|
for (const b of bad) {
|
|
196
|
-
console.log(
|
|
325
|
+
console.log(failedLine(b))
|
|
326
|
+
for (const l of failureDetail(b)) console.log(' ' + l)
|
|
197
327
|
}
|
|
198
328
|
for (const [f, why] of Object.entries(NEEDS_ARGS)) {
|
|
199
329
|
console.log(' SKIPPED (needs CLI args): ' + f + ' — ' + why + (presentSet.has(f) ? '' : ' [not present in this checkout]'))
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// In-repo mutant harness for the runner-diagnostics guard (ships: `tests/*.mutants.mjs`).
|
|
2
|
+
// It proves the `--self-check` guard can REDDEN: strip the named-failure detail from a COPY of
|
|
3
|
+
// run-tests.mjs, then run that copy's own `--self-check` and require it to fail.
|
|
4
|
+
// Usage: node tests/run-tests.mutants.mjs
|
|
5
|
+
import { mkdtempSync, copyFileSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
|
|
6
|
+
import { tmpdir } from 'node:os'
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
import { spawnSync } from 'node:child_process'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
|
|
11
|
+
const HERE = fileURLToPath(new URL('./', import.meta.url))
|
|
12
|
+
const REPO = join(HERE, '..')
|
|
13
|
+
const RUNNER = join(HERE, 'run-tests.mjs')
|
|
14
|
+
|
|
15
|
+
let passed = 0, failed = 0
|
|
16
|
+
const ok = (cond, label, detail) => {
|
|
17
|
+
if (cond) { passed++; console.log(' ok ' + label); return true }
|
|
18
|
+
failed++; console.log(' FAIL ' + label + (detail ? ' — ' + detail : '')); return false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// baseline: the real runner's self-check must pass
|
|
22
|
+
const base = spawnSync(process.execPath, [RUNNER, '--self-check'], { cwd: REPO, encoding: 'utf8' })
|
|
23
|
+
ok(base.status === 0 && /SELF-CHECK PASS/.test(base.stdout || ''),
|
|
24
|
+
'baseline: the runner self-check passes (a failing child yields a named assertion)',
|
|
25
|
+
JSON.stringify(String(base.stdout || '').trim().slice(0, 80)))
|
|
26
|
+
|
|
27
|
+
// mutant: strip the named-failure extraction (fall back to a constant) -> the self-check must REDDEN
|
|
28
|
+
const dir = mkdtempSync(join(tmpdir(), 'runner-mut-'))
|
|
29
|
+
// Normalise CRLF so the anchor is line-ending agnostic (the runner is checked out with CRLF on Windows).
|
|
30
|
+
const src = readFileSync(RUNNER, 'utf8').replace(/\r\n/g, '\n')
|
|
31
|
+
const from = " const chosen = (named.length ? named : lines).slice(-40)\n return chosen.length ? chosen : ['(no captured output)']"
|
|
32
|
+
if (src.indexOf(from) === -1) {
|
|
33
|
+
ok(false, 'mutant anchor applies (the named-failure extraction is present)', 'ANCHOR MISS')
|
|
34
|
+
} else {
|
|
35
|
+
writeFileSync(join(dir, 'run-tests.mjs'), src.replace(from, " return ['(no captured output)']"))
|
|
36
|
+
const mut = spawnSync(process.execPath, [join(dir, 'run-tests.mjs'), '--self-check'], { cwd: REPO, encoding: 'utf8' })
|
|
37
|
+
ok(mut.status !== 0 && /SELF-CHECK FAIL/.test(String(mut.stdout || '')),
|
|
38
|
+
'★ mutant (named-failure detail stripped) makes the self-check RED',
|
|
39
|
+
'exit=' + mut.status + ' out=' + JSON.stringify(String(mut.stdout || '').trim().slice(0, 80)))
|
|
40
|
+
}
|
|
41
|
+
rmSync(dir, { recursive: true, force: true })
|
|
42
|
+
// TIMEOUT path, BOTH directions (a criterion must be validated against a known-positive AND a
|
|
43
|
+
// known-negative before it gates anything): the same real suite under a 1 s limit must be killed
|
|
44
|
+
// and NAMED in the failure list; under the default limit it must not be reported as a timeout.
|
|
45
|
+
const slowRun = spawnSync(process.execPath, [RUNNER, '--only', 'audit-prompt-invariants'], { cwd: REPO, encoding: 'utf8', env: Object.assign({}, process.env, { GATE_SUITE_TIMEOUT_MS: '1000' }) })
|
|
46
|
+
const slowOut = String(slowRun.stdout || '') + String(slowRun.stderr || '')
|
|
47
|
+
const failList = slowOut.split('\n').filter((l) => /^\s*FAILED:/.test(l))
|
|
48
|
+
const timedOutLine = failList.find((l) => /\[probe\] \(TIMEOUT after 1s\)/.test(l)) || ''
|
|
49
|
+
ok(slowRun.status !== 0 && !!timedOutLine,
|
|
50
|
+
'★ POSITIVE: a 1 s limit makes the runner kill the suite and NAME the timeout in the failure list',
|
|
51
|
+
'exit=' + slowRun.status + ' line=' + timedOutLine.trim())
|
|
52
|
+
// NEGATIVE (two readings, so the criterion cannot be always-red):
|
|
53
|
+
// (a) ANY suite under the DEFAULT limit produces no TIMEOUT marker, whatever its own verdict;
|
|
54
|
+
// (b) a known-green fast suite exits 0 (no false timeout).
|
|
55
|
+
// (a) is deliberately verdict-agnostic: at the time of writing audit-prompt-invariants is red for an
|
|
56
|
+
// unrelated in-flight reason (v2/v3 I13/I14: the parameter schema parses to 0 keys), and a proof tool
|
|
57
|
+
// must not inherit someone else's red.
|
|
58
|
+
const fastRun = spawnSync(process.execPath, [RUNNER, '--only', 'audit-prompt-invariants'], { cwd: REPO, encoding: 'utf8' })
|
|
59
|
+
const fastOut = String(fastRun.stdout || '') + String(fastRun.stderr || '')
|
|
60
|
+
ok(!/TIMEOUT after/.test(fastOut),
|
|
61
|
+
'NEGATIVE (a): with the default limit no TIMEOUT marker is produced (the criterion is not always-red)',
|
|
62
|
+
'exit=' + fastRun.status + ' (its own verdict is another owner\'s concern)')
|
|
63
|
+
const greenRun = spawnSync(process.execPath, [RUNNER, '--only', 'audit-package-membership'], { cwd: REPO, encoding: 'utf8' })
|
|
64
|
+
const greenOut = String(greenRun.stdout || '') + String(greenRun.stderr || '')
|
|
65
|
+
ok(greenRun.status === 0 && !/TIMEOUT after/.test(greenOut),
|
|
66
|
+
'NEGATIVE (b): a known-green suite exits 0 under the default limit (no false timeout)',
|
|
67
|
+
'exit=' + greenRun.status)
|
|
68
|
+
console.log('')
|
|
69
|
+
console.log('=== RUNNER DIAGNOSTICS MUTANTS: ' + passed + ' passed, ' + failed + ' failed ===')
|
|
70
|
+
process.exit(failed === 0 ? 0 : 1)
|
package/tests/selfdrive-v5.mjs
CHANGED
|
@@ -257,7 +257,21 @@ async function drainVerifyRound(n) {
|
|
|
257
257
|
console.log('-- V5 self-drive --')
|
|
258
258
|
|
|
259
259
|
// ---------- founding ----------
|
|
260
|
-
|
|
260
|
+
// F6 tightening (staff): the persona parameter must actually REACH the member prompts. MEASURED: a
|
|
261
|
+
// single-site mutant that drops `params.staffPersona` (`const extra = ''`) is caught by NO current
|
|
262
|
+
// suite (audit-participant-set-parity ok, audit-persona-sensitivity ok, audit-persona-surface 276/0).
|
|
263
|
+
await callTool('vibe_v5_set', { staffPersona: 'STAFF-MARKER-X' })
|
|
264
|
+
// V5-A5 (path contract, docs-vs-code class): `memberPathContractOk()` (vibe-math-v5.js:993) compares
|
|
265
|
+
// the DOCUMENTED member root with the root the framework actually writes; a divergence logs a NAMED
|
|
266
|
+
// error and folds into noteLoadProblem. MEASURED: no suite referenced it before this assertion.
|
|
267
|
+
const pathErrs = []
|
|
268
|
+
const realErrA5 = console.error
|
|
269
|
+
console.error = (...a) => { const line = a.map(String).join(' '); if (/member library path contract BROKEN/.test(line)) pathErrs.push(line); realErrA5(...a) }
|
|
270
|
+
let started
|
|
271
|
+
try {
|
|
272
|
+
started = await callTool('vibe_v5_start', { problem: '证明:不存在整数解 x^n + y^n = z^n(n>2)的初等情形', researcherCount: 3 })
|
|
273
|
+
} finally { console.error = realErrA5 }
|
|
274
|
+
assert(pathErrs.length === 0, '* V5-A5 the documented member root equals the root the framework writes (no path-contract divergence; got ' + JSON.stringify(pathErrs.slice(0, 1)) + ')')
|
|
261
275
|
assert(started.ok === true, 'vibe_v5_start ok (' + JSON.stringify(started).slice(0, 150) + ')')
|
|
262
276
|
assert(spawns.length === 4, 'founded 1 academician + 3 researchers (got ' + spawns.length + ')')
|
|
263
277
|
assert(!!spawnOf('acad'), 'academician acad exists')
|
|
@@ -332,6 +346,27 @@ assert(!!sv.verify, 'verification of ' + target + ' is in flight right after pro
|
|
|
332
346
|
JSON.stringify({ verify: sv.verify, queue: sv.verifyQueue, undecided: sv.undecided, running: sv.running, autoDone: sv.autoDone, phase: sv.phase, meeting: sv.meeting, parked: sv.parkedMeeting, tasks: sv.tasks.length }))
|
|
333
347
|
if (sv.verify) {
|
|
334
348
|
assert(sv.verify.m === 3 && sv.verify.P === 4, 'verification reports m=3 over P=4 voters')
|
|
349
|
+
// V5-A1: P=4 is a COUNT - assert the semantic unit too. The voter set is the roster identity that
|
|
350
|
+
// quorum is computed over, so a duplicate id (P unchanged) must not pass. Measured shape:
|
|
351
|
+
// status.verify.voters is the id array (product: `voters: E` in the verdict payload).
|
|
352
|
+
// V5-A1: the SHAPE was measured first - status.verify carries {target,kind,stage,round,voted,m,P} but
|
|
353
|
+
// NO voter list (vibe-math-v5.js:7316); the voter IDS live in `status.quorum` (quorumView(), :7277/:1262).
|
|
354
|
+
const voterIds = (sv.quorum && Array.isArray(sv.quorum.voters)) ? sv.quorum.voters.map(String).sort() : null
|
|
355
|
+
assert(!!voterIds && voterIds.join(',') === ['acad', 'r-1', 'r-2', 'r-3'].sort().join(','), '* V5-A1 the voter id SET is exactly {acad, r-1, r-2, r-3} (P=4 keeps the count; got ' + JSON.stringify(voterIds) + ')')
|
|
356
|
+
// V5-A2 (roster): the published participant SET and its own COUNT must agree, and the ids must be
|
|
357
|
+
// distinct. MEASURED: a mutant that publishes `vs.slice(1)` as `voters` while keeping `voterCount`
|
|
358
|
+
// is caught by NO current suite (the parity audit checks field PRESENCE, not agreement).
|
|
359
|
+
const qv = sv.quorum || {}
|
|
360
|
+
const qvoters = Array.isArray(qv.voters) ? qv.voters.map(String) : null
|
|
361
|
+
assert(!!qvoters && qvoters.length === Number(qv.voterCount), '* V5-A2 the published voter SET agrees with its own count (voters=' + (qvoters ? qvoters.length : 'none') + ' voterCount=' + qv.voterCount + ')')
|
|
362
|
+
assert(!!qvoters && new Set(qvoters).size === qvoters.length, '* V5-A2 and the published voter ids are DISTINCT (got ' + JSON.stringify(qvoters) + ')')
|
|
363
|
+
// V5-A3 (staff): the staffPersona set before founding must appear in the member PERSONA/prompts.
|
|
364
|
+
// NOTE (measured): the existing `every member got the full charter` length check stays GREEN under
|
|
365
|
+
// the mutant, so the marker search is the only discriminator.
|
|
366
|
+
const marker = 'STAFF-MARKER-X'
|
|
367
|
+
const sawStaff = spawns.some((sp) => String(sp.persona || '').indexOf(marker) !== -1)
|
|
368
|
+
|| wakes.some((w) => (((w.blocks && w.blocks[0] && w.blocks[0].text) || '')).indexOf(marker) !== -1)
|
|
369
|
+
assert(sawStaff, '* V5-A3 the staff persona reaches the member persona/prompts (marker=' + marker + '; spawns=' + spawns.length + ', wakes=' + wakes.length + ')')
|
|
335
370
|
|
|
336
371
|
// Round: only ONE boolean vote (< m) must NOT verify.
|
|
337
372
|
plannedVotes = new Map([['acad', 0.9], ['r-1', 1], ['r-2', 0.5], ['r-3', 0.7]])
|
|
@@ -396,6 +431,23 @@ const t1 = await callTool('vibe_v5_task_create', { subject: '整理已知特例'
|
|
|
396
431
|
assert(t1.ok === true && t1.task.revision === 1, 'a member can open a task (revision 1)')
|
|
397
432
|
const stale = await callTool('vibe_v5_task_update', { task_id: t1.task.id, expected_revision: 99, action: 'claim' }, childAgent(childOf('r-3')))
|
|
398
433
|
assert(stale.ok === false && stale.code === 'V5_TASK_STALE_REVISION', 'a stale revision is refused (compare-and-set)')
|
|
434
|
+
// V5-A4 (CAS semantic unit) - R17 scope refinement: the assertion detects any refused-but-written
|
|
435
|
+
// change VISIBLE IN THE OBSERVABLE TRIPLE (revision|ownerId|status). Measured by R17: a
|
|
436
|
+
// `task.status='done'` write inside the refusal branch => V5-A4 RED (`before=1||pending after=1||done`);
|
|
437
|
+
// a write OUTSIDE the triple (e.g. `task.notes`) => V5-A4 green and remains a MEASURED BOUNDARY (the
|
|
438
|
+
// harness cannot see it). The earlier, broader phrasing ("cannot be the sole discriminator") is
|
|
439
|
+
// superseded by this scope statement.
|
|
440
|
+
// The assertions above (here,
|
|
441
|
+
// in e2e-v5-round2 and in prompt-v5-integrity) only check the RETURN VALUE, so a mutant that refuses
|
|
442
|
+
// but still writes - or writes first and checks afterwards - would pass all of them. Read the task
|
|
443
|
+
// back and compare the observable triple.
|
|
444
|
+
const t1AfterStale = await callTool('vibe_v5_task_get', { task_id: t1.task.id }, childAgent(childOf('r-3')))
|
|
445
|
+
const taskOf = (r) => (r && r.task) ? r.task : (r && r.id ? r : null)
|
|
446
|
+
const t1Before = taskOf(t1) || {}
|
|
447
|
+
const t1Now = taskOf(t1AfterStale) || {}
|
|
448
|
+
const tupleOf = (t) => [t.revision, t.ownerId || '', t.status || ''].join('|')
|
|
449
|
+
assert(!!t1Now.revision, '* V5-A4 the task is readable back after the refusal (got ' + JSON.stringify(t1AfterStale).slice(0, 120) + ')')
|
|
450
|
+
assert(tupleOf(t1Now) === tupleOf(t1Before), '* V5-A4 a REFUSED stale CAS leaves the task UNCHANGED (before=' + tupleOf(t1Before) + ' after=' + tupleOf(t1Now) + ')')
|
|
399
451
|
const claimed = await callTool('vibe_v5_task_update', { task_id: t1.task.id, expected_revision: 1, action: 'claim' }, childAgent(childOf('r-3')))
|
|
400
452
|
assert(claimed.ok === true && claimed.task.ownerId === 'r-3', 'claiming sets the owner')
|
|
401
453
|
const blocked = await callTool('vibe_v5_task_create', { subject: '依赖前一个', description: 'd', blocked_by: [t1.task.id] }, childAgent(childOf('r-3')))
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Registered mutant harness for the v5 self-drive suite — the suite carries its own proof.
|
|
2
|
+
// Mirrors tests/formal-verify-v4.mutants.mjs / formal-verify-v3.mutants.mjs: a SINGLE content-anchored
|
|
3
|
+
// mutation of the v5 preset (no line numbers), run through the V5_PLUGIN seam, bounded child runs (a
|
|
4
|
+
// timeout is a HANG, never a pass), skipped families printed and counted, and the family's TOTAL WALL
|
|
5
|
+
// TIME reported (per-family numbers alone can hide a timeout).
|
|
6
|
+
// Run: node tests/selfdrive-v5.mutants.mjs
|
|
7
|
+
import { readFileSync, writeFileSync, mkdirSync, copyFileSync, rmSync, readdirSync } from 'node:fs'
|
|
8
|
+
import { execFileSync } from 'node:child_process'
|
|
9
|
+
import { dirname, join } from 'node:path'
|
|
10
|
+
import { tmpdir } from 'node:os'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
13
|
+
const REPO = join(HERE, '..')
|
|
14
|
+
const PRESET = 'vibe-math-v5'
|
|
15
|
+
const MAIN = PRESET + '.js'
|
|
16
|
+
const SUITE = 'tests/selfdrive-v5.mjs'
|
|
17
|
+
const ENV = 'V5_PLUGIN'
|
|
18
|
+
// Measured: a selfdrive-v5 child run is ~2 s; MUTANT_CHILD_TIMEOUT_MS overrides it.
|
|
19
|
+
const CHILD_TIMEOUT_MS = Number(process.env.MUTANT_CHILD_TIMEOUT_MS || 300000)
|
|
20
|
+
const TIMES = []
|
|
21
|
+
const hangs = []
|
|
22
|
+
const skipped = []
|
|
23
|
+
function copyGraph(file, dest) {
|
|
24
|
+
const src = join(REPO, PRESET, file)
|
|
25
|
+
copyFileSync(src, join(dest, file))
|
|
26
|
+
for (const m of readFileSync(src, 'utf8').matchAll(/from\s+'(\.\/[A-Za-z0-9_.-]+\.js)'/g)) {
|
|
27
|
+
const dep = m[1].slice(2)
|
|
28
|
+
if (!readdirSync(dest).includes(dep)) copyGraph(dep, dest)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function runFamily(f) {
|
|
32
|
+
const dest = join(tmpdir(), 'v5sd-mut-' + Math.random().toString(36).slice(2, 10))
|
|
33
|
+
mkdirSync(dest, { recursive: true })
|
|
34
|
+
copyGraph(MAIN, dest)
|
|
35
|
+
const target = join(dest, f.editFile || MAIN)
|
|
36
|
+
const before = readFileSync(target, 'utf8')
|
|
37
|
+
const n = before.split(f.from).length - 1
|
|
38
|
+
const okCount = f.lastOnly || f.all ? n >= 1 : n === 1
|
|
39
|
+
if (!okCount) {
|
|
40
|
+
skipped.push(f.name + ' (anchor x' + n + ')')
|
|
41
|
+
console.error(' SKIP - ' + f.name + ' (anchor x' + n + ')')
|
|
42
|
+
rmSync(dest, { recursive: true, force: true })
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
writeFileSync(target, before.split(f.from).join(f.to), 'utf8')
|
|
46
|
+
try { execFileSync(process.execPath, ['--check', target], { stdio: 'pipe' }) }
|
|
47
|
+
catch (e) {
|
|
48
|
+
writeFileSync(target, before, 'utf8')
|
|
49
|
+
skipped.push(f.name + ' (mutation did not compile)')
|
|
50
|
+
console.error(' SKIP - ' + f.name + ' (mutation did not compile; restored)')
|
|
51
|
+
rmSync(dest, { recursive: true, force: true })
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
const t0 = Date.now()
|
|
55
|
+
let out = ''
|
|
56
|
+
let code = 0
|
|
57
|
+
let hang = false
|
|
58
|
+
try {
|
|
59
|
+
out = execFileSync(process.execPath, [SUITE], {
|
|
60
|
+
cwd: REPO, encoding: 'utf8', timeout: CHILD_TIMEOUT_MS, killSignal: 'SIGKILL',
|
|
61
|
+
env: Object.assign({}, process.env, { [ENV]: join(dest, MAIN) }),
|
|
62
|
+
})
|
|
63
|
+
} catch (e) {
|
|
64
|
+
if (e && (e.killed || e.signal === 'SIGKILL')) hang = true
|
|
65
|
+
code = (e && e.status) || 1
|
|
66
|
+
out = String((e && e.stdout) || '') + String((e && e.stderr) || '')
|
|
67
|
+
}
|
|
68
|
+
const ms = Date.now() - t0
|
|
69
|
+
TIMES.push([f.name, ms])
|
|
70
|
+
if (hang) hangs.push(f.name + '(' + Math.round(ms / 1000) + 's)')
|
|
71
|
+
const named = out.split(/\r?\n/).filter((l) => /^\s*(FAIL - | {2}- )/.test(l) && f.expect.test(l)).map((l) => l.trim())
|
|
72
|
+
const ok = !hang && code !== 0 && named.length > 0
|
|
73
|
+
console.log((hang ? ' HANG - ' : (ok ? ' ok - ' : ' FAIL - ')) + f.name + ' [' + ms + 'ms]' + (named.length ? ' :: ' + named[0].slice(0, 150) : ' :: no named red (exit=' + code + ')'))
|
|
74
|
+
rmSync(dest, { recursive: true, force: true })
|
|
75
|
+
return ok
|
|
76
|
+
}
|
|
77
|
+
const FAMILIES = [
|
|
78
|
+
{
|
|
79
|
+
// V5-A1: the LAST voter is replaced by a DUPLICATE of the first, so P stays 4 (the count assertion
|
|
80
|
+
// passes) while the voter id SET loses a member - the roster identity, not the head-count.
|
|
81
|
+
name: 'V5-A1: the last voter is a duplicate of the first (P still 4)',
|
|
82
|
+
from: "const voters = () => activeMembers().filter((m) => m.kind === 'academician' || m.kind === 'researcher')",
|
|
83
|
+
to: "const voters = () => { const v = activeMembers().filter((m) => m.kind === 'academician' || m.kind === 'researcher'); return v.length > 1 ? v.slice(0, -1).concat([v[0]]) : v }",
|
|
84
|
+
expect: /V5-A1 the voter id SET is exactly/,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
// F6 tightening (roster): the published voter set drops one member while its COUNT stays put.
|
|
88
|
+
name: 'V5-A2: the published voter set disagrees with its own count',
|
|
89
|
+
from: 'voters: vs.map((m) => m.id),',
|
|
90
|
+
to: 'voters: vs.slice(1).map((m) => m.id),',
|
|
91
|
+
expect: /V5-A2 the published voter SET agrees with its own count/,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
// F6 tightening (staff): the member persona ignores `params.staffPersona` again.
|
|
95
|
+
name: 'V5-A3: the staff persona never reaches the prompts',
|
|
96
|
+
from: "const extra = String(params.staffPersona || '').trim()",
|
|
97
|
+
to: "const extra = ''",
|
|
98
|
+
expect: /V5-A3 the staff persona reaches the member persona\/prompts/,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
// ④ CAS: the stale-revision guard is disabled, so a stale update lands (the return-value
|
|
102
|
+
// assertions AND the new "unchanged after refusal" assertion redden).
|
|
103
|
+
// R17 scope refinement for V5-A4: it detects any refused-but-written change VISIBLE IN THE
|
|
104
|
+
// OBSERVABLE TRIPLE (revision|ownerId|status) - measured: an in-branch `task.status='done'`
|
|
105
|
+
// makes it RED (`before=1||pending after=1||done`), while a write OUTSIDE the triple (e.g.
|
|
106
|
+
// `task.notes`) keeps it green and remains a MEASURED BOUNDARY (invisible to the harness).
|
|
107
|
+
// assertions AND the new "unchanged after refusal" assertion redden).
|
|
108
|
+
name: 'V5-A4: the stale-revision guard is disabled (a stale CAS lands)',
|
|
109
|
+
from: 'if (expected !== task.revision) {',
|
|
110
|
+
to: 'if (false) {',
|
|
111
|
+
expect: /V5-A4 a REFUSED stale CAS leaves the task UNCHANGED|a stale revision is refused/,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
// ⑤ path contract: ONLY the documented composition moves (instRootRel), while the framework
|
|
115
|
+
// keeps writing through instRoot() - exactly the v4 58fff7f defect class this guard exists for.
|
|
116
|
+
name: 'V5-A5: the documented member root diverges from the written root',
|
|
117
|
+
from: "const instRootRel = () => 'VibeMath/Projects/' + project + '/Institutes/' + instituteName",
|
|
118
|
+
to: "const instRootRel = () => 'VibeMath/Projects/' + project + '/Institutes/' + instituteName + 'X'",
|
|
119
|
+
expect: /V5-A5 the documented member root equals the root the framework writes/
|
|
120
|
+
},
|
|
121
|
+
]
|
|
122
|
+
let red = 0
|
|
123
|
+
for (const f of FAMILIES) { const ok = runFamily(f); if (ok) red++ }
|
|
124
|
+
const totalMs = TIMES.reduce((a, t) => a + t[1], 0)
|
|
125
|
+
console.log('')
|
|
126
|
+
console.log('mutant families reddening the v5 self-drive suite by name: ' + red + '/' + FAMILIES.length)
|
|
127
|
+
console.log('timings (per family): ' + TIMES.map((t) => String(t[0]).split(':')[0] + '=' + t[1] + 'ms').join(' '))
|
|
128
|
+
console.log('TOTAL WALL TIME (all families + setup): ' + totalMs + 'ms (' + Math.round(totalMs / 1000) + 's)')
|
|
129
|
+
console.log('hangs=[' + hangs.join(' | ') + ']')
|
|
130
|
+
console.log('skipped=[' + skipped.join(' | ') + ']')
|
|
131
|
+
if (red !== FAMILIES.length || skipped.length || hangs.length) process.exit(1)
|
|
132
|
+
console.log('ALL MUTANTS RED AS REQUIRED')
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Registered mutant harness for tests/v2-fix-probes.test.mjs — the probe carries its own proof.
|
|
2
|
+
// Defect classes: surface-shape contract (F1), declared asymmetry (F-A), path-base mixing (F6),
|
|
3
|
+
// silent-failure surfacing (F6a/F6b/F6c). Single content-anchored mutations; V2_PLUGIN seam.
|
|
4
|
+
// Exit criterion: every family reddens BY NAME; any skipped family is printed and counted (never a
|
|
5
|
+
// silent NO-OP impersonating a pass); ALL MUTANTS RED AS REQUIRED + exit 0 only then.
|
|
6
|
+
// Run: node tests/v2-fix-probes.mutants.mjs
|
|
7
|
+
import { readFileSync, writeFileSync, mkdirSync, copyFileSync, rmSync, readdirSync } from 'node:fs'
|
|
8
|
+
import { execFileSync } from 'node:child_process'
|
|
9
|
+
import { dirname, join } from 'node:path'
|
|
10
|
+
import { tmpdir } from 'node:os'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
|
|
13
|
+
// Bound every child run: run-tests has no per-suite timeout, so an unbounded child hangs the gate.
|
|
14
|
+
// A timeout is reported as HANG, counts as a failure for that family, and is listed in hangs=[].
|
|
15
|
+
const CHILD_TIMEOUT_MS = Number(process.env.MUTANT_CHILD_TIMEOUT_MS || 120000)
|
|
16
|
+
const TIMES = []
|
|
17
|
+
// R15: whole-process wall time, printed at the end (the timeout row quotes this line).
|
|
18
|
+
const START_MS = Date.now()
|
|
19
|
+
const hangs = []
|
|
20
|
+
let lastHang = false
|
|
21
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
22
|
+
const REPO = join(HERE, '..')
|
|
23
|
+
const PRESET = 'vibe-math-v2'
|
|
24
|
+
const MAIN = PRESET + '.js'
|
|
25
|
+
const SUITE = 'tests/v2-fix-probes.test.mjs'
|
|
26
|
+
const ENV = 'V2_PLUGIN'
|
|
27
|
+
function copyGraph(file, dest) {
|
|
28
|
+
const src = join(REPO, PRESET, file)
|
|
29
|
+
copyFileSync(src, join(dest, file))
|
|
30
|
+
for (const m of readFileSync(src, 'utf8').matchAll(/from\s+'(\.\/[A-Za-z0-9_.-]+\.js)'/g)) {
|
|
31
|
+
const dep = m[1].slice(2)
|
|
32
|
+
if (!readdirSync(dest).includes(dep)) copyGraph(dep, dest)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const skipped = []
|
|
36
|
+
function runFamily(f) {
|
|
37
|
+
const dest = join(tmpdir(), 'v2probes-mut-' + Math.random().toString(36).slice(2, 10))
|
|
38
|
+
mkdirSync(dest, { recursive: true })
|
|
39
|
+
copyGraph(MAIN, dest)
|
|
40
|
+
const target = join(dest, f.editFile || MAIN)
|
|
41
|
+
const before = readFileSync(target, 'utf8')
|
|
42
|
+
const n = before.split(f.from).length - 1
|
|
43
|
+
const okCount = f.lastOnly || f.firstOnly || f.all ? n >= 1 : n === 1
|
|
44
|
+
if (!okCount) { skipped.push(f.name + ' (anchor x' + n + ')'); console.error(' SKIP - ' + f.name + ' (anchor x' + n + ')'); rmSync(dest, { recursive: true, force: true }); return false }
|
|
45
|
+
let mutated
|
|
46
|
+
if (f.lastOnly) { const i = before.lastIndexOf(f.from); mutated = before.slice(0, i) + f.to + before.slice(i + f.from.length) }
|
|
47
|
+
else if (f.firstOnly) { const i = before.indexOf(f.from); mutated = before.slice(0, i) + f.to + before.slice(i + f.from.length) }
|
|
48
|
+
else if (f.all) mutated = before.split(f.from).join(f.to)
|
|
49
|
+
else mutated = before.split(f.from).join(f.to)
|
|
50
|
+
writeFileSync(target, mutated, 'utf8')
|
|
51
|
+
try { execFileSync(process.execPath, ['--check', target], { stdio: 'pipe' }) }
|
|
52
|
+
catch (e) { writeFileSync(target, before, 'utf8'); skipped.push(f.name + ' (mutation did not compile)'); console.error(' SKIP - ' + f.name + ' (mutation did not compile; restored)'); rmSync(dest, { recursive: true, force: true }); return false }
|
|
53
|
+
let out = ''
|
|
54
|
+
let code = 0
|
|
55
|
+
try { out = execFileSync(process.execPath, [SUITE], { cwd: REPO, encoding: 'utf8', timeout: CHILD_TIMEOUT_MS, killSignal: 'SIGKILL', env: Object.assign({}, process.env, { [ENV]: join(dest, MAIN) }) }) }
|
|
56
|
+
catch (e) { if (e && (e.killed || e.signal === 'SIGKILL')) lastHang = true; code = (e && e.status) || 1; out = String((e && e.stdout) || '') + String((e && e.stderr) || '') }
|
|
57
|
+
const named = out.split(/\r?\n/).filter((l) => /^\s*(FAIL - | {2}- )/.test(l) && f.expect.test(l)).map((l) => l.trim())
|
|
58
|
+
const ok = code !== 0 && named.length > 0
|
|
59
|
+
console.log((ok ? ' ok - ' : ' FAIL - ') + f.name + (named.length ? ' :: ' + named[0].slice(0, 130) : ' :: no named red (exit=' + code + ')'))
|
|
60
|
+
rmSync(dest, { recursive: true, force: true })
|
|
61
|
+
return ok
|
|
62
|
+
}
|
|
63
|
+
const ANCHOR_LEN = "pendingDecisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).length,"
|
|
64
|
+
const FAMILIES = [
|
|
65
|
+
{ name: 'F1: report pendingDecisions back to an array', expect: /\[F1\] pendingDecisions 两面同形/, from: ANCHOR_LEN, firstOnly: true,
|
|
66
|
+
to: "pendingDecisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).map(function (d) { return { id: d.id, node: d.node, context: d.context } })," },
|
|
67
|
+
{ name: 'F-A: status gains pendingDecisionItems', expect: /\[F-A\] status \*\*不得\*\*携带明细/, from: ANCHOR_LEN,
|
|
68
|
+
to: ANCHOR_LEN + "\n pendingDecisionItems: decisionQueue.filter(function (d) { return d.status === 'pending' }).map(function (d) { return { id: d.id, node: d.node, context: d.context } }),", lastOnly: true },
|
|
69
|
+
{ name: 'F6: paths base dropped from both surfaces', expect: /\[F6\] status\.formal\.paths 带 base \+ note/,
|
|
70
|
+
from: "paths: { base: frameworkRoot() + '/', project: 'Formal/', proofs: 'Verified/Lean/', lib:",
|
|
71
|
+
to: "paths: { project: 'Formal/', proofs: 'Verified/Lean/', lib:", all: true },
|
|
72
|
+
{ name: 'F6a: provider list-failure warning removed', expect: /\[F6a\]/,
|
|
73
|
+
from: "warnProviderFallback('subagents.list() failed: ' + ((e && e.message) || e))", to: 'void e' },
|
|
74
|
+
{ name: 'F6b: interrupt emit statement deleted (anchored by content)', expect: /\[F6b\/v2\]/,
|
|
75
|
+
from: "if (r && r.ok === false) logActivity('interrupt', '中断失败(' + why + '):' + String(cid) + ' — ' + String(r.message || r.code || ''))", to: 'void r' },
|
|
76
|
+
{ name: 'F6c: project-pointer write failure re-silenced', expect: /\[F6c\]/,
|
|
77
|
+
from: "noteStateWriteFailure('current.' + safeId(sessionId) + '.json', (e && e.message) || e)", to: 'void e' },
|
|
78
|
+
]
|
|
79
|
+
let red = 0
|
|
80
|
+
FAMILIES.push(
|
|
81
|
+
{ name: 'F5/v2: fence-root comparison removed', expect: /\[F5\/v2\] 围栏根漂移/,
|
|
82
|
+
from: 'if (actual && expected && actual !== expected) warnFenceDriftOnce(actual, expected)', to: 'void actual' },
|
|
83
|
+
)
|
|
84
|
+
FAMILIES.push(
|
|
85
|
+
{ name: 'F-B: derivation drops the source descriptions', expect: /\[F-B\/v2\] 每个机读描述都来自真源且非空/,
|
|
86
|
+
from: 'if (e.description) p.description = e.description', to: 'void e /* MUTANT F-B: no source description */' },
|
|
87
|
+
{ name: 'F-B: one exception entry deleted', expect: /\[F-B\/v2\] 例外集合 == docs\/parameter-schema\.md/,
|
|
88
|
+
from: "solverToolAllow: { type: 'array', items: { type: 'string' } },", to: '' },
|
|
89
|
+
{ name: 'F-B: one registration site reverted to a literal', expect: /\[F-B\/v2\] 两处注册点都由 paramProps\(\)/,
|
|
90
|
+
from: "objParams(paramProps()), 'vibe_math_set_params')", to: "objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }), 'vibe_math_set_params')" },
|
|
91
|
+
)
|
|
92
|
+
for (const f of FAMILIES) { const t0 = Date.now(); lastHang = false; const ok = runFamily(f); const ms = Date.now() - t0; TIMES.push([f.name, ms]); if (lastHang) hangs.push(f.name + "(" + Math.round(ms / 1000) + "s)"); if (ok) red++ }
|
|
93
|
+
console.log('')
|
|
94
|
+
console.log('mutant families reddening the v2 probe by name: ' + red + '/' + FAMILIES.length)
|
|
95
|
+
console.log('skipped=[' + skipped.join(' | ') + ']')
|
|
96
|
+
console.log('timings: ' + TIMES.map((t) => String(t[0]).split(':')[0] + '=' + t[1] + 'ms').join(' '))
|
|
97
|
+
console.log('TOTAL WALL TIME (all families + setup): ' + (Date.now() - START_MS) + 'ms (' + Math.round((Date.now() - START_MS) / 1000) + 's)')
|
|
98
|
+
console.log('hangs=[' + hangs.join(' | ') + ']')
|
|
99
|
+
if (red !== FAMILIES.length || skipped.length || hangs.length) process.exit(1)
|
|
100
|
+
console.log('ALL MUTANTS RED AS REQUIRED')
|