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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Registered mutant harness for tests/audit-math-computation-contract.mjs — the audit carries its own proof.
|
|
2
|
+
// Defect shapes (all real, from this project's history):
|
|
3
|
+
// (a) the SINGLE SOURCE (PARAM_PROPS_KEYS) drops a math param ⇒ the derived schema loses it
|
|
4
|
+
// (b) the FIRST (session-layer) registration reverts to a hand-written literal without the math keys
|
|
5
|
+
// (c) the APPLY-LAYER registration does the same — only visible because the audit now checks EVERY site
|
|
6
|
+
// Each family mutates a COPY of the tree (the audit's MC_CONTRACT_ROOT seam) and must make the audit redden
|
|
7
|
+
// BY NAME. Child runs are bounded (a timeout is a HANG); skipped families are printed and counted.
|
|
8
|
+
// Exit criterion: every family reddens by name; ALL MUTANTS RED AS REQUIRED + exit 0 only then.
|
|
9
|
+
// Run: node tests/audit-math-computation-contract.mutants.mjs
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, copyFileSync, rmSync, readdirSync } from 'node:fs'
|
|
11
|
+
import { execFileSync, spawnSync } from 'node:child_process'
|
|
12
|
+
import { dirname, join } from 'node:path'
|
|
13
|
+
import { tmpdir } from 'node:os'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
16
|
+
const REPO = join(HERE, '..')
|
|
17
|
+
const AUDIT = 'tests/audit-math-computation-contract.mjs'
|
|
18
|
+
// Bound every child run: run-tests has no per-suite timeout, so an unbounded child hangs the gate.
|
|
19
|
+
// A timeout is reported as HANG, counts as a failure for that family, and is listed in hangs=[].
|
|
20
|
+
const CHILD_TIMEOUT_MS = Number(process.env.MUTANT_CHILD_TIMEOUT_MS || 120000)
|
|
21
|
+
const TIMES = []
|
|
22
|
+
const hangs = []
|
|
23
|
+
const skipped = []
|
|
24
|
+
function copyTree(from, to) {
|
|
25
|
+
mkdirSync(to, { recursive: true })
|
|
26
|
+
for (const e of readdirSync(from, { withFileTypes: true })) {
|
|
27
|
+
if (e.name === '.git' || e.name === 'node_modules' || e.name === '_oneoff') continue
|
|
28
|
+
if (e.isDirectory()) copyTree(join(from, e.name), join(to, e.name))
|
|
29
|
+
else copyFileSync(join(from, e.name), join(to, e.name))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function runFamily(f) {
|
|
33
|
+
const dest = join(tmpdir(), 'mc-contract-mut-' + Math.random().toString(36).slice(2, 10))
|
|
34
|
+
copyTree(REPO, dest)
|
|
35
|
+
const target = join(dest, f.rel)
|
|
36
|
+
const before = readFileSync(target, 'utf8')
|
|
37
|
+
const n = before.split(f.from).length - 1
|
|
38
|
+
if (f.firstOnly ? n < 1 : n !== 1) {
|
|
39
|
+
skipped.push(f.name + ' (anchor x' + n + ')')
|
|
40
|
+
console.error(' SKIP - ' + f.name + ' (anchor x' + n + ')')
|
|
41
|
+
rmSync(dest, { recursive: true, force: true })
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
const mutated = f.firstOnly
|
|
45
|
+
? (() => { const i = before.indexOf(f.from); return before.slice(0, i) + f.to + before.slice(i + f.from.length) })()
|
|
46
|
+
: before.split(f.from).join(f.to)
|
|
47
|
+
writeFileSync(target, mutated, 'utf8')
|
|
48
|
+
try { execFileSync(process.execPath, ['--check', target], { stdio: 'pipe' }) }
|
|
49
|
+
catch (e) {
|
|
50
|
+
writeFileSync(target, before, 'utf8')
|
|
51
|
+
skipped.push(f.name + ' (mutation did not compile)')
|
|
52
|
+
console.error(' SKIP - ' + f.name + ' (mutation did not compile; restored)')
|
|
53
|
+
rmSync(dest, { recursive: true, force: true })
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
const t0 = Date.now()
|
|
57
|
+
const r = spawnSync(process.execPath, [AUDIT], {
|
|
58
|
+
cwd: REPO, encoding: 'utf8', timeout: CHILD_TIMEOUT_MS, killSignal: 'SIGKILL',
|
|
59
|
+
env: Object.assign({}, process.env, { MC_CONTRACT_ROOT: dest }),
|
|
60
|
+
})
|
|
61
|
+
const ms = Date.now() - t0
|
|
62
|
+
TIMES.push([f.name, ms])
|
|
63
|
+
const hang = !!(r.error && (r.error.code === 'ETIMEDOUT' || r.signal === 'SIGKILL'))
|
|
64
|
+
if (hang) hangs.push(f.name + '(' + Math.round(ms / 1000) + 's)')
|
|
65
|
+
const out = String(r.stdout || '') + String(r.stderr || '')
|
|
66
|
+
// this audit prints failures as ` - <label> — <detail>` (not `FAIL - ...`), so match the label itself
|
|
67
|
+
const named = out.split(/\r?\n/).filter((l) => f.expect.test(l)).map((l) => l.trim())
|
|
68
|
+
const ok = !hang && r.status === 1 && named.length > 0
|
|
69
|
+
console.log((hang ? ' HANG - ' : (ok ? ' ok - ' : ' FAIL - ')) + f.name + ' [' + ms + 'ms]' + (named.length ? ' :: ' + named[0].slice(0, 150) : ' :: no named red (exit=' + r.status + ')'))
|
|
70
|
+
rmSync(dest, { recursive: true, force: true })
|
|
71
|
+
return ok
|
|
72
|
+
}
|
|
73
|
+
const WITHOUT_MATH_KEYS = "objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } })"
|
|
74
|
+
const FAMILIES = [
|
|
75
|
+
{
|
|
76
|
+
name: 'the SINGLE SOURCE drops mathMode (derived key table)',
|
|
77
|
+
rel: 'vibe-math-v3/vibe-math-v3.js',
|
|
78
|
+
from: "'mathMode', 'mathEngines'",
|
|
79
|
+
to: "'mathEngines'",
|
|
80
|
+
expect: /set schema has the property mathMode/,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'the FIRST (session-layer) registration reverts to a literal without the math keys',
|
|
84
|
+
rel: 'vibe-math-v3/vibe-math-v3.js',
|
|
85
|
+
from: 'objParams(paramProps()), async function',
|
|
86
|
+
to: WITHOUT_MATH_KEYS + ', async function',
|
|
87
|
+
firstOnly: true,
|
|
88
|
+
expect: /site#0 set schema has the property mathComputation/,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'the APPLY-LAYER registration reverts to a literal without the math keys',
|
|
92
|
+
rel: 'vibe-math-v3/vibe-math-v3.js',
|
|
93
|
+
from: "objParams(paramProps()), 'vibe_math_set_params')",
|
|
94
|
+
to: WITHOUT_MATH_KEYS + ", 'vibe_math_set_params')",
|
|
95
|
+
expect: /site#1 set schema has the property mathComputation/,
|
|
96
|
+
},
|
|
97
|
+
]
|
|
98
|
+
let red = 0
|
|
99
|
+
for (const f of FAMILIES) { const ok = runFamily(f); if (ok) red++ }
|
|
100
|
+
console.log('')
|
|
101
|
+
console.log('mutant families reddening the math-contract audit by name: ' + red + '/' + FAMILIES.length)
|
|
102
|
+
console.log('timings: ' + TIMES.map((t) => String(t[0]).split('(')[0].trim().slice(0, 18) + '=' + t[1] + 'ms').join(' '))
|
|
103
|
+
console.log('hangs=[' + hangs.join(' | ') + ']')
|
|
104
|
+
console.log('skipped=[' + skipped.join(' | ') + ']')
|
|
105
|
+
if (red !== FAMILIES.length || skipped.length || hangs.length) process.exit(1)
|
|
106
|
+
console.log('ALL MUTANTS RED AS REQUIRED')
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { createHash } from 'node:crypto'
|
|
18
18
|
import { existsSync, readFileSync } from 'node:fs'
|
|
19
19
|
import { join, resolve } from 'node:path'
|
|
20
|
-
import { fileURLToPath } from 'node:url'
|
|
20
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
21
21
|
|
|
22
22
|
const HERE = fileURLToPath(new URL('./', import.meta.url))
|
|
23
23
|
const REPO = resolve(HERE, '..')
|
|
@@ -37,6 +37,101 @@ function ok(cond, label, detail) {
|
|
|
37
37
|
const sha = (p) => createHash('sha256').update(readFileSync(p)).digest('hex')
|
|
38
38
|
const read = (rel) => readFileSync(join(REPO, rel), 'utf8')
|
|
39
39
|
|
|
40
|
+
// ---- extracted predicates (shared by the checks below AND by --self-probe) -----------------------
|
|
41
|
+
// §7/§8 used to compute these inline; extracting them is what lets the self-probe feed the SAME
|
|
42
|
+
// predicates deliberately broken strings. Keep the two users in sync by construction: both call these.
|
|
43
|
+
const SUPPORTED_PLACEHOLDERS = {
|
|
44
|
+
'<script>': "split('<script>').join(",
|
|
45
|
+
'<expr>': "split('<expr>').join(",
|
|
46
|
+
'<pkgs>': "a === '<pkgs>'",
|
|
47
|
+
'<pkgs...>': "a === '<pkgs...>'",
|
|
48
|
+
'<probeCode>': "a === '<probeCode>'",
|
|
49
|
+
'<exe>': ".replace(/<exe>/g, exe)",
|
|
50
|
+
'<pkg>': ".replace(/<pkg>/g, pkg)",
|
|
51
|
+
'__PKGS__': ".replace(/__PKGS__/g, plain)",
|
|
52
|
+
'__QPKGS__': ".replace(/__QPKGS__/g, quoted)",
|
|
53
|
+
'__PKG__': ".replace(/__PKG__/g, pkg)",
|
|
54
|
+
}
|
|
55
|
+
const stringsOfDescriptor = (d) => [].concat(
|
|
56
|
+
Object.values(d.scriptArgv || {}).filter((x) => typeof x === 'string'),
|
|
57
|
+
Object.values(d.evalArgv || {}).filter((x) => typeof x === 'string'),
|
|
58
|
+
((d.packageProbe && d.packageProbe.argv) || []).filter((x) => typeof x === 'string'),
|
|
59
|
+
typeof d.probeCode === 'string' ? [d.probeCode] : [],
|
|
60
|
+
(d.install ? [].concat(d.install.userArgv || [], d.install.systemArgv || [], d.install.uninstallArgv || []) : []).filter((x) => typeof x === 'string'),
|
|
61
|
+
)
|
|
62
|
+
/** Placeholders used by descriptors that the module does not KNOW about. Unit: placeholder occurrences. */
|
|
63
|
+
function unknownPlaceholders(src, all) {
|
|
64
|
+
const out = []
|
|
65
|
+
for (const name of Object.keys(all)) {
|
|
66
|
+
for (const str of stringsOfDescriptor(all[name])) {
|
|
67
|
+
for (const m of (str.match(/<[a-z.]+>|__[A-Z]+__/g) || [])) if (!(m in SUPPORTED_PLACEHOLDERS)) out.push(name + ':' + m)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out
|
|
71
|
+
}
|
|
72
|
+
/** Known placeholders whose implementation is missing from the module. Unit: placeholder occurrences. */
|
|
73
|
+
function unimplementedPlaceholders(src, all) {
|
|
74
|
+
const out = []
|
|
75
|
+
for (const name of Object.keys(all)) {
|
|
76
|
+
for (const str of stringsOfDescriptor(all[name])) {
|
|
77
|
+
for (const m of (str.match(/<[a-z.]+>|__[A-Z]+__/g) || [])) {
|
|
78
|
+
if ((m in SUPPORTED_PLACEHOLDERS) && SUPPORTED_PLACEHOLDERS[m] && src.indexOf(SUPPORTED_PLACEHOLDERS[m]) === -1) out.push(name + ':' + m)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return out
|
|
83
|
+
}
|
|
84
|
+
/** The substitution must be STRING-level so embedded forms (matlab run('<script>')) are covered. */
|
|
85
|
+
const stringLevelSubstitution = (src) => /String\(a\)\.split\('<script>'\)\.join\(/.test(src) && /String\(a\)\.split\('<expr>'\)\.join\(/.test(src)
|
|
86
|
+
/** §8: the descriptor declares the policy / the module consumes it / nothing is hard-coded. */
|
|
87
|
+
const cliDeclaresPolicy = (eng) => /policy:\s*\{\s*requiresMathMode:\s*'typed\+shell',\s*requiresEngineInList:\s*true\s*\}/.test(eng)
|
|
88
|
+
const cliConsumesPolicy = (mod) => (mod.match(/CLI_POLICY\.requiresMathMode/g) || []).length >= 2 && (mod.match(/CLI_POLICY\.requiresEngineInList/g) || []).length >= 1
|
|
89
|
+
const cliNoHardCoded = (mod) => !/mathMode !== 'typed\+shell'/.test(mod)
|
|
90
|
+
/** §8: the shared refusal shape must surface the descriptor value (the OLD literal had only reason). */
|
|
91
|
+
const refusalSurfacesPolicy = (n) => !!n && n.reason === 'policy' && n.requiresMathMode === 'typed+shell'
|
|
92
|
+
|
|
93
|
+
// ---- `--self-probe`: the SAME predicates against deliberately broken inputs (shipped form of the
|
|
94
|
+
// was-dev-only proofs `descriptor-sweep-proof.mjs` + `cli-policy-proof.mjs`). No repo mutation.
|
|
95
|
+
async function selfProbe() {
|
|
96
|
+
const mod = read('vibe-math-v2/math-computation.js')
|
|
97
|
+
const eng = read('vibe-math-v2/math-engines.js')
|
|
98
|
+
const { MATH_ENGINES, MATH_P2_ENGINES } = await import(pathToFileURL(join(REPO, 'vibe-math-v2', 'math-engines.js')).href)
|
|
99
|
+
const all = Object.assign({}, MATH_ENGINES, MATH_P2_ENGINES)
|
|
100
|
+
const LIT = /<script>/
|
|
101
|
+
const matlabTemplate = ['-batch', "run('<script>')"]
|
|
102
|
+
const preFix = (tmpl, abs) => tmpl.map((a) => (a === '<script>' ? abs : a))
|
|
103
|
+
const postFix = (tmpl, abs) => tmpl.map((a) => String(a).split('<script>').join(abs))
|
|
104
|
+
const cases = [
|
|
105
|
+
['§7 substitution is STRING-level (matlab embedding covered)', stringLevelSubstitution(mod),
|
|
106
|
+
stringLevelSubstitution(mod.replace("String(a).split('<script>').join(payload.scriptAbs)", "a === '<script>' ? payload.scriptAbs : a"))],
|
|
107
|
+
['§7 descriptors use only KNOWN placeholders', unknownPlaceholders(mod, all).length === 0,
|
|
108
|
+
unknownPlaceholders(mod, Object.assign({}, all, { probe: { scriptArgv: { x: '<mysterySlot>' } } })).length > 0],
|
|
109
|
+
['§7 known placeholders are IMPLEMENTED in the module', unimplementedPlaceholders(mod, all).length === 0,
|
|
110
|
+
unimplementedPlaceholders('/* no implementation */', all).length === 0],
|
|
111
|
+
['§7 the guard predicate catches what the OLD substitution produced', postFix(matlabTemplate, 'ABS').every((a) => !LIT.test(a)),
|
|
112
|
+
preFix(matlabTemplate, 'ABS').every((a) => !LIT.test(a))],
|
|
113
|
+
['§8 the module CONSUMES the descriptor policy', cliConsumesPolicy(mod),
|
|
114
|
+
cliConsumesPolicy(mod.replace(/CLI_POLICY\./g, 'HARDCODED.'))],
|
|
115
|
+
['§8 no hard-coded policy comparison remains', cliNoHardCoded(mod),
|
|
116
|
+
cliNoHardCoded(mod + "\\n if (params.mathMode !== 'typed+shell') { /* hard-coded revert */ }")],
|
|
117
|
+
['§8 descriptor DECLARES the policy', cliDeclaresPolicy(eng),
|
|
118
|
+
cliDeclaresPolicy(eng.replace("policy: { requiresMathMode: 'typed+shell', requiresEngineInList: true }", 'policy: null'))],
|
|
119
|
+
['§8 the shared refusal surfaces the descriptor value (OLD literal caught)',
|
|
120
|
+
refusalSurfacesPolicy({ reason: 'policy', requiresMathMode: 'typed+shell' }),
|
|
121
|
+
refusalSurfacesPolicy({ reason: 'policy' })],
|
|
122
|
+
]
|
|
123
|
+
let bad = 0
|
|
124
|
+
for (const [name, greenNow, redWhenBroken] of cases) {
|
|
125
|
+
const good = greenNow === true && redWhenBroken === false
|
|
126
|
+
if (!good) bad++
|
|
127
|
+
console.log((good ? ' ok ' : ' FAIL ') + name + ' :: green-now=' + greenNow + ' broken-goes-red=' + !redWhenBroken)
|
|
128
|
+
}
|
|
129
|
+
console.log('=== MATH PARITY SELF-PROBE: ' + (cases.length - bad) + '/' + cases.length + ' as required ===')
|
|
130
|
+
return bad === 0
|
|
131
|
+
}
|
|
132
|
+
if (process.argv.includes('--self-probe')) process.exit(await selfProbe() ? 0 : 1)
|
|
133
|
+
|
|
134
|
+
|
|
40
135
|
console.log('-- math_computation parity --')
|
|
41
136
|
|
|
42
137
|
// ---- 1/2. the four copies are identical, and match the canonical source ----
|
|
@@ -106,7 +201,12 @@ for (const f of MODULES) {
|
|
|
106
201
|
: new URL('../vibe-math-v2/math-engines.js', import.meta.url).href)
|
|
107
202
|
const order = E.MATH_ENGINE_ORDER
|
|
108
203
|
ok(JSON.stringify(order) === JSON.stringify(['python', 'r', 'octave', 'julia', 'matlab', 'maple', 'wolfram', 'cli']), 'MATH_ENGINE_ORDER is exactly the P1 set')
|
|
109
|
-
|
|
204
|
+
// Item-3 ruling: `defaultOn` was declared but never consumed - cli's default-on property is
|
|
205
|
+
// already pinned above through the REAL single source (MATH_PARAM_DEFAULTS.mathEngines).
|
|
206
|
+
const zombie = ['winPrefix', 'stdinArgv', 'defaultOn']
|
|
207
|
+
.filter((f) => JSON.stringify(E.MATH_ENGINES).indexOf('"' + f + '"') !== -1 || JSON.stringify(E.MATH_P2_ENGINES).indexOf('"' + f + '"') !== -1)
|
|
208
|
+
ok(zombie.length === 0, 'no zombie descriptor field is declared (winPrefix/stdinArgv/defaultOn were removed)', JSON.stringify(zombie))
|
|
209
|
+
ok(!/<cliArgv/.test(readFileSync(join(REPO, 'vibe-math-v2', 'math-engines.js'), 'utf8')), 'the decorative <cliArgv...> marker is gone from the descriptor table')
|
|
110
210
|
ok(E.MATH_ENGINES.cli.policy && E.MATH_ENGINES.cli.policy.requiresMathMode === 'typed+shell' && E.MATH_ENGINES.cli.policy.requiresEngineInList === true, 'cli policy gate declared')
|
|
111
211
|
ok(E.MATH_ENGINES.maple.verify === true && !!E.MATH_ENGINES.maple.verifyReason, 'Maple template is flagged VERIFY with a reason')
|
|
112
212
|
for (const name of ['matlab', 'maple', 'wolfram']) {
|
|
@@ -192,7 +292,68 @@ for (const f of MODULES) {
|
|
|
192
292
|
for (const r of reasons) ok(doc.indexOf('`' + r + '`') !== -1, 'docs/math-computation.md names next.reason ' + r)
|
|
193
293
|
}
|
|
194
294
|
|
|
195
|
-
|
|
295
|
+
// ── 7. descriptor sweep: every argv placeholder dialect must be one the module IMPLEMENTS
|
|
296
|
+
// Sweep finding (HIGH, fixed): substitution was exact-element (`a === '<script>'`), so matlab's embedded
|
|
297
|
+
// `run('<script>')` reached the engine literally. The fix is string-level; this guard keeps the two
|
|
298
|
+
// sides in sync: no descriptor may use a placeholder the module does not substitute.
|
|
299
|
+
{
|
|
300
|
+
const E = await import(pathToFileURL(join(REPO, 'vibe-math-v2', 'math-engines.js')).href)
|
|
301
|
+
const modPath = join(REPO, 'vibe-math-v2', 'math-computation.js')
|
|
302
|
+
const src = existsSync(modPath) ? readFileSync(modPath, 'utf8') : ''
|
|
303
|
+
const all = Object.assign({}, E.MATH_ENGINES, E.MATH_P2_ENGINES)
|
|
304
|
+
const unknown = unknownPlaceholders(src, all)
|
|
305
|
+
const unimplemented = unimplementedPlaceholders(src, all)
|
|
306
|
+
ok(stringLevelSubstitution(src),
|
|
307
|
+
'substitution is STRING-level for <script>/<expr> (embedded forms like matlab run(\'<script>\') are covered)')
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── 8. the cli policy is declared ONCE (descriptor) and consumed, never hard-coded
|
|
311
|
+
// Sweep ruling: `cli.policy.{requiresMathMode,requiresEngineInList}` were declared but never consumed,
|
|
312
|
+
// while the same rules were hard-coded twice in the module (so they could drift). The module now reads
|
|
313
|
+
// the descriptor; this guard makes a hard-coded revert impossible to land.
|
|
314
|
+
{
|
|
315
|
+
const engPath = join(REPO, 'vibe-math-v2', 'math-engines.js')
|
|
316
|
+
const modPath = join(REPO, 'vibe-math-v2', 'math-computation.js')
|
|
317
|
+
const eng = existsSync(engPath) ? readFileSync(engPath, 'utf8') : ''
|
|
318
|
+
const mod = existsSync(modPath) ? readFileSync(modPath, 'utf8') : ''
|
|
319
|
+
ok(cliDeclaresPolicy(eng),
|
|
320
|
+
'the cli descriptor DECLARES its policy (requiresMathMode + requiresEngineInList)')
|
|
321
|
+
ok(cliConsumesPolicy(mod),
|
|
322
|
+
'the module CONSUMES the descriptor policy (both refusal sites read CLI_POLICY)')
|
|
323
|
+
ok(cliNoHardCoded(mod),
|
|
324
|
+
'no hard-coded policy comparison is left in the module (the rule lives only in the descriptor)')
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── 9. doc-sync: the v5 editor closed set and v2's single-source status/report helper are documented
|
|
328
|
+
{
|
|
329
|
+
const v5 = existsSync(join(REPO, 'vibe-math-v5', 'vibe-math-v5.js')) ? readFileSync(join(REPO, 'vibe-math-v5', 'vibe-math-v5.js'), 'utf8') : ''
|
|
330
|
+
const fp = existsSync(join(REPO, 'docs', 'final-paper.md')) ? readFileSync(join(REPO, 'docs', 'final-paper.md'), 'utf8') : ''
|
|
331
|
+
// The closed set appears twice in v5 (the coerce call and the tool schema enum); the separator may be
|
|
332
|
+
// written with or without a space, so accept both rather than pinning one spelling.
|
|
333
|
+
ok(/\[\s*'office'\s*,\s*'academician'\s*\]/.test(v5) && /V5_INVALID_ARGUMENT/.test(v5), 'v5 validates the paper editor against the closed set and returns V5_INVALID_ARGUMENT')
|
|
334
|
+
ok(fp.indexOf('office|academician') !== -1 && fp.indexOf('V5_INVALID_ARGUMENT') !== -1, 'docs/final-paper.md documents the v5 editor closed set + its error code')
|
|
335
|
+
const v2src = existsSync(join(REPO, 'vibe-math-v2', 'vibe-math-v2.js')) ? readFileSync(join(REPO, 'vibe-math-v2', 'vibe-math-v2.js'), 'utf8') : ''
|
|
336
|
+
const v2doc = existsSync(join(REPO, 'vibe-math-v2', '实现方案.md')) ? readFileSync(join(REPO, 'vibe-math-v2', '实现方案.md'), 'utf8') : ''
|
|
337
|
+
ok((v2src.match(/verifyTasksView\(/g) || []).length >= 3, 'v2 produces status+report through ONE verifyTasksView helper')
|
|
338
|
+
ok(v2doc.indexOf('verifyTasksView') !== -1, 'the v2 design doc states the single-source status/report helper')
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// S4/S5 (install-failure discrimination) is asserted in tests/math-computation-shared.test.mjs §33, NOT
|
|
342
|
+
// here: parity reads the shipped copy unconditionally, so a mutant cannot reach what it reads (the
|
|
343
|
+
// assertion would exist but could never redden). What parity keeps is the canonical<->copy byte identity,
|
|
344
|
+
// which makes a canonical-only change impossible to ship unnoticed - a stronger defence than a text grep.
|
|
345
|
+
// ── 11. S6: the version policy has ONE wording (constant + helper), not one per path.
|
|
346
|
+
{
|
|
347
|
+
const mc = readFileSync(join(REPO, 'vibe-math-v2', 'math-computation.js'), 'utf8')
|
|
348
|
+
ok((mc.match(/version-constraints-are-existence-only: /g) || []).length === 1, '★ S6: the policy wording appears exactly ONCE (in MATH_VERSION_POLICY)')
|
|
349
|
+
ok(/function mathVersionPolicy\(/.test(mc), 'S6: the shared helper mathVersionPolicy() exists')
|
|
350
|
+
ok(mc.indexOf('版本求解交给包管理器(本工具只检查是否已安装') === -1, '★ S6: the old divergent install-plan wording is gone')
|
|
351
|
+
ok(/versionPolicy: mathVersionPolicy\(args\.packages/.test(mc), 'S6: the install plan builds its wording through the helper')
|
|
352
|
+
ok((mc.match(/只检查是否存在;版本求解交给包管理器/g) || []).length === 1, '★ S6: the policy clause literal appears exactly ONCE (MATH_VERSION_POLICY_CLAUSE)')
|
|
353
|
+
ok(/const MATH_VERSION_POLICY = .*MATH_VERSION_POLICY_CLAUSE/.test(mc), 'S6: the long policy sentence is DERIVED from the clause')
|
|
354
|
+
ok(/MATH_MISSING_PACKAGES[\s\S]{0,120}MATH_VERSION_POLICY_CLAUSE/.test(mc), 'S6: the missing-package detail derives the clause')
|
|
355
|
+
}
|
|
356
|
+
|
|
196
357
|
console.log('=== MATH COMPUTATION PARITY: ' + passed + ' passed, ' + failed + ' failed ===')
|
|
197
358
|
if (failures.length) for (const f of failures) console.error(' - ' + f)
|
|
198
359
|
process.exit(failed === 0 ? 0 : 1)
|
|
@@ -34,8 +34,10 @@ const PROBES = [
|
|
|
34
34
|
{
|
|
35
35
|
name: 'cli-policy-ignored',
|
|
36
36
|
file: 'math-computation.js',
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Re-anchored after the single-source ruling: the policy now comes from the DESCRIPTOR (CLI_POLICY),
|
|
38
|
+
// so the probe disables the descriptor-driven guard. Ignoring it must turn the shared suite RED.
|
|
39
|
+
from: 'if (CLI_POLICY.requiresMathMode && p.mathMode !== CLI_POLICY.requiresMathMode)',
|
|
40
|
+
to: 'if (false && CLI_POLICY.requiresMathMode)',
|
|
39
41
|
expect: 'cli refused while mathMode=typed',
|
|
40
42
|
},
|
|
41
43
|
{
|
|
@@ -77,8 +79,8 @@ const PROBES = [
|
|
|
77
79
|
{
|
|
78
80
|
name: 'script-fields-dropped-from-return',
|
|
79
81
|
file: 'math-computation.js',
|
|
80
|
-
from: '
|
|
81
|
-
to: '
|
|
82
|
+
from: ' engineInfo: receipt.engine,\n argv: assembled.argv.slice(),\n scriptPath: scriptRel, scriptHash: scriptHash,',
|
|
83
|
+
to: ' engineInfo: receipt.engine,\n argv: assembled.argv.slice(),',
|
|
82
84
|
expect: 'scriptPath + scriptHash are in the return value AND the receipt',
|
|
83
85
|
},
|
|
84
86
|
{
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PACKAGE MEMBERSHIP - every guard/harness meant to ship is listed in package.json#files.
|
|
4
|
+
*
|
|
5
|
+
* DEFECT CLASS: "shipped guard whose proving harness is not in the tarball" (the ship rule: a guard
|
|
6
|
+
* ships together with its harness). A package reader could not reproduce the proof.
|
|
7
|
+
*
|
|
8
|
+
* The intended shipped set is stated EXPLICITLY, not globbed blindly: every tests/*.mutants.mjs on
|
|
9
|
+
* disk must be listed, and every tests/*.mjs cited by the guard index must be listed unless it is in
|
|
10
|
+
* EXCLUDED (documented, deliberate exclusions - e.g. audit-market-metadata.test.mjs asserts its own
|
|
11
|
+
* absence because it checks repo-only image artifacts).
|
|
12
|
+
*
|
|
13
|
+
* Run: node tests/audit-package-membership.mjs
|
|
14
|
+
* Env: PACKAGE_JSON=<path> (used by the mutants harness to point at a mutated copy)
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs'
|
|
17
|
+
import { dirname, join } from 'node:path'
|
|
18
|
+
import { fileURLToPath } from 'node:url'
|
|
19
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
20
|
+
const REPO = join(HERE, '..')
|
|
21
|
+
const PKG = process.env.PACKAGE_JSON || join(REPO, 'package.json')
|
|
22
|
+
const EXCLUDED = new Set(['tests/audit-market-metadata.test.mjs', 'tests/audit-diagram-assets.test.mjs'])
|
|
23
|
+
let passed = 0, failed = 0
|
|
24
|
+
const failures = []
|
|
25
|
+
const ok = (cond, label, detail) => { if (cond) { passed++ } else { failed++; failures.push(label + (detail ? ' — ' + detail : '')) } }
|
|
26
|
+
const pkg = JSON.parse(readFileSync(PKG, 'utf8'))
|
|
27
|
+
const listed = new Set(pkg.files)
|
|
28
|
+
const disk = readdirSync(join(REPO, 'tests')).filter((f) => f.endsWith('.mjs'))
|
|
29
|
+
const mutants = disk.filter((f) => /\.mutants\.mjs$/.test(f)).map((f) => 'tests/' + f).sort()
|
|
30
|
+
const unlistedMutants = mutants.filter((f) => !listed.has(f) && !EXCLUDED.has(f))
|
|
31
|
+
ok(mutants.length > 0, 'there is at least one proving harness on disk')
|
|
32
|
+
ok(unlistedMutants.length === 0, 'every tests/*.mutants.mjs on disk is listed in package.json#files', JSON.stringify(unlistedMutants))
|
|
33
|
+
// guard-index citations: tests/*.mjs paths mentioned in the checklist
|
|
34
|
+
const idx = readFileSync(join(REPO, 'docs', 'AUDIT-CHECKLIST.md'), 'utf8')
|
|
35
|
+
const cited = [...new Set((idx.match(/tests\/[A-Za-z0-9._-]+\.mjs/g) || []))]
|
|
36
|
+
const unlistedCited = cited.filter((f) => !listed.has(f) && !EXCLUDED.has(f))
|
|
37
|
+
ok(cited.length > 0, 'the guard index cites at least one test file')
|
|
38
|
+
ok(unlistedCited.length === 0, 'every guard-index-cited test file is listed (or explicitly excluded)', JSON.stringify(unlistedCited))
|
|
39
|
+
ok(listed.has('tests/audit-package-membership.mjs'), 'this audit itself ships')
|
|
40
|
+
ok(!listed.has('tests/audit-market-metadata.test.mjs'), 'the self-excluded repo-only guard is NOT listed (its own assertion)')
|
|
41
|
+
const missingPaths = [...listed].filter((f) => !existsSync(join(REPO, f)))
|
|
42
|
+
// docs sibling rule: a doc cited by a SHIPPED file must ship too (docs/** was previously uncovered).
|
|
43
|
+
const citers = ['README.md', 'README.en.md', 'docs/AUDIT-CHECKLIST.md', 'docs/COMPAT-AUDIT-ROUND2.md']
|
|
44
|
+
const docRefs = new Set()
|
|
45
|
+
for (const c of citers) {
|
|
46
|
+
const cp = join(REPO, c)
|
|
47
|
+
if (!existsSync(cp)) continue
|
|
48
|
+
for (const m of readFileSync(cp, 'utf8').matchAll(/docs\/[A-Za-z0-9._-]+\.md/g)) docRefs.add(m[0])
|
|
49
|
+
}
|
|
50
|
+
ok(docRefs.size > 0, 'shipped files cite at least one docs/*.md')
|
|
51
|
+
const unlistedDocs = [...docRefs].filter((f) => !listed.has(f))
|
|
52
|
+
ok(unlistedDocs.length === 0, 'every docs/*.md cited by a shipped file is listed in package.json#files', JSON.stringify(unlistedDocs))
|
|
53
|
+
ok(missingPaths.length === 0, 'every listed path exists', JSON.stringify(missingPaths))
|
|
54
|
+
console.log('')
|
|
55
|
+
console.log('=== PACKAGE MEMBERSHIP: ' + passed + ' passed, ' + failed + ' failed ===')
|
|
56
|
+
for (const f of failures) console.error(' - ' + f)
|
|
57
|
+
process.exit(failed === 0 ? 0 : 1)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Mutant harness for the package-membership audit: drop one listed entry from a COPY of package.json
|
|
4
|
+
* and require the audit to go named-red. Run: node tests/audit-package-membership.mutants.mjs
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs'
|
|
7
|
+
import { tmpdir } from 'node:os'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
import { spawnSync } from 'node:child_process'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
const HERE = fileURLToPath(new URL('./', import.meta.url))
|
|
12
|
+
const REPO = join(HERE, '..')
|
|
13
|
+
let passed = 0, failed = 0
|
|
14
|
+
const ok = (c, l, d) => { if (c) { passed++; console.log(' ok ' + l) } else { failed++; console.log(' FAIL ' + l + (d ? ' — ' + d : '')) } }
|
|
15
|
+
const base = spawnSync(process.execPath, [join(HERE, 'audit-package-membership.mjs')], { cwd: REPO, encoding: 'utf8' })
|
|
16
|
+
ok(base.status === 0, 'baseline: the membership audit is green', String(base.stdout || '').trim().split('\n').slice(-1)[0])
|
|
17
|
+
const dir = mkdtempSync(join(tmpdir(), 'pkgmem-'))
|
|
18
|
+
const pkg = JSON.parse(readFileSync(join(REPO, 'package.json'), 'utf8'))
|
|
19
|
+
const victim = 'tests/audit-package-membership.mjs'
|
|
20
|
+
pkg.files = pkg.files.filter((f) => f !== victim)
|
|
21
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify(pkg, null, 2))
|
|
22
|
+
const mut = spawnSync(process.execPath, [join(HERE, 'audit-package-membership.mjs')], { cwd: REPO, encoding: 'utf8', env: Object.assign({}, process.env, { PACKAGE_JSON: join(dir, 'package.json') }) })
|
|
23
|
+
const out = String(mut.stdout || '') + String(mut.stderr || '')
|
|
24
|
+
ok(mut.status !== 0 && /this audit itself ships/.test(out), '★ removing a listed entry makes the membership audit RED (named assertion)', 'exit=' + mut.status)
|
|
25
|
+
rmSync(dir, { recursive: true, force: true })
|
|
26
|
+
console.log('')
|
|
27
|
+
console.log('=== PACKAGE MEMBERSHIP MUTANTS: ' + passed + ' passed, ' + failed + ' failed ===')
|
|
28
|
+
process.exit(failed === 0 ? 0 : 1)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// ============================================================================================
|
|
2
|
+
// CROSS-PRESET PARTICIPANT-SET PARITY (P4 / item 6.3)
|
|
3
|
+
//
|
|
4
|
+
// Expectation source: `_oneoff/review/participant-set-map.md` (table first, then this guard).
|
|
5
|
+
//
|
|
6
|
+
// The four presets deliberately use DIFFERENT names for the same concept - "which set of members is
|
|
7
|
+
// this decision counting, and as of which roster state":
|
|
8
|
+
// · v2 `voteCount(t, round)` -> { participants, expected, round, allParticipantsReported }
|
|
9
|
+
// · v3 `voteCount(t, round)` -> { participants, expected, round } (NO allParticipantsReported:
|
|
10
|
+
// its readers compose their own predicate - a deliberate difference)
|
|
11
|
+
// · v4 `activeRosterSnapshot()` -> `frozen: { version, participants, kind }` (atomic; deprecated
|
|
12
|
+
// alias `frozenParticipants`)
|
|
13
|
+
// · v5 `rosterSnapshot()` -> `quorumView(): { m, mode, voters, voterCount, started, phase, rosterVersion }`
|
|
14
|
+
//
|
|
15
|
+
// So a behavioural "the numbers agree" test would have to unify those contracts, which is exactly what
|
|
16
|
+
// must NOT happen (v2's P2 regression came from collapsing two predicates). This guard is therefore
|
|
17
|
+
// STATIC: it pins, per preset, (a) that the participant set has exactly ONE producer, (b) that the set
|
|
18
|
+
// and its roster version/expectation are surfaced TOGETHER, and (c) the deliberate v2/v3 difference.
|
|
19
|
+
//
|
|
20
|
+
// Usage: node tests/audit-participant-set-parity.mjs
|
|
21
|
+
// ============================================================================================
|
|
22
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
23
|
+
import { join, resolve } from 'node:path'
|
|
24
|
+
import { fileURLToPath } from 'node:url'
|
|
25
|
+
|
|
26
|
+
const REPO = resolve(fileURLToPath(new URL('..', import.meta.url)))
|
|
27
|
+
const read = (rel) => existsSync(join(REPO, rel)) ? readFileSync(join(REPO, rel), 'utf8') : ''
|
|
28
|
+
|
|
29
|
+
let passed = 0, failed = 0
|
|
30
|
+
const failures = []
|
|
31
|
+
function ok(cond, label, detail) {
|
|
32
|
+
if (cond) { passed++; console.log(' ok ' + label); return true }
|
|
33
|
+
failed++; failures.push(label + (detail ? ' — ' + detail : '')); console.log(' FAIL ' + label + (detail ? ' — ' + detail : ''))
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
const occurrences = (s, re) => (s.match(re) || []).length
|
|
37
|
+
/** The body of a named function, by brace counting from its `function <name>(` occurrence. */
|
|
38
|
+
function body(src, name) {
|
|
39
|
+
const i = src.indexOf('function ' + name + '(')
|
|
40
|
+
if (i === -1) return ''
|
|
41
|
+
let depth = 0, started = false
|
|
42
|
+
for (let j = i; j < src.length; j++) {
|
|
43
|
+
if (src[j] === '{') { depth++; started = true }
|
|
44
|
+
else if (src[j] === '}') { depth--; if (started && depth === 0) return src.slice(i, j + 1) }
|
|
45
|
+
}
|
|
46
|
+
return src.slice(i)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---- `--self-probe`: the SAME predicates against deliberately broken source strings -----------
|
|
50
|
+
// Shipped form of the load-bearing proof (was `_oneoff/auditR2/participant-set-proof.mjs`): six cases,
|
|
51
|
+
// each GREEN on the real source and RED on the mutated string. No repo mutation, no fixtures.
|
|
52
|
+
// The predicates mirror the numbered assertions below — update them together.
|
|
53
|
+
function selfProbe() {
|
|
54
|
+
const v2s = read('vibe-math-v2/vibe-math-v2.js')
|
|
55
|
+
const v3s = read('vibe-math-v3/vibe-math-v3.js')
|
|
56
|
+
const v4s = read('vibe-math-v4/vibe-math-v4.js')
|
|
57
|
+
const v5s = read('vibe-math-v5/vibe-math-v5.js')
|
|
58
|
+
const ATOMIC = /frozen:\s*\(function\(sn\)\{\s*return \{version: sn\?sn\.rosterVersion:null, participants: sn\?sn\.rosterSnapshot:null, kind:/
|
|
59
|
+
const cases = [
|
|
60
|
+
['v2 produces allParticipantsReported',
|
|
61
|
+
/allParticipantsReported: participants\.length > 0 && thisRoundIds\.length === participants\.length/.test(v2s),
|
|
62
|
+
/allParticipantsReported: participants\.length > 0 && thisRoundIds\.length === participants\.length/.test(v2s.replace(/allParticipantsReported:/g, 'renamedField:'))],
|
|
63
|
+
['v3 deliberately LACKS the field', !/allParticipantsReported/.test(v3s),
|
|
64
|
+
!/allParticipantsReported/.test(v3s.replace('function voteCount(t, round) {', 'function voteCount(t, round) { const allParticipantsReported = 1;'))],
|
|
65
|
+
['v4 frozen is atomic', ATOMIC.test(v4s),
|
|
66
|
+
ATOMIC.test(v4s.replace(/frozen:\s*\(function\(sn\)\{\s*return \{version: sn\?sn\.rosterVersion:null, participants: sn\?sn\.rosterSnapshot:null, kind:/, 'frozen: {version: rosterVersion, participants: null, kind: null}, x: (function(sn){ return {version: sn?sn.rosterVersion:null, participants: sn?sn.rosterSnapshot:null, kind:'))],
|
|
67
|
+
['v4 removal prunes both frozen sets', occurrences(v4s.slice(v4s.indexOf('function removeMember')), /rosterSnapshot\.filter\(x=>x!==id\)/g) >= 2,
|
|
68
|
+
occurrences(v4s.slice(v4s.indexOf('function removeMember')).replace(/rosterSnapshot\.filter\(x=>x!==id\)/g, 'noop()'), /rosterSnapshot\.filter\(x=>x!==id\)/g) >= 2],
|
|
69
|
+
['v5 quorumView surfaces rosterVersion', /\brosterVersion:/.test(v5s.slice(v5s.indexOf('function quorumView'), v5s.indexOf('function quorumView') + 900)),
|
|
70
|
+
/\brosterVersion:/.test(v5s.slice(v5s.indexOf('function quorumView'), v5s.indexOf('function quorumView') + 900).replace(/rosterVersion:/g, 'ver:'))],
|
|
71
|
+
['exactly ONE v2 producer', occurrences(v2s, /function voteCount\(t, round\)/g) === 1,
|
|
72
|
+
occurrences(v2s + '\n function voteCount(t, round) { return null }\n', /function voteCount\(t, round\)/g) === 1],
|
|
73
|
+
]
|
|
74
|
+
let bad = 0
|
|
75
|
+
for (const [name, greenNow, redWhenBroken] of cases) {
|
|
76
|
+
const good = greenNow === true && redWhenBroken === false
|
|
77
|
+
if (!good) bad++
|
|
78
|
+
console.log((good ? ' ok ' : ' FAIL ') + name + ' :: green-now=' + greenNow + ' broken-goes-red=' + !redWhenBroken)
|
|
79
|
+
}
|
|
80
|
+
console.log('=== PARTICIPANT-SET SELF-PROBE: ' + (cases.length - bad) + '/' + cases.length + ' as required ===')
|
|
81
|
+
return bad === 0
|
|
82
|
+
}
|
|
83
|
+
if (process.argv.includes('--self-probe')) process.exit(selfProbe() ? 0 : 1)
|
|
84
|
+
const v2 = read('vibe-math-v2/vibe-math-v2.js')
|
|
85
|
+
const v3 = read('vibe-math-v3/vibe-math-v3.js')
|
|
86
|
+
const v4 = read('vibe-math-v4/vibe-math-v4.js')
|
|
87
|
+
const v5 = read('vibe-math-v5/vibe-math-v5.js')
|
|
88
|
+
|
|
89
|
+
console.log('-- participant-set parity across the four presets --')
|
|
90
|
+
|
|
91
|
+
// ---- 1. v2: one producer, set + expectation + the field, and it is CONSUMED ----
|
|
92
|
+
{
|
|
93
|
+
const b = body(v2, 'voteCount')
|
|
94
|
+
ok(occurrences(v2, /function voteCount\(t, round\)/g) === 1, 'v2: exactly ONE participant-set producer (voteCount)')
|
|
95
|
+
ok(/participants: participants/.test(b) && /expected: expected/.test(b) && /round: r/.test(b),
|
|
96
|
+
'v2: voteCount returns the SET together with its expectation + round')
|
|
97
|
+
ok(/allParticipantsReported: participants\.length > 0 && thisRoundIds\.length === participants\.length/.test(b),
|
|
98
|
+
'v2: voteCount produces allParticipantsReported (its single predicate)')
|
|
99
|
+
ok(occurrences(v2, /voteCount\(t, (?:t|meta)\.round\)\.allParticipantsReported/g) >= 2,
|
|
100
|
+
'v2: the field is CONSUMED (both readers go through voteCount, not a re-derivation)')
|
|
101
|
+
const expectedLine = /const expected = Math\.max\(MIN_REVIEWERS, Number\(tt\.expectedCount\) \|\| 0\)/.test(b)
|
|
102
|
+
ok(expectedLine, 'v2: expected comes from the CREATION-TIME snapshot with the MIN_REVIEWERS floor (deliberately kept)')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- 2. v3: one producer, NO allParticipantsReported (the documented difference), snapshot-only quorum ----
|
|
106
|
+
{
|
|
107
|
+
const b = body(v3, 'voteCount')
|
|
108
|
+
ok(occurrences(v3, /function voteCount\(t, round\)/g) === 1, 'v3: exactly ONE participant-set producer (voteCount)')
|
|
109
|
+
ok(/participants: participants/.test(b) && /expected: expected/.test(b), 'v3: voteCount returns the SET together with its expectation')
|
|
110
|
+
ok(!/allParticipantsReported/.test(v3), 'v3: deliberately has NO allParticipantsReported (readers compose their own predicate - do not "unify" it away)')
|
|
111
|
+
const mv = body(v3, 'minVotes')
|
|
112
|
+
ok(mv.length > 0 && !/params\.verifierCount/.test(mv), 'v3: minVotes reads the SNAPSHOT only (live params.verifierCount must not leak back in)')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---- 3. v4: one producer, ATOMIC set+version, deprecated alias, removal prunes ----
|
|
116
|
+
{
|
|
117
|
+
ok(occurrences(v4, /function activeRosterSnapshot\(/g) === 1, 'v4: exactly ONE participant-set producer (activeRosterSnapshot)')
|
|
118
|
+
ok(/frozen:\s*\(function\(sn\)\{\s*return \{version: sn\?sn\.rosterVersion:null, participants: sn\?sn\.rosterSnapshot:null, kind:/.test(v4),
|
|
119
|
+
'v4: `frozen` is the ATOMIC triple {version, participants, kind}')
|
|
120
|
+
ok(/frozenParticipants:\s*\(activeRosterSnapshot\(\)\|\|\{\}\)\.rosterSnapshot\|\|null/.test(v4) && /deprecated/.test(v4),
|
|
121
|
+
'v4: `frozenParticipants` stays only as a deprecated alias of frozen.participants')
|
|
122
|
+
const rm = body(v4, 'removeMember')
|
|
123
|
+
ok(occurrences(rm, /rosterSnapshot=(\w+)\.rosterSnapshot\.filter\(x=>x!==id\)|rosterSnapshot\.filter\(x=>x!==id\)/g) >= 2,
|
|
124
|
+
'v4: removal prunes BOTH frozen sets (that is what keeps "removal releases the wait" true)')
|
|
125
|
+
ok(/function bumpRoster\(/.test(v4) && /rosterVersion\+\+/.test(body(v4, 'bumpRoster')), 'v4: exactly one roster-version bump helper')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---- 4. v5: one producer, quorum view surfaces the set + version together ----
|
|
129
|
+
{
|
|
130
|
+
ok(occurrences(v5, /function rosterSnapshot\(\)/g) === 1, 'v5: exactly ONE participant-set producer (rosterSnapshot)')
|
|
131
|
+
const qv = body(v5, 'quorumView')
|
|
132
|
+
const keys = ['m', 'mode', 'voters', 'voterCount', 'started', 'phase', 'rosterVersion']
|
|
133
|
+
const missing = keys.filter((k) => !new RegExp('\\b' + k + ':').test(qv))
|
|
134
|
+
ok(qv.length > 0 && missing.length === 0, 'v5: quorumView surfaces the set (voters/voterCount) TOGETHER with m + rosterVersion', 'missing: ' + JSON.stringify(missing))
|
|
135
|
+
ok(/return \{[\s\S]*?version: rosterVersion\(\)[\s\S]*?voters:/.test(body(v5, 'rosterSnapshot')),
|
|
136
|
+
'v5: the snapshot captures version + voters in one object (no re-derivation between them)')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---- 5. the two cross-preset invariants, stated once (so a future preset cannot skip them) ----
|
|
140
|
+
{
|
|
141
|
+
const producers = [
|
|
142
|
+
['v2', occurrences(v2, /function voteCount\(t, round\)/g)],
|
|
143
|
+
['v3', occurrences(v3, /function voteCount\(t, round\)/g)],
|
|
144
|
+
['v4', occurrences(v4, /function activeRosterSnapshot\(/g)],
|
|
145
|
+
['v5', occurrences(v5, /function rosterSnapshot\(\)/g)],
|
|
146
|
+
]
|
|
147
|
+
ok(producers.every(([, n]) => n === 1), 'every preset has EXACTLY ONE participant-set producer ("produced only once")', JSON.stringify(producers))
|
|
148
|
+
const setAndVersion = [
|
|
149
|
+
['v2', /participants: participants/.test(body(v2, 'voteCount')) && /expected: expected/.test(body(v2, 'voteCount'))],
|
|
150
|
+
['v3', /participants: participants/.test(body(v3, 'voteCount')) && /expected: expected/.test(body(v3, 'voteCount'))],
|
|
151
|
+
['v4', /participants: sn\?sn\.rosterSnapshot:null/.test(v4) && /version: sn\?sn\.rosterVersion:null/.test(v4)],
|
|
152
|
+
['v5', /voterCount: snap\.voterCount/.test(body(v5, 'quorumView')) && /rosterVersion: snap\.version/.test(body(v5, 'quorumView'))],
|
|
153
|
+
]
|
|
154
|
+
ok(setAndVersion.every(([, b]) => b), 'every preset surfaces the set AND its roster version/expectation together', JSON.stringify(setAndVersion.filter(([, b]) => !b).map(([n]) => n)))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log('')
|
|
158
|
+
console.log('=== PARTICIPANT SET PARITY: ' + passed + ' passed, ' + failed + ' failed ===')
|
|
159
|
+
if (failures.length) for (const f of failures) console.error(' - ' + f)
|
|
160
|
+
process.exit(failed === 0 ? 0 : 1)
|