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.
- package/LICENSE +21 -21
- package/README.en.md +682 -681
- package/README.md +683 -682
- package/cordis.patch.yml +112 -16
- package/docs/AUDIT-CHECKLIST.md +755 -518
- package/docs/COMPAT-AUDIT-ROUND2.md +354 -328
- package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
- package/docs/audit-methodology.md +93 -0
- package/docs/final-paper.md +172 -172
- package/docs/formal-verification.md +613 -588
- package/docs/generate_framework_diagram_v2.py +114 -114
- package/docs/generate_framework_diagram_v3.py +127 -127
- package/docs/math-computation.md +195 -187
- package/docs/parameter-schema.md +37 -0
- package/docs/release-notes/README.en.md +2 -0
- package/docs/release-notes/README.md +2 -0
- package/docs/release-notes/RELEASE-NOTES-2.0.22.md +112 -112
- package/docs/release-notes/RELEASE-NOTES-2.1.0.md +143 -143
- package/docs/release-notes/RELEASE-NOTES-2.2.0.md +266 -266
- package/docs/release-notes/RELEASE-NOTES-2.2.1.md +43 -43
- package/docs/release-notes/RELEASE-NOTES-2.2.2.md +88 -88
- package/docs/release-notes/RELEASE-NOTES-2.3.0.md +207 -207
- package/docs/release-notes/RELEASE-NOTES-2.3.1.md +134 -134
- package/docs/release-notes/RELEASE-NOTES-2.3.10.md +105 -105
- package/docs/release-notes/RELEASE-NOTES-2.3.11.md +57 -57
- package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -80
- package/docs/release-notes/RELEASE-NOTES-2.3.2.md +145 -145
- package/docs/release-notes/RELEASE-NOTES-2.3.3.md +115 -115
- package/docs/release-notes/RELEASE-NOTES-2.3.4.md +69 -69
- package/docs/release-notes/RELEASE-NOTES-2.3.5.md +63 -63
- package/docs/release-notes/RELEASE-NOTES-2.3.6.md +66 -66
- package/docs/release-notes/RELEASE-NOTES-2.3.7.md +59 -59
- package/docs/release-notes/RELEASE-NOTES-2.3.8.md +45 -45
- package/docs/release-notes/RELEASE-NOTES-2.3.9.md +70 -70
- package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +2 -2
- package/docs/release-notes/RELEASE-NOTES-2.7.2.md +3 -3
- package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.0.en.md +65 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.0.md +65 -0
- package/docs/release-notes/TEMPLATE.md +6 -0
- package/docs/release-playbook.md +103 -0
- package/docs/slv-playbook.md +117 -0
- package/docs/status-report-fields.md +167 -0
- package/docs/test-timing.md +158 -154
- package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
- package/installer.js +793 -783
- package/package.json +260 -203
- package/prompt-corpus-persona/persona-corpus.json +9 -9
- package/prompt-corpus-persona/persona-corpus.md +225 -33
- package/prompt-corpus-v2/formal-verify-v2.json +295 -115
- package/prompt-corpus-v2/formal-verify-v2.md +2290 -282
- package/prompt-corpus-v3/formal-verify-v3.json +112 -112
- package/prompt-corpus-v3/formal-verify-v3.md +227 -143
- package/prompt-corpus-v4/formal-verify-v4.json +18 -18
- package/prompt-corpus-v4/formal-verify-v4.md +49 -44
- package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
- package/prompt-corpus-v5/prompt-corpus-v5.md +573 -207
- package/scripts/clean-temp.mjs +234 -0
- package/scripts/update-doc-counts.mjs +71 -0
- package/tests/audit-artifact-docs.mjs +72 -0
- package/tests/audit-artifact-docs.mutants.mjs +58 -0
- package/tests/audit-engine-faces.mjs +298 -0
- package/tests/audit-formal-sensitivity.mjs +345 -342
- package/tests/audit-installer-assertions.mutants.mjs +63 -0
- package/tests/audit-installer-compat.test.mjs +256 -208
- package/tests/audit-installer-policy.test.mjs +41 -8
- package/tests/audit-math-computation-contract.mjs +64 -8
- package/tests/audit-math-computation-contract.mutants.mjs +106 -0
- package/tests/audit-math-computation-parity.mjs +164 -3
- package/tests/audit-math-computation-sensitivity.mjs +5 -3
- package/tests/audit-package-membership.mjs +57 -0
- package/tests/audit-package-membership.mutants.mjs +28 -0
- package/tests/audit-participant-set-parity.mjs +160 -0
- package/tests/audit-path-discipline.mjs +83 -0
- package/tests/audit-path-discipline.mutants.mjs +51 -0
- package/tests/audit-persona-sensitivity.mjs +323 -309
- package/tests/audit-persona-surface.mutants.mjs +69 -0
- package/tests/audit-persona-surface.test.mjs +431 -390
- package/tests/audit-prompt-duplication.mjs +143 -0
- package/tests/audit-prompt-duplication.mutants.mjs +51 -0
- package/tests/audit-prompt-invariants.mjs +589 -508
- package/tests/audit-readme-bilingual.test.mjs +152 -0
- package/tests/audit-readme-counts.mjs +95 -0
- package/tests/audit-readme-counts.mutants.mjs +52 -0
- package/tests/audit-registration.mjs +78 -0
- package/tests/audit-spec-traceability.mjs +193 -193
- package/tests/audit-status-report-fields.mjs +172 -0
- package/tests/audit-status-report-fields.mutants.mjs +64 -0
- package/tests/audit-v3-registration-parity.mjs +156 -0
- package/tests/audit-v5-integrity.mjs +795 -508
- package/tests/audit-v5-lean-abstention.mjs +392 -0
- package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
- package/tests/audit-v5-prompt-duplication.mjs +88 -0
- package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
- package/tests/audit-v5-sensitivity.mjs +433 -427
- package/tests/e2e-identity-a6.mutants.mjs +65 -0
- package/tests/e2e-v5-round2.test.mjs +2453 -1981
- package/tests/formal-verify-v2.test.mjs +1778 -1724
- package/tests/formal-verify-v3.mutants.mjs +117 -0
- package/tests/formal-verify-v3.test.mjs +1724 -1671
- package/tests/formal-verify-v4.mutants.mjs +152 -0
- package/tests/formal-verify-v4.test.mjs +2114 -1574
- package/tests/formal-verify-v5.test.mjs +790 -724
- package/tests/helpers/doc-counts-format.mjs +41 -0
- package/tests/helpers/math-computation-fake-seam.mjs +4 -0
- package/tests/math-computation-a1.mutants.mjs +72 -0
- package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
- package/tests/math-computation-discovery.mutants.mjs +76 -0
- package/tests/math-computation-shared.test.mjs +392 -6
- package/tests/math-computation-v2.test.mjs +779 -0
- package/tests/math-computation-v3.test.mjs +771 -0
- package/tests/math-computation-v4.mutants.mjs +133 -0
- package/tests/math-computation-v4.test.mjs +739 -0
- package/tests/prompt-v5-integrity.test.mjs +1291 -1280
- package/tests/run-tests.mjs +393 -202
- package/tests/run-tests.mutants.mjs +70 -0
- package/tests/selfdrive-v5.mjs +560 -508
- package/tests/selfdrive-v5.mutants.mjs +132 -0
- package/tests/temp-hygiene.mutants.mjs +80 -0
- package/tests/v2-fix-probes.mutants.mjs +104 -0
- package/tests/v2-fix-probes.test.mjs +1040 -0
- package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
- package/tests/v2v3-interrupt.mutants.mjs +111 -0
- package/tests/v3-fix-probes.mutants.mjs +99 -0
- package/tests/v3-fix-probes.test.mjs +892 -0
- package/tests/v4-final-paper.mutants.mjs +108 -0
- package/tests/v5-institute-fixes.mutants.mjs +218 -0
- package/vibe-math-v2/agent.cordis.yml +378 -322
- package/vibe-math-v2/math-computation.js +144 -53
- package/vibe-math-v2/math-engines.js +12 -12
- package/vibe-math-v2/preset.yml +2 -2
- package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
- package/vibe-math-v2/vibe-math-v2.js +5097 -4755
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +491 -487
- package/vibe-math-v3/agent.cordis.yml +422 -356
- package/vibe-math-v3/math-computation.js +144 -53
- package/vibe-math-v3/math-engines.js +12 -12
- package/vibe-math-v3/preset.yml +2 -2
- package/vibe-math-v3/vibe-math-v3.js +6235 -5873
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
- package/vibe-math-v4/agent.cordis.yml +438 -402
- package/vibe-math-v4/math-computation.js +144 -53
- package/vibe-math-v4/math-engines.js +12 -12
- package/vibe-math-v4/preset.yml +2 -2
- package/vibe-math-v4/vibe-math-v4.js +4490 -4269
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1013
- package/vibe-math-v5/agent.cordis.yml +46 -12
- package/vibe-math-v5/math-computation.js +144 -53
- package/vibe-math-v5/math-engines.js +12 -12
- package/vibe-math-v5/vibe-math-v5.js +1266 -229
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1660
- 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, '')
|
|
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);
|