dsh-vibe-math 2.7.1 → 2.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.en.md +682 -690
  2. package/README.md +19 -27
  3. package/cordis.patch.yml +112 -16
  4. package/docs/AUDIT-CHECKLIST.md +744 -518
  5. package/docs/COMPAT-AUDIT-ROUND2.md +35 -9
  6. package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
  7. package/docs/final-paper.md +172 -172
  8. package/docs/formal-verification.md +588 -588
  9. package/docs/math-computation.md +194 -163
  10. package/docs/parameter-schema.md +37 -0
  11. package/docs/release-notes/README.en.md +49 -0
  12. package/docs/release-notes/README.md +48 -0
  13. package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +72 -0
  14. package/docs/release-notes/RELEASE-NOTES-2.7.2.md +72 -0
  15. package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
  16. package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
  17. package/docs/release-notes/TEMPLATE.md +126 -0
  18. package/docs/status-report-fields.md +167 -0
  19. package/docs/test-timing.md +157 -154
  20. package/installer.js +27 -17
  21. package/package.json +55 -3
  22. package/prompt-corpus-persona/persona-corpus.json +9 -9
  23. package/prompt-corpus-persona/persona-corpus.md +225 -33
  24. package/prompt-corpus-v2/formal-verify-v2.json +305 -113
  25. package/prompt-corpus-v2/formal-verify-v2.md +2399 -279
  26. package/prompt-corpus-v3/formal-verify-v3.json +134 -110
  27. package/prompt-corpus-v3/formal-verify-v3.md +409 -141
  28. package/prompt-corpus-v4/formal-verify-v4.json +18 -18
  29. package/prompt-corpus-v4/formal-verify-v4.md +49 -44
  30. package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
  31. package/prompt-corpus-v5/prompt-corpus-v5.md +513 -259
  32. package/scripts/update-doc-counts.mjs +71 -0
  33. package/tests/audit-artifact-docs.mjs +72 -0
  34. package/tests/audit-artifact-docs.mutants.mjs +58 -0
  35. package/tests/audit-installer-assertions.mutants.mjs +63 -0
  36. package/tests/audit-installer-compat.test.mjs +52 -4
  37. package/tests/audit-installer-policy.test.mjs +41 -8
  38. package/tests/audit-math-computation-contract.mjs +107 -9
  39. package/tests/audit-math-computation-contract.mutants.mjs +106 -0
  40. package/tests/audit-math-computation-parity.mjs +164 -3
  41. package/tests/audit-math-computation-sensitivity.mjs +6 -4
  42. package/tests/audit-package-membership.mjs +57 -0
  43. package/tests/audit-package-membership.mutants.mjs +28 -0
  44. package/tests/audit-participant-set-parity.mjs +160 -0
  45. package/tests/audit-path-discipline.mjs +83 -0
  46. package/tests/audit-path-discipline.mutants.mjs +51 -0
  47. package/tests/audit-persona-sensitivity.mjs +14 -0
  48. package/tests/audit-persona-surface.mutants.mjs +69 -0
  49. package/tests/audit-persona-surface.test.mjs +63 -22
  50. package/tests/audit-prompt-duplication.mjs +143 -0
  51. package/tests/audit-prompt-duplication.mutants.mjs +50 -0
  52. package/tests/audit-prompt-invariants.mjs +43 -5
  53. package/tests/audit-readme-bilingual.test.mjs +152 -0
  54. package/tests/audit-readme-counts.mjs +95 -0
  55. package/tests/audit-readme-counts.mutants.mjs +52 -0
  56. package/tests/audit-registration.mjs +78 -0
  57. package/tests/audit-status-report-fields.mjs +172 -0
  58. package/tests/audit-status-report-fields.mutants.mjs +64 -0
  59. package/tests/audit-v3-registration-parity.mjs +156 -0
  60. package/tests/audit-v5-integrity.mjs +300 -13
  61. package/tests/audit-v5-lean-abstention.mjs +392 -0
  62. package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
  63. package/tests/audit-v5-prompt-duplication.mjs +88 -0
  64. package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
  65. package/tests/audit-v5-sensitivity.mjs +15 -8
  66. package/tests/e2e-identity-a6.mutants.mjs +65 -0
  67. package/tests/e2e-v5-round2.test.mjs +566 -12
  68. package/tests/formal-verify-v2.test.mjs +91 -5
  69. package/tests/formal-verify-v3.mutants.mjs +117 -0
  70. package/tests/formal-verify-v3.test.mjs +85 -6
  71. package/tests/formal-verify-v4.mutants.mjs +152 -0
  72. package/tests/formal-verify-v4.test.mjs +628 -9
  73. package/tests/formal-verify-v5.test.mjs +68 -9
  74. package/tests/helpers/doc-counts-format.mjs +41 -0
  75. package/tests/helpers/math-computation-fake-seam.mjs +4 -0
  76. package/tests/math-computation-a1.mutants.mjs +72 -0
  77. package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
  78. package/tests/math-computation-discovery.mutants.mjs +76 -0
  79. package/tests/math-computation-shared.test.mjs +396 -2
  80. package/tests/math-computation-v2.test.mjs +767 -0
  81. package/tests/math-computation-v3.test.mjs +759 -0
  82. package/tests/math-computation-v4.mutants.mjs +133 -0
  83. package/tests/math-computation-v4.test.mjs +739 -0
  84. package/tests/prompt-v5-integrity.test.mjs +13 -2
  85. package/tests/run-tests.mjs +139 -9
  86. package/tests/run-tests.mutants.mjs +70 -0
  87. package/tests/selfdrive-v5.mjs +53 -1
  88. package/tests/selfdrive-v5.mutants.mjs +132 -0
  89. package/tests/v2-fix-probes.mutants.mjs +100 -0
  90. package/tests/v2-fix-probes.test.mjs +1039 -0
  91. package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
  92. package/tests/v3-fix-probes.mutants.mjs +95 -0
  93. package/tests/v3-fix-probes.test.mjs +891 -0
  94. package/tests/v5-institute-fixes.mutants.mjs +190 -0
  95. package/vibe-math-v2/agent.cordis.yml +62 -6
  96. package/vibe-math-v2/math-computation.js +229 -29
  97. package/vibe-math-v2/math-engines.js +12 -12
  98. package/vibe-math-v2/vibe-math-v2.js +458 -119
  99. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +5 -1
  100. package/vibe-math-v3/agent.cordis.yml +72 -6
  101. package/vibe-math-v3/math-computation.js +229 -29
  102. package/vibe-math-v3/math-engines.js +12 -12
  103. package/vibe-math-v3/vibe-math-v3.js +482 -139
  104. package/vibe-math-v4/agent.cordis.yml +44 -8
  105. package/vibe-math-v4/math-computation.js +229 -29
  106. package/vibe-math-v4/math-engines.js +12 -12
  107. package/vibe-math-v4/vibe-math-v4.js +245 -52
  108. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +2 -0
  109. package/vibe-math-v5/agent.cordis.yml +46 -12
  110. package/vibe-math-v5/math-computation.js +229 -29
  111. package/vibe-math-v5/math-engines.js +12 -12
  112. package/vibe-math-v5/vibe-math-v5.js +1307 -243
  113. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1655
  114. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -277,7 +277,7 @@ console.log('-- V5 Lean formal verification --')
277
277
  section("1 'off' (default) is a true no-op")
278
278
  const RA = makeRoot()
279
279
  const st0 = await foundInstitute(RA, '形式化开关默认关闭测试')
280
- assert(st0.params.formalVerify === 'off', "the default is 'off' (got " + st0.params.formalVerify + ')')
280
+ assert((st0.params||{}).formalVerify === 'off', "the default is 'off' (got " + (st0.params||{}).formalVerify + ')')
281
281
 
282
282
  // ★ The persistence contract (the DSH 0.2.0 audit fix). v5 used to keep the institute in a
283
283
  // host-only session projection: it appended `vibe5/*` events to the user's OWN session log, and
@@ -294,8 +294,9 @@ assert(st0.params.formalVerify === 'off', "the default is 'off' (got " + st0.par
294
294
  const before = await callTool('vibe_v5_status', {}, RA)
295
295
  const RA2 = makeRoot(wsOf(RA)) // fresh root, fresh backend, same workspace = a reload
296
296
  const after = await callTool('vibe_v5_status', {}, RA2)
297
- assert(after.members.length === before.members.length && after.params.formalVerify === before.params.formalVerify,
298
- '★ a reload reads the roster and the parameters back out of the JSON file (' + after.members.length + ' members, formalVerify=' + after.params.formalVerify + ')')
297
+ assert(before.params && after.params && typeof before.params === 'object' && typeof after.params === 'object', 'non-vacuous: BOTH reload views must EXPOSE params before they are compared (undefined === undefined is not a pass)')
298
+ assert(after.members.length === before.members.length && (after.params||{}).formalVerify === (before.params||{}).formalVerify,
299
+ '★ a reload reads the roster and the parameters back out of the JSON file (' + after.members.length + ' members, formalVerify=' + (after.params||{}).formalVerify + ')')
299
300
  assert(RA2.session._events.length === 0, '★ the reloading root appends nothing to its session log either')
300
301
  }
301
302
  {
@@ -360,16 +361,16 @@ section('2 parameter validation and runtime switching')
360
361
  const RB = makeRoot()
361
362
  await foundInstitute(RB, '形式化参数校验测试')
362
363
  const bad = await callTool('vibe_v5_set', { formalVerify: 'banana' }, RB)
363
- assert(bad.params.formalVerify === 'off', "an unknown mode degrades to 'off', never to a stronger mode (got " + bad.params.formalVerify + ')')
364
+ assert((bad.params||{}).formalVerify === 'off', "an unknown mode degrades to 'off', never to a stronger mode (got " + (bad.params||{}).formalVerify + ')')
364
365
  const enc = await callTool('vibe_v5_set', { formalVerify: 'encourage' }, RB)
365
- assert(enc.params.formalVerify === 'encourage', "'encourage' is accepted")
366
+ assert((enc.params||{}).formalVerify === 'encourage', "'encourage' is accepted")
366
367
  const req = await callTool('vibe_v5_set', { formalVerify: 'require', leanTimeoutMs: -5, leanCommand: ' ' }, RB)
367
- assert(req.params.formalVerify === 'require', "'require' is accepted")
368
- assert(req.params.leanTimeoutMs === 120000, 'a non-positive leanTimeoutMs falls back to the default (' + req.params.leanTimeoutMs + ')')
369
- assert(req.params.leanCommand === 'lean', 'a blank leanCommand falls back to "lean"')
368
+ assert((req.params||{}).formalVerify === 'require', "'require' is accepted")
369
+ assert((req.params||{}).leanTimeoutMs === 120000, 'a non-positive leanTimeoutMs falls back to the default (' + (req.params||{}).leanTimeoutMs + ')')
370
+ assert((req.params||{}).leanCommand === 'lean', 'a blank leanCommand falls back to "lean"')
370
371
  await callTool('vibe_v5_set', { leanCommand: 'lake', leanArgs: ['env', 'lean'] }, RB)
371
372
  const stL = await callTool('vibe_v5_status', {}, RB)
372
- assert(stL.params.leanCommand === 'lake' && stL.params.leanArgs.join(' ') === 'env lean', 'leanCommand/leanArgs are settable (lake env lean)')
373
+ assert((stL.params||{}).leanCommand === 'lake' && ((stL.params||{}).leanArgs||[]).join(' ') === 'env lean', 'leanCommand/leanArgs are settable (lake env lean)')
373
374
 
374
375
  // ---------- 3. 'encourage' injection ----------
375
376
  section("3 'encourage' injects the Lean section into the right prompts")
@@ -717,6 +718,64 @@ assert(stDef.undecided.indexOf('p-def') !== -1, '★ it is recorded as 未定论
717
718
  assert(!existsSync(join(instG, 'Verified', '命题', 'p-def.md')), 'no Verified card is written for it')
718
719
  assert(/p-def/.test(readIf(join(instG, 'Formal', 'TODO.md'))), 'it is on the formalisation TODO list')
719
720
 
721
+
722
+ // ===============================================================
723
+ // N. v5 LIB_SPEC path basis (D3 class): every member-visible library declaration must carry the
724
+ // SAME root the code's readers/writers use (Members/<id>/Progress/progress.md etc.), otherwise a
725
+ // compliant member writes a file nobody reads (the vm-selftest3 symptom).
726
+ // ===============================================================
727
+ section('N v5 LIB_SPEC declares the same path basis the code reads/writes (D3 class)')
728
+ {
729
+ const { readFileSync } = await import('node:fs')
730
+ const src = readFileSync(PLUGIN, 'utf8')
731
+ const start = src.indexOf('const LIB_SPEC = [')
732
+ assert(start > 0, 'the charter LIB_SPEC block exists')
733
+ const spec = src.slice(start, src.indexOf("].join('\\n')", start))
734
+ for (const d of ['Progress/progress.md', 'Propos/<id>.md', 'Methods/<id>.md', 'Subproblems/<id>.md']) {
735
+ assert(spec.includes('Members/<你>/' + d), '★ LIB_SPEC declares ' + d + ' under the member root (Members/<你>/' + d + ') - the root the readers/writers use')
736
+ }
737
+ assert(!/·\s+(?:Progress|Propos|Methods|Subproblems)\/<你>\//.test(spec), '★ no LIB_SPEC declaration keeps the legacy bare Progress/<你>/ shape (a member copying it writes outside Members/<id>/)')
738
+ assert(src.includes("const rel = 'Members/' + memberId + '/Progress/progress.md'"), '★ the code writer path shares that root (Members/<id>/Progress/progress.md)')
739
+ }
740
+ // ===============================================================
741
+ // N2. v5 assign/prioritize/nudge: the tool DESCRIPTION's allowed-caller set must match the GATE.
742
+ // All three share one gate (office always; academician only when academicianLeads), while the
743
+ // descriptions used to claim academician-only - so a reader (or a model) would never call them
744
+ // from the office, which the prompt explicitly allows.
745
+ // ===============================================================
746
+ section('N2 v5 assign/prioritize/nudge descriptions match their shared gate')
747
+ {
748
+ const { readFileSync } = await import('node:fs')
749
+ const src = readFileSync(PLUGIN, 'utf8')
750
+ const gate = 'if (!isOffice(memberId) && !(isAcademician(memberId) && params.academicianLeads)) {'
751
+ const gates = src.split(gate).length - 1
752
+ assert(gates >= 2, '* assign + prioritize share the office-or-academician gate condition (found ' + gates + ')')
753
+ assert(/async function nudge[\s\S]{0,900}isOffice\((?:memberId|callerId)\)/.test(src), '* the nudge gate also admits the office (isOffice, whatever the caller parameter is named)')
754
+ for (const tool of ['vibe_v5_assign', 'vibe_v5_prioritize', 'vibe_v5_nudge']) {
755
+ const at = src.indexOf("registerTool('" + tool + "', '")
756
+ assert(at > 0, '* ' + tool + ' is registered')
757
+ const dstart = at + ("registerTool('" + tool + "', '").length
758
+ const desc = src.slice(dstart, src.indexOf("',", dstart))
759
+ assert(/\(office/.test(desc) && /academician/.test(desc), '* ' + tool + ' description names BOTH allowed callers (office + academician), matching its gate')
760
+ assert(!desc.includes("'(academician)"), '* ' + tool + ' description does not claim academician-only while the gate allows the office')
761
+ }
762
+ }
763
+ // ===============================================================
764
+ // N3. DEFECT-1 contract for EVERY refusal: a caller that cannot be identified must still get a
765
+ // next{tool,hint}, not just a code+message (the office/session root and stale children hit
766
+ // these 'no calling member' returns).
767
+ // ===============================================================
768
+ section('N3 every "no calling member" refusal carries a next{tool,hint}')
769
+ {
770
+ const { readFileSync } = await import('node:fs')
771
+ const src = readFileSync(PLUGIN, 'utf8')
772
+ const msgs = [...src.matchAll(/message: 'no calling member:[\s\S]*?'/g)]
773
+ assert(msgs.length >= 6, '* the member-scoped writing tools refuse an unidentified caller (' + msgs.length + ' sites)')
774
+ for (const m of msgs) {
775
+ const after = src.slice(m.index + m[0].length, m.index + m[0].length + 260)
776
+ assert(/next:\s*\{\s*kind:/.test(after), '* refusal "' + m[0].slice(0, 60) + '..." carries next{tool,hint} (not a bare code+message)')
777
+ }
778
+ }
720
779
  console.log('')
721
780
  console.log('passed=' + passed + ' failed=' + failed)
722
781
  if (failed) { console.error('FAILURES:'); for (const f of failures) console.error(' - ' + f); process.exit(1) }
@@ -0,0 +1,41 @@
1
+ // ONE definition of every count sentence (D1). The writer (scripts/update-doc-counts.mjs), the guard
2
+ // (tests/audit-readme-counts.mjs) and the mutant harness all import this module, so the three cannot
3
+ // disagree about the shape. Sentences are rebuilt WHOLE (never partially regex-edited), which makes the
4
+ // writer idempotent — `--check` converges.
5
+ //
6
+ // VOCABULARY: `derived.*` are JOB counts (the gate's own job list: base jobs + VARIANTS jobs), NOT file
7
+ // counts (a naive file count gives 82/38). `shipped.*` ARE file counts (package.json#files is a file list).
8
+
9
+ export function claims(derived, shipped) {
10
+ const zhTotal = '`TOTAL ' + derived.total + '`(' + derived.suites + ' 套件 + ' + derived.probes + ' 探针/变体,**作业计数**(job count),不是文件计数)'
11
+ const zhShipped = shipped.total + ' 个 `tests/*.mjs`(' + shipped.suites + ' 个套件 + ' + shipped.probes + ' 个探针/脚本)'
12
+ const enTotal = '`TOTAL ' + derived.total + ' (' + derived.suites + ' suites + ' + derived.probes + ' probes/variants — JOB counts, not file counts)'
13
+ const enShipped = 'which ' + shipped.total + ' of the `tests/*.mjs` files ship in the package (' + shipped.suites + ' suites + ' + shipped.probes + ' probe/script files)'
14
+ const ttRow = '| `tests/run-tests.mjs`(**' + derived.total + ' 项作业**(job count)= ' + derived.suites + ' 套件 + '
15
+ + derived.probes + ' 探针/变体;**由 `--counts` 派生**) | ≈ 424 s(历史基线) | **≈ 121 s**(历史基线;当前实测墙钟 ≈ 200 s,并发 4) | `TOTAL '
16
+ + derived.total + ' PASS ' + derived.total + ' FAIL 0 (suites ' + derived.suites + ' · probes ' + derived.probes + ')`;实测 `wall 120.7s · sum 424.2s`,关键路径 = `e2e-v4-fixes`(≈ 98 s) |'
17
+ const ttOutput = '`TOTAL ' + derived.total + ' PASS ' + derived.total + ' FAIL 0 (suites ' + derived.suites + ' · probes ' + derived.probes + ')`'
18
+ const ckClaim = derived.total + ' 项作业(job count)= ' + derived.suites + ' 套件 + ' + derived.probes + ' 探针/变体'
19
+ const ckShipped = '只发 `tests/` 的 **' + shipped.total + '** 项(**文件计数**;其中 `.test.mjs` **' + shipped.suites + '** 个)'
20
+ const d1Row = '- **计数(实测)**:`TOTAL ' + derived.total + '`(' + derived.suites + ' 套件 + ' + derived.probes
21
+ + ' 探针/变体,**作业计数/job count**,不是文件计数);随包 `tests/*.mjs` = ' + shipped.total + '(**文件计数**:' + shipped.suites
22
+ + ' 个套件 + ' + shipped.probes + ' 个探针/脚本)。'
23
+ const zhBullet = '- **测试耗时基线与并行跑法**:[`docs/test-timing.md`](docs/test-timing.md)(`node tests/run-tests.mjs` 并行跑**全部套件 + 全部探针**:'
24
+ + zhTotal + ';**这些数字必须派生**:`node tests/run-tests.mjs --counts` 是权威来源(**作业计数**,不是文件计数),并由 '
25
+ + '[`tests/audit-readme-counts.mjs`](tests/audit-readme-counts.mjs) 校验本页与另两份文档;随包发布的是 `files[]` 里的 '
26
+ + zhShipped + '(文件计数),其余仅开发检出可见,清单见该文档 §1.1;每个 runner 都会打印耗时/加速比供下次选策略)'
27
+ const enBullet = '- **Test timing baseline and parallel run recipes**: [`docs/test-timing.md`](docs/test-timing.md) '
28
+ + '(`node tests/run-tests.mjs` runs **every suite AND every probe** in parallel: ' + enTotal + '; the authority is '
29
+ + '`node tests/run-tests.mjs --counts` (JOB counts, not file counts), checked by '
30
+ + '[`tests/audit-readme-counts.mjs`](tests/audit-readme-counts.mjs); ' + enShipped
31
+ + ' and which are repository-only is in §1.1 of that document; every runner prints its elapsed time/speedup so the next strategy can be chosen)'
32
+ return { zhTotal, zhShipped, enTotal, enShipped, ttRow, ttOutput, ckClaim, ckShipped, d1Row, zhBullet, enBullet }
33
+ }
34
+
35
+ /** Stable line markers: each canonical line replaces the line that carries its marker. */
36
+ export const MARKERS = {
37
+ zhBullet: '[`docs/test-timing.md`](docs/test-timing.md)(`node tests/run-tests.mjs`',
38
+ enBullet: '[`docs/test-timing.md`](docs/test-timing.md) (`node tests/run-tests.mjs`',
39
+ ttRow: '| `tests/run-tests.mjs`(',
40
+ d1Row: '- **计数(实测',
41
+ }
@@ -88,6 +88,10 @@ export function makeFakeHost(opts = {}) {
88
88
  // `absTree` maps an absolute directory to its entries, so a test can fake DSH's layout
89
89
  // (`<root>/dsh-runtimes/<tree>/dependencies/python/python.exe`).
90
90
  runtimeRoots: ('runtimeRoots' in opts) ? (async () => (opts.runtimeRoots || [])) : undefined,
91
+ // round-B (F4): OPTIONAL per-engine install roots. When supplied, the module uses THESE instead of
92
+ // this process's environment, so a test never depends on the ambient machine's %ProgramFiles% /
93
+ // %LOCALAPPDATA% (or on which engines the developer happens to have installed).
94
+ installRoots: opts.installRoots ? (async (engine) => (opts.installRoots[engine] || [])) : undefined,
91
95
  listDirAbs: opts.absTree ? (async (abs) => (opts.absTree[String(abs).replace(/\\/g, '/')] || [])) : undefined,
92
96
  resolveExecutable: async (cmd) => {
93
97
  if (opts.resolveThrows) throw new Error('resolve failed: ' + cmd)
@@ -0,0 +1,72 @@
1
+ // Registered mutant harness for the A1 skip-ledger contract (tests/math-computation-v2|v3.test.mjs).
2
+ // A1 is load-bearing only if NON-EXERCISE reddens it: each family nulls the copy module's
3
+ // packageManager/packageManagerAvailable and must redden the named line. Two families (one per preset).
4
+ // Exit criterion: every family reddens BY NAME; any skipped family prints, is counted, and forces exit 1.
5
+ // Run: node tests/math-computation-a1.mutants.mjs
6
+ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, rmSync, readdirSync } from 'node:fs'
7
+ import { execFileSync } from 'node:child_process'
8
+ import { dirname, join } from 'node:path'
9
+ import { tmpdir } from 'node:os'
10
+ import { fileURLToPath } from 'node:url'
11
+
12
+ // Bound every child run: run-tests has no per-suite timeout, so an unbounded child hangs the gate.
13
+ // A timeout is reported as HANG, counts as a failure for that family, and is listed in hangs=[].
14
+ const CHILD_TIMEOUT_MS = Number(process.env.MUTANT_CHILD_TIMEOUT_MS || 120000)
15
+ const TIMES = []
16
+ const hangs = []
17
+ let lastHang = false
18
+ const HERE = dirname(fileURLToPath(import.meta.url))
19
+ const REPO = join(HERE, '..')
20
+ function copyGraph(preset, file, dest) {
21
+ const src = join(REPO, preset, file)
22
+ copyFileSync(src, join(dest, file))
23
+ for (const m of readFileSync(src, 'utf8').matchAll(/from\s+'(\.\/[A-Za-z0-9_.-]+\.js)'/g)) {
24
+ const dep = m[1].slice(2)
25
+ if (!readdirSync(dest).includes(dep)) copyGraph(preset, dep, dest)
26
+ }
27
+ }
28
+ const skipped = []
29
+ function runFamily(f) {
30
+ const dest = join(tmpdir(), 'a1-mut-' + Math.random().toString(36).slice(2, 10))
31
+ mkdirSync(dest, { recursive: true })
32
+ copyGraph(f.preset, f.preset + '.js', dest)
33
+ const target = join(dest, 'math-computation.js')
34
+ const before = readFileSync(target, 'utf8')
35
+ let mutated = before
36
+ for (const [from, to] of f.edits) {
37
+ const n = mutated.split(from).length - 1
38
+ if (n !== 1) { skipped.push(f.name + ' (' + from.slice(0, 24) + ' x' + n + ')'); console.error(' SKIP - ' + f.name + ' (anchor x' + n + ')'); rmSync(dest, { recursive: true, force: true }); return false }
39
+ mutated = mutated.split(from).join(to)
40
+ }
41
+ writeFileSync(target, mutated, 'utf8')
42
+ try { execFileSync(process.execPath, ['--check', target], { stdio: 'pipe' }) }
43
+ catch (e) { writeFileSync(target, before, 'utf8'); skipped.push(f.name + ' (mutation did not compile)'); console.error(' SKIP - ' + f.name + ' (mutation did not compile; restored)'); rmSync(dest, { recursive: true, force: true }); return false }
44
+ let out = ''
45
+ let code = 0
46
+ try { out = execFileSync(process.execPath, [f.suite], { cwd: REPO, encoding: 'utf8', timeout: CHILD_TIMEOUT_MS, killSignal: 'SIGKILL', env: Object.assign({}, process.env, { [f.env]: join(dest, f.preset + '.js') }) }) }
47
+ catch (e) { if (e && (e.killed || e.signal === 'SIGKILL')) lastHang = true; code = (e && e.status) || 1; out = String((e && e.stdout) || '') + String((e && e.stderr) || '') }
48
+ const named = out.split(/\r?\n/).filter((l) => /^\s*(FAIL - | {2}- )/.test(l) && f.expect.test(l)).map((l) => l.trim())
49
+ const ok = code !== 0 && named.length > 0
50
+ console.log((ok ? ' ok - ' : ' FAIL - ') + f.name + (named.length ? ' :: ' + named[0].slice(0, 135) : ' :: no named red (exit=' + code + ')'))
51
+ rmSync(dest, { recursive: true, force: true })
52
+ return ok
53
+ }
54
+ const EDITS = [
55
+ [' packageManager: mgr,', ' packageManager: null,'],
56
+ [' packageManagerAvailable: mgr ? available : null,', ' packageManagerAvailable: null,'],
57
+ ]
58
+ const FAMILIES = [
59
+ { name: 'A1-v2 (packageManager nulled)', preset: 'vibe-math-v2', suite: 'tests/math-computation-v2.test.mjs', env: 'MC_V2_PLUGIN', edits: EDITS,
60
+ expect: /A1:「包管理器存在 ⇒ 可执行命令」这半契约必须真的跑过/ },
61
+ { name: 'A1-v3 (packageManager nulled)', preset: 'vibe-math-v3', suite: 'tests/math-computation-v3.test.mjs', env: 'MC_V3_PLUGIN', edits: EDITS,
62
+ expect: /A1:「包管理器存在 ⇒ 可执行命令」这半契约必须真的跑过/ },
63
+ ]
64
+ let red = 0
65
+ for (const f of FAMILIES) { const t0 = Date.now(); lastHang = false; const ok = runFamily(f); const ms = Date.now() - t0; TIMES.push([f.name, ms]); if (lastHang) hangs.push(f.name + "(" + Math.round(ms / 1000) + "s)"); if (ok) red++ }
66
+ console.log('')
67
+ console.log('mutant families reddening the A1 contract by name: ' + red + '/' + FAMILIES.length)
68
+ console.log('skipped=[' + skipped.join(' | ') + ']')
69
+ console.log('timings: ' + TIMES.map((t) => String(t[0]).split(':')[0] + '=' + t[1] + 'ms').join(' '))
70
+ console.log('hangs=[' + hangs.join(' | ') + ']')
71
+ if (red !== FAMILIES.length || skipped.length || hangs.length) process.exit(1)
72
+ console.log('ALL MUTANTS RED AS REQUIRED')
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Mutant harness for tests/math-computation-shared.test.mjs §26 — ARCHIVE / RERUN provenance (ships:
4
+ * `tests/*.mutants.mjs`). Replaces the dev-only draft `_oneoff/auditR2/roundA-mutants.mjs` (a1/a2).
5
+ *
6
+ * §26: re-running an EDITED original source keeps the same archive (attempt >= 2, `scriptChanged`), while
7
+ * pointing at an ARCHIVED copy yields a NEW archive and flags `fileIsArchivedScript` /
8
+ * `ARCHIVED_SCRIPT_RERUN`.
9
+ *
10
+ * Same shape as `math-computation-discovery.mutants.mjs`: copy `math-engines.js` + a ONE-SITE-mutated
11
+ * `math-computation.js` into a private dir, run the SHIPPED suite through `MATH_COMPUTATION_MODULE`, and
12
+ * require the named §26 assertion to redden. Baseline: the same copied pair, UNMUTATED, stays green.
13
+ *
14
+ * Run: node tests/math-computation-archive-rerun.mutants.mjs
15
+ */
16
+ import { mkdtempSync, copyFileSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
17
+ import { tmpdir } from 'node:os'
18
+ import { join } from 'node:path'
19
+ import { spawnSync } from 'node:child_process'
20
+ import { fileURLToPath } from 'node:url'
21
+
22
+ const HERE = fileURLToPath(new URL('./', import.meta.url))
23
+ const REPO = join(HERE, '..')
24
+ const SRC = join(REPO, 'vibe-math-v2')
25
+ const SUITE = join(HERE, 'math-computation-shared.test.mjs')
26
+ let passed = 0, failed = 0
27
+ const ok = (c, l, d) => { if (c) { passed++; console.log(' ok ' + l) } else { failed++; console.log(' FAIL ' + l + (d ? ' — ' + d : '')) } }
28
+
29
+ function runSuite(dir) {
30
+ const r = spawnSync(process.execPath, [SUITE], { cwd: REPO, encoding: 'utf8', maxBuffer: 1 << 26, env: Object.assign({}, process.env, { MATH_COMPUTATION_MODULE: join(dir, 'math-computation.js') }) })
31
+ const o = String(r.stdout || '') + String(r.stderr || '')
32
+ return { status: r.status, out: o, failLines: o.split('\n').filter((l) => /^\s*FAIL\b/.test(l)), sum: (o.split('\n').filter((l) => /passed, \d+ failed/.test(l)).slice(-1)[0] || '').trim() }
33
+ }
34
+ const CASES = [
35
+ { name: 'a1: the ARCHIVED_SCRIPT_RERUN warning is dropped',
36
+ expect: 'the response warns ARCHIVED_SCRIPT_RERUN',
37
+ edit: (t) => t.replace(/^[ \t]*if \(archivedScriptSource\) warnings\.push\(warning\('ARCHIVED_SCRIPT_RERUN'.*$/m, '') },
38
+ { name: 'a2: an archived source is never detected (always false)',
39
+ expect: 'the response flags fileIsArchivedScript',
40
+ edit: (t) => t.split('archivedScriptSource = /^Computation\\//.test(String(fileRel))').join('archivedScriptSource = false') },
41
+ ]
42
+ {
43
+ const dir = mkdtempSync(join(tmpdir(), 'mc-archive-base-'))
44
+ copyFileSync(join(SRC, 'math-engines.js'), join(dir, 'math-engines.js'))
45
+ copyFileSync(join(SRC, 'math-computation.js'), join(dir, 'math-computation.js'))
46
+ const b = runSuite(dir)
47
+ ok(b.status === 0, 'baseline: the shipped suite is green on the copied module pair (so a red below is the mutation)', b.sum.slice(0, 70))
48
+ rmSync(dir, { recursive: true, force: true })
49
+ }
50
+ const ORIGIN = readFileSync(join(SRC, 'math-computation.js'), 'utf8')
51
+ for (const c of CASES) {
52
+ const mutated = c.edit(ORIGIN)
53
+ if (mutated === ORIGIN) { ok(false, c.name + ' — the single-site anchor applies', 'ANCHOR MISS'); continue }
54
+ const dir = mkdtempSync(join(tmpdir(), 'mc-archive-mut-'))
55
+ copyFileSync(join(SRC, 'math-engines.js'), join(dir, 'math-engines.js'))
56
+ writeFileSync(join(dir, 'math-computation.js'), mutated)
57
+ const r = runSuite(dir)
58
+ const named = r.failLines.some((l) => l.indexOf(c.expect) !== -1)
59
+ ok(r.status !== 0 && named, '★ ' + c.name + ' -> ' + r.sum, named ? '' : 'named red not found (unit: named §26 assertion labels)')
60
+ if (!named) for (const l of r.failLines.slice(0, 3)) console.log(' ' + l.trim().slice(0, 140))
61
+ rmSync(dir, { recursive: true, force: true })
62
+ }
63
+ console.log('')
64
+ console.log('=== MATH ARCHIVE/RERUN MUTANTS: ' + passed + ' passed, ' + failed + ' failed ===')
65
+ process.exit(failed === 0 ? 0 : 1)
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Mutant harness for tests/math-computation-shared.test.mjs §23 / §23b (ENGINE DISCOVERY) and §24 (install
4
+ * suggestions must be runnable). Ships: `tests/*.mutants.mjs`. Replaces the dev-only drafts
5
+ * `_oneoff/auditR2/round9-mutants.mjs` (m3, m4) and `_oneoff/auditR2/roundB-mutants.mjs` (b1/b2).
6
+ *
7
+ * §23: an engine INSTALLED but not on PATH must still be discovered (R's default install dir); §23b: the
8
+ * same walker covers Octave and Julia, and every engine with a well-defined default dir declares per-OS
9
+ * roots. The roots are INJECTED through the host seam, so the fixtures do not depend on this machine's
10
+ * `ProgramFiles`/`LOCALAPPDATA` (the round-B F4 fix).
11
+ *
12
+ * Each case copies `math-engines.js` + a ONE-SITE-mutated `math-computation.js` into a private dir and runs
13
+ * the SHIPPED suite through `MATH_COMPUTATION_MODULE`, requiring the named §23/§23b assertion to redden.
14
+ * Baseline: the same copied setup, UNMUTATED, must stay green (so the mutation is the only red cause).
15
+ *
16
+ * Run: node tests/math-computation-discovery.mutants.mjs
17
+ */
18
+ import { mkdtempSync, copyFileSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
19
+ import { tmpdir } from 'node:os'
20
+ import { join } from 'node:path'
21
+ import { spawnSync } from 'node:child_process'
22
+ import { fileURLToPath } from 'node:url'
23
+
24
+ const HERE = fileURLToPath(new URL('./', import.meta.url))
25
+ const REPO = join(HERE, '..')
26
+ const SRC = join(REPO, 'vibe-math-v2')
27
+ const SUITE = join(HERE, 'math-computation-shared.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
+ function runSuite(dir) {
32
+ const r = spawnSync(process.execPath, [SUITE], { cwd: REPO, encoding: 'utf8', maxBuffer: 1 << 26, env: Object.assign({}, process.env, { MATH_COMPUTATION_MODULE: join(dir, 'math-computation.js') }) })
33
+ const o = String(r.stdout || '') + String(r.stderr || '')
34
+ return { status: r.status, out: o, failLines: o.split('\n').filter((l) => /^\s*FAIL\b/.test(l)), sum: (o.split('\n').filter((l) => /passed, \d+ failed/.test(l)).slice(-1)[0] || '').trim() }
35
+ }
36
+ const M3_FROM = " // round-9: then the known per-OS install locations (an engine installed without touching PATH).\n for (const p of await knownInstallCandidates(H, d.name)) {\n if (typeof p === 'string' && p) return p\n }\n"
37
+ const CASES = [
38
+ { name: 'm3: the known-install-location lookup is dropped (PATH + bundled runtime only)',
39
+ expect: 'a Rscript that exists ONLY in the default install dir is discovered',
40
+ edit: (t) => t.replace(M3_FROM, '') },
41
+ { name: 'b1: the host-injected install roots are ignored (back to ambient env)',
42
+ expect: 'a Rscript that exists ONLY in the default install dir is discovered',
43
+ edit: (t) => t.replace('const roots = injected || mathInstallRoots(engineName, env, win)', 'const roots = mathInstallRoots(engineName, env, win)') },
44
+ { name: 'b2: the octave/julia per-OS root entries are removed',
45
+ expect: 'every engine with a well-defined default install dir declares per-OS roots: octave',
46
+ edit: (t) => t.split('octave: win').join('octaveDisabled: win').split('julia: win').join('juliaDisabled: win') },
47
+ { name: 'm4: a package manager is assumed available (a command that cannot run is offered)',
48
+ expect: 'no runnable-looking command when its package manager is absent',
49
+ edit: (t) => t.replace(" try { available = !!(await H.resolveExecutable(mgr)) } catch (e) { available = false }", ' available = true') },
50
+ ]
51
+
52
+ // baseline: the copied setup, unmutated, must stay green
53
+ {
54
+ const dir = mkdtempSync(join(tmpdir(), 'mc-discovery-base-'))
55
+ copyFileSync(join(SRC, 'math-engines.js'), join(dir, 'math-engines.js'))
56
+ copyFileSync(join(SRC, 'math-computation.js'), join(dir, 'math-computation.js'))
57
+ const b = runSuite(dir)
58
+ ok(b.status === 0, 'baseline: the shipped suite is green on the copied module pair (so a red below is the mutation)', b.sum.slice(0, 70))
59
+ rmSync(dir, { recursive: true, force: true })
60
+ }
61
+ const ORIGIN = readFileSync(join(SRC, 'math-computation.js'), 'utf8')
62
+ for (const c of CASES) {
63
+ const mutated = c.edit(ORIGIN)
64
+ if (mutated === ORIGIN) { ok(false, c.name + ' — the single-site anchor applies', 'ANCHOR MISS'); continue }
65
+ const dir = mkdtempSync(join(tmpdir(), 'mc-discovery-mut-'))
66
+ copyFileSync(join(SRC, 'math-engines.js'), join(dir, 'math-engines.js'))
67
+ writeFileSync(join(dir, 'math-computation.js'), mutated)
68
+ const r = runSuite(dir)
69
+ const named = r.failLines.some((l) => l.indexOf(c.expect) !== -1)
70
+ ok(r.status !== 0 && named, '★ ' + c.name + ' -> ' + r.sum, named ? '' : 'named red not found (unit: named §23/§23b assertion labels)')
71
+ if (!named) for (const l of r.failLines.slice(0, 3)) console.log(' ' + l.trim().slice(0, 140))
72
+ rmSync(dir, { recursive: true, force: true })
73
+ }
74
+ console.log('')
75
+ console.log('=== MATH DISCOVERY MUTANTS: ' + passed + ' passed, ' + failed + ' failed ===')
76
+ process.exit(failed === 0 ? 0 : 1)