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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Regenerate the count sentences quoted by shipped docs FROM THE TREE (D1: derive, don't type).
|
|
4
|
+
*
|
|
5
|
+
* node scripts/update-doc-counts.mjs # rewrite in place
|
|
6
|
+
* node scripts/update-doc-counts.mjs --check # exit 1 if anything would change (converges: whole-line rewrites)
|
|
7
|
+
*
|
|
8
|
+
* Authority: `node tests/run-tests.mjs --counts` (the gate's own JOB list) + `package.json#files`
|
|
9
|
+
* (shipped FILE counts). Shapes come from tests/helpers/doc-counts-format.mjs, shared with the guard
|
|
10
|
+
* (tests/audit-readme-counts.mjs) and its mutant harness. Adding a test file reddens the guard; the fix
|
|
11
|
+
* is to run this script. Whole lines are rebuilt (no partial regex edits), so a corrupted previous
|
|
12
|
+
* shape is repaired rather than half-matched.
|
|
13
|
+
*
|
|
14
|
+
* Grep anchors: update-doc-counts, doc-counts-format.
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
17
|
+
import { dirname, join } from 'node:path'
|
|
18
|
+
import { fileURLToPath } from 'node:url'
|
|
19
|
+
import { spawnSync } from 'node:child_process'
|
|
20
|
+
import { claims, MARKERS } from '../tests/helpers/doc-counts-format.mjs'
|
|
21
|
+
|
|
22
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
23
|
+
const REPO = join(HERE, '..')
|
|
24
|
+
const checkOnly = process.argv.includes('--check')
|
|
25
|
+
|
|
26
|
+
const derived = JSON.parse(spawnSync(process.execPath, [join(REPO, 'tests', 'run-tests.mjs'), '--counts'], { cwd: REPO, encoding: 'utf8' }).stdout.trim())
|
|
27
|
+
const pkg = JSON.parse(readFileSync(join(REPO, 'package.json'), 'utf8'))
|
|
28
|
+
const shippedFiles = pkg.files.filter((f) => f.startsWith('tests/') && f.endsWith('.mjs'))
|
|
29
|
+
const shipped = { total: shippedFiles.length, suites: shippedFiles.filter((f) => f.endsWith('.test.mjs')).length }
|
|
30
|
+
shipped.probes = shipped.total - shipped.suites
|
|
31
|
+
const c = claims(derived, shipped)
|
|
32
|
+
|
|
33
|
+
function replaceLine(text, marker, canonical) {
|
|
34
|
+
const lines = text.split(/\r?\n/)
|
|
35
|
+
const i = lines.findIndex((l) => l.indexOf(marker) !== -1)
|
|
36
|
+
if (i === -1) return null
|
|
37
|
+
if (lines[i] === canonical) return text
|
|
38
|
+
lines[i] = canonical
|
|
39
|
+
return lines.join(text.includes('\r\n') ? '\r\n' : '\n')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const log = []
|
|
43
|
+
let changed = 0
|
|
44
|
+
const apply = (file, fn) => {
|
|
45
|
+
const p = join(REPO, file)
|
|
46
|
+
const before = readFileSync(p, 'utf8')
|
|
47
|
+
const after = fn(before)
|
|
48
|
+
if (after === null) { log.push('MARKER MISS: ' + file); return }
|
|
49
|
+
if (after !== before) {
|
|
50
|
+
changed++
|
|
51
|
+
if (checkOnly) log.push('WOULD CHANGE ' + file)
|
|
52
|
+
else { writeFileSync(p, after); log.push('updated ' + file) }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
apply('README.md', (s) => replaceLine(s, MARKERS.zhBullet, c.zhBullet))
|
|
57
|
+
apply('README.en.md', (s) => replaceLine(s, MARKERS.enBullet, c.enBullet))
|
|
58
|
+
apply('docs/test-timing.md', (s) => replaceLine(s, MARKERS.ttRow, c.ttRow))
|
|
59
|
+
apply('docs/AUDIT-CHECKLIST.md', (s) => {
|
|
60
|
+
let out = replaceLine(s, MARKERS.d1Row, c.d1Row)
|
|
61
|
+
if (out === null) return null
|
|
62
|
+
out = out.replace(/\d+ 项作业(job count)= \d+ 套件 \+ \d+ 探针\/变体/, c.ckClaim)
|
|
63
|
+
out = out.replace(/只发 `tests\/` 的 .*?([^)]*)/, c.ckShipped)
|
|
64
|
+
return out
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
log.push((checkOnly ? 'check: ' : '') + 'derived JOBS total=' + derived.total + ' suites=' + derived.suites + ' probes=' + derived.probes
|
|
68
|
+
+ ' | shipped FILES tests/*.mjs=' + shipped.total + ' (' + shipped.suites + ' suites + ' + shipped.probes + ' probes/scripts)'
|
|
69
|
+
+ ' | files changed=' + changed)
|
|
70
|
+
console.log(log.join('\n'))
|
|
71
|
+
process.exit(checkOnly && changed ? 1 : 0)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ARTIFACT DOCS vs CODE — the READMEs and the contract doc must not name an artifact a preset does not
|
|
4
|
+
* write, and must name the ones it does.
|
|
5
|
+
*
|
|
6
|
+
* DEFECT CLASS (D2): `Paper/<id>/paper.lock.json` was listed in the README's v4 section as "the directory's
|
|
7
|
+
* write lock, serialising concurrent rewrites/recompiles" — while v4 writes NO such file and
|
|
8
|
+
* `vibe-math-v4.js` says the shared-file lock is **NOT IMPLEMENTED** ("nothing is reserved, nothing is
|
|
9
|
+
* serialized"). The README promised a mechanism the code explicitly disclaims. The inverse direction
|
|
10
|
+
* matters too: v2/v3 DO write it (vibe-math-v2.js:3725/3732, vibe-math-v3.js:4318/4325) and their
|
|
11
|
+
* sections must list it.
|
|
12
|
+
*
|
|
13
|
+
* Run: node tests/audit-artifact-docs.mjs
|
|
14
|
+
* Env (mutant harness): ARTIFACT_README / ARTIFACT_README_EN / ARTIFACT_DOC / ARTIFACT_V2_JS /
|
|
15
|
+
* ARTIFACT_V3_JS / ARTIFACT_V4_JS / ARTIFACT_V5_JS (absolute or repo-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
|
+
const ARTIFACT = 'paper.lock.json'
|
|
24
|
+
let passed = 0, failed = 0
|
|
25
|
+
const failures = []
|
|
26
|
+
const ok = (cond, label, detail) => { if (cond) passed++; else { failed++; failures.push(label + (detail ? ' — ' + detail : '')) } }
|
|
27
|
+
|
|
28
|
+
const read = (envName, rel) => {
|
|
29
|
+
const v = process.env[envName]
|
|
30
|
+
return readFileSync(v ? (isAbsolute(v) ? v : join(REPO, v)) : join(REPO, rel), 'utf8')
|
|
31
|
+
}
|
|
32
|
+
const count = (text, needle) => text.split(needle).length - 1
|
|
33
|
+
|
|
34
|
+
// 1) CODE TRUTH: which presets write the artifact?
|
|
35
|
+
const presets = {
|
|
36
|
+
v2: read('ARTIFACT_V2_JS', 'vibe-math-v2/vibe-math-v2.js'),
|
|
37
|
+
v3: read('ARTIFACT_V3_JS', 'vibe-math-v3/vibe-math-v3.js'),
|
|
38
|
+
v4: read('ARTIFACT_V4_JS', 'vibe-math-v4/vibe-math-v4.js'),
|
|
39
|
+
v5: read('ARTIFACT_V5_JS', 'vibe-math-v5/vibe-math-v5.js'),
|
|
40
|
+
}
|
|
41
|
+
const writers = Object.keys(presets).filter((k) => count(presets[k], ARTIFACT) > 0)
|
|
42
|
+
const nonWriters = Object.keys(presets).filter((k) => count(presets[k], ARTIFACT) === 0)
|
|
43
|
+
ok(writers.length > 0, 'at least one preset writes ' + ARTIFACT + ' (else the doc expectation is empty)', JSON.stringify(writers))
|
|
44
|
+
ok(writers.join(',') === 'v2,v3', 'the writing presets are exactly v2,v3 (derived from code)', JSON.stringify({ writers, nonWriters }))
|
|
45
|
+
|
|
46
|
+
// 2) every README mention must sit in a writer's context OR be an explicit denial
|
|
47
|
+
const DENY = /(不写|未实现|无此锁|not implemented|does NOT write|no such|not write)/i
|
|
48
|
+
for (const [name, envName, rel] of [['README.md', 'ARTIFACT_README', 'README.md'], ['README.en.md', 'ARTIFACT_README_EN', 'README.en.md']]) {
|
|
49
|
+
const lines = read(envName, rel).split(/\r?\n/)
|
|
50
|
+
const mentions = lines.map((l, i) => ({ n: i + 1, l })).filter((x) => x.l.indexOf(ARTIFACT) !== -1)
|
|
51
|
+
ok(mentions.length > 0, name + ' mentions ' + ARTIFACT + ' somewhere (it is a real artifact for v2/v3)')
|
|
52
|
+
const bad = mentions.filter((m) => !/v2|v3/.test(m.l) && !DENY.test(m.l))
|
|
53
|
+
ok(bad.length === 0, '★ ' + name + ': every ' + ARTIFACT + ' mention names a WRITING preset or denies the claim', JSON.stringify(bad.map((b) => b.n)))
|
|
54
|
+
const listed = mentions.filter((m) => /Paper\/<[^>]*>\/\{[^}]*paper\.lock\.json/.test(m.l))
|
|
55
|
+
ok(listed.length >= 2, '★ ' + name + ' lists ' + ARTIFACT + ' inside the v2/v3 artifact braces', 'brace mentions=' + listed.length)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 3) the contract doc must state the scope (v2/v3 write it; v4/v5 do not)
|
|
59
|
+
const doc = read('ARTIFACT_DOC', 'docs/final-paper.md')
|
|
60
|
+
const docLines = doc.split(/\r?\n/).filter((l) => l.indexOf(ARTIFACT) !== -1)
|
|
61
|
+
ok(docLines.length > 0, 'docs/final-paper.md documents ' + ARTIFACT)
|
|
62
|
+
const docScope = docLines.some((l) => /v2\/v3/.test(l) && DENY.test(l))
|
|
63
|
+
ok(docScope, '★ docs/final-paper.md scopes ' + ARTIFACT + ' to v2/v3 AND states that v4/v5 do not write it', docLines[0] ? docLines[0].slice(0, 80) : '(no line)')
|
|
64
|
+
|
|
65
|
+
// 4) the non-writer's own disclaimer must exist in code (so the doc denial is backed by the source)
|
|
66
|
+
ok(/NOT IMPLEMENTED/.test(presets.v4) && /claim_write/.test(presets.v4),
|
|
67
|
+
'vibe-math-v4.js still carries the explicit NOT-IMPLEMENTED shared-lock disclaimer (the source of the doc denial)')
|
|
68
|
+
|
|
69
|
+
console.log('')
|
|
70
|
+
console.log('=== ARTIFACT DOCS: ' + passed + ' passed, ' + failed + ' failed ===')
|
|
71
|
+
for (const f of failures) console.error(' - ' + f)
|
|
72
|
+
process.exit(failed === 0 ? 0 : 1)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Mutant harness for tests/audit-artifact-docs.mjs (ships: `tests/*.mutants.mjs`).
|
|
4
|
+
*
|
|
5
|
+
* Two single-site mutations, ONE PER DIRECTION (the guard must bite both ways):
|
|
6
|
+
* A) a COPY of README.md whose v2/v3 brace list loses `paper.lock.json` -> the listing assertion reddens
|
|
7
|
+
* B) a COPY of vibe-math-v4.js that mentions `paper.lock.json` -> the "writers are exactly v2,v3"
|
|
8
|
+
* assertion reddens (i.e. if v4 ever gains the artifact, the docs expectation moves with the code)
|
|
9
|
+
*
|
|
10
|
+
* Run: node tests/audit-artifact-docs.mutants.mjs
|
|
11
|
+
*/
|
|
12
|
+
import { mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
|
|
13
|
+
import { tmpdir } from 'node:os'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
import { spawnSync } from 'node:child_process'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
|
|
18
|
+
const HERE = fileURLToPath(new URL('./', import.meta.url))
|
|
19
|
+
const REPO = join(HERE, '..')
|
|
20
|
+
const GUARD = join(HERE, 'audit-artifact-docs.mjs')
|
|
21
|
+
const ARTIFACT = 'paper.lock.json'
|
|
22
|
+
let passed = 0, failed = 0
|
|
23
|
+
const ok = (c, l, d) => { if (c) { passed++; console.log(' ok ' + l) } else { failed++; console.log(' FAIL ' + l + (d ? ' — ' + d : '')) } }
|
|
24
|
+
|
|
25
|
+
const run = (env) => {
|
|
26
|
+
const r = spawnSync(process.execPath, [GUARD], { cwd: REPO, encoding: 'utf8', env: Object.assign({}, process.env, env || {}) })
|
|
27
|
+
const out = String(r.stdout || '') + String(r.stderr || '')
|
|
28
|
+
return { status: r.status, out, failLines: out.split('\n').filter((l) => /^ - /.test(l)) }
|
|
29
|
+
}
|
|
30
|
+
const baseline = run(null)
|
|
31
|
+
ok(baseline.status === 0, 'baseline: the artifact-docs guard is green',
|
|
32
|
+
(baseline.out.trim().split('\n').filter(Boolean).slice(-1)[0] || '').slice(0, 80))
|
|
33
|
+
|
|
34
|
+
const dir = mkdtempSync(join(tmpdir(), 'artifact-mut-'))
|
|
35
|
+
// A) README loses the v2/v3 brace listing
|
|
36
|
+
const zh = readFileSync(join(REPO, 'README.md'), 'utf8')
|
|
37
|
+
const braces = zh.match(/Paper\/<[^>]*>\/\{[^}]*paper\.lock\.json[^}]*\}/g) || []
|
|
38
|
+
if (!braces.length) ok(false, 'A: the README v2/v3 brace listing exists (harness and guard agree on the shape)', 'ANCHOR MISS')
|
|
39
|
+
else {
|
|
40
|
+
const tampered = zh.split(braces[0]).join(braces[0].replace(',' + ARTIFACT, ''))
|
|
41
|
+
writeFileSync(join(dir, 'README.md'), tampered)
|
|
42
|
+
const a = run({ ARTIFACT_README: join(dir, 'README.md') })
|
|
43
|
+
const named = a.failLines.some((l) => l.indexOf('lists ' + ARTIFACT + ' inside the v2/v3 artifact braces') !== -1)
|
|
44
|
+
ok(a.status !== 0 && named, '★ A: dropping the artifact from the v2/v3 brace list reddens the guard by name', 'exit=' + a.status)
|
|
45
|
+
if (!named) for (const l of a.failLines.slice(0, 3)) console.log(' guard failure: ' + l.trim().slice(0, 130))
|
|
46
|
+
}
|
|
47
|
+
// B) v4 "gains" the artifact
|
|
48
|
+
const v4 = readFileSync(join(REPO, 'vibe-math-v4/vibe-math-v4.js'), 'utf8')
|
|
49
|
+
writeFileSync(join(dir, 'vibe-math-v4.js'), v4 + '\n// mutant: pretend v4 writes ' + ARTIFACT + '\n')
|
|
50
|
+
const b = run({ ARTIFACT_V4_JS: join(dir, 'vibe-math-v4.js') })
|
|
51
|
+
const namedB = b.failLines.some((l) => l.indexOf('the writing presets are exactly v2,v3') !== -1)
|
|
52
|
+
ok(b.status !== 0 && namedB, '★ B: a v4 that mentions the artifact reddens the "writers are exactly v2,v3" assertion', 'exit=' + b.status)
|
|
53
|
+
if (!namedB) for (const l of b.failLines.slice(0, 3)) console.log(' guard failure: ' + l.trim().slice(0, 130))
|
|
54
|
+
|
|
55
|
+
rmSync(dir, { recursive: true, force: true })
|
|
56
|
+
console.log('')
|
|
57
|
+
console.log('=== ARTIFACT DOCS MUTANTS: ' + passed + ' passed, ' + failed + ' failed ===')
|
|
58
|
+
process.exit(failed === 0 ? 0 : 1)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Mutant harness for the installer ASSERTION-QUALITY work (ships: `tests/*.mutants.mjs`).
|
|
4
|
+
*
|
|
5
|
+
* It replaces the dev-only scratch proofs (`_oneoff/auditR2/a78-mutants.mjs`, `a8-mutant.mjs`) so a package
|
|
6
|
+
* reader can reproduce them from the tarball. Every case is a SINGLE-SITE edit to a COPY of installer.js,
|
|
7
|
+
* injected through the guards' `INSTALLER_JS` env seam, and must redden the NAMED assertion:
|
|
8
|
+
*
|
|
9
|
+
* A7-version perturb the structured version the probe returns -> compat: "STRUCTURED field: version"
|
|
10
|
+
* A7-source make the structured source stop naming the service -> compat: "structured field naming the service"
|
|
11
|
+
* A8-failure inject one failure line in the backup path -> policy: "NOT reported as a failure (report-level"
|
|
12
|
+
* DL-label revert one label to English-only -> policy: "English-only label"
|
|
13
|
+
* DL-duplicate report the cleanup action a second time -> policy: "reported EXACTLY ONCE"
|
|
14
|
+
*
|
|
15
|
+
* Run: node tests/audit-installer-assertions.mutants.mjs
|
|
16
|
+
*/
|
|
17
|
+
import { mkdtempSync, copyFileSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
|
|
18
|
+
import { tmpdir } from 'node:os'
|
|
19
|
+
import { join } from 'node:path'
|
|
20
|
+
import { spawnSync } from 'node:child_process'
|
|
21
|
+
import { fileURLToPath } from 'node:url'
|
|
22
|
+
|
|
23
|
+
const HERE = fileURLToPath(new URL('./', import.meta.url))
|
|
24
|
+
const REPO = join(HERE, '..')
|
|
25
|
+
const SRC = join(REPO, 'installer.js')
|
|
26
|
+
const COMPAT = join(HERE, 'audit-installer-compat.test.mjs')
|
|
27
|
+
const POLICY = join(HERE, 'audit-installer-policy.test.mjs')
|
|
28
|
+
let passed = 0, failed = 0
|
|
29
|
+
const ok = (c, l, d) => { if (c) { passed++; console.log(' ok ' + l) } else { failed++; console.log(' FAIL ' + l + (d ? ' — ' + d : '')) } }
|
|
30
|
+
|
|
31
|
+
const ORIGIN = readFileSync(SRC, 'utf8')
|
|
32
|
+
const OUT_LINE = 'const out = { version: first.version, source: first.source, probes: probes.slice() }'
|
|
33
|
+
const CLEANUP_IF = 'if (removedFiles > 0 || removedDirs.length > 0 || cleanupNotes.length > 0) {'
|
|
34
|
+
const CASES = [
|
|
35
|
+
{ name: 'A7-version: the structured version is perturbed', guard: COMPAT, expect: 'STRUCTURED field: version',
|
|
36
|
+
edit: (s) => s.replace(OUT_LINE, "const out = { version: String(first.version) + '-mutated', source: first.source, probes: probes.slice() }") },
|
|
37
|
+
{ name: 'A7-source: the structured source stops naming the service', guard: COMPAT, expect: 'structured field naming the service',
|
|
38
|
+
edit: (s) => s.replace(OUT_LINE, "const out = { version: first.version, source: 'pm', probes: probes.slice() }") },
|
|
39
|
+
{ name: 'A8-failure: a failure line is injected in the backup path', guard: POLICY, expect: 'NOT reported as a failure (report-level',
|
|
40
|
+
edit: (s) => s.replace('const backupFailures = []', "const backupFailures = []\n logger?.warn?.('[dsh-vibe-math] 预设安装/更新失败:mutant injected failure line')") },
|
|
41
|
+
{ name: 'DL-label: one label is reverted to English-only', guard: POLICY, expect: 'English-only label',
|
|
42
|
+
edit: (s) => s.replace("'[dsh-vibe-math] 预设清理:", "'[dsh-vibe-math] preset cleanup: ") },
|
|
43
|
+
{ name: 'DL-duplicate: the cleanup action is reported twice', guard: POLICY, expect: 'reported EXACTLY ONCE',
|
|
44
|
+
edit: (s) => s.replace(CLEANUP_IF, CLEANUP_IF + "\n logger?.info?.('[dsh-vibe-math] 预设清理:再次报告(删除 ' + removedFiles + ' 个文件)')") },
|
|
45
|
+
]
|
|
46
|
+
for (const c of CASES) {
|
|
47
|
+
const mutated = c.edit(ORIGIN)
|
|
48
|
+
if (mutated === ORIGIN) { ok(false, c.name + ' — the single-site anchor applies', 'ANCHOR MISS'); continue }
|
|
49
|
+
const dir = mkdtempSync(join(tmpdir(), 'installer-assert-mut-'))
|
|
50
|
+
copyFileSync(SRC, join(dir, 'installer.js'))
|
|
51
|
+
writeFileSync(join(dir, 'installer.js'), mutated)
|
|
52
|
+
const r = spawnSync(process.execPath, [c.guard], { cwd: REPO, encoding: 'utf8', maxBuffer: 1 << 26, env: Object.assign({}, process.env, { INSTALLER_JS: join(dir, 'installer.js') }) })
|
|
53
|
+
const o = String(r.stdout || '') + String(r.stderr || '')
|
|
54
|
+
const failLines = o.split('\n').filter((l) => /^ - /.test(l))
|
|
55
|
+
const named = failLines.some((l) => l.indexOf(c.expect) !== -1)
|
|
56
|
+
const sum = (o.split('\n').filter((l) => /passed, \d+ failed/.test(l)).slice(-1)[0] || '').trim()
|
|
57
|
+
ok(r.status !== 0 && named, '★ ' + c.name + ' -> ' + sum, r.status === 0 ? 'the guard stayed GREEN' : (named ? '' : 'named red not found'))
|
|
58
|
+
if (!named) for (const l of failLines.slice(0, 3)) console.log(' ' + l.trim().slice(0, 140))
|
|
59
|
+
rmSync(dir, { recursive: true, force: true })
|
|
60
|
+
}
|
|
61
|
+
console.log('')
|
|
62
|
+
console.log('=== INSTALLER ASSERTION MUTANTS: ' + passed + ' red as required, ' + failed + ' problem(s) ===')
|
|
63
|
+
process.exit(failed === 0 ? 0 : 1)
|
|
@@ -19,7 +19,7 @@ import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync
|
|
|
19
19
|
import { tmpdir } from 'node:os'
|
|
20
20
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
21
21
|
import { join, resolve } from 'node:path'
|
|
22
|
-
import { satisfiesDshRange, dshVersionVerdict, PRESETS } from '../installer.js'
|
|
22
|
+
import { satisfiesDshRange, dshVersionVerdict, detectDshVersion, PRESETS } from '../installer.js'
|
|
23
23
|
|
|
24
24
|
const HERE = fileURLToPath(new URL('../', import.meta.url))
|
|
25
25
|
const pkg = JSON.parse(readFileSync(join(HERE, 'package.json'), 'utf8'))
|
|
@@ -196,13 +196,61 @@ console.log('=== 4. a pluginManager that never answers must not hold the row ===
|
|
|
196
196
|
// the timeout must NOT replace a working probe: a prompt service still supplies the version
|
|
197
197
|
const quick = run({ listBundles: async () => [{ name: '@deepseek-ai/dsh-base', version: '0.2.0-rc.2' }] })
|
|
198
198
|
await mod.apply(quick.ctx)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
199
|
+
// A7 (assertion quality): the version+source claim is asserted on the STRUCTURED result - a bare
|
|
200
|
+
// `logs.some(l => l.includes('0.2.0-rc.2'))` would also pass if the string appeared anywhere for any
|
|
201
|
+
// reason, and `l.includes('source')` would pass on the English word alone. The log keeps ONE
|
|
202
|
+
// shape-anchored check.
|
|
203
|
+
const probeQuick = (await import(pathToFileURL(INSTALLER_SRC).href + '?t=' + Date.now())).detectDshVersion || detectDshVersion
|
|
204
|
+
const detQuick = await probeQuick(quick.ctx)
|
|
205
|
+
ok(detQuick && detQuick.version === '0.2.0-rc.2',
|
|
206
|
+
'★ a prompt listBundles() still supplies the version (STRUCTURED field: version)', JSON.stringify(detQuick && { version: detQuick.version, source: detQuick.source }))
|
|
207
|
+
ok(!!detQuick && typeof detQuick.source === 'string' && detQuick.source.indexOf('pluginManager') === 0,
|
|
208
|
+
'★ ...and the winning SOURCE is a structured field naming the service (not the word "source" in a log line)', JSON.stringify(detQuick && detQuick.source))
|
|
209
|
+
ok(quick.logs.some((l) => /v?0\.2\.0-rc\.2/.test(l)),
|
|
210
|
+
'...and a log line SHAPE carries that version (shape-anchored, not a bare substring)')
|
|
202
211
|
rmSync(tmp, { recursive: true, force: true })
|
|
203
212
|
}
|
|
204
213
|
|
|
214
|
+
// Installer review (finding): several sources can report the host's version and "first available wins"
|
|
215
|
+
// hid a conflicting host state. The probe records EVERY value+source, keeps the first available
|
|
216
|
+
// authoritative (priority unchanged), and assembles `disagreement` ONLY when one exists.
|
|
217
|
+
{
|
|
218
|
+
const prev = process.env.DSH_VERSION
|
|
219
|
+
try {
|
|
220
|
+
// Import through INSTALLER_SRC (env-overridable) so a single-site mutant on a COPY of installer.js
|
|
221
|
+
// reaches these assertions - that is what makes the mutant below able to bite.
|
|
222
|
+
const mod = await import(pathToFileURL(INSTALLER_SRC).href + '?t=' + Date.now())
|
|
223
|
+
const probe = mod.detectDshVersion || detectDshVersion
|
|
224
|
+
process.env.DSH_VERSION = '9.9.9'
|
|
225
|
+
const disagreeing = { get: (n) => (n === 'pluginManager' ? { listBundles: async () => [{ name: '@deepseek-ai/dsh', version: '8.8.8' }] } : undefined) }
|
|
226
|
+
const d1 = await probe(disagreeing)
|
|
227
|
+
ok(d1 && d1.version === '9.9.9' && d1.source === 'DSH_VERSION', 'the FIRST available source stays authoritative (which version wins is unchanged)', JSON.stringify(d1))
|
|
228
|
+
ok(d1 && Array.isArray(d1.probes) && d1.probes.length >= 2 && d1.probes.some((p) => p.source.indexOf('pluginManager') === 0 && p.version === '8.8.8'),
|
|
229
|
+
'★ every probe value+source is recorded (nothing is silently dropped)', JSON.stringify(d1 && d1.probes))
|
|
230
|
+
ok(!!(d1 && d1.disagreement) && d1.disagreement.primary === 'DSH_VERSION' && d1.disagreement.conflicts.some((c) => c.version === '8.8.8'),
|
|
231
|
+
'★ DISAGREEING probes surface a disagreement slot naming the conflict', JSON.stringify(d1 && d1.disagreement))
|
|
232
|
+
const agreeing = { get: (n) => (n === 'pluginManager' ? { listBundles: async () => [{ name: '@deepseek-ai/dsh', version: '9.9.9' }] } : undefined) }
|
|
233
|
+
const d2 = await probe(agreeing)
|
|
234
|
+
ok(d2 && d2.version === '9.9.9' && !('disagreement' in d2), '★ AGREEING probes print NO disagreement slot (assemble-only-on-conflict)', JSON.stringify(d2))
|
|
235
|
+
// The `%s` placeholder that could reach the user: every installer log line must pre-concatenate.
|
|
236
|
+
const isrc = readFileSync(INSTALLER_SRC, 'utf8')
|
|
237
|
+
const printf = isrc.split('\n').filter((l) => /logger|console/.test(l) && /%[sd]/.test(l))
|
|
238
|
+
ok(printf.length === 0, '★ no installer log line carries a bare printf placeholder (%s/%d)', JSON.stringify(printf.slice(0, 2)))
|
|
239
|
+
} finally {
|
|
240
|
+
if (prev === undefined) delete process.env.DSH_VERSION; else process.env.DSH_VERSION = prev
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
205
244
|
console.log('')
|
|
245
|
+
// The self-check proves service/API SHAPE only - never runtime SEMANTICS. The caveat must be on the
|
|
246
|
+
// USER-VISIBLE pass line, so a passing self-check is not read as a compatibility claim.
|
|
247
|
+
{
|
|
248
|
+
const src = readFileSync(INSTALLER_SRC, 'utf8')
|
|
249
|
+
const line = src.split(/\r?\n/).find((l) => l.indexOf('自检通过') !== -1 && l.indexOf('logger') !== -1) || ''
|
|
250
|
+
ok(line.length > 0, 'the installer reports a passing host self-check')
|
|
251
|
+
ok(line.indexOf('形状') !== -1 && line.indexOf('行为') !== -1, '★ self-check caveat: the PASS line states that SHAPE is proven and BEHAVIOUR is not', JSON.stringify(line.slice(0, 90)))
|
|
252
|
+
}
|
|
253
|
+
|
|
206
254
|
console.log('=== INSTALLER COMPAT: ' + passed + ' passed, ' + failed + ' failed ===')
|
|
207
255
|
if (failed) { for (const f of failures) console.error(' - ' + f); process.exit(1) }
|
|
208
256
|
console.log('ALL GREEN')
|
|
@@ -79,7 +79,10 @@ async function applyFrom(pkgDir, home, logs) {
|
|
|
79
79
|
return logs
|
|
80
80
|
}
|
|
81
81
|
const sha = (buf) => createHash('sha256').update(buf).digest('hex')
|
|
82
|
-
|
|
82
|
+
// Label-tolerant on purpose: the installer's user-facing failure line is emitted by the logger helper
|
|
83
|
+
// (not console.*) and its label is localised; the FACT asserted here is that a failure line exists.
|
|
84
|
+
const FAILURE_LINE = /预设安装\/更新失败|preset install\/update failed/
|
|
85
|
+
const failedLog = (logs) => logs.filter((l) => FAILURE_LINE.test(l))
|
|
83
86
|
|
|
84
87
|
const tmp = mkdtempSync(join(tmpdir(), 'vibe-installer-policy-'))
|
|
85
88
|
const home = join(tmp, 'dshhome')
|
|
@@ -153,7 +156,9 @@ let userJsBytes = null
|
|
|
153
156
|
const state = JSON.parse(readFileSync(stateFile, 'utf8'))
|
|
154
157
|
ok(state.version === '2.0.0', 'the state records the new version', String(state.version))
|
|
155
158
|
ok(logs.some((l) => l.includes('.vibe-math-backup')), 'the log names the backup location')
|
|
156
|
-
|
|
159
|
+
// A8: the FACT (a version change backs up the user's edit) is asserted structurally above (backup path
|
|
160
|
+
// + byte equality). What remains is guidance wording, so this is the ONE labelled wording smoke check.
|
|
161
|
+
ok(logs.some((l) => /复制一份/.test(l)), '[wording smoke check — the ONE allowed] the log tells the user how to customize a preset properly')
|
|
157
162
|
ok(failedLog(logs).length === 0, 'apply() swallowed no failure', failedLog(logs)[0])
|
|
158
163
|
}
|
|
159
164
|
|
|
@@ -164,7 +169,13 @@ console.log('=== 4. a deleted file at the SAME version: restored, nothing backed
|
|
|
164
169
|
ok(existsSync(at(PRESETS[2], 'agent.cordis.yml')), 'a deleted managed file is restored')
|
|
165
170
|
ok(isCopyOf(PRESETS[2], 'agent.cordis.yml'), 'the restored file is byte-identical to the shipped one')
|
|
166
171
|
ok(!existsSync(join(backupRoot, '2.0.0')), 'a run that replaced nothing creates no backup directory')
|
|
167
|
-
|
|
172
|
+
// Guard-sync: the installer's user-visible stream is Chinese by convention (one language, see the
|
|
173
|
+
// comment at the cleanup line), so these assert the FACT (an action keyword + the count) rather than
|
|
174
|
+
// an English word.
|
|
175
|
+
// "one action, one line": the restore must be reported, and reported EXACTLY ONCE (a second report of the
|
|
176
|
+
// same action is the defect the duplicate-log review was about).
|
|
177
|
+
const restoreLines = logs.filter((l) => /\u8fd8\u539f\u4e86\s*\d+\s*\u4e2a\u7f3a\u5931\u7684\u9884\u8bbe\u6587\u4ef6/.test(l))
|
|
178
|
+
ok(restoreLines.length === 1, '★ the restore is reported EXACTLY ONCE (one action, one line)', JSON.stringify(restoreLines.map((l) => l.slice(0, 60))))
|
|
168
179
|
ok(failedLog(logs).length === 0, 'apply() swallowed no failure', failedLog(logs)[0])
|
|
169
180
|
}
|
|
170
181
|
|
|
@@ -182,7 +193,8 @@ console.log('=== 5. a preset this bundle no longer ships ===')
|
|
|
182
193
|
ok(!existsSync(join(presetRoot, 'vibe-math-v1', 'legacy.js')), 'a package-owned file of a dropped preset is removed')
|
|
183
194
|
ok(!existsSync(join(presetRoot, 'vibe-math-v1')), 'the dropped preset directory is removed once empty')
|
|
184
195
|
ok(existsSync(join(presetRoot, 'vibe-math-v0', 'mine.js')), 'a file recorded as user-owned in a dropped preset is kept')
|
|
185
|
-
|
|
196
|
+
const cleanupLines = logs.filter((l) => /\u9884\u8bbe\u6e05\u7406/.test(l) && /\u5220\u9664\s*\d+\s*\u4e2a\u6587\u4ef6/.test(l))
|
|
197
|
+
ok(cleanupLines.length === 1, '★ the cleanup is reported EXACTLY ONCE (one action, one line)', JSON.stringify(cleanupLines.map((l) => l.slice(0, 60))))
|
|
186
198
|
ok(failedLog(logs).length === 0, 'apply() swallowed no failure', failedLog(logs)[0])
|
|
187
199
|
}
|
|
188
200
|
|
|
@@ -224,7 +236,9 @@ console.log('=== 7. an unreadable package manifest replaces nothing (and self-he
|
|
|
224
236
|
ok(readFileSync(at(PRESETS[1], 'preset.yml')).equals(readFileSync(shipped(PRESETS[1], 'preset.yml', pkgC))),
|
|
225
237
|
'the next readable run replaces the drift as a normal version change')
|
|
226
238
|
ok(readFileSync(earlier, 'utf8') === 'EARLIER COPY\n', 'a backup that already exists for that version is kept byte-for-byte (the earliest copy wins)')
|
|
227
|
-
|
|
239
|
+
// A8: report-level, not wording-level - a pre-existing backup must not be counted as a failure by the
|
|
240
|
+
// guard's own failure-line helper (which matches the localised failure label, either spelling).
|
|
241
|
+
ok(failedLog(logs2).length === 0, '★ a pre-existing backup for the same version is NOT reported as a failure (report-level check, not a wording check)', failedLog(logs2)[0])
|
|
228
242
|
ok(JSON.parse(readFileSync(stateFile, 'utf8')).version === '3.0.0', 'the state records the new version')
|
|
229
243
|
|
|
230
244
|
// the backup directory must never be readable as a preset: DSH only accepts [a-z0-9][a-z0-9-]* ids
|
|
@@ -255,7 +269,7 @@ console.log('=== 9. a recorded version NEWER than this package (a downgrade) say
|
|
|
255
269
|
const backup = join(backupRoot, '3.0.0', PRESETS[0].dst, 'preset.yml')
|
|
256
270
|
ok(existsSync(backup) && readFileSync(backup).equals(newerBytes), 'the replaced newer bytes are backed up under the version they came from')
|
|
257
271
|
ok(JSON.parse(readFileSync(stateFile, 'utf8')).version === '2.0.0', 'the state records the version that is actually installed now')
|
|
258
|
-
ok(
|
|
272
|
+
ok(failedLog(logs).length === 0, 'apply() swallowed no failure')
|
|
259
273
|
}
|
|
260
274
|
|
|
261
275
|
// ============================================================================================
|
|
@@ -302,7 +316,7 @@ console.log('=== 10. preset writes are ATOMIC: a failed write cannot touch the d
|
|
|
302
316
|
const logs = await applyFrom(pkgB, H.home, [])
|
|
303
317
|
ok(readText(H.at(target.p, target.f)) === userBytes.toString(),
|
|
304
318
|
'a failed preset write leaves the destination byte-for-byte intact (tmp+rename, never in place)')
|
|
305
|
-
ok(logs
|
|
319
|
+
ok(failedLog(logs).length > 0,
|
|
306
320
|
'...and the failure is reported instead of being swallowed', logs.filter((l) => l.includes('failed')).join(' | ').slice(0, 200))
|
|
307
321
|
ok((readJson(H.state) || {}).version === '1.0.0',
|
|
308
322
|
'the state still describes the bytes that are actually on disk, so the next boot self-heals')
|
|
@@ -414,7 +428,7 @@ console.log('=== 15. a failed state write is reported (not swallowed), and does
|
|
|
414
428
|
const logs = await applyFrom(pkgA, H.home, [])
|
|
415
429
|
ok(logs.some((l) => l.includes('状态文件写入失败')),
|
|
416
430
|
'a failed state write is REPORTED (it used to fail silently)', logs.filter((l) => l.includes('写入失败')).join(' | ').slice(0, 160))
|
|
417
|
-
ok(
|
|
431
|
+
ok(failedLog(logs).length === 0, '...without aborting the preset step')
|
|
418
432
|
ok((readJson(H.state) || {}).version === '1.0.0',
|
|
419
433
|
'the previous state file is still parseable (tmp+rename left no half-written file)')
|
|
420
434
|
rmSync(H.state + '.' + process.pid + '.0.tmp', { recursive: true, force: true })
|
|
@@ -478,6 +492,25 @@ console.log('=== 16b. a stale file whose BACKUP FAILS is kept, not deleted unbac
|
|
|
478
492
|
rmSync(join(H.root, '.vibe-math-backup'), { force: true })
|
|
479
493
|
}
|
|
480
494
|
|
|
495
|
+
console.log('=== 16. one action, one language: no user-facing line carries an English-only label ===')
|
|
496
|
+
{
|
|
497
|
+
// The convention (docs/AUDIT-CHECKLIST.md): installer user-facing output is Chinese; an English-only
|
|
498
|
+
// LABEL is a defect (an English domain word inside a Chinese label, like `preset 声明方式`, is fine).
|
|
499
|
+
// This is a CONVENTION sweep, not a wording pin: any CJK-bearing label passes.
|
|
500
|
+
const src = readFileSync(INSTALLER_SRC, 'utf8')
|
|
501
|
+
const labelOf = (l) => { const m = /\[dsh-vibe-math\]\s*([^::]*)/.exec(l); return m ? m[1].trim() : '' }
|
|
502
|
+
const asciiOnly = (lab) => lab.length > 0 && /^[\x20-\x7e]+$/.test(lab)
|
|
503
|
+
const sites = src.split(/\r?\n/).filter((l) => /logger\?\.(info|warn|error)\?\.\(/.test(l))
|
|
504
|
+
ok(sites.length > 0, 'the installer has user-facing log sites to sweep (found ' + sites.length + ')')
|
|
505
|
+
const badSites = sites.filter((l) => asciiOnly(labelOf(l)))
|
|
506
|
+
ok(badSites.length === 0, '★ no user-facing log SITE carries an English-only label (installer output is Chinese)',
|
|
507
|
+
JSON.stringify(badSites.map((l) => l.trim().slice(0, 70))))
|
|
508
|
+
const runtimeLogs = await applyFrom(pkgB, home, [])
|
|
509
|
+
const badRuntime = runtimeLogs.filter((l) => asciiOnly(labelOf(l)))
|
|
510
|
+
ok(badRuntime.length === 0, '★ ...and no CAPTURED line does either (code and convention agree at runtime)',
|
|
511
|
+
JSON.stringify(badRuntime.slice(0, 2)))
|
|
512
|
+
}
|
|
513
|
+
|
|
481
514
|
rmSync(tmp, { recursive: true, force: true })
|
|
482
515
|
|
|
483
516
|
console.log('')
|
|
@@ -108,14 +108,48 @@ for (const P of PRESETS) {
|
|
|
108
108
|
? "registerTool('vibe_math_set_params'"
|
|
109
109
|
: (P.dir === 'vibe-math-v4' ? "registerTool('vibe_v4_set'" : "registerTool('vibe_v5_set'")
|
|
110
110
|
{
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
111
|
+
// §9.7 ㉓/㉜: EVERY registration site is checked, not just the first. This preset class is defined by
|
|
112
|
+
// having TWO independent registration layers (session `makeSession` + `apply`, each with its own
|
|
113
|
+
// `objParams`), so an audit that inspects only the first cannot see the second diverge — the F-B defect
|
|
114
|
+
// class. If a site ever legitimately carries a narrower schema, record that difference ON THAT SITE
|
|
115
|
+
// explicitly instead of narrowing the loop back to "first site only".
|
|
116
|
+
const sites = []
|
|
117
|
+
for (let at = js.indexOf(SET_TOOL); at !== -1; at = js.indexOf(SET_TOOL, at + 1)) sites.push(at)
|
|
118
|
+
ok(sites.length >= 1, tag + 'the set tool is registered', 'sites=' + sites.length)
|
|
119
|
+
// F-B (docs/parameter-schema.md): a schema argument has two shapes —
|
|
120
|
+
// objParams({ ...literal... }) the historical hand-written table
|
|
121
|
+
// objParams(paramProps()) derived from the SINGLE source PARAM_SCHEMA; the machine key set is
|
|
122
|
+
// pinned by PARAM_PROPS_KEYS (+ PARAM_PROPS_EXTRA)
|
|
123
|
+
// The derived form has NO `{` after `objParams(`, so a naive slice grabbed unrelated text (331-char
|
|
124
|
+
// fragment). Resolve it to the single-source key list, searched FILE-WIDE (that table is module scope).
|
|
125
|
+
const derivedKeys = (() => {
|
|
126
|
+
const km = /const PARAM_PROPS_KEYS = \[([^\]]*)\]/.exec(js)
|
|
127
|
+
if (!km) return null
|
|
128
|
+
const keys = km[1].split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean)
|
|
129
|
+
const em = /const PARAM_PROPS_EXTRA = \{([\s\S]*?)\n\s*\}/.exec(js)
|
|
130
|
+
const extra = em ? [...em[1].matchAll(/^\s*([A-Za-z_$][\w$]*)\s*:/gm)].map((m) => m[1]) : []
|
|
131
|
+
return [...new Set(keys.concat(extra))]
|
|
132
|
+
})()
|
|
133
|
+
sites.forEach((idx, n) => {
|
|
134
|
+
const next = js.indexOf('registerTool(', idx + 1)
|
|
135
|
+
const callText = js.slice(idx, next === -1 ? idx + 4000 : next)
|
|
136
|
+
const oi = callText.lastIndexOf('objParams(')
|
|
137
|
+
const setSchema = oi === -1 ? '' : callText.slice(oi)
|
|
138
|
+
const at = oi === -1 ? -1 : oi + 'objParams('.length
|
|
139
|
+
const open = at === -1 ? -1 : callText.indexOf('{', at)
|
|
140
|
+
const close = at === -1 ? -1 : callText.indexOf(')', at)
|
|
141
|
+
const literalForm = open !== -1 && (close === -1 || open < close)
|
|
142
|
+
ok(literalForm ? setSchema.length > 0 : !!derivedKeys,
|
|
143
|
+
tag + 'site#' + n + ' takes an objParams(...) schema argument (literal table or single-source derivation)',
|
|
144
|
+
literalForm ? 'schema len ' + setSchema.length : 'derived keys ' + (derivedKeys ? derivedKeys.length : 0))
|
|
145
|
+
for (const k of SIX) {
|
|
146
|
+
const present = literalForm
|
|
147
|
+
? new RegExp('(^|[{,\\s])' + k + '\\s*:').test(setSchema)
|
|
148
|
+
: !!derivedKeys && derivedKeys.includes(k)
|
|
149
|
+
ok(present, tag + 'site#' + n + ' set schema has the property ' + k + ' (property-level, not prose)',
|
|
150
|
+
literalForm ? 'schema len ' + setSchema.length : 'derived keys ' + (derivedKeys ? derivedKeys.length : 0))
|
|
151
|
+
}
|
|
152
|
+
})
|
|
119
153
|
}
|
|
120
154
|
if (P.dir === 'vibe-math-v2' || P.dir === 'vibe-math-v3') {
|
|
121
155
|
const schema = arrayBlock(js, 'const PARAM_SCHEMA')
|
|
@@ -238,6 +272,28 @@ for (const f of ['README.md', 'README.en.md']) {
|
|
|
238
272
|
ok(doc.indexOf('Octave / Julia 未安装') !== -1 && doc.indexOf('VERIFY') !== -1 && doc.indexOf('无法强制禁网') !== -1, 'docs §6 keeps the still-unverified set (no Octave/Julia, commercial VERIFY, no network/write enforcement)')
|
|
239
273
|
ok(doc.indexOf('只通过假 subprocess seam 验证') === -1, 'docs §6 no longer claims engine execution was verified ONLY through the fake seam (stale claim removed)')
|
|
240
274
|
ok(doc.indexOf('串行') !== -1 && doc.indexOf('跨进程') !== -1, 'docs §6 states the in-process serialisation and the cross-process limitation')
|
|
275
|
+
// ── P1/6.1: the v4 frozen participant set is documented as the ATOMIC triple, and the deprecated
|
|
276
|
+
// alias is described as an alias - both in the doc AND verifiable in the preset source.
|
|
277
|
+
{
|
|
278
|
+
const v4 = existsSync(join(ROOT, 'vibe-math-v4/vibe-math-v4.js')) ? read('vibe-math-v4/vibe-math-v4.js') : ''
|
|
279
|
+
const atomic = /frozen:\s*\(function\(sn\)\{\s*return \{version: sn\?sn\.rosterVersion:null, participants: sn\?sn\.rosterSnapshot:null, kind:/.test(v4)
|
|
280
|
+
ok(atomic, 'v4 exposes the frozen participant set as ONE atomic object {version, participants, kind}')
|
|
281
|
+
const alias = /frozenParticipants:\s*\(activeRosterSnapshot\(\)\|\|\{\}\)\.rosterSnapshot\|\|null/.test(v4) && /deprecated/.test(v4)
|
|
282
|
+
ok(alias, 'v4 keeps `frozenParticipants` only as a deprecated alias of frozen.participants')
|
|
283
|
+
ok(doc.indexOf('frozen') !== -1 && doc.indexOf('deprecated') !== -1 && doc.indexOf('原子') !== -1,
|
|
284
|
+
'docs document the atomic `frozen{version,participants,kind}` and mark the alias deprecated')
|
|
285
|
+
}
|
|
286
|
+
// ── P1/6.2: the evidence-field list cannot drift from the code again - every timeout evidence field
|
|
287
|
+
// the module produces must be named in the doc, and vice versa for the two receipt-only ones.
|
|
288
|
+
{
|
|
289
|
+
const mod = readFileSync(join(ROOT, 'vibe-math-v2/math-computation.js'), 'utf8')
|
|
290
|
+
const timeoutFields = ['partialStdout', 'partialStderr', 'stdout', 'stderr', 'exit', 'timedOut']
|
|
291
|
+
const docMissing = timeoutFields.filter((f) => doc.indexOf(f) === -1)
|
|
292
|
+
ok(docMissing.length === 0, 'docs name every MATH_TIMEOUT evidence field', 'missing in doc: ' + JSON.stringify(docMissing))
|
|
293
|
+
const codeMissing = ['partialStdout', 'partialStderr'].filter((f) => mod.indexOf(f) === -1)
|
|
294
|
+
ok(codeMissing.length === 0, 'the doc-sync check is anchored in the code too (partialStdout/partialStderr exist in the module)',
|
|
295
|
+
'missing in code: ' + JSON.stringify(codeMissing))
|
|
296
|
+
}
|
|
241
297
|
}
|
|
242
298
|
|
|
243
299
|
console.log('')
|