dsh-vibe-math 2.7.2 → 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.
Files changed (112) hide show
  1. package/README.en.md +11 -10
  2. package/README.md +13 -12
  3. package/cordis.patch.yml +112 -16
  4. package/docs/AUDIT-CHECKLIST.md +229 -3
  5. package/docs/COMPAT-AUDIT-ROUND2.md +35 -9
  6. package/docs/final-paper.md +2 -2
  7. package/docs/math-computation.md +13 -6
  8. package/docs/parameter-schema.md +37 -0
  9. package/docs/release-notes/README.en.md +1 -0
  10. package/docs/release-notes/README.md +1 -0
  11. package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +2 -2
  12. package/docs/release-notes/RELEASE-NOTES-2.7.2.md +3 -3
  13. package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
  14. package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
  15. package/docs/release-notes/TEMPLATE.md +6 -0
  16. package/docs/status-report-fields.md +167 -0
  17. package/docs/test-timing.md +8 -5
  18. package/installer.js +27 -17
  19. package/package.json +250 -203
  20. package/prompt-corpus-persona/persona-corpus.json +9 -9
  21. package/prompt-corpus-persona/persona-corpus.md +225 -33
  22. package/prompt-corpus-v2/formal-verify-v2.json +295 -115
  23. package/prompt-corpus-v2/formal-verify-v2.md +2287 -283
  24. package/prompt-corpus-v3/formal-verify-v3.json +105 -105
  25. package/prompt-corpus-v3/formal-verify-v3.md +213 -143
  26. package/prompt-corpus-v4/formal-verify-v4.json +18 -18
  27. package/prompt-corpus-v4/formal-verify-v4.md +49 -44
  28. package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
  29. package/prompt-corpus-v5/prompt-corpus-v5.md +451 -207
  30. package/scripts/update-doc-counts.mjs +71 -0
  31. package/tests/audit-artifact-docs.mjs +72 -0
  32. package/tests/audit-artifact-docs.mutants.mjs +58 -0
  33. package/tests/audit-installer-assertions.mutants.mjs +63 -0
  34. package/tests/audit-installer-compat.test.mjs +52 -4
  35. package/tests/audit-installer-policy.test.mjs +41 -8
  36. package/tests/audit-math-computation-contract.mjs +64 -8
  37. package/tests/audit-math-computation-contract.mutants.mjs +106 -0
  38. package/tests/audit-math-computation-parity.mjs +164 -3
  39. package/tests/audit-math-computation-sensitivity.mjs +4 -2
  40. package/tests/audit-package-membership.mjs +57 -0
  41. package/tests/audit-package-membership.mutants.mjs +28 -0
  42. package/tests/audit-participant-set-parity.mjs +160 -0
  43. package/tests/audit-path-discipline.mjs +83 -0
  44. package/tests/audit-path-discipline.mutants.mjs +51 -0
  45. package/tests/audit-persona-sensitivity.mjs +14 -0
  46. package/tests/audit-persona-surface.mutants.mjs +69 -0
  47. package/tests/audit-persona-surface.test.mjs +63 -22
  48. package/tests/audit-prompt-duplication.mjs +143 -0
  49. package/tests/audit-prompt-duplication.mutants.mjs +50 -0
  50. package/tests/audit-prompt-invariants.mjs +43 -5
  51. package/tests/audit-readme-bilingual.test.mjs +152 -0
  52. package/tests/audit-readme-counts.mjs +95 -0
  53. package/tests/audit-readme-counts.mutants.mjs +52 -0
  54. package/tests/audit-registration.mjs +78 -0
  55. package/tests/audit-status-report-fields.mjs +172 -0
  56. package/tests/audit-status-report-fields.mutants.mjs +64 -0
  57. package/tests/audit-v3-registration-parity.mjs +156 -0
  58. package/tests/audit-v5-integrity.mjs +300 -13
  59. package/tests/audit-v5-lean-abstention.mjs +392 -0
  60. package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
  61. package/tests/audit-v5-prompt-duplication.mjs +88 -0
  62. package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
  63. package/tests/audit-v5-sensitivity.mjs +12 -7
  64. package/tests/e2e-identity-a6.mutants.mjs +65 -0
  65. package/tests/e2e-v5-round2.test.mjs +458 -13
  66. package/tests/formal-verify-v2.test.mjs +91 -43
  67. package/tests/formal-verify-v3.mutants.mjs +117 -0
  68. package/tests/formal-verify-v3.test.mjs +85 -39
  69. package/tests/formal-verify-v4.mutants.mjs +152 -0
  70. package/tests/formal-verify-v4.test.mjs +541 -9
  71. package/tests/formal-verify-v5.test.mjs +68 -9
  72. package/tests/helpers/doc-counts-format.mjs +41 -0
  73. package/tests/helpers/math-computation-fake-seam.mjs +4 -0
  74. package/tests/math-computation-a1.mutants.mjs +72 -0
  75. package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
  76. package/tests/math-computation-discovery.mutants.mjs +76 -0
  77. package/tests/math-computation-shared.test.mjs +274 -5
  78. package/tests/math-computation-v2.test.mjs +767 -0
  79. package/tests/math-computation-v3.test.mjs +759 -0
  80. package/tests/math-computation-v4.mutants.mjs +133 -0
  81. package/tests/math-computation-v4.test.mjs +739 -0
  82. package/tests/prompt-v5-integrity.test.mjs +13 -2
  83. package/tests/run-tests.mjs +139 -9
  84. package/tests/run-tests.mutants.mjs +70 -0
  85. package/tests/selfdrive-v5.mjs +53 -1
  86. package/tests/selfdrive-v5.mutants.mjs +132 -0
  87. package/tests/v2-fix-probes.mutants.mjs +100 -0
  88. package/tests/v2-fix-probes.test.mjs +1039 -0
  89. package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
  90. package/tests/v3-fix-probes.mutants.mjs +95 -0
  91. package/tests/v3-fix-probes.test.mjs +891 -0
  92. package/tests/v5-institute-fixes.mutants.mjs +190 -0
  93. package/vibe-math-v2/agent.cordis.yml +62 -6
  94. package/vibe-math-v2/math-computation.js +130 -41
  95. package/vibe-math-v2/math-engines.js +12 -12
  96. package/vibe-math-v2/vibe-math-v2.js +388 -113
  97. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +5 -1
  98. package/vibe-math-v3/agent.cordis.yml +72 -6
  99. package/vibe-math-v3/math-computation.js +130 -41
  100. package/vibe-math-v3/math-engines.js +12 -12
  101. package/vibe-math-v3/vibe-math-v3.js +422 -126
  102. package/vibe-math-v4/agent.cordis.yml +44 -8
  103. package/vibe-math-v4/math-computation.js +130 -41
  104. package/vibe-math-v4/math-engines.js +12 -12
  105. package/vibe-math-v4/vibe-math-v4.js +196 -43
  106. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +2 -0
  107. package/vibe-math-v5/agent.cordis.yml +46 -12
  108. package/vibe-math-v5/math-computation.js +130 -41
  109. package/vibe-math-v5/math-engines.js +12 -12
  110. package/vibe-math-v5/vibe-math-v5.js +1173 -218
  111. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +80 -12
  112. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +2 -2
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Regenerate the count sentences quoted by shipped docs FROM THE TREE (D1: derive, don't type).
4
+ *
5
+ * node scripts/update-doc-counts.mjs # rewrite in place
6
+ * node scripts/update-doc-counts.mjs --check # exit 1 if anything would change (converges: whole-line rewrites)
7
+ *
8
+ * Authority: `node tests/run-tests.mjs --counts` (the gate's own JOB list) + `package.json#files`
9
+ * (shipped FILE counts). Shapes come from tests/helpers/doc-counts-format.mjs, shared with the guard
10
+ * (tests/audit-readme-counts.mjs) and its mutant harness. Adding a test file reddens the guard; the fix
11
+ * is to run this script. Whole lines are rebuilt (no partial regex edits), so a corrupted previous
12
+ * shape is repaired rather than half-matched.
13
+ *
14
+ * Grep anchors: update-doc-counts, doc-counts-format.
15
+ */
16
+ import { readFileSync, writeFileSync } from 'node:fs'
17
+ import { dirname, join } from 'node:path'
18
+ import { fileURLToPath } from 'node:url'
19
+ import { spawnSync } from 'node:child_process'
20
+ import { claims, MARKERS } from '../tests/helpers/doc-counts-format.mjs'
21
+
22
+ const HERE = dirname(fileURLToPath(import.meta.url))
23
+ const REPO = join(HERE, '..')
24
+ const checkOnly = process.argv.includes('--check')
25
+
26
+ const derived = JSON.parse(spawnSync(process.execPath, [join(REPO, 'tests', 'run-tests.mjs'), '--counts'], { cwd: REPO, encoding: 'utf8' }).stdout.trim())
27
+ const pkg = JSON.parse(readFileSync(join(REPO, 'package.json'), 'utf8'))
28
+ const shippedFiles = pkg.files.filter((f) => f.startsWith('tests/') && f.endsWith('.mjs'))
29
+ const shipped = { total: shippedFiles.length, suites: shippedFiles.filter((f) => f.endsWith('.test.mjs')).length }
30
+ shipped.probes = shipped.total - shipped.suites
31
+ const c = claims(derived, shipped)
32
+
33
+ function replaceLine(text, marker, canonical) {
34
+ const lines = text.split(/\r?\n/)
35
+ const i = lines.findIndex((l) => l.indexOf(marker) !== -1)
36
+ if (i === -1) return null
37
+ if (lines[i] === canonical) return text
38
+ lines[i] = canonical
39
+ return lines.join(text.includes('\r\n') ? '\r\n' : '\n')
40
+ }
41
+
42
+ const log = []
43
+ let changed = 0
44
+ const apply = (file, fn) => {
45
+ const p = join(REPO, file)
46
+ const before = readFileSync(p, 'utf8')
47
+ const after = fn(before)
48
+ if (after === null) { log.push('MARKER MISS: ' + file); return }
49
+ if (after !== before) {
50
+ changed++
51
+ if (checkOnly) log.push('WOULD CHANGE ' + file)
52
+ else { writeFileSync(p, after); log.push('updated ' + file) }
53
+ }
54
+ }
55
+
56
+ apply('README.md', (s) => replaceLine(s, MARKERS.zhBullet, c.zhBullet))
57
+ apply('README.en.md', (s) => replaceLine(s, MARKERS.enBullet, c.enBullet))
58
+ apply('docs/test-timing.md', (s) => replaceLine(s, MARKERS.ttRow, c.ttRow))
59
+ apply('docs/AUDIT-CHECKLIST.md', (s) => {
60
+ let out = replaceLine(s, MARKERS.d1Row, c.d1Row)
61
+ if (out === null) return null
62
+ out = out.replace(/\d+ 项作业(job count)= \d+ 套件 \+ \d+ 探针\/变体/, c.ckClaim)
63
+ out = out.replace(/只发 `tests\/` 的 .*?([^)]*)/, c.ckShipped)
64
+ return out
65
+ })
66
+
67
+ log.push((checkOnly ? 'check: ' : '') + 'derived JOBS total=' + derived.total + ' suites=' + derived.suites + ' probes=' + derived.probes
68
+ + ' | shipped FILES tests/*.mjs=' + shipped.total + ' (' + shipped.suites + ' suites + ' + shipped.probes + ' probes/scripts)'
69
+ + ' | files changed=' + changed)
70
+ console.log(log.join('\n'))
71
+ process.exit(checkOnly && changed ? 1 : 0)
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ARTIFACT DOCS vs CODE — the READMEs and the contract doc must not name an artifact a preset does not
4
+ * write, and must name the ones it does.
5
+ *
6
+ * DEFECT CLASS (D2): `Paper/<id>/paper.lock.json` was listed in the README's v4 section as "the directory's
7
+ * write lock, serialising concurrent rewrites/recompiles" — while v4 writes NO such file and
8
+ * `vibe-math-v4.js` says the shared-file lock is **NOT IMPLEMENTED** ("nothing is reserved, nothing is
9
+ * serialized"). The README promised a mechanism the code explicitly disclaims. The inverse direction
10
+ * matters too: v2/v3 DO write it (vibe-math-v2.js:3725/3732, vibe-math-v3.js:4318/4325) and their
11
+ * sections must list it.
12
+ *
13
+ * Run: node tests/audit-artifact-docs.mjs
14
+ * Env (mutant harness): ARTIFACT_README / ARTIFACT_README_EN / ARTIFACT_DOC / ARTIFACT_V2_JS /
15
+ * ARTIFACT_V3_JS / ARTIFACT_V4_JS / ARTIFACT_V5_JS (absolute or repo-relative).
16
+ */
17
+ import { readFileSync } from 'node:fs'
18
+ import { dirname, isAbsolute, join } from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+
21
+ const HERE = dirname(fileURLToPath(import.meta.url))
22
+ const REPO = join(HERE, '..')
23
+ const ARTIFACT = 'paper.lock.json'
24
+ let passed = 0, failed = 0
25
+ const failures = []
26
+ const ok = (cond, label, detail) => { if (cond) passed++; else { failed++; failures.push(label + (detail ? ' — ' + detail : '')) } }
27
+
28
+ const read = (envName, rel) => {
29
+ const v = process.env[envName]
30
+ return readFileSync(v ? (isAbsolute(v) ? v : join(REPO, v)) : join(REPO, rel), 'utf8')
31
+ }
32
+ const count = (text, needle) => text.split(needle).length - 1
33
+
34
+ // 1) CODE TRUTH: which presets write the artifact?
35
+ const presets = {
36
+ v2: read('ARTIFACT_V2_JS', 'vibe-math-v2/vibe-math-v2.js'),
37
+ v3: read('ARTIFACT_V3_JS', 'vibe-math-v3/vibe-math-v3.js'),
38
+ v4: read('ARTIFACT_V4_JS', 'vibe-math-v4/vibe-math-v4.js'),
39
+ v5: read('ARTIFACT_V5_JS', 'vibe-math-v5/vibe-math-v5.js'),
40
+ }
41
+ const writers = Object.keys(presets).filter((k) => count(presets[k], ARTIFACT) > 0)
42
+ const nonWriters = Object.keys(presets).filter((k) => count(presets[k], ARTIFACT) === 0)
43
+ ok(writers.length > 0, 'at least one preset writes ' + ARTIFACT + ' (else the doc expectation is empty)', JSON.stringify(writers))
44
+ ok(writers.join(',') === 'v2,v3', 'the writing presets are exactly v2,v3 (derived from code)', JSON.stringify({ writers, nonWriters }))
45
+
46
+ // 2) every README mention must sit in a writer's context OR be an explicit denial
47
+ const DENY = /(不写|未实现|无此锁|not implemented|does NOT write|no such|not write)/i
48
+ for (const [name, envName, rel] of [['README.md', 'ARTIFACT_README', 'README.md'], ['README.en.md', 'ARTIFACT_README_EN', 'README.en.md']]) {
49
+ const lines = read(envName, rel).split(/\r?\n/)
50
+ const mentions = lines.map((l, i) => ({ n: i + 1, l })).filter((x) => x.l.indexOf(ARTIFACT) !== -1)
51
+ ok(mentions.length > 0, name + ' mentions ' + ARTIFACT + ' somewhere (it is a real artifact for v2/v3)')
52
+ const bad = mentions.filter((m) => !/v2|v3/.test(m.l) && !DENY.test(m.l))
53
+ ok(bad.length === 0, '★ ' + name + ': every ' + ARTIFACT + ' mention names a WRITING preset or denies the claim', JSON.stringify(bad.map((b) => b.n)))
54
+ const listed = mentions.filter((m) => /Paper\/<[^>]*>\/\{[^}]*paper\.lock\.json/.test(m.l))
55
+ ok(listed.length >= 2, '★ ' + name + ' lists ' + ARTIFACT + ' inside the v2/v3 artifact braces', 'brace mentions=' + listed.length)
56
+ }
57
+
58
+ // 3) the contract doc must state the scope (v2/v3 write it; v4/v5 do not)
59
+ const doc = read('ARTIFACT_DOC', 'docs/final-paper.md')
60
+ const docLines = doc.split(/\r?\n/).filter((l) => l.indexOf(ARTIFACT) !== -1)
61
+ ok(docLines.length > 0, 'docs/final-paper.md documents ' + ARTIFACT)
62
+ const docScope = docLines.some((l) => /v2\/v3/.test(l) && DENY.test(l))
63
+ ok(docScope, '★ docs/final-paper.md scopes ' + ARTIFACT + ' to v2/v3 AND states that v4/v5 do not write it', docLines[0] ? docLines[0].slice(0, 80) : '(no line)')
64
+
65
+ // 4) the non-writer's own disclaimer must exist in code (so the doc denial is backed by the source)
66
+ ok(/NOT IMPLEMENTED/.test(presets.v4) && /claim_write/.test(presets.v4),
67
+ 'vibe-math-v4.js still carries the explicit NOT-IMPLEMENTED shared-lock disclaimer (the source of the doc denial)')
68
+
69
+ console.log('')
70
+ console.log('=== ARTIFACT DOCS: ' + passed + ' passed, ' + failed + ' failed ===')
71
+ for (const f of failures) console.error(' - ' + f)
72
+ process.exit(failed === 0 ? 0 : 1)
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Mutant harness for tests/audit-artifact-docs.mjs (ships: `tests/*.mutants.mjs`).
4
+ *
5
+ * Two single-site mutations, ONE PER DIRECTION (the guard must bite both ways):
6
+ * A) a COPY of README.md whose v2/v3 brace list loses `paper.lock.json` -> the listing assertion reddens
7
+ * B) a COPY of vibe-math-v4.js that mentions `paper.lock.json` -> the "writers are exactly v2,v3"
8
+ * assertion reddens (i.e. if v4 ever gains the artifact, the docs expectation moves with the code)
9
+ *
10
+ * Run: node tests/audit-artifact-docs.mutants.mjs
11
+ */
12
+ import { mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
13
+ import { tmpdir } from 'node:os'
14
+ import { join } from 'node:path'
15
+ import { spawnSync } from 'node:child_process'
16
+ import { fileURLToPath } from 'node:url'
17
+
18
+ const HERE = fileURLToPath(new URL('./', import.meta.url))
19
+ const REPO = join(HERE, '..')
20
+ const GUARD = join(HERE, 'audit-artifact-docs.mjs')
21
+ const ARTIFACT = 'paper.lock.json'
22
+ let passed = 0, failed = 0
23
+ const ok = (c, l, d) => { if (c) { passed++; console.log(' ok ' + l) } else { failed++; console.log(' FAIL ' + l + (d ? ' — ' + d : '')) } }
24
+
25
+ const run = (env) => {
26
+ const r = spawnSync(process.execPath, [GUARD], { cwd: REPO, encoding: 'utf8', env: Object.assign({}, process.env, env || {}) })
27
+ const out = String(r.stdout || '') + String(r.stderr || '')
28
+ return { status: r.status, out, failLines: out.split('\n').filter((l) => /^ - /.test(l)) }
29
+ }
30
+ const baseline = run(null)
31
+ ok(baseline.status === 0, 'baseline: the artifact-docs guard is green',
32
+ (baseline.out.trim().split('\n').filter(Boolean).slice(-1)[0] || '').slice(0, 80))
33
+
34
+ const dir = mkdtempSync(join(tmpdir(), 'artifact-mut-'))
35
+ // A) README loses the v2/v3 brace listing
36
+ const zh = readFileSync(join(REPO, 'README.md'), 'utf8')
37
+ const braces = zh.match(/Paper\/<[^>]*>\/\{[^}]*paper\.lock\.json[^}]*\}/g) || []
38
+ if (!braces.length) ok(false, 'A: the README v2/v3 brace listing exists (harness and guard agree on the shape)', 'ANCHOR MISS')
39
+ else {
40
+ const tampered = zh.split(braces[0]).join(braces[0].replace(',' + ARTIFACT, ''))
41
+ writeFileSync(join(dir, 'README.md'), tampered)
42
+ const a = run({ ARTIFACT_README: join(dir, 'README.md') })
43
+ const named = a.failLines.some((l) => l.indexOf('lists ' + ARTIFACT + ' inside the v2/v3 artifact braces') !== -1)
44
+ ok(a.status !== 0 && named, '★ A: dropping the artifact from the v2/v3 brace list reddens the guard by name', 'exit=' + a.status)
45
+ if (!named) for (const l of a.failLines.slice(0, 3)) console.log(' guard failure: ' + l.trim().slice(0, 130))
46
+ }
47
+ // B) v4 "gains" the artifact
48
+ const v4 = readFileSync(join(REPO, 'vibe-math-v4/vibe-math-v4.js'), 'utf8')
49
+ writeFileSync(join(dir, 'vibe-math-v4.js'), v4 + '\n// mutant: pretend v4 writes ' + ARTIFACT + '\n')
50
+ const b = run({ ARTIFACT_V4_JS: join(dir, 'vibe-math-v4.js') })
51
+ const namedB = b.failLines.some((l) => l.indexOf('the writing presets are exactly v2,v3') !== -1)
52
+ ok(b.status !== 0 && namedB, '★ B: a v4 that mentions the artifact reddens the "writers are exactly v2,v3" assertion', 'exit=' + b.status)
53
+ if (!namedB) for (const l of b.failLines.slice(0, 3)) console.log(' guard failure: ' + l.trim().slice(0, 130))
54
+
55
+ rmSync(dir, { recursive: true, force: true })
56
+ console.log('')
57
+ console.log('=== ARTIFACT DOCS MUTANTS: ' + passed + ' passed, ' + failed + ' failed ===')
58
+ process.exit(failed === 0 ? 0 : 1)
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Mutant harness for the installer ASSERTION-QUALITY work (ships: `tests/*.mutants.mjs`).
4
+ *
5
+ * It replaces the dev-only scratch proofs (`_oneoff/auditR2/a78-mutants.mjs`, `a8-mutant.mjs`) so a package
6
+ * reader can reproduce them from the tarball. Every case is a SINGLE-SITE edit to a COPY of installer.js,
7
+ * injected through the guards' `INSTALLER_JS` env seam, and must redden the NAMED assertion:
8
+ *
9
+ * A7-version perturb the structured version the probe returns -> compat: "STRUCTURED field: version"
10
+ * A7-source make the structured source stop naming the service -> compat: "structured field naming the service"
11
+ * A8-failure inject one failure line in the backup path -> policy: "NOT reported as a failure (report-level"
12
+ * DL-label revert one label to English-only -> policy: "English-only label"
13
+ * DL-duplicate report the cleanup action a second time -> policy: "reported EXACTLY ONCE"
14
+ *
15
+ * Run: node tests/audit-installer-assertions.mutants.mjs
16
+ */
17
+ import { mkdtempSync, copyFileSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
18
+ import { tmpdir } from 'node:os'
19
+ import { join } from 'node:path'
20
+ import { spawnSync } from 'node:child_process'
21
+ import { fileURLToPath } from 'node:url'
22
+
23
+ const HERE = fileURLToPath(new URL('./', import.meta.url))
24
+ const REPO = join(HERE, '..')
25
+ const SRC = join(REPO, 'installer.js')
26
+ const COMPAT = join(HERE, 'audit-installer-compat.test.mjs')
27
+ const POLICY = join(HERE, 'audit-installer-policy.test.mjs')
28
+ let passed = 0, failed = 0
29
+ const ok = (c, l, d) => { if (c) { passed++; console.log(' ok ' + l) } else { failed++; console.log(' FAIL ' + l + (d ? ' — ' + d : '')) } }
30
+
31
+ const ORIGIN = readFileSync(SRC, 'utf8')
32
+ const OUT_LINE = 'const out = { version: first.version, source: first.source, probes: probes.slice() }'
33
+ const CLEANUP_IF = 'if (removedFiles > 0 || removedDirs.length > 0 || cleanupNotes.length > 0) {'
34
+ const CASES = [
35
+ { name: 'A7-version: the structured version is perturbed', guard: COMPAT, expect: 'STRUCTURED field: version',
36
+ edit: (s) => s.replace(OUT_LINE, "const out = { version: String(first.version) + '-mutated', source: first.source, probes: probes.slice() }") },
37
+ { name: 'A7-source: the structured source stops naming the service', guard: COMPAT, expect: 'structured field naming the service',
38
+ edit: (s) => s.replace(OUT_LINE, "const out = { version: first.version, source: 'pm', probes: probes.slice() }") },
39
+ { name: 'A8-failure: a failure line is injected in the backup path', guard: POLICY, expect: 'NOT reported as a failure (report-level',
40
+ edit: (s) => s.replace('const backupFailures = []', "const backupFailures = []\n logger?.warn?.('[dsh-vibe-math] 预设安装/更新失败:mutant injected failure line')") },
41
+ { name: 'DL-label: one label is reverted to English-only', guard: POLICY, expect: 'English-only label',
42
+ edit: (s) => s.replace("'[dsh-vibe-math] 预设清理:", "'[dsh-vibe-math] preset cleanup: ") },
43
+ { name: 'DL-duplicate: the cleanup action is reported twice', guard: POLICY, expect: 'reported EXACTLY ONCE',
44
+ edit: (s) => s.replace(CLEANUP_IF, CLEANUP_IF + "\n logger?.info?.('[dsh-vibe-math] 预设清理:再次报告(删除 ' + removedFiles + ' 个文件)')") },
45
+ ]
46
+ for (const c of CASES) {
47
+ const mutated = c.edit(ORIGIN)
48
+ if (mutated === ORIGIN) { ok(false, c.name + ' — the single-site anchor applies', 'ANCHOR MISS'); continue }
49
+ const dir = mkdtempSync(join(tmpdir(), 'installer-assert-mut-'))
50
+ copyFileSync(SRC, join(dir, 'installer.js'))
51
+ writeFileSync(join(dir, 'installer.js'), mutated)
52
+ const r = spawnSync(process.execPath, [c.guard], { cwd: REPO, encoding: 'utf8', maxBuffer: 1 << 26, env: Object.assign({}, process.env, { INSTALLER_JS: join(dir, 'installer.js') }) })
53
+ const o = String(r.stdout || '') + String(r.stderr || '')
54
+ const failLines = o.split('\n').filter((l) => /^ - /.test(l))
55
+ const named = failLines.some((l) => l.indexOf(c.expect) !== -1)
56
+ const sum = (o.split('\n').filter((l) => /passed, \d+ failed/.test(l)).slice(-1)[0] || '').trim()
57
+ ok(r.status !== 0 && named, '★ ' + c.name + ' -> ' + sum, r.status === 0 ? 'the guard stayed GREEN' : (named ? '' : 'named red not found'))
58
+ if (!named) for (const l of failLines.slice(0, 3)) console.log(' ' + l.trim().slice(0, 140))
59
+ rmSync(dir, { recursive: true, force: true })
60
+ }
61
+ console.log('')
62
+ console.log('=== INSTALLER ASSERTION MUTANTS: ' + passed + ' red as required, ' + failed + ' problem(s) ===')
63
+ process.exit(failed === 0 ? 0 : 1)
@@ -19,7 +19,7 @@ import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync
19
19
  import { tmpdir } from 'node:os'
20
20
  import { fileURLToPath, pathToFileURL } from 'node:url'
21
21
  import { join, resolve } from 'node:path'
22
- import { satisfiesDshRange, dshVersionVerdict, PRESETS } from '../installer.js'
22
+ import { satisfiesDshRange, dshVersionVerdict, detectDshVersion, PRESETS } from '../installer.js'
23
23
 
24
24
  const HERE = fileURLToPath(new URL('../', import.meta.url))
25
25
  const pkg = JSON.parse(readFileSync(join(HERE, 'package.json'), 'utf8'))
@@ -196,13 +196,61 @@ console.log('=== 4. a pluginManager that never answers must not hold the row ===
196
196
  // the timeout must NOT replace a working probe: a prompt service still supplies the version
197
197
  const quick = run({ listBundles: async () => [{ name: '@deepseek-ai/dsh-base', version: '0.2.0-rc.2' }] })
198
198
  await mod.apply(quick.ctx)
199
- ok(quick.logs.some((l) => l.includes('0.2.0-rc.2')),
200
- 'a prompt listBundles() still supplies the version (the timeout is a fallback, not a replacement)',
201
- quick.logs.filter((l) => l.includes('source')).join(' | ').slice(0, 160))
199
+ // A7 (assertion quality): the version+source claim is asserted on the STRUCTURED result - a bare
200
+ // `logs.some(l => l.includes('0.2.0-rc.2'))` would also pass if the string appeared anywhere for any
201
+ // reason, and `l.includes('source')` would pass on the English word alone. The log keeps ONE
202
+ // shape-anchored check.
203
+ const probeQuick = (await import(pathToFileURL(INSTALLER_SRC).href + '?t=' + Date.now())).detectDshVersion || detectDshVersion
204
+ const detQuick = await probeQuick(quick.ctx)
205
+ ok(detQuick && detQuick.version === '0.2.0-rc.2',
206
+ '★ a prompt listBundles() still supplies the version (STRUCTURED field: version)', JSON.stringify(detQuick && { version: detQuick.version, source: detQuick.source }))
207
+ ok(!!detQuick && typeof detQuick.source === 'string' && detQuick.source.indexOf('pluginManager') === 0,
208
+ '★ ...and the winning SOURCE is a structured field naming the service (not the word "source" in a log line)', JSON.stringify(detQuick && detQuick.source))
209
+ ok(quick.logs.some((l) => /v?0\.2\.0-rc\.2/.test(l)),
210
+ '...and a log line SHAPE carries that version (shape-anchored, not a bare substring)')
202
211
  rmSync(tmp, { recursive: true, force: true })
203
212
  }
204
213
 
214
+ // Installer review (finding): several sources can report the host's version and "first available wins"
215
+ // hid a conflicting host state. The probe records EVERY value+source, keeps the first available
216
+ // authoritative (priority unchanged), and assembles `disagreement` ONLY when one exists.
217
+ {
218
+ const prev = process.env.DSH_VERSION
219
+ try {
220
+ // Import through INSTALLER_SRC (env-overridable) so a single-site mutant on a COPY of installer.js
221
+ // reaches these assertions - that is what makes the mutant below able to bite.
222
+ const mod = await import(pathToFileURL(INSTALLER_SRC).href + '?t=' + Date.now())
223
+ const probe = mod.detectDshVersion || detectDshVersion
224
+ process.env.DSH_VERSION = '9.9.9'
225
+ const disagreeing = { get: (n) => (n === 'pluginManager' ? { listBundles: async () => [{ name: '@deepseek-ai/dsh', version: '8.8.8' }] } : undefined) }
226
+ const d1 = await probe(disagreeing)
227
+ ok(d1 && d1.version === '9.9.9' && d1.source === 'DSH_VERSION', 'the FIRST available source stays authoritative (which version wins is unchanged)', JSON.stringify(d1))
228
+ ok(d1 && Array.isArray(d1.probes) && d1.probes.length >= 2 && d1.probes.some((p) => p.source.indexOf('pluginManager') === 0 && p.version === '8.8.8'),
229
+ '★ every probe value+source is recorded (nothing is silently dropped)', JSON.stringify(d1 && d1.probes))
230
+ ok(!!(d1 && d1.disagreement) && d1.disagreement.primary === 'DSH_VERSION' && d1.disagreement.conflicts.some((c) => c.version === '8.8.8'),
231
+ '★ DISAGREEING probes surface a disagreement slot naming the conflict', JSON.stringify(d1 && d1.disagreement))
232
+ const agreeing = { get: (n) => (n === 'pluginManager' ? { listBundles: async () => [{ name: '@deepseek-ai/dsh', version: '9.9.9' }] } : undefined) }
233
+ const d2 = await probe(agreeing)
234
+ ok(d2 && d2.version === '9.9.9' && !('disagreement' in d2), '★ AGREEING probes print NO disagreement slot (assemble-only-on-conflict)', JSON.stringify(d2))
235
+ // The `%s` placeholder that could reach the user: every installer log line must pre-concatenate.
236
+ const isrc = readFileSync(INSTALLER_SRC, 'utf8')
237
+ const printf = isrc.split('\n').filter((l) => /logger|console/.test(l) && /%[sd]/.test(l))
238
+ ok(printf.length === 0, '★ no installer log line carries a bare printf placeholder (%s/%d)', JSON.stringify(printf.slice(0, 2)))
239
+ } finally {
240
+ if (prev === undefined) delete process.env.DSH_VERSION; else process.env.DSH_VERSION = prev
241
+ }
242
+ }
243
+
205
244
  console.log('')
245
+ // The self-check proves service/API SHAPE only - never runtime SEMANTICS. The caveat must be on the
246
+ // USER-VISIBLE pass line, so a passing self-check is not read as a compatibility claim.
247
+ {
248
+ const src = readFileSync(INSTALLER_SRC, 'utf8')
249
+ const line = src.split(/\r?\n/).find((l) => l.indexOf('自检通过') !== -1 && l.indexOf('logger') !== -1) || ''
250
+ ok(line.length > 0, 'the installer reports a passing host self-check')
251
+ ok(line.indexOf('形状') !== -1 && line.indexOf('行为') !== -1, '★ self-check caveat: the PASS line states that SHAPE is proven and BEHAVIOUR is not', JSON.stringify(line.slice(0, 90)))
252
+ }
253
+
206
254
  console.log('=== INSTALLER COMPAT: ' + passed + ' passed, ' + failed + ' failed ===')
207
255
  if (failed) { for (const f of failures) console.error(' - ' + f); process.exit(1) }
208
256
  console.log('ALL GREEN')
@@ -79,7 +79,10 @@ async function applyFrom(pkgDir, home, logs) {
79
79
  return logs
80
80
  }
81
81
  const sha = (buf) => createHash('sha256').update(buf).digest('hex')
82
- const failedLog = (logs) => logs.filter((l) => l.includes('preset install/update failed'))
82
+ // Label-tolerant on purpose: the installer's user-facing failure line is emitted by the logger helper
83
+ // (not console.*) and its label is localised; the FACT asserted here is that a failure line exists.
84
+ const FAILURE_LINE = /预设安装\/更新失败|preset install\/update failed/
85
+ const failedLog = (logs) => logs.filter((l) => FAILURE_LINE.test(l))
83
86
 
84
87
  const tmp = mkdtempSync(join(tmpdir(), 'vibe-installer-policy-'))
85
88
  const home = join(tmp, 'dshhome')
@@ -153,7 +156,9 @@ let userJsBytes = null
153
156
  const state = JSON.parse(readFileSync(stateFile, 'utf8'))
154
157
  ok(state.version === '2.0.0', 'the state records the new version', String(state.version))
155
158
  ok(logs.some((l) => l.includes('.vibe-math-backup')), 'the log names the backup location')
156
- ok(logs.some((l) => l.includes('复制一份')), 'the log tells the user how to customize a preset properly')
159
+ // A8: the FACT (a version change backs up the user's edit) is asserted structurally above (backup path
160
+ // + byte equality). What remains is guidance wording, so this is the ONE labelled wording smoke check.
161
+ ok(logs.some((l) => /复制一份/.test(l)), '[wording smoke check — the ONE allowed] the log tells the user how to customize a preset properly')
157
162
  ok(failedLog(logs).length === 0, 'apply() swallowed no failure', failedLog(logs)[0])
158
163
  }
159
164
 
@@ -164,7 +169,13 @@ console.log('=== 4. a deleted file at the SAME version: restored, nothing backed
164
169
  ok(existsSync(at(PRESETS[2], 'agent.cordis.yml')), 'a deleted managed file is restored')
165
170
  ok(isCopyOf(PRESETS[2], 'agent.cordis.yml'), 'the restored file is byte-identical to the shipped one')
166
171
  ok(!existsSync(join(backupRoot, '2.0.0')), 'a run that replaced nothing creates no backup directory')
167
- ok(logs.some((l) => l.includes('restored')), 'the restore is reported')
172
+ // Guard-sync: the installer's user-visible stream is Chinese by convention (one language, see the
173
+ // comment at the cleanup line), so these assert the FACT (an action keyword + the count) rather than
174
+ // an English word.
175
+ // "one action, one line": the restore must be reported, and reported EXACTLY ONCE (a second report of the
176
+ // same action is the defect the duplicate-log review was about).
177
+ const restoreLines = logs.filter((l) => /\u8fd8\u539f\u4e86\s*\d+\s*\u4e2a\u7f3a\u5931\u7684\u9884\u8bbe\u6587\u4ef6/.test(l))
178
+ ok(restoreLines.length === 1, '★ the restore is reported EXACTLY ONCE (one action, one line)', JSON.stringify(restoreLines.map((l) => l.slice(0, 60))))
168
179
  ok(failedLog(logs).length === 0, 'apply() swallowed no failure', failedLog(logs)[0])
169
180
  }
170
181
 
@@ -182,7 +193,8 @@ console.log('=== 5. a preset this bundle no longer ships ===')
182
193
  ok(!existsSync(join(presetRoot, 'vibe-math-v1', 'legacy.js')), 'a package-owned file of a dropped preset is removed')
183
194
  ok(!existsSync(join(presetRoot, 'vibe-math-v1')), 'the dropped preset directory is removed once empty')
184
195
  ok(existsSync(join(presetRoot, 'vibe-math-v0', 'mine.js')), 'a file recorded as user-owned in a dropped preset is kept')
185
- ok(logs.some((l) => l.includes('cleanup')), 'the cleanup is reported')
196
+ const cleanupLines = logs.filter((l) => /\u9884\u8bbe\u6e05\u7406/.test(l) && /\u5220\u9664\s*\d+\s*\u4e2a\u6587\u4ef6/.test(l))
197
+ ok(cleanupLines.length === 1, '★ the cleanup is reported EXACTLY ONCE (one action, one line)', JSON.stringify(cleanupLines.map((l) => l.slice(0, 60))))
186
198
  ok(failedLog(logs).length === 0, 'apply() swallowed no failure', failedLog(logs)[0])
187
199
  }
188
200
 
@@ -224,7 +236,9 @@ console.log('=== 7. an unreadable package manifest replaces nothing (and self-he
224
236
  ok(readFileSync(at(PRESETS[1], 'preset.yml')).equals(readFileSync(shipped(PRESETS[1], 'preset.yml', pkgC))),
225
237
  'the next readable run replaces the drift as a normal version change')
226
238
  ok(readFileSync(earlier, 'utf8') === 'EARLIER COPY\n', 'a backup that already exists for that version is kept byte-for-byte (the earliest copy wins)')
227
- ok(logs2.every((l) => !l.includes('备份失败')), 'a pre-existing backup for the same version is NOT reported as a failure')
239
+ // A8: report-level, not wording-level - a pre-existing backup must not be counted as a failure by the
240
+ // guard's own failure-line helper (which matches the localised failure label, either spelling).
241
+ ok(failedLog(logs2).length === 0, '★ a pre-existing backup for the same version is NOT reported as a failure (report-level check, not a wording check)', failedLog(logs2)[0])
228
242
  ok(JSON.parse(readFileSync(stateFile, 'utf8')).version === '3.0.0', 'the state records the new version')
229
243
 
230
244
  // the backup directory must never be readable as a preset: DSH only accepts [a-z0-9][a-z0-9-]* ids
@@ -255,7 +269,7 @@ console.log('=== 9. a recorded version NEWER than this package (a downgrade) say
255
269
  const backup = join(backupRoot, '3.0.0', PRESETS[0].dst, 'preset.yml')
256
270
  ok(existsSync(backup) && readFileSync(backup).equals(newerBytes), 'the replaced newer bytes are backed up under the version they came from')
257
271
  ok(JSON.parse(readFileSync(stateFile, 'utf8')).version === '2.0.0', 'the state records the version that is actually installed now')
258
- ok(!logs.some((l) => l.includes('preset install/update failed')), 'apply() swallowed no failure')
272
+ ok(failedLog(logs).length === 0, 'apply() swallowed no failure')
259
273
  }
260
274
 
261
275
  // ============================================================================================
@@ -302,7 +316,7 @@ console.log('=== 10. preset writes are ATOMIC: a failed write cannot touch the d
302
316
  const logs = await applyFrom(pkgB, H.home, [])
303
317
  ok(readText(H.at(target.p, target.f)) === userBytes.toString(),
304
318
  'a failed preset write leaves the destination byte-for-byte intact (tmp+rename, never in place)')
305
- ok(logs.some((l) => l.includes('preset install/update failed')),
319
+ ok(failedLog(logs).length > 0,
306
320
  '...and the failure is reported instead of being swallowed', logs.filter((l) => l.includes('failed')).join(' | ').slice(0, 200))
307
321
  ok((readJson(H.state) || {}).version === '1.0.0',
308
322
  'the state still describes the bytes that are actually on disk, so the next boot self-heals')
@@ -414,7 +428,7 @@ console.log('=== 15. a failed state write is reported (not swallowed), and does
414
428
  const logs = await applyFrom(pkgA, H.home, [])
415
429
  ok(logs.some((l) => l.includes('状态文件写入失败')),
416
430
  'a failed state write is REPORTED (it used to fail silently)', logs.filter((l) => l.includes('写入失败')).join(' | ').slice(0, 160))
417
- ok(!logs.some((l) => l.includes('preset install/update failed')), '...without aborting the preset step')
431
+ ok(failedLog(logs).length === 0, '...without aborting the preset step')
418
432
  ok((readJson(H.state) || {}).version === '1.0.0',
419
433
  'the previous state file is still parseable (tmp+rename left no half-written file)')
420
434
  rmSync(H.state + '.' + process.pid + '.0.tmp', { recursive: true, force: true })
@@ -478,6 +492,25 @@ console.log('=== 16b. a stale file whose BACKUP FAILS is kept, not deleted unbac
478
492
  rmSync(join(H.root, '.vibe-math-backup'), { force: true })
479
493
  }
480
494
 
495
+ console.log('=== 16. one action, one language: no user-facing line carries an English-only label ===')
496
+ {
497
+ // The convention (docs/AUDIT-CHECKLIST.md): installer user-facing output is Chinese; an English-only
498
+ // LABEL is a defect (an English domain word inside a Chinese label, like `preset 声明方式`, is fine).
499
+ // This is a CONVENTION sweep, not a wording pin: any CJK-bearing label passes.
500
+ const src = readFileSync(INSTALLER_SRC, 'utf8')
501
+ const labelOf = (l) => { const m = /\[dsh-vibe-math\]\s*([^::]*)/.exec(l); return m ? m[1].trim() : '' }
502
+ const asciiOnly = (lab) => lab.length > 0 && /^[\x20-\x7e]+$/.test(lab)
503
+ const sites = src.split(/\r?\n/).filter((l) => /logger\?\.(info|warn|error)\?\.\(/.test(l))
504
+ ok(sites.length > 0, 'the installer has user-facing log sites to sweep (found ' + sites.length + ')')
505
+ const badSites = sites.filter((l) => asciiOnly(labelOf(l)))
506
+ ok(badSites.length === 0, '★ no user-facing log SITE carries an English-only label (installer output is Chinese)',
507
+ JSON.stringify(badSites.map((l) => l.trim().slice(0, 70))))
508
+ const runtimeLogs = await applyFrom(pkgB, home, [])
509
+ const badRuntime = runtimeLogs.filter((l) => asciiOnly(labelOf(l)))
510
+ ok(badRuntime.length === 0, '★ ...and no CAPTURED line does either (code and convention agree at runtime)',
511
+ JSON.stringify(badRuntime.slice(0, 2)))
512
+ }
513
+
481
514
  rmSync(tmp, { recursive: true, force: true })
482
515
 
483
516
  console.log('')
@@ -108,14 +108,48 @@ for (const P of PRESETS) {
108
108
  ? "registerTool('vibe_math_set_params'"
109
109
  : (P.dir === 'vibe-math-v4' ? "registerTool('vibe_v4_set'" : "registerTool('vibe_v5_set'")
110
110
  {
111
- const i = js.indexOf(SET_TOOL)
112
- const callText = i === -1 ? '' : js.slice(i, js.indexOf('registerTool(', i + 1) === -1 ? i + 4000 : js.indexOf('registerTool(', i + 1))
113
- const oi = callText.lastIndexOf('objParams(')
114
- const setSchema = oi === -1 ? '' : callText.slice(oi)
115
- ok(setSchema.length > 0, tag + 'set tool takes an objParams({...}) schema argument')
116
- for (const k of SIX) {
117
- ok(new RegExp('(^|[{,\\s])' + k + '\\s*:').test(setSchema), tag + 'set schema has the property ' + k + ' (property-level, not prose)', 'schema len ' + setSchema.length)
118
- }
111
+ // §9.7 ㉓/㉜: EVERY registration site is checked, not just the first. This preset class is defined by
112
+ // having TWO independent registration layers (session `makeSession` + `apply`, each with its own
113
+ // `objParams`), so an audit that inspects only the first cannot see the second diverge — the F-B defect
114
+ // class. If a site ever legitimately carries a narrower schema, record that difference ON THAT SITE
115
+ // explicitly instead of narrowing the loop back to "first site only".
116
+ const sites = []
117
+ for (let at = js.indexOf(SET_TOOL); at !== -1; at = js.indexOf(SET_TOOL, at + 1)) sites.push(at)
118
+ ok(sites.length >= 1, tag + 'the set tool is registered', 'sites=' + sites.length)
119
+ // F-B (docs/parameter-schema.md): a schema argument has two shapes —
120
+ // objParams({ ...literal... }) the historical hand-written table
121
+ // objParams(paramProps()) derived from the SINGLE source PARAM_SCHEMA; the machine key set is
122
+ // pinned by PARAM_PROPS_KEYS (+ PARAM_PROPS_EXTRA)
123
+ // The derived form has NO `{` after `objParams(`, so a naive slice grabbed unrelated text (331-char
124
+ // fragment). Resolve it to the single-source key list, searched FILE-WIDE (that table is module scope).
125
+ const derivedKeys = (() => {
126
+ const km = /const PARAM_PROPS_KEYS = \[([^\]]*)\]/.exec(js)
127
+ if (!km) return null
128
+ const keys = km[1].split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean)
129
+ const em = /const PARAM_PROPS_EXTRA = \{([\s\S]*?)\n\s*\}/.exec(js)
130
+ const extra = em ? [...em[1].matchAll(/^\s*([A-Za-z_$][\w$]*)\s*:/gm)].map((m) => m[1]) : []
131
+ return [...new Set(keys.concat(extra))]
132
+ })()
133
+ sites.forEach((idx, n) => {
134
+ const next = js.indexOf('registerTool(', idx + 1)
135
+ const callText = js.slice(idx, next === -1 ? idx + 4000 : next)
136
+ const oi = callText.lastIndexOf('objParams(')
137
+ const setSchema = oi === -1 ? '' : callText.slice(oi)
138
+ const at = oi === -1 ? -1 : oi + 'objParams('.length
139
+ const open = at === -1 ? -1 : callText.indexOf('{', at)
140
+ const close = at === -1 ? -1 : callText.indexOf(')', at)
141
+ const literalForm = open !== -1 && (close === -1 || open < close)
142
+ ok(literalForm ? setSchema.length > 0 : !!derivedKeys,
143
+ tag + 'site#' + n + ' takes an objParams(...) schema argument (literal table or single-source derivation)',
144
+ literalForm ? 'schema len ' + setSchema.length : 'derived keys ' + (derivedKeys ? derivedKeys.length : 0))
145
+ for (const k of SIX) {
146
+ const present = literalForm
147
+ ? new RegExp('(^|[{,\\s])' + k + '\\s*:').test(setSchema)
148
+ : !!derivedKeys && derivedKeys.includes(k)
149
+ ok(present, tag + 'site#' + n + ' set schema has the property ' + k + ' (property-level, not prose)',
150
+ literalForm ? 'schema len ' + setSchema.length : 'derived keys ' + (derivedKeys ? derivedKeys.length : 0))
151
+ }
152
+ })
119
153
  }
120
154
  if (P.dir === 'vibe-math-v2' || P.dir === 'vibe-math-v3') {
121
155
  const schema = arrayBlock(js, 'const PARAM_SCHEMA')
@@ -238,6 +272,28 @@ for (const f of ['README.md', 'README.en.md']) {
238
272
  ok(doc.indexOf('Octave / Julia 未安装') !== -1 && doc.indexOf('VERIFY') !== -1 && doc.indexOf('无法强制禁网') !== -1, 'docs §6 keeps the still-unverified set (no Octave/Julia, commercial VERIFY, no network/write enforcement)')
239
273
  ok(doc.indexOf('只通过假 subprocess seam 验证') === -1, 'docs §6 no longer claims engine execution was verified ONLY through the fake seam (stale claim removed)')
240
274
  ok(doc.indexOf('串行') !== -1 && doc.indexOf('跨进程') !== -1, 'docs §6 states the in-process serialisation and the cross-process limitation')
275
+ // ── P1/6.1: the v4 frozen participant set is documented as the ATOMIC triple, and the deprecated
276
+ // alias is described as an alias - both in the doc AND verifiable in the preset source.
277
+ {
278
+ const v4 = existsSync(join(ROOT, 'vibe-math-v4/vibe-math-v4.js')) ? read('vibe-math-v4/vibe-math-v4.js') : ''
279
+ const atomic = /frozen:\s*\(function\(sn\)\{\s*return \{version: sn\?sn\.rosterVersion:null, participants: sn\?sn\.rosterSnapshot:null, kind:/.test(v4)
280
+ ok(atomic, 'v4 exposes the frozen participant set as ONE atomic object {version, participants, kind}')
281
+ const alias = /frozenParticipants:\s*\(activeRosterSnapshot\(\)\|\|\{\}\)\.rosterSnapshot\|\|null/.test(v4) && /deprecated/.test(v4)
282
+ ok(alias, 'v4 keeps `frozenParticipants` only as a deprecated alias of frozen.participants')
283
+ ok(doc.indexOf('frozen') !== -1 && doc.indexOf('deprecated') !== -1 && doc.indexOf('原子') !== -1,
284
+ 'docs document the atomic `frozen{version,participants,kind}` and mark the alias deprecated')
285
+ }
286
+ // ── P1/6.2: the evidence-field list cannot drift from the code again - every timeout evidence field
287
+ // the module produces must be named in the doc, and vice versa for the two receipt-only ones.
288
+ {
289
+ const mod = readFileSync(join(ROOT, 'vibe-math-v2/math-computation.js'), 'utf8')
290
+ const timeoutFields = ['partialStdout', 'partialStderr', 'stdout', 'stderr', 'exit', 'timedOut']
291
+ const docMissing = timeoutFields.filter((f) => doc.indexOf(f) === -1)
292
+ ok(docMissing.length === 0, 'docs name every MATH_TIMEOUT evidence field', 'missing in doc: ' + JSON.stringify(docMissing))
293
+ const codeMissing = ['partialStdout', 'partialStderr'].filter((f) => mod.indexOf(f) === -1)
294
+ ok(codeMissing.length === 0, 'the doc-sync check is anchored in the code too (partialStdout/partialStderr exist in the module)',
295
+ 'missing in code: ' + JSON.stringify(codeMissing))
296
+ }
241
297
  }
242
298
 
243
299
  console.log('')