dsh-vibe-math 2.7.2 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +682 -681
  3. package/README.md +683 -682
  4. package/cordis.patch.yml +112 -16
  5. package/docs/AUDIT-CHECKLIST.md +755 -518
  6. package/docs/COMPAT-AUDIT-ROUND2.md +354 -328
  7. package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
  8. package/docs/audit-methodology.md +93 -0
  9. package/docs/final-paper.md +172 -172
  10. package/docs/formal-verification.md +613 -588
  11. package/docs/generate_framework_diagram_v2.py +114 -114
  12. package/docs/generate_framework_diagram_v3.py +127 -127
  13. package/docs/math-computation.md +195 -187
  14. package/docs/parameter-schema.md +37 -0
  15. package/docs/release-notes/README.en.md +2 -0
  16. package/docs/release-notes/README.md +2 -0
  17. package/docs/release-notes/RELEASE-NOTES-2.0.22.md +112 -112
  18. package/docs/release-notes/RELEASE-NOTES-2.1.0.md +143 -143
  19. package/docs/release-notes/RELEASE-NOTES-2.2.0.md +266 -266
  20. package/docs/release-notes/RELEASE-NOTES-2.2.1.md +43 -43
  21. package/docs/release-notes/RELEASE-NOTES-2.2.2.md +88 -88
  22. package/docs/release-notes/RELEASE-NOTES-2.3.0.md +207 -207
  23. package/docs/release-notes/RELEASE-NOTES-2.3.1.md +134 -134
  24. package/docs/release-notes/RELEASE-NOTES-2.3.10.md +105 -105
  25. package/docs/release-notes/RELEASE-NOTES-2.3.11.md +57 -57
  26. package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -80
  27. package/docs/release-notes/RELEASE-NOTES-2.3.2.md +145 -145
  28. package/docs/release-notes/RELEASE-NOTES-2.3.3.md +115 -115
  29. package/docs/release-notes/RELEASE-NOTES-2.3.4.md +69 -69
  30. package/docs/release-notes/RELEASE-NOTES-2.3.5.md +63 -63
  31. package/docs/release-notes/RELEASE-NOTES-2.3.6.md +66 -66
  32. package/docs/release-notes/RELEASE-NOTES-2.3.7.md +59 -59
  33. package/docs/release-notes/RELEASE-NOTES-2.3.8.md +45 -45
  34. package/docs/release-notes/RELEASE-NOTES-2.3.9.md +70 -70
  35. package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +2 -2
  36. package/docs/release-notes/RELEASE-NOTES-2.7.2.md +3 -3
  37. package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
  38. package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
  39. package/docs/release-notes/RELEASE-NOTES-2.8.0.en.md +65 -0
  40. package/docs/release-notes/RELEASE-NOTES-2.8.0.md +65 -0
  41. package/docs/release-notes/TEMPLATE.md +6 -0
  42. package/docs/release-playbook.md +103 -0
  43. package/docs/slv-playbook.md +117 -0
  44. package/docs/status-report-fields.md +167 -0
  45. package/docs/test-timing.md +158 -154
  46. package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
  47. package/installer.js +793 -783
  48. package/package.json +260 -203
  49. package/prompt-corpus-persona/persona-corpus.json +9 -9
  50. package/prompt-corpus-persona/persona-corpus.md +225 -33
  51. package/prompt-corpus-v2/formal-verify-v2.json +295 -115
  52. package/prompt-corpus-v2/formal-verify-v2.md +2290 -282
  53. package/prompt-corpus-v3/formal-verify-v3.json +112 -112
  54. package/prompt-corpus-v3/formal-verify-v3.md +227 -143
  55. package/prompt-corpus-v4/formal-verify-v4.json +18 -18
  56. package/prompt-corpus-v4/formal-verify-v4.md +49 -44
  57. package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
  58. package/prompt-corpus-v5/prompt-corpus-v5.md +573 -207
  59. package/scripts/clean-temp.mjs +234 -0
  60. package/scripts/update-doc-counts.mjs +71 -0
  61. package/tests/audit-artifact-docs.mjs +72 -0
  62. package/tests/audit-artifact-docs.mutants.mjs +58 -0
  63. package/tests/audit-engine-faces.mjs +298 -0
  64. package/tests/audit-formal-sensitivity.mjs +345 -342
  65. package/tests/audit-installer-assertions.mutants.mjs +63 -0
  66. package/tests/audit-installer-compat.test.mjs +256 -208
  67. package/tests/audit-installer-policy.test.mjs +41 -8
  68. package/tests/audit-math-computation-contract.mjs +64 -8
  69. package/tests/audit-math-computation-contract.mutants.mjs +106 -0
  70. package/tests/audit-math-computation-parity.mjs +164 -3
  71. package/tests/audit-math-computation-sensitivity.mjs +5 -3
  72. package/tests/audit-package-membership.mjs +57 -0
  73. package/tests/audit-package-membership.mutants.mjs +28 -0
  74. package/tests/audit-participant-set-parity.mjs +160 -0
  75. package/tests/audit-path-discipline.mjs +83 -0
  76. package/tests/audit-path-discipline.mutants.mjs +51 -0
  77. package/tests/audit-persona-sensitivity.mjs +323 -309
  78. package/tests/audit-persona-surface.mutants.mjs +69 -0
  79. package/tests/audit-persona-surface.test.mjs +431 -390
  80. package/tests/audit-prompt-duplication.mjs +143 -0
  81. package/tests/audit-prompt-duplication.mutants.mjs +51 -0
  82. package/tests/audit-prompt-invariants.mjs +589 -508
  83. package/tests/audit-readme-bilingual.test.mjs +152 -0
  84. package/tests/audit-readme-counts.mjs +95 -0
  85. package/tests/audit-readme-counts.mutants.mjs +52 -0
  86. package/tests/audit-registration.mjs +78 -0
  87. package/tests/audit-spec-traceability.mjs +193 -193
  88. package/tests/audit-status-report-fields.mjs +172 -0
  89. package/tests/audit-status-report-fields.mutants.mjs +64 -0
  90. package/tests/audit-v3-registration-parity.mjs +156 -0
  91. package/tests/audit-v5-integrity.mjs +795 -508
  92. package/tests/audit-v5-lean-abstention.mjs +392 -0
  93. package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
  94. package/tests/audit-v5-prompt-duplication.mjs +88 -0
  95. package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
  96. package/tests/audit-v5-sensitivity.mjs +433 -427
  97. package/tests/e2e-identity-a6.mutants.mjs +65 -0
  98. package/tests/e2e-v5-round2.test.mjs +2453 -1981
  99. package/tests/formal-verify-v2.test.mjs +1778 -1724
  100. package/tests/formal-verify-v3.mutants.mjs +117 -0
  101. package/tests/formal-verify-v3.test.mjs +1724 -1671
  102. package/tests/formal-verify-v4.mutants.mjs +152 -0
  103. package/tests/formal-verify-v4.test.mjs +2114 -1574
  104. package/tests/formal-verify-v5.test.mjs +790 -724
  105. package/tests/helpers/doc-counts-format.mjs +41 -0
  106. package/tests/helpers/math-computation-fake-seam.mjs +4 -0
  107. package/tests/math-computation-a1.mutants.mjs +72 -0
  108. package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
  109. package/tests/math-computation-discovery.mutants.mjs +76 -0
  110. package/tests/math-computation-shared.test.mjs +392 -6
  111. package/tests/math-computation-v2.test.mjs +779 -0
  112. package/tests/math-computation-v3.test.mjs +771 -0
  113. package/tests/math-computation-v4.mutants.mjs +133 -0
  114. package/tests/math-computation-v4.test.mjs +739 -0
  115. package/tests/prompt-v5-integrity.test.mjs +1291 -1280
  116. package/tests/run-tests.mjs +393 -202
  117. package/tests/run-tests.mutants.mjs +70 -0
  118. package/tests/selfdrive-v5.mjs +560 -508
  119. package/tests/selfdrive-v5.mutants.mjs +132 -0
  120. package/tests/temp-hygiene.mutants.mjs +80 -0
  121. package/tests/v2-fix-probes.mutants.mjs +104 -0
  122. package/tests/v2-fix-probes.test.mjs +1040 -0
  123. package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
  124. package/tests/v2v3-interrupt.mutants.mjs +111 -0
  125. package/tests/v3-fix-probes.mutants.mjs +99 -0
  126. package/tests/v3-fix-probes.test.mjs +892 -0
  127. package/tests/v4-final-paper.mutants.mjs +108 -0
  128. package/tests/v5-institute-fixes.mutants.mjs +218 -0
  129. package/vibe-math-v2/agent.cordis.yml +378 -322
  130. package/vibe-math-v2/math-computation.js +144 -53
  131. package/vibe-math-v2/math-engines.js +12 -12
  132. package/vibe-math-v2/preset.yml +2 -2
  133. package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
  134. package/vibe-math-v2/vibe-math-v2.js +5097 -4755
  135. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +491 -487
  136. package/vibe-math-v3/agent.cordis.yml +422 -356
  137. package/vibe-math-v3/math-computation.js +144 -53
  138. package/vibe-math-v3/math-engines.js +12 -12
  139. package/vibe-math-v3/preset.yml +2 -2
  140. package/vibe-math-v3/vibe-math-v3.js +6235 -5873
  141. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
  142. package/vibe-math-v4/agent.cordis.yml +438 -402
  143. package/vibe-math-v4/math-computation.js +144 -53
  144. package/vibe-math-v4/math-engines.js +12 -12
  145. package/vibe-math-v4/preset.yml +2 -2
  146. package/vibe-math-v4/vibe-math-v4.js +4490 -4269
  147. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1013
  148. package/vibe-math-v5/agent.cordis.yml +46 -12
  149. package/vibe-math-v5/math-computation.js +144 -53
  150. package/vibe-math-v5/math-engines.js +12 -12
  151. package/vibe-math-v5/vibe-math-v5.js +1266 -229
  152. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1660
  153. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -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('')
@@ -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
- ok(E.MATH_ENGINES.cli.defaultOn === true, 'cli is flagged defaultOn')
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
- console.log('')
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
- from: "if (p.mathMode !== 'typed+shell') return bad('engine=cli is disabled while mathMode=' + p.mathMode, 'MATH_REFUSED', next('reason', { reason: 'policy' }))",
38
- to: 'if (false) return bad(\'noop\')',
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
  {
@@ -208,7 +210,7 @@ function runSuite(modulePath) {
208
210
  for (const p of PROBES) {
209
211
  const dir = mkdtempSync(join(tmpdir(), 'mc-sens-' + p.name + '-'))
210
212
  copyFileSync(join(SRC, 'math-engines.js'), join(dir, 'math-engines.js'))
211
- const src = readFileSync(join(SRC, p.file), 'utf8')
213
+ const src = readFileSync(join(SRC, p.file), 'utf8').replace(/\r\n/g, '\n') // anchors are LF: normalize CRLF checkouts
212
214
  if (src.indexOf(p.from) === -1) {
213
215
  bad++
214
216
  console.log(' FAIL ' + p.name + ' — mutation anchor no longer applies (drift): ' + JSON.stringify(p.from.slice(0, 60)))
@@ -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)