dsh-vibe-math 2.7.2 → 2.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +11 -10
- package/README.md +13 -12
- package/cordis.patch.yml +112 -16
- package/docs/AUDIT-CHECKLIST.md +229 -3
- package/docs/COMPAT-AUDIT-ROUND2.md +35 -9
- package/docs/final-paper.md +2 -2
- package/docs/math-computation.md +13 -6
- package/docs/parameter-schema.md +37 -0
- package/docs/release-notes/README.en.md +1 -0
- package/docs/release-notes/README.md +1 -0
- 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/TEMPLATE.md +6 -0
- package/docs/status-report-fields.md +167 -0
- package/docs/test-timing.md +8 -5
- package/installer.js +27 -17
- package/package.json +250 -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 +2287 -283
- package/prompt-corpus-v3/formal-verify-v3.json +105 -105
- package/prompt-corpus-v3/formal-verify-v3.md +213 -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 +451 -207
- 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-installer-assertions.mutants.mjs +63 -0
- package/tests/audit-installer-compat.test.mjs +52 -4
- 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 +4 -2
- 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 +14 -0
- package/tests/audit-persona-surface.mutants.mjs +69 -0
- package/tests/audit-persona-surface.test.mjs +63 -22
- package/tests/audit-prompt-duplication.mjs +143 -0
- package/tests/audit-prompt-duplication.mutants.mjs +50 -0
- package/tests/audit-prompt-invariants.mjs +43 -5
- 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-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 +300 -13
- 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 +12 -7
- package/tests/e2e-identity-a6.mutants.mjs +65 -0
- package/tests/e2e-v5-round2.test.mjs +458 -13
- package/tests/formal-verify-v2.test.mjs +91 -43
- package/tests/formal-verify-v3.mutants.mjs +117 -0
- package/tests/formal-verify-v3.test.mjs +85 -39
- package/tests/formal-verify-v4.mutants.mjs +152 -0
- package/tests/formal-verify-v4.test.mjs +541 -9
- package/tests/formal-verify-v5.test.mjs +68 -9
- 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 +274 -5
- package/tests/math-computation-v2.test.mjs +767 -0
- package/tests/math-computation-v3.test.mjs +759 -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 +13 -2
- package/tests/run-tests.mjs +139 -9
- package/tests/run-tests.mutants.mjs +70 -0
- package/tests/selfdrive-v5.mjs +53 -1
- package/tests/selfdrive-v5.mutants.mjs +132 -0
- package/tests/v2-fix-probes.mutants.mjs +100 -0
- package/tests/v2-fix-probes.test.mjs +1039 -0
- package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
- package/tests/v3-fix-probes.mutants.mjs +95 -0
- package/tests/v3-fix-probes.test.mjs +891 -0
- package/tests/v5-institute-fixes.mutants.mjs +190 -0
- package/vibe-math-v2/agent.cordis.yml +62 -6
- package/vibe-math-v2/math-computation.js +130 -41
- package/vibe-math-v2/math-engines.js +12 -12
- package/vibe-math-v2/vibe-math-v2.js +388 -113
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +5 -1
- package/vibe-math-v3/agent.cordis.yml +72 -6
- package/vibe-math-v3/math-computation.js +130 -41
- package/vibe-math-v3/math-engines.js +12 -12
- package/vibe-math-v3/vibe-math-v3.js +422 -126
- package/vibe-math-v4/agent.cordis.yml +44 -8
- package/vibe-math-v4/math-computation.js +130 -41
- package/vibe-math-v4/math-engines.js +12 -12
- package/vibe-math-v4/vibe-math-v4.js +196 -43
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +2 -0
- package/vibe-math-v5/agent.cordis.yml +46 -12
- package/vibe-math-v5/math-computation.js +130 -41
- package/vibe-math-v5/math-engines.js +12 -12
- package/vibe-math-v5/vibe-math-v5.js +1173 -218
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +80 -12
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +2 -2
|
@@ -60,10 +60,24 @@ const read = readRaw
|
|
|
60
60
|
const SELF_PROBE_MUTATIONS = [
|
|
61
61
|
{ name: 'control (no mutation)', rel: '', from: '', to: '', expect: '', control: true },
|
|
62
62
|
{
|
|
63
|
-
name: '
|
|
63
|
+
name: 'v5: the formal gate is REMOVED but a comment still quotes it (I8 must read code, not comments)',
|
|
64
|
+
rel: 'vibe-math-v5/vibe-math-v5.js',
|
|
65
|
+
from: 'formalOn() && p.formal',
|
|
66
|
+
to: 'true /* removed gate: formalOn() && p.formal */',
|
|
67
|
+
expect: 'v5 I8: the reply channel is gated on formalOn()',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'v2: one registration site reverts to a literal without the Lean params (I13, mixed shapes)',
|
|
71
|
+
rel: 'vibe-math-v2/vibe-math-v2.js',
|
|
72
|
+
from: "objParams(paramProps()), 'vibe_math_set_params')",
|
|
73
|
+
to: "objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }), 'vibe_math_set_params')",
|
|
74
|
+
expect: 'v2 I13: every vibe_math_set_params schema advertises',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'v3: one registration site reverts to a literal without the Lean params (I13, mixed shapes)',
|
|
64
78
|
rel: 'vibe-math-v3/vibe-math-v3.js',
|
|
65
|
-
from:
|
|
66
|
-
to:
|
|
79
|
+
from: "objParams(paramProps()), 'vibe_math_set_params')",
|
|
80
|
+
to: "objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }), 'vibe_math_set_params')",
|
|
67
81
|
expect: 'v3 I13: every vibe_math_set_params schema advertises',
|
|
68
82
|
},
|
|
69
83
|
{
|
|
@@ -244,6 +258,28 @@ function objectKeys(src, at) {
|
|
|
244
258
|
.filter(Boolean)
|
|
245
259
|
}
|
|
246
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Keys of the `objParams(...)` argument. Two shapes are supported since F-B (see docs/parameter-schema.md):
|
|
263
|
+
* objParams({ ...literal... }) — the historical hand-written table
|
|
264
|
+
* objParams(paramProps()) — derived from the SINGLE source PARAM_SCHEMA; the machine key set is
|
|
265
|
+
* pinned by PARAM_PROPS_KEYS (+ PARAM_PROPS_EXTRA for source-unexpressible
|
|
266
|
+
* entries), so that is where the advertised keys now come from.
|
|
267
|
+
* Without the second branch the extractor scanned past `objParams(` to the next `{` anywhere in the file and
|
|
268
|
+
* returned 0 keys — a silent extraction failure, not a product regression.
|
|
269
|
+
*/
|
|
270
|
+
function schemaKeysFor(full, at) {
|
|
271
|
+
const open = full.indexOf('{', at)
|
|
272
|
+
const close = full.indexOf(')', at)
|
|
273
|
+
if (open !== -1 && (close === -1 || open < close)) return objectKeys(full, at)
|
|
274
|
+
// Derived form: the key set lives at MODULE scope (before the registration), so search the WHOLE file.
|
|
275
|
+
const km = /const PARAM_PROPS_KEYS = \[([^\]]*)\]/.exec(full)
|
|
276
|
+
if (!km) return null
|
|
277
|
+
const keys = km[1].split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean)
|
|
278
|
+
const em = /const PARAM_PROPS_EXTRA = \{([\s\S]*?)\n\s*\}/.exec(full)
|
|
279
|
+
const extra = em ? [...em[1].matchAll(/^\s*([A-Za-z_$][\w$]*)\s*:/gm)].map((m) => m[1]) : []
|
|
280
|
+
return [...new Set(keys.concat(extra))]
|
|
281
|
+
}
|
|
282
|
+
|
|
247
283
|
const PRESETS = [
|
|
248
284
|
{ tag: 'v2', js: 'vibe-math-v2/vibe-math-v2.js', suite: 'tests/formal-verify-v2.test.mjs', corpus: 'prompt-corpus-v2/formal-verify-v2.md', valueField: 'Result', prefix: 'vibe_math_', setTool: 'vibe_math_set_params' },
|
|
249
285
|
{ tag: 'v3', js: 'vibe-math-v3/vibe-math-v3.js', suite: 'tests/formal-verify-v3.test.mjs', corpus: 'prompt-corpus-v3/formal-verify-v3.md', valueField: 'Result', prefix: 'vibe_math_', setTool: 'vibe_math_set_params' },
|
|
@@ -318,7 +354,9 @@ for (const P of PRESETS) {
|
|
|
318
354
|
v4: /applyFormalReply[\s\S]{0,900}!formalOn\(\)/,
|
|
319
355
|
v5: /formalOn\(\)\s*&&\s*p\.formal/,
|
|
320
356
|
}
|
|
321
|
-
|
|
357
|
+
// A text gate must read CODE, not comments: a comment quoting `formalOn() && p.formal` satisfied
|
|
358
|
+
// the raw-source version of this check, which hid a removed gate (self-probe mutation below proves it).
|
|
359
|
+
check(offGuards[P.tag].test(code), P.tag + ' I8: the reply channel is gated on formalOn() (off stays a no-op)')
|
|
322
360
|
|
|
323
361
|
// I9 — the corpus covers every mode and is deterministic / machine-path free.
|
|
324
362
|
check(corpus.includes('【Lean 形式化验证(鼓励模式)】'), P.tag + ' I9: corpus covers encourage mode')
|
|
@@ -381,7 +419,7 @@ for (const P of PRESETS) {
|
|
|
381
419
|
while ((m = regRe.exec(code))) {
|
|
382
420
|
const rest = code.slice(m.index)
|
|
383
421
|
const oi = rest.indexOf('objParams(')
|
|
384
|
-
schemas.push(oi < 0 ? null :
|
|
422
|
+
schemas.push(oi < 0 ? null : schemaKeysFor(code, m.index + oi + 'objParams('.length))
|
|
385
423
|
}
|
|
386
424
|
check(schemas.length >= 1, P.tag + ' I13: the parameter tool ' + P.setTool + ' is registered',
|
|
387
425
|
'no registerTool(\'' + P.setTool + '\') call found')
|
|
@@ -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);
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* STATUS/REPORT FIELD TABLE vs SOURCE (G-6) — the doc's per-preset field/scope rows must be exactly the
|
|
4
|
+
* fields the code declares, in BOTH directions, and each row's scope must match the source classification.
|
|
5
|
+
*
|
|
6
|
+
* DEFECT CLASS: "machine-face field documented nowhere / documented field that does not exist" (F12/F3):
|
|
7
|
+
* `实现方案.md §14.9` claimed every `status()` field had a row while >=13 top-level keys had none; and a
|
|
8
|
+
* table written by hand drifts the moment a field is added. Sources are derived BY MARKER (line numbers
|
|
9
|
+
* are claims, not anchors):
|
|
10
|
+
* v2/v3 -> `fieldScopes: { session: [...], durable: [...] }` (20 / 25 entries)
|
|
11
|
+
* v5 -> the top-level keys of the object literal `status()` returns (its `fieldScopes` is coarse)
|
|
12
|
+
* v4 -> has NO `fieldScopes`; the table must say so rather than invent rows
|
|
13
|
+
*
|
|
14
|
+
* Run: node tests/audit-status-report-fields.mjs
|
|
15
|
+
* Env (mutant harness): FIELD_DOC, FIELD_V2_JS, FIELD_V3_JS, FIELD_V5_JS, FIELD_V4_JS (absolute or relative).
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync } from 'node:fs'
|
|
18
|
+
import { dirname, isAbsolute, join } from 'node:path'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
|
|
21
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
22
|
+
const REPO = join(HERE, '..')
|
|
23
|
+
let passed = 0, failed = 0
|
|
24
|
+
const failures = []
|
|
25
|
+
const ok = (cond, label, detail) => { if (cond) passed++; else { failed++; failures.push(label + (detail ? ' — ' + detail : '')) } }
|
|
26
|
+
const read = (envName, rel) => {
|
|
27
|
+
const v = process.env[envName]
|
|
28
|
+
return readFileSync(v ? (isAbsolute(v) ? v : join(REPO, v)) : join(REPO, rel), 'utf8')
|
|
29
|
+
}
|
|
30
|
+
function fieldScopesOf(text) {
|
|
31
|
+
const lines = text.split(/\r?\n/)
|
|
32
|
+
const start = lines.findIndex((l) => /^\s*fieldScopes:\s*\{/.test(l))
|
|
33
|
+
if (start === -1) return null
|
|
34
|
+
let depth = 0
|
|
35
|
+
const buf = []
|
|
36
|
+
for (let i = start; i < lines.length; i++) {
|
|
37
|
+
buf.push(lines[i]); depth += (lines[i].match(/\{/g) || []).length - (lines[i].match(/\}/g) || []).length
|
|
38
|
+
if (i > start && depth <= 0) break
|
|
39
|
+
}
|
|
40
|
+
const out = {}
|
|
41
|
+
for (const m of buf.join('\n').matchAll(/(\w+):\s*\[([\s\S]*?)\]/g)) {
|
|
42
|
+
out[m[1]] = m[2].split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean)
|
|
43
|
+
}
|
|
44
|
+
return out
|
|
45
|
+
}
|
|
46
|
+
function statusKeysOf(text) {
|
|
47
|
+
// Robust: brace/comment/string aware, and it reads SHORTHAND properties too —
|
|
48
|
+
// `institute: instituteName, project, key, phase,` is FOUR keys, not one. The earlier line-anchored
|
|
49
|
+
// version missed 8 real keys (R19 F-A: project/key/phase/running/autoDone/runId/lastProgressAt/params).
|
|
50
|
+
const fn = text.search(/function\s+status\s*\(/)
|
|
51
|
+
if (fn === -1) return null
|
|
52
|
+
const ret = text.indexOf('return {', fn)
|
|
53
|
+
if (ret === -1) return null
|
|
54
|
+
const start = text.indexOf('{', ret)
|
|
55
|
+
let d0 = 0, stop = start, k = start
|
|
56
|
+
while (k < text.length) {
|
|
57
|
+
const c = text[k]
|
|
58
|
+
if (c === '/' && text[k + 1] === '/') { while (k < text.length && text[k] !== '\n') k++; continue }
|
|
59
|
+
if (c === '/' && text[k + 1] === '*') { k += 2; while (k < text.length && !(text[k] === '*' && text[k + 1] === '/')) k++; k += 2; continue }
|
|
60
|
+
if (c === '"' || c === "'" || c === '`') { const q = c; k++; while (k < text.length) { if (text[k] === '\\') k += 2; else if (text[k] === q) break; else k++ } k++; continue }
|
|
61
|
+
if (c === '{' || c === '[' || c === '(') d0++
|
|
62
|
+
else if (c === '}' || c === ']' || c === ')') { d0--; if (d0 === 0) { stop = k; break } }
|
|
63
|
+
k++
|
|
64
|
+
}
|
|
65
|
+
if (d0 !== 0) return null
|
|
66
|
+
const body = text.slice(start + 1, stop)
|
|
67
|
+
const segs = []
|
|
68
|
+
let buf = '', d = 0, j = 0
|
|
69
|
+
while (j < body.length) {
|
|
70
|
+
const c = body[j]
|
|
71
|
+
if (c === '/' && body[j + 1] === '/') { while (j < body.length && body[j] !== '\n') j++; continue }
|
|
72
|
+
if (c === '/' && body[j + 1] === '*') { j += 2; while (j < body.length && !(body[j] === '*' && body[j + 1] === '/')) j++; j += 2; continue }
|
|
73
|
+
if (c === '"' || c === "'" || c === '`') { const q = c; buf += c; j++; while (j < body.length) { buf += body[j]; if (body[j] === '\\') { j++; buf += body[j] } else if (body[j] === q) break; j++ } j++; continue }
|
|
74
|
+
if (c === '{' || c === '[' || c === '(') d++
|
|
75
|
+
if (c === '}' || c === ']' || c === ')') d--
|
|
76
|
+
if (c === ',' && d === 0) { segs.push(buf); buf = ''; j++; continue }
|
|
77
|
+
buf += c; j++
|
|
78
|
+
}
|
|
79
|
+
if (buf.trim()) segs.push(buf)
|
|
80
|
+
const keys = []
|
|
81
|
+
for (const seg of segs) {
|
|
82
|
+
const t = seg.trim()
|
|
83
|
+
if (!t) continue
|
|
84
|
+
const m = /^([A-Za-z_$][\w$]*)\s*(?::|$|,)/.exec(t)
|
|
85
|
+
if (m) keys.push(m[1])
|
|
86
|
+
}
|
|
87
|
+
return [...new Set(keys)]
|
|
88
|
+
}
|
|
89
|
+
/** Rows of the doc section that starts with `headingPrefix`, up to the next `### `. */
|
|
90
|
+
function docRows(doc, headingPrefix) {
|
|
91
|
+
const lines = doc.split(/\r?\n/)
|
|
92
|
+
const start = lines.findIndex((l) => l.startsWith(headingPrefix))
|
|
93
|
+
if (start === -1) return null
|
|
94
|
+
let end = lines.findIndex((l, i) => i > start && /^### /.test(l))
|
|
95
|
+
if (end === -1) end = lines.length
|
|
96
|
+
const rows = []
|
|
97
|
+
for (let i = start; i < end; i++) {
|
|
98
|
+
const m = /^\|\s*`([^`]+)`\s*\|\s*([^|]+?)\s*\|/.exec(lines[i])
|
|
99
|
+
if (m) rows.push({ name: m[1], scope: m[2] })
|
|
100
|
+
}
|
|
101
|
+
return rows
|
|
102
|
+
}
|
|
103
|
+
const diff = (a, b) => [...a].filter((x) => !b.has(x))
|
|
104
|
+
|
|
105
|
+
const doc = read('FIELD_DOC', 'docs/status-report-fields.md')
|
|
106
|
+
ok(doc.indexOf('G-6-FIELD-TABLE-START') !== -1 && doc.indexOf('G-6-FIELD-TABLE-END') !== -1,
|
|
107
|
+
'the doc carries the machine-generated field table between its markers (so the guard knows what to read)')
|
|
108
|
+
|
|
109
|
+
const f2 = fieldScopesOf(read('FIELD_V2_JS', 'vibe-math-v2/vibe-math-v2.js'))
|
|
110
|
+
const f3 = fieldScopesOf(read('FIELD_V3_JS', 'vibe-math-v3/vibe-math-v3.js'))
|
|
111
|
+
const s5 = statusKeysOf(read('FIELD_V5_JS', 'vibe-math-v5/vibe-math-v5.js'))
|
|
112
|
+
const v4 = read('FIELD_V4_JS', 'vibe-math-v4/vibe-math-v4.js')
|
|
113
|
+
ok(!!f2 && !!f3 && !!s5, 'all three sources are parseable by marker (v2/v3 fieldScopes, v5 status())')
|
|
114
|
+
|
|
115
|
+
if (f2 && f3 && s5) {
|
|
116
|
+
const cases = [
|
|
117
|
+
{ preset: 'v2', src: [...(f2.session || []), ...(f2.durable || [])], head: '### v2 的字段与作用域', scopeMap: { session: '会话', durable: '耐久' }, fs: f2 },
|
|
118
|
+
{ preset: 'v3', src: [...(f3.session || []), ...(f3.durable || [])], head: '### v3 的字段与作用域', scopeMap: { session: '会话', durable: '耐久' }, fs: f3 },
|
|
119
|
+
{ preset: 'v5', src: s5, head: '### v5 的字段与作用域', scopeMap: null, fs: null },
|
|
120
|
+
]
|
|
121
|
+
for (const c of cases) {
|
|
122
|
+
const rows = docRows(doc, c.head)
|
|
123
|
+
ok(!!rows && rows.length > 0, c.preset + ': the doc section exists and has rows', String(rows && rows.length))
|
|
124
|
+
if (!rows) continue
|
|
125
|
+
const docSet = new Set(rows.map((r) => r.name))
|
|
126
|
+
const srcSet = new Set(c.src)
|
|
127
|
+
const missingInDoc = diff(srcSet, docSet)
|
|
128
|
+
const extraInDoc = diff(docSet, srcSet)
|
|
129
|
+
ok(missingInDoc.length === 0, '★ ' + c.preset + ': every source field has a doc row (doc↔source, source→doc)', JSON.stringify(missingInDoc))
|
|
130
|
+
ok(extraInDoc.length === 0, '★ ' + c.preset + ': every doc row names a real source field (doc↔source, doc→source)', JSON.stringify(extraInDoc))
|
|
131
|
+
if (c.scopeMap && c.fs) {
|
|
132
|
+
const wrong = []
|
|
133
|
+
for (const scope of ['session', 'durable']) {
|
|
134
|
+
for (const name of c.fs[scope] || []) {
|
|
135
|
+
const r = rows.find((x) => x.name === name)
|
|
136
|
+
if (r && r.scope !== c.scopeMap[scope]) wrong.push(name + '=' + r.scope)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
ok(wrong.length === 0, '★ ' + c.preset + ': every row scope matches the source classification', JSON.stringify(wrong))
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// ---- code -> doc (R19 F-A): EVERY status() top-level key lands in the v5 table OR is an explicit
|
|
143
|
+
// exception. The exceptions list is empty today; adding one requires a reason HERE, in this file.
|
|
144
|
+
const V5_EXCEPTIONS = []
|
|
145
|
+
{
|
|
146
|
+
const v5DocRows = docRows(doc, '### v5 的字段与作用域') || []
|
|
147
|
+
const docSet5 = new Set(v5DocRows.map((r) => r.name))
|
|
148
|
+
const frozen = s5
|
|
149
|
+
const exceptions = V5_EXCEPTIONS
|
|
150
|
+
console.log(' counts: frozen=' + frozen.length + ' doc-rows=' + docSet5.size + ' exceptions=' + exceptions.length)
|
|
151
|
+
ok(frozen.length > 0 && docSet5.size > 0, '★ the three counts are non-zero (the comparison cannot pass vacuously)',
|
|
152
|
+
'frozen=' + frozen.length + ' doc-rows=' + docSet5.size + ' exceptions=' + exceptions.length)
|
|
153
|
+
const covered = new Set([...docSet5, ...exceptions])
|
|
154
|
+
const uncovered = frozen.filter((k) => !covered.has(k))
|
|
155
|
+
ok(uncovered.length === 0, '★ every status() top-level key is documented or an explicit exception (code->doc)',
|
|
156
|
+
JSON.stringify(uncovered))
|
|
157
|
+
const phantom = [...docSet5].filter((k) => frozen.indexOf(k) === -1 && exceptions.indexOf(k) === -1)
|
|
158
|
+
ok(phantom.length === 0, '★ no v5 doc row names a key status() does not return (doc->code)',
|
|
159
|
+
JSON.stringify(phantom))
|
|
160
|
+
}
|
|
161
|
+
// v4 must be stated as having none, and the source must agree
|
|
162
|
+
const v4Rows = docRows(doc, '### v4')
|
|
163
|
+
ok(!v4Rows || v4Rows.length === 0, '★ v4: no rows are invented (it has no fieldScopes to derive from)', String(v4Rows && v4Rows.length))
|
|
164
|
+
ok(doc.indexOf('**没有** `fieldScopes`') !== -1 || doc.indexOf('不存在** `fieldScopes`') !== -1,
|
|
165
|
+
'★ v4: the doc says explicitly that v4 has no fieldScopes')
|
|
166
|
+
ok(!/fieldScopes/.test(v4), '★ v4: the source confirms it (zero occurrences of fieldScopes)')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log('')
|
|
170
|
+
console.log('=== STATUS FIELD TABLE: ' + passed + ' passed, ' + failed + ' failed ===')
|
|
171
|
+
for (const f of failures) console.error(' - ' + f)
|
|
172
|
+
process.exit(failed === 0 ? 0 : 1)
|