dsh-vibe-math 2.7.2 → 2.8.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/LICENSE +21 -21
- package/README.en.md +682 -681
- package/README.md +683 -682
- package/cordis.patch.yml +112 -16
- package/docs/AUDIT-CHECKLIST.md +755 -518
- package/docs/COMPAT-AUDIT-ROUND2.md +354 -328
- package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
- package/docs/audit-methodology.md +93 -0
- package/docs/final-paper.md +172 -172
- package/docs/formal-verification.md +613 -588
- package/docs/generate_framework_diagram_v2.py +114 -114
- package/docs/generate_framework_diagram_v3.py +127 -127
- package/docs/math-computation.md +195 -187
- package/docs/parameter-schema.md +37 -0
- package/docs/release-notes/README.en.md +2 -0
- package/docs/release-notes/README.md +2 -0
- package/docs/release-notes/RELEASE-NOTES-2.0.22.md +112 -112
- package/docs/release-notes/RELEASE-NOTES-2.1.0.md +143 -143
- package/docs/release-notes/RELEASE-NOTES-2.2.0.md +266 -266
- package/docs/release-notes/RELEASE-NOTES-2.2.1.md +43 -43
- package/docs/release-notes/RELEASE-NOTES-2.2.2.md +88 -88
- package/docs/release-notes/RELEASE-NOTES-2.3.0.md +207 -207
- package/docs/release-notes/RELEASE-NOTES-2.3.1.md +134 -134
- package/docs/release-notes/RELEASE-NOTES-2.3.10.md +105 -105
- package/docs/release-notes/RELEASE-NOTES-2.3.11.md +57 -57
- package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -80
- package/docs/release-notes/RELEASE-NOTES-2.3.2.md +145 -145
- package/docs/release-notes/RELEASE-NOTES-2.3.3.md +115 -115
- package/docs/release-notes/RELEASE-NOTES-2.3.4.md +69 -69
- package/docs/release-notes/RELEASE-NOTES-2.3.5.md +63 -63
- package/docs/release-notes/RELEASE-NOTES-2.3.6.md +66 -66
- package/docs/release-notes/RELEASE-NOTES-2.3.7.md +59 -59
- package/docs/release-notes/RELEASE-NOTES-2.3.8.md +45 -45
- package/docs/release-notes/RELEASE-NOTES-2.3.9.md +70 -70
- package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +2 -2
- package/docs/release-notes/RELEASE-NOTES-2.7.2.md +3 -3
- 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/RELEASE-NOTES-2.8.0.en.md +65 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.0.md +65 -0
- package/docs/release-notes/TEMPLATE.md +6 -0
- package/docs/release-playbook.md +103 -0
- package/docs/slv-playbook.md +117 -0
- package/docs/status-report-fields.md +167 -0
- package/docs/test-timing.md +158 -154
- package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
- package/installer.js +793 -783
- package/package.json +260 -203
- 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 +295 -115
- package/prompt-corpus-v2/formal-verify-v2.md +2290 -282
- package/prompt-corpus-v3/formal-verify-v3.json +112 -112
- package/prompt-corpus-v3/formal-verify-v3.md +227 -143
- 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 +573 -207
- package/scripts/clean-temp.mjs +234 -0
- 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-engine-faces.mjs +298 -0
- package/tests/audit-formal-sensitivity.mjs +345 -342
- package/tests/audit-installer-assertions.mutants.mjs +63 -0
- package/tests/audit-installer-compat.test.mjs +256 -208
- package/tests/audit-installer-policy.test.mjs +41 -8
- package/tests/audit-math-computation-contract.mjs +64 -8
- 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 +5 -3
- 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 +323 -309
- package/tests/audit-persona-surface.mutants.mjs +69 -0
- package/tests/audit-persona-surface.test.mjs +431 -390
- package/tests/audit-prompt-duplication.mjs +143 -0
- package/tests/audit-prompt-duplication.mutants.mjs +51 -0
- package/tests/audit-prompt-invariants.mjs +589 -508
- 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-spec-traceability.mjs +193 -193
- 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 +795 -508
- 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 +433 -427
- package/tests/e2e-identity-a6.mutants.mjs +65 -0
- package/tests/e2e-v5-round2.test.mjs +2453 -1981
- package/tests/formal-verify-v2.test.mjs +1778 -1724
- package/tests/formal-verify-v3.mutants.mjs +117 -0
- package/tests/formal-verify-v3.test.mjs +1724 -1671
- package/tests/formal-verify-v4.mutants.mjs +152 -0
- package/tests/formal-verify-v4.test.mjs +2114 -1574
- package/tests/formal-verify-v5.test.mjs +790 -724
- 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 +392 -6
- package/tests/math-computation-v2.test.mjs +779 -0
- package/tests/math-computation-v3.test.mjs +771 -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 +1291 -1280
- package/tests/run-tests.mjs +393 -202
- package/tests/run-tests.mutants.mjs +70 -0
- package/tests/selfdrive-v5.mjs +560 -508
- package/tests/selfdrive-v5.mutants.mjs +132 -0
- package/tests/temp-hygiene.mutants.mjs +80 -0
- package/tests/v2-fix-probes.mutants.mjs +104 -0
- package/tests/v2-fix-probes.test.mjs +1040 -0
- package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
- package/tests/v2v3-interrupt.mutants.mjs +111 -0
- package/tests/v3-fix-probes.mutants.mjs +99 -0
- package/tests/v3-fix-probes.test.mjs +892 -0
- package/tests/v4-final-paper.mutants.mjs +108 -0
- package/tests/v5-institute-fixes.mutants.mjs +218 -0
- package/vibe-math-v2/agent.cordis.yml +378 -322
- package/vibe-math-v2/math-computation.js +144 -53
- package/vibe-math-v2/math-engines.js +12 -12
- package/vibe-math-v2/preset.yml +2 -2
- package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
- package/vibe-math-v2/vibe-math-v2.js +5097 -4755
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +491 -487
- package/vibe-math-v3/agent.cordis.yml +422 -356
- package/vibe-math-v3/math-computation.js +144 -53
- package/vibe-math-v3/math-engines.js +12 -12
- package/vibe-math-v3/preset.yml +2 -2
- package/vibe-math-v3/vibe-math-v3.js +6235 -5873
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
- package/vibe-math-v4/agent.cordis.yml +438 -402
- package/vibe-math-v4/math-computation.js +144 -53
- package/vibe-math-v4/math-engines.js +12 -12
- package/vibe-math-v4/preset.yml +2 -2
- package/vibe-math-v4/vibe-math-v4.js +4490 -4269
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1013
- package/vibe-math-v5/agent.cordis.yml +46 -12
- package/vibe-math-v5/math-computation.js +144 -53
- package/vibe-math-v5/math-engines.js +12 -12
- package/vibe-math-v5/vibe-math-v5.js +1266 -229
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1660
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
|
@@ -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,80 @@
|
|
|
1
|
+
// Falsifiability driver for scripts/clean-temp.mjs (task-12): a SINGLE-SITE edit in the sweeper must
|
|
2
|
+
// turn its own guard (`--self-test`) NAMED-red, while the unmutated control stays green. The mutated
|
|
3
|
+
// copy is written as a DOT-SIBLING inside scripts/ so the module still resolves REPO/tests correctly
|
|
4
|
+
// (a copy in %TEMP% would fail the prefix-extraction check for the wrong reason), and it is always
|
|
5
|
+
// removed in `finally`.
|
|
6
|
+
import { spawnSync } from 'node:child_process'
|
|
7
|
+
import { readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import { dirname, join } from 'node:path'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
|
|
11
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const REPO = join(HERE, '..')
|
|
13
|
+
const SRC = join(REPO, 'scripts', 'clean-temp.mjs')
|
|
14
|
+
|
|
15
|
+
const FAMILIES = [
|
|
16
|
+
{
|
|
17
|
+
// The age threshold is the whole safety property: without it every suite dir looks deletable.
|
|
18
|
+
name: 'the age threshold is dropped (a FRESH suite dir starts looking stale)',
|
|
19
|
+
from: '.filter((e) => Number(e.mtimeMs) < cut)',
|
|
20
|
+
to: '.filter(() => true)',
|
|
21
|
+
expect: /a FRESH suite dir is never swept/,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
// The prefix test keeps the sweeper from touching ANY top-level directory of the temp root.
|
|
25
|
+
name: 'the suite-prefix test is dropped (any top-level dir starts looking like ours)',
|
|
26
|
+
from: 'prefixes.some((p) => String(e.name).startsWith(p))',
|
|
27
|
+
to: 'true',
|
|
28
|
+
expect: /only exact suite prefixes are swept/,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
// task-15: "parent is gone" is the safety property - dropping it makes OUR OWN running gate look like
|
|
32
|
+
// a leftover, which is exactly the false positive that would make this tool dangerous to auto-run.
|
|
33
|
+
name: 'the parent-gone test is dropped (a live-parented gate looks like a leftover)',
|
|
34
|
+
from: 'return e.parentGone === true',
|
|
35
|
+
to: 'return true',
|
|
36
|
+
expect: /a process with a LIVE parent .* is never called a leftover/,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
// task-15: the command-line gate is what keeps unrelated programs (editors, other stacks) out.
|
|
40
|
+
name: 'the command-line gate is dropped (any orphaned program looks like ours)',
|
|
41
|
+
from: "if (!patterns.some((re) => re.test(String(e.cmd || '')))) return false",
|
|
42
|
+
to: 'if (false) return false',
|
|
43
|
+
expect: /an unrelated program is never reported/,
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
function runSelftest(scriptPath) {
|
|
48
|
+
const r = spawnSync(process.execPath, [scriptPath, '--self-test'], { encoding: 'utf8', timeout: 60000, cwd: REPO })
|
|
49
|
+
return { code: r.status, out: String(r.stdout || '') + String(r.stderr || '') }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const control = runSelftest(SRC)
|
|
53
|
+
console.log('control (unmutated sweeper): exit=' + control.code + (control.code === 0 ? ' green, as required' : ' NOT GREEN'))
|
|
54
|
+
if (control.code !== 0) console.log(control.out.split(/\r?\n/).filter((l) => /FAIL/.test(l)).slice(0, 3).join('\n'))
|
|
55
|
+
|
|
56
|
+
let red = 0
|
|
57
|
+
for (const f of FAMILIES) {
|
|
58
|
+
const dest = join(REPO, 'scripts', '.tmp-hygiene-' + Math.random().toString(36).slice(2, 10) + '.mjs')
|
|
59
|
+
let ok = false
|
|
60
|
+
try {
|
|
61
|
+
let text = readFileSync(SRC, 'utf8').replace(/\r\n?/g, '\n')
|
|
62
|
+
if (/\r/.test(text)) { console.error('SETUP-FAIL - the anchor source was not EOL-normalised (CR leaked into anchor matching)'); process.exit(1) }
|
|
63
|
+
const hits = text.split(f.from).length - 1
|
|
64
|
+
if (hits !== 1) { console.error('SETUP-FAIL - anchor ' + JSON.stringify(f.from) + ' matched ' + hits + ' time(s), expected exactly 1'); process.exit(1) }
|
|
65
|
+
writeFileSync(dest, text.replace(f.from, f.to), 'utf8')
|
|
66
|
+
const r = runSelftest(dest)
|
|
67
|
+
const named = r.out.split(/\r?\n/).filter((l) => /^\s*FAIL - /.test(l) && f.expect.test(l)).map((l) => l.trim())
|
|
68
|
+
ok = r.code !== 0 && named.length > 0
|
|
69
|
+
console.log((ok ? ' ok - ' : ' FAIL - ') + f.name + ' [exit=' + r.code + ']' + (named.length ? ' :: ' + named[0].slice(0, 140) : ' :: no named red (exit=' + r.code + ')'))
|
|
70
|
+
if (ok) red++
|
|
71
|
+
} finally {
|
|
72
|
+
try { rmSync(dest, { force: true }) } catch (e) { /* best effort */ }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log('')
|
|
77
|
+
console.log('mutant families reddening the temp-hygiene guard by name: ' + red + '/' + FAMILIES.length)
|
|
78
|
+
console.log('control green: ' + (control.code === 0))
|
|
79
|
+
if (red !== FAMILIES.length || control.code !== 0) process.exit(1)
|
|
80
|
+
console.log('ALL MUTANTS RED AS REQUIRED')
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
FAMILIES.push(
|
|
93
|
+
{ name: 'task-9/v2: the not-detected log drops the actionable sentence', expect: /\[task-9\/v2\]/,
|
|
94
|
+
from: '已探测 PATH 与文档化的常见 TeX 根;可用 paperLatexCommand 指定绝对路径。', to: '' },
|
|
95
|
+
)
|
|
96
|
+
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++ }
|
|
97
|
+
console.log('')
|
|
98
|
+
console.log('mutant families reddening the v2 probe by name: ' + red + '/' + FAMILIES.length)
|
|
99
|
+
console.log('skipped=[' + skipped.join(' | ') + ']')
|
|
100
|
+
console.log('timings: ' + TIMES.map((t) => String(t[0]).split(':')[0] + '=' + t[1] + 'ms').join(' '))
|
|
101
|
+
console.log('TOTAL WALL TIME (all families + setup): ' + (Date.now() - START_MS) + 'ms (' + Math.round((Date.now() - START_MS) / 1000) + 's)')
|
|
102
|
+
console.log('hangs=[' + hangs.join(' | ') + ']')
|
|
103
|
+
if (red !== FAMILIES.length || skipped.length || hangs.length) process.exit(1)
|
|
104
|
+
console.log('ALL MUTANTS RED AS REQUIRED')
|