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
@@ -0,0 +1,152 @@
1
+ // ============================================================================================
2
+ // BILINGUAL README — the Chinese README is the default; README.en.md is a full translation of it.
3
+ // Two documents that are supposed to say the same thing rot apart silently, so this suite pins the
4
+ // parts that must stay identical no matter how either side is edited:
5
+ //
6
+ // · a switcher line near the top of each file linking to the other;
7
+ // · identical heading STRUCTURE (same ordered sequence of heading levels, ignoring fenced code —
8
+ // a `#` shell comment inside a code fence is not a heading);
9
+ // · identical image lists, identical local link targets, identical table-row and code-fence counts
10
+ // (a translation preserves rows 1:1, so a mismatch means content was dropped or merged);
11
+ // · the English file must actually be English outside code (CJK characters are only allowed in
12
+ // fenced/inline code, where the rules keep user-facing example strings in the original language);
13
+ // · both files ship in `package.json` `files`;
14
+ // · the Chinese README is still the default `README.md` (npm and GitHub render that one).
15
+ //
16
+ // Usage: node tests/audit-readme-bilingual.test.mjs
17
+ // ============================================================================================
18
+ import { readFileSync, existsSync } from 'node:fs'
19
+ import { fileURLToPath } from 'node:url'
20
+ import { join, dirname, resolve } from 'node:path'
21
+
22
+ const HERE = fileURLToPath(new URL('./', import.meta.url))
23
+ const REPO = resolve(HERE, '..')
24
+ const pkg = JSON.parse(readFileSync(join(REPO, 'package.json'), 'utf8'))
25
+
26
+ let passed = 0, failed = 0
27
+ const failures = []
28
+ function ok(cond, label, detail) {
29
+ if (cond) { passed++; console.log(' ok ' + label); return true }
30
+ failed++; failures.push(label + (detail ? ' — ' + detail : ''))
31
+ console.error(' FAIL ' + label + (detail ? ' — ' + detail : ''))
32
+ return false
33
+ }
34
+
35
+ const ZH = 'README.md'
36
+ const EN = 'README.en.md'
37
+ if (!ok(existsSync(join(REPO, ZH)) && existsSync(join(REPO, EN)), 'both README.md and README.en.md exist')) {
38
+ console.log('\n=== BILINGUAL README: ' + passed + ' passed, ' + failed + ' failed ===')
39
+ process.exit(1)
40
+ }
41
+ const zh = readFileSync(join(REPO, ZH), 'utf8')
42
+ const en = readFileSync(join(REPO, EN), 'utf8')
43
+
44
+ // ---- 1. switcher ------------------------------------------------------------
45
+ {
46
+ const head = (t) => t.split(/\r?\n/).slice(0, 12).join('\n')
47
+ ok(/\[English\]\(README\.en\.md\)/.test(head(zh)), 'the Chinese README links to the English one near the top')
48
+ ok(/\[中文\]\(README\.md\)/.test(head(en)) || /\[简体中文\]\(README\.md\)/.test(head(en)),
49
+ 'the English README links back to the Chinese one near the top')
50
+ ok(zh.trimStart().startsWith('# ') && en.trimStart().startsWith('# '), 'both files start with an H1 title')
51
+ }
52
+
53
+ // ---- 2. strip code before structure comparison ------------------------------
54
+ /** Markdown with fenced blocks and inline code replaced by placeholders (so their contents — which
55
+ * are allowed to differ in language — cannot influence counting or CJK checks). */
56
+ function stripCode(t) {
57
+ return t
58
+ .replace(/```[\s\S]*?```/g, '\n```CODE```\n')
59
+ .replace(/`[^`\n]*`/g, '`CODE`')
60
+ }
61
+ const zhPlain = stripCode(zh)
62
+ const enPlain = stripCode(en)
63
+
64
+ // ---- 3. structure -----------------------------------------------------------
65
+ const headingLevels = (t) => [...t.matchAll(/^(#{1,6})\s+/gm)].map((m) => m[1].length)
66
+ const zhH = headingLevels(zhPlain), enH = headingLevels(enPlain)
67
+ ok(zhH.length === enH.length && zhH.every((l, i) => l === enH[i]),
68
+ `the heading structure matches (${zhH.length} headings, levels in the same order)`,
69
+ `zh=[${zhH.join(',')}] en=[${enH.join(',')}]`)
70
+
71
+ const tableRows = (t) => t.split('\n').filter((l) => /^\s*\|/.test(l)).length
72
+ ok(tableRows(zhPlain) === tableRows(enPlain), 'the table-row count matches (translations preserve rows 1:1)',
73
+ `zh=${tableRows(zhPlain)} en=${tableRows(enPlain)}`)
74
+
75
+ const fences = (t) => (t.match(/^```/gm) || []).length
76
+ ok(fences(zh) === fences(en), 'the code-fence count matches', `zh=${fences(zh)} en=${fences(en)}`)
77
+
78
+ // ---- 4. assets and links ----------------------------------------------------
79
+ const images = (t) => [...t.matchAll(/!\[[^\]]*\]\(([^)\s]+)\)/g)].map((m) => m[1])
80
+ const zhImg = images(zh), enImg = images(en)
81
+ // The four architecture diagrams deliberately differ by language: the English README points at the
82
+ // `-en` asset of the same diagram, every other image must be the very same file.
83
+ const enVariant = (p) => p.replace(/^(示例图\/框架图-v[0-9]+)\.(png|svg)$/, '$1-en.svg')
84
+ ok(zhImg.length > 0 && zhImg.length === enImg.length, 'both READMEs show the same number of images',
85
+ `zh=[${zhImg.join(', ')}] en=[${enImg.join(', ')}]`)
86
+ const imgMismatch = zhImg.filter((p, i) => enImg[i] !== p && enImg[i] !== enVariant(p))
87
+ ok(imgMismatch.length === 0,
88
+ 'every English image is the same asset as the Chinese one, or its -en variant, in the same order',
89
+ `mismatch=[${imgMismatch.join(', ')}] en=[${enImg.join(', ')}]`)
90
+ const enDiagrams = enImg.filter((p) => /示例图\/框架图-v[0-9]+-en\.svg$/.test(p))
91
+ ok(enDiagrams.length === 4, 'the English README references all four English architecture diagrams',
92
+ `en=[${enImg.join(', ')}]`)
93
+
94
+ // Internal anchors must resolve inside their OWN file: translating a heading renames its slug, so
95
+ // an anchor copied from the Chinese README breaks. (GitHub maps EVERY whitespace to a hyphen and
96
+ // does not collapse runs — a slugger that collapses them reports valid anchors as broken.)
97
+ const slug = (heading) => heading.replace(/^#{1,6}\s+/, '').trim().toLowerCase()
98
+ .replace(/[^\p{L}\p{N}\s-]/gu, '').replace(/\s/g, '-')
99
+ const headingsWithSlugs = (file) => {
100
+ const text = readFileSync(join(REPO, file), 'utf8')
101
+ const inFence = []
102
+ let fence = false
103
+ for (const l of text.split('\n')) { if (/^```/.test(l)) { fence = !fence; inFence.push(false); continue } inFence.push(fence) }
104
+ return { text, slugs: text.split('\n').filter((l, i) => !inFence[i] && /^#{1,6}\s+/.test(l)).map(slug) }
105
+ }
106
+ for (const file of [ZH, EN]) {
107
+ const { text, slugs } = headingsWithSlugs(file)
108
+ const anchors = [...new Set([...text.matchAll(/\]\(#([^)]+)\)/g)].map((m) => m[1]))]
109
+ const broken = anchors.filter((a) => !slugs.includes(a))
110
+ ok(broken.length === 0, `${file}: every internal anchor matches one of its own headings (${anchors.length} anchors)`,
111
+ 'broken: #' + broken.join(', #'))
112
+ }
113
+
114
+ const localLinks = (t) => [...new Set([...t.matchAll(/\[[^\]]*\]\(([^)\s]+)\)/g)].map((m) => m[1]))]
115
+ .filter((p) => !/^[a-z][a-z0-9+.-]*:/i.test(p) && !p.startsWith('#'))
116
+ .filter((p) => p !== 'README.md' && p !== 'README.en.md') // the switcher is intentionally asymmetric
117
+ .sort()
118
+ const zhLinks = localLinks(zh), enLinks = localLinks(en)
119
+ // The four architecture diagrams are the single documented exception: the English README points at
120
+ // the `-en` asset of the same diagram. Normalizing just that difference keeps the guard strict about
121
+ // every other local target (a genuinely missing or extra link still fails).
122
+ const diagramKey = (p) => {
123
+ const m = /^示例图\/框架图-(v[0-9]+)(?:-en)?\.(?:png|svg)$/.exec(p)
124
+ return m ? '示例图/框架图-' + m[1] + '.<diagram>' : p
125
+ }
126
+ const zhKeys = zhLinks.map(diagramKey).sort(), enKeys = enLinks.map(diagramKey).sort()
127
+ ok(zhKeys.join('|') === enKeys.join('|'),
128
+ 'both READMEs link to the same local targets, up to the four language-specific diagrams',
129
+ 'only in zh: ' + zhLinks.filter((p) => !enKeys.includes(diagramKey(p))).join(', ')
130
+ + ' | only in en: ' + enLinks.filter((p) => !zhKeys.includes(diagramKey(p))).join(', '))
131
+
132
+ // ---- 5. the English file is actually English outside code --------------------
133
+ {
134
+ // Link and image TARGETS legitimately stay Chinese (real file and directory names), as do the real
135
+ // JSON keys, the literal session-rebuild marker and the switcher label. What is left is that small
136
+ // set of literals — a real untranslated paragraph would be in the hundreds.
137
+ const withoutTargets = enPlain.replace(/\]\([^)\s]+\)/g, '](TARGET)').replace(/!\[[^\]]*\](\([^)\s]+\))/g, '![img]($1)')
138
+ const cjk = (withoutTargets.match(/[\u3400-\u9fff\uf900-\ufaff]/g) || []).length
139
+ ok(cjk <= 150, `no untranslated prose outside code and link targets (${cjk} CJK characters, budget 150)`,
140
+ 'a count in the hundreds means a section was left in Chinese')
141
+ }
142
+
143
+ // ---- 6. packaging + default -------------------------------------------------
144
+ ok((pkg.files || []).includes(ZH) && (pkg.files || []).includes(EN), 'both READMEs are listed in package.json files',
145
+ JSON.stringify((pkg.files || []).filter((f) => /^README/.test(f))))
146
+ ok(existsSync(join(REPO, 'README.zh.md')) === false,
147
+ 'the Chinese README is still the default README.md (no README.zh.md duplicate to maintain)')
148
+
149
+ console.log('')
150
+ console.log('=== BILINGUAL README: ' + passed + ' passed, ' + failed + ' failed ===')
151
+ if (failed) { for (const f of failures) console.error(' - ' + f); process.exit(1) }
152
+ console.log('ALL GREEN')
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * README/COUNT CONSISTENCY — the numbers quoted in shipped docs are DERIVED, not typed, and each is
4
+ * labelled as a JOB count or a FILE count.
5
+ *
6
+ * DEFECT CLASS: "documented count drifts from the tree" (D1). README quoted `TOTAL 57` (39 suites /
7
+ * 18 probes, "18 items ship") while the runner produced 65, then 85/86, and docs/test-timing.md and
8
+ * docs/AUDIT-CHECKLIST.md disagreed with each other. Authority: `node tests/run-tests.mjs --counts`
9
+ * (the gate's own JOB list — base jobs + VARIANTS jobs) plus `package.json#files` (shipped FILE counts).
10
+ * Shapes come from tests/helpers/doc-counts-format.mjs, the same module the writer
11
+ * (scripts/update-doc-counts.mjs) and the mutant harness use, so the three cannot disagree.
12
+ *
13
+ * Run: node tests/audit-readme-counts.mjs
14
+ * Env (mutant harness): COUNTS_README / COUNTS_README_EN / COUNTS_TIMING / COUNTS_CHECKLIST / COUNTS_FIELDS /
15
+ * COUNTS_PAPER / COUNTS_MATH (absolute or repo-relative). The last three serve the F-C live-doc sweep.
16
+ */
17
+ import { readFileSync } from 'node:fs'
18
+ import { dirname, isAbsolute, join } from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+ import { spawnSync } from 'node:child_process'
21
+ import { claims } from './helpers/doc-counts-format.mjs'
22
+
23
+ const HERE = dirname(fileURLToPath(import.meta.url))
24
+ const REPO = join(HERE, '..')
25
+ let passed = 0, failed = 0
26
+ const failures = []
27
+ const ok = (cond, label, detail) => { if (cond) passed++; else { failed++; failures.push(label + (detail ? ' — ' + detail : '')) } }
28
+
29
+ const derivedRaw = spawnSync(process.execPath, [join(HERE, 'run-tests.mjs'), '--counts'], { cwd: REPO, encoding: 'utf8' })
30
+ let derived = null
31
+ try { derived = JSON.parse(String(derivedRaw.stdout || '').trim()) } catch (e) { /* reported below */ }
32
+ ok(!!derived && derived.total > 0 && derived.suites > 0,
33
+ 'the runner --counts mode produces the derived JOB totals (suites/probes/total)', String(derivedRaw.stdout || '').slice(0, 70))
34
+
35
+ const pkg = JSON.parse(readFileSync(join(REPO, 'package.json'), 'utf8'))
36
+ const shippedFiles = pkg.files.filter((f) => f.startsWith('tests/') && f.endsWith('.mjs'))
37
+ const shipped = { total: shippedFiles.length, suites: shippedFiles.filter((f) => f.endsWith('.test.mjs')).length }
38
+ shipped.probes = shipped.total - shipped.suites
39
+ ok(shipped.total > 0 && shipped.suites > 0, 'package.json#files yields the shipped FILE split',
40
+ JSON.stringify({ shipped: shipped.total, suites: shipped.suites, probes: shipped.probes }))
41
+
42
+ const read = (envName, rel) => {
43
+ const v = process.env[envName]
44
+ return readFileSync(v ? (isAbsolute(v) ? v : join(REPO, v)) : join(REPO, rel), 'utf8')
45
+ }
46
+ const zh = read('COUNTS_README', 'README.md')
47
+ const en = read('COUNTS_README_EN', 'README.en.md')
48
+ const tt = read('COUNTS_TIMING', 'docs/test-timing.md')
49
+ const ck = read('COUNTS_CHECKLIST', 'docs/AUDIT-CHECKLIST.md')
50
+
51
+ if (derived) {
52
+ const c = claims(derived, shipped)
53
+ const has = (doc, label, needle) => ok(doc.indexOf(needle) !== -1, label, 'looked for ' + needle)
54
+ has(zh, '★ README.md carries the canonical JOB-count bullet (derived totals + shipped FILE count)', c.zhBullet)
55
+ has(en, '★ README.en.md carries the canonical JOB-count bullet', c.enBullet)
56
+ has(tt, '★ docs/test-timing.md carries the canonical runner row (derived JOB counts + output shape)', c.ttRow)
57
+ has(ck, '★ docs/AUDIT-CHECKLIST.md quotes the derived JOB counts', c.ckClaim)
58
+ has(ck, '★ docs/AUDIT-CHECKLIST.md quotes the derived shipped FILE counts (published subset)', c.ckShipped)
59
+ has(ck, '★ docs/AUDIT-CHECKLIST.md D1 row carries the derived counts', c.d1Row)
60
+ ok(!/TOTAL 57/.test(zh), 'the stale README claim (TOTAL 57) is gone')
61
+ ok(!/TOTAL 65 PASS 65/.test(tt), 'the old test-timing baseline row (TOTAL 65 PASS 65) is gone')
62
+ ok(!/\*\*65 项\*\* = 44 套件 \+ 21/.test(tt + ck), 'no bolded stale 65-item claim remains (prose about the migration is fine)')
63
+
64
+ // ── F-C: EVERY `TOTAL <n>` in a LIVE doc equals the derived job count ────────────────────────────
65
+ // Published release notes under `docs/release-notes/**` are FROZEN history: they record what the gate
66
+ // read at their own release (`TOTAL 65` in 2.7.0/2.7.1), so they are excluded BY NAME — and the
67
+ // exclusion is asserted non-empty and reported, so the sweep cannot silently skip everything.
68
+ const FROZEN_EXCLUDED = ['docs/release-notes/RELEASE-NOTES-2.7.0.md', 'docs/release-notes/RELEASE-NOTES-2.7.0.en.md', 'docs/release-notes/RELEASE-NOTES-2.7.1.md', 'docs/release-notes/RELEASE-NOTES-2.7.1.en.md', 'docs/release-notes/RELEASE-NOTES-2.7.2.md']
69
+ const LIVE_DOCS = [
70
+ ['COUNTS_README', 'README.md'], ['COUNTS_README_EN', 'README.en.md'], ['COUNTS_TIMING', 'docs/test-timing.md'],
71
+ ['COUNTS_CHECKLIST', 'docs/AUDIT-CHECKLIST.md'],
72
+ ['COUNTS_FIELDS', 'docs/status-report-fields.md'], ['COUNTS_PAPER', 'docs/final-paper.md'], ['COUNTS_MATH', 'docs/math-computation.md'],
73
+ ]
74
+ let occurrences = 0, scanned = 0
75
+ for (const [env, rel] of LIVE_DOCS) {
76
+ let text = ''
77
+ try { text = read(env, rel) } catch (e) { continue }
78
+ scanned++
79
+ for (const m of text.matchAll(/TOTAL\s+(\d+)/g)) {
80
+ occurrences++
81
+ ok(Number(m[1]) === derived.total, '★ ' + rel + ': every TOTAL <n> equals the derived job count (' + m[1] + ' vs ' + derived.total + ')',
82
+ 'occurrence: ' + m[0])
83
+ }
84
+ }
85
+ console.log(' live-docs scanned=' + scanned + ' TOTAL-occurrences=' + occurrences + ' frozen-excluded=' + FROZEN_EXCLUDED.length)
86
+ ok(scanned > 0 && occurrences > 0, '★ the F-C sweep is non-vacuous (it scanned live docs and found TOTAL occurrences)',
87
+ 'scanned=' + scanned + ' occurrences=' + occurrences)
88
+ ok(FROZEN_EXCLUDED.length > 0, '★ the frozen release notes are excluded BY NAME, not by a pattern that could swallow the sweep',
89
+ JSON.stringify(FROZEN_EXCLUDED.length))
90
+ }
91
+
92
+ console.log('')
93
+ console.log('=== README COUNTS: ' + passed + ' passed, ' + failed + ' failed ===')
94
+ for (const f of failures) console.error(' - ' + f)
95
+ process.exit(failed === 0 ? 0 : 1)
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Mutant harness for tests/audit-readme-counts.mjs (ships: `tests/*.mutants.mjs`).
4
+ *
5
+ * It proves the count guard can REDDEN: tamper the quoted TOTAL in a COPY of README.md (via the
6
+ * guard's COUNTS_README seam) and require the NAMED assertion "README.md quotes the DERIVED totals"
7
+ * in the failure list. Expected reds must name the LOAD-BEARING assertion, not any substring.
8
+ *
9
+ * Run: node tests/audit-readme-counts.mutants.mjs
10
+ */
11
+ import { mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
12
+ import { tmpdir } from 'node:os'
13
+ import { join } from 'node:path'
14
+ import { spawnSync } from 'node:child_process'
15
+ import { fileURLToPath } from 'node:url'
16
+ import { claims } from './helpers/doc-counts-format.mjs'
17
+
18
+ const HERE = fileURLToPath(new URL('./', import.meta.url))
19
+ const REPO = join(HERE, '..')
20
+ const GUARD = join(HERE, 'audit-readme-counts.mjs')
21
+ let passed = 0, failed = 0
22
+ const ok = (c, l, d) => { if (c) { passed++; console.log(' ok ' + l) } else { failed++; console.log(' FAIL ' + l + (d ? ' — ' + d : '')) } }
23
+
24
+ const base = spawnSync(process.execPath, [GUARD], { cwd: REPO, encoding: 'utf8' })
25
+ const baseOut = String(base.stdout || '') + String(base.stderr || '')
26
+ ok(base.status === 0 && /10 passed, 0 failed|passed, 0 failed/.test(baseOut), 'baseline: the count guard is green',
27
+ (baseOut.trim().split('\n').filter(Boolean).slice(-1)[0] || '').slice(0, 80))
28
+
29
+ // Tamper the EXACT quoted needle (not a random first match), so the red names the load-bearing check.
30
+ const derived = JSON.parse(spawnSync(process.execPath, [join(HERE, 'run-tests.mjs'), '--counts'], { cwd: REPO, encoding: 'utf8' }).stdout.trim())
31
+ const pkg = JSON.parse(readFileSync(join(REPO, 'package.json'), 'utf8'))
32
+ const dir = mkdtempSync(join(tmpdir(), 'counts-mut-'))
33
+ const zh = readFileSync(join(REPO, 'README.md'), 'utf8')
34
+ const shippedFiles = pkg.files.filter((f) => f.startsWith('tests/') && f.endsWith('.mjs'))
35
+ const shipped = { total: shippedFiles.length, suites: shippedFiles.filter((f) => f.endsWith('.test.mjs')).length }
36
+ shipped.probes = shipped.total - shipped.suites
37
+ const needle = claims(derived, shipped).zhTotal
38
+ if (zh.indexOf(needle) === -1) {
39
+ ok(false, 'the tamper anchor is present in README.md (the guard and the harness agree on the shape)', 'ANCHOR MISS: ' + needle)
40
+ } else {
41
+ writeFileSync(join(dir, 'README.md'), zh.replace(needle, '`TOTAL 57`(39 套件 + 18 探针/变体)'))
42
+ const mut = spawnSync(process.execPath, [GUARD], { cwd: REPO, encoding: 'utf8', env: Object.assign({}, process.env, { COUNTS_README: join(dir, 'README.md') }) })
43
+ const out = String(mut.stdout || '') + String(mut.stderr || '')
44
+ const failLines = out.split('\n').filter((l) => /^ - /.test(l))
45
+ const named = failLines.some((l) => l.indexOf('README.md carries the canonical JOB-count bullet') !== -1)
46
+ ok(mut.status !== 0 && named, '★ tampering the quoted TOTAL makes the count guard RED by name', 'exit=' + mut.status)
47
+ if (!named) for (const l of failLines.slice(0, 4)) console.log(' guard failure: ' + l.trim().slice(0, 130))
48
+ }
49
+ rmSync(dir, { recursive: true, force: true })
50
+ console.log('')
51
+ console.log('=== README COUNTS MUTANTS: ' + passed + ' passed, ' + failed + ' failed ===')
52
+ process.exit(failed === 0 ? 0 : 1)
@@ -0,0 +1,78 @@
1
+ /**
2
+ * ROUND 2 — verify the tool-registration surface against the REAL host contract.
3
+ *
4
+ * `dsh-tools` register(): "duplicates within one layer ... fail". So if a preset
5
+ * registers the same tool name twice in the same scope, the preset cannot mount.
6
+ * The e2e mocks only push to an array, so they cannot see this. This counts what the
7
+ * plugin actually registers and reports duplicates.
8
+ */
9
+ import { mkdtempSync, rmSync, existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import { join, dirname, resolve, isAbsolute } from 'node:path';
12
+ import { fileURLToPath, pathToFileURL } from 'node:url';
13
+
14
+ const WS = mkdtempSync(join(tmpdir(), 'reg-'));
15
+ const toolRegs = [];
16
+ const cmdRegs = [];
17
+ const ctx = {
18
+ get(n) {
19
+ if (n === 'subprocess') return { async spawn({ argv }) {
20
+ const s = argv[argv.length - 1] || '';
21
+ if (/New-Item/.test(s)) { const m = s.match(/-Path\s+'((?:[^']|'')*)'/); if (m) m[1].split(',').forEach((p) => { if (p) mkdirSync(p.replace(/''/g, "'"), { recursive: true }); }); }
22
+ return { done: Promise.resolve({ exitCode: 0 }) };
23
+ } };
24
+ return undefined;
25
+ },
26
+ on() {}, effect(fn) { const d = fn(); return () => { if (typeof d === 'function') d(); } },
27
+ logger: { info() {}, warn() {}, error() {} },
28
+ tools: { register(spec) { toolRegs.push(spec); } },
29
+ commands: { register(spec) { cmdRegs.push(spec); } },
30
+ subagents: { list() { return ['spawn']; }, async startContinuable() { return { childId: 'c1' }; }, async sendMessage() {}, async followup() {}, interrupt() {} },
31
+ agents: { roots() { return []; }, get() { return undefined; } },
32
+ fs: {
33
+ async resolve(rel, o) { return join((o && o.cwd) || WS, ...String(rel).split('/')); },
34
+ async stat(t) { return existsSync(t) ? { type: 'file' } : undefined; },
35
+ async readText(t) { return readFileSync(t, 'utf8'); },
36
+ async writeText(t, c) { mkdirSync(dirname(t), { recursive: true }); writeFileSync(t, c, 'utf8'); },
37
+ async listDir(t) { if (!existsSync(t)) return []; return readdirSync(t, { withFileTypes: true }).map((e) => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file' })); },
38
+ },
39
+ };
40
+
41
+ const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..');
42
+ const FILE = process.argv[2] || 'vibe-math-v3/vibe-math-v3.js';
43
+ const TARGET = isAbsolute(FILE) ? FILE : resolve(REPO, FILE);
44
+ const mod = await import(pathToFileURL(TARGET).href + '?t=' + Date.now());
45
+ const plugin = mod.default || mod;
46
+
47
+ console.log(`=== ${FILE} ===`);
48
+ console.log(`exported name : ${plugin.name}`);
49
+ console.log(`exported inject : ${JSON.stringify(plugin.inject)}`);
50
+
51
+ let applyError = null;
52
+ try { plugin.apply(ctx); } catch (e) { applyError = e; }
53
+ if (applyError) { console.log(`apply() threw : ${applyError.message}`); }
54
+
55
+ console.log(`tools.register calls : ${toolRegs.length}`);
56
+ console.log(`commands.register calls: ${cmdRegs.length}`);
57
+
58
+ const dup = (specs) => {
59
+ const seen = new Map();
60
+ for (const s of specs) seen.set(s.name, (seen.get(s.name) || 0) + 1);
61
+ return [...seen.entries()].filter(([, c]) => c > 1);
62
+ };
63
+ const toolDup = dup(toolRegs);
64
+ const cmdDup = dup(cmdRegs);
65
+
66
+ console.log(`DUPLICATE tool names : ${toolDup.length === 0 ? 'none' : JSON.stringify(toolDup)}`);
67
+ console.log(`DUPLICATE command names: ${cmdDup.length === 0 ? 'none' : JSON.stringify(cmdDup)}`);
68
+
69
+ // Every registered tool must satisfy the host's mandatory output contract.
70
+ const missingOutput = toolRegs.filter((s) => !s.output || typeof s.output.render !== 'function' || !s.output.schema);
71
+ console.log(`tools missing output{}/render: ${missingOutput.length === 0 ? 'none' : missingOutput.map((s) => s.name).join(', ')}`);
72
+ const badName = toolRegs.filter((s) => typeof s.name !== 'string' || s.name === '');
73
+ console.log(`tools with a bad name : ${badName.length === 0 ? 'none' : badName.length}`);
74
+
75
+ console.log(`\nfirst 8 tool names: ${toolRegs.slice(0, 8).map((s) => s.name).join(', ')}`);
76
+
77
+ rmSync(WS, { recursive: true, force: true });
78
+ process.exit(toolDup.length > 0 || cmdDup.length > 0 || missingOutput.length > 0 ? 1 : 0);