dsh-vibe-math 2.8.0 → 2.8.2
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 +2 -2
- package/README.md +2 -2
- package/docs/AUDIT-CHECKLIST.md +8 -3
- package/docs/maintenance-policy.md +31 -0
- package/docs/release-notes/README.en.md +2 -0
- package/docs/release-notes/README.md +2 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.1.en.md +49 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.1.md +49 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.2.en.md +47 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.2.md +47 -0
- package/docs/release-playbook.md +14 -0
- package/docs/slv-playbook.md +5 -1
- package/docs/test-timing.md +1 -1
- package/package.json +11 -3
- package/scripts/release-check.mjs +181 -0
- package/tests/e2e-v5-round2.test.mjs +14 -0
- package/tests/release-check.mutants.mjs +78 -0
- package/tests/v4-final-paper.mutants.mjs +8 -0
- package/tests/v4-final-paper.test.mjs +628 -0
- package/tests/v5-institute-fixes.mutants.mjs +23 -6
- package/vibe-math-v4/vibe-math-v4.js +30 -0
- package/vibe-math-v5/vibe-math-v5.js +27 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* RELEASE CHECK (task-8 follow-up) — the checks a maintainer had to run BY HAND for 2.8.0, in one command.
|
|
4
|
+
*
|
|
5
|
+
* `docs/release-playbook.md` lists them as prose across §1/§2/§6; this script makes them mechanical, so the
|
|
6
|
+
* next release cannot ship a drifted version, a missing release note, an unregistered note, a manifest that
|
|
7
|
+
* lies about the tarball, or CRLF line endings inside the package.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/release-check.mjs # static checks + a real `npm pack` + tarball EOL scan
|
|
11
|
+
* node scripts/release-check.mjs --skip-pack # static checks only (no packing, no temp extraction)
|
|
12
|
+
* node scripts/release-check.mjs --registry # ALSO compare the published shasum (post-publish only)
|
|
13
|
+
* node scripts/release-check.mjs --self-test # prove the predicates below are not vacuous
|
|
14
|
+
*
|
|
15
|
+
* Exit 0 = every check passed. Exit 1 = at least one FAIL (printed with a `FAIL ` prefix).
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from 'node:fs'
|
|
18
|
+
import { execFileSync } from 'node:child_process'
|
|
19
|
+
import { createHash } from 'node:crypto'
|
|
20
|
+
import { dirname, join } from 'node:path'
|
|
21
|
+
import { fileURLToPath } from 'node:url'
|
|
22
|
+
|
|
23
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
24
|
+
const REPO = join(HERE, '..')
|
|
25
|
+
const TMP = process.env.RELEASE_CHECK_TMP || join(process.env.TEMP || process.env.TMPDIR || '/tmp', 'release-check')
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------- pure predicates (fixture-testable)
|
|
28
|
+
/** The version must agree in all three places the market guard reads (playbook §1). */
|
|
29
|
+
export function versionProblems({ pkgVersion, lockTop, lockEmpty }) {
|
|
30
|
+
const out = []
|
|
31
|
+
if (!pkgVersion) out.push('package.json has no version')
|
|
32
|
+
if (lockTop !== pkgVersion) out.push('package-lock.json top-level version ' + lockTop + ' != package.json ' + pkgVersion)
|
|
33
|
+
if (lockEmpty !== pkgVersion) out.push('package-lock.json packages[""].version ' + lockEmpty + ' != package.json ' + pkgVersion)
|
|
34
|
+
return out
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Release notes must carry the template's sections, in the template's order (playbook §1 / TEMPLATE.md). */
|
|
38
|
+
export const SECTIONS = {
|
|
39
|
+
zh: ['## 概述', '## 新增', '## 变更', '## 修复', '## 兼容性与迁移', '## 已知限制', '## 验证方式', '## 依赖'],
|
|
40
|
+
en: ['## Overview', '## Added', '## Changed', '## Fixed', '## Compatibility & Migration', '## Known Limitations', '## Verification', '## Dependencies'],
|
|
41
|
+
}
|
|
42
|
+
export function sectionProblems(headings, lang) {
|
|
43
|
+
const want = SECTIONS[lang]
|
|
44
|
+
if (!want) return ['unknown language ' + lang]
|
|
45
|
+
const got = (headings || []).map((h) => String(h).trim())
|
|
46
|
+
const out = []
|
|
47
|
+
if (got.length !== want.length) out.push('expected ' + want.length + ' sections, found ' + got.length)
|
|
48
|
+
for (let i = 0; i < want.length; i++) if (got[i] !== want[i]) out.push('section ' + (i + 1) + ' is ' + JSON.stringify(got[i]) + ', expected ' + JSON.stringify(want[i]))
|
|
49
|
+
return out
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Every declared file must exist in the packed tarball (playbook §2/§6.2). */
|
|
53
|
+
export function manifestProblems(declared, packed) {
|
|
54
|
+
const have = new Set(packed || [])
|
|
55
|
+
return (declared || []).filter((f) => !have.has(f)).map((f) => 'declared but missing from the tarball: ' + f)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Nothing may ship that is repository-only tooling (playbook §6.4). */
|
|
59
|
+
export function devOnlyProblems(packed, devOnly = ['.gitattributes']) {
|
|
60
|
+
const have = new Set(packed || [])
|
|
61
|
+
return devOnly.filter((f) => have.has(f)).map((f) => 'repository-only file shipped in the tarball: ' + f)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Packaged text must be LF-only so the tarball is byte-reproducible (playbook §7). */
|
|
65
|
+
export function crlfProblems(files) {
|
|
66
|
+
const out = []
|
|
67
|
+
for (const { path, crlf } of files || []) if (Number(crlf) > 0) out.push('CRLF in the packaged file: ' + path + ' (' + crlf + ' lines)')
|
|
68
|
+
return out
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** `compatNote` is user-visible and must name the version being released (playbook §2). */
|
|
72
|
+
export function compatNoteProblems(compatNote, version) {
|
|
73
|
+
if (!compatNote) return ['dsh.compatNote is missing']
|
|
74
|
+
return String(compatNote).includes(version) ? [] : ['dsh.compatNote does not mention the current version ' + version]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------- self-test (the family mutates these)
|
|
78
|
+
if (process.argv.includes('--self-test')) {
|
|
79
|
+
const failures = []
|
|
80
|
+
const check = (cond, label) => { console.log((cond ? 'PASS ' : 'FAIL - ') + label); if (!cond) failures.push(label) }
|
|
81
|
+
|
|
82
|
+
check(versionProblems({ pkgVersion: '9.9.9', lockTop: '9.9.9', lockEmpty: '9.9.9' }).length === 0,
|
|
83
|
+
'RELEASE-CHECK: a consistent version triple passes')
|
|
84
|
+
check(versionProblems({ pkgVersion: '9.9.9', lockTop: '9.9.8', lockEmpty: '9.9.9' }).some((p) => /top-level/.test(p)),
|
|
85
|
+
'RELEASE-CHECK: a drifted lock top-level version is caught')
|
|
86
|
+
check(versionProblems({ pkgVersion: '9.9.9', lockTop: '9.9.9', lockEmpty: '9.9.8' }).some((p) => /packages/.test(p)),
|
|
87
|
+
'RELEASE-CHECK: a drifted packages[""] version is caught')
|
|
88
|
+
|
|
89
|
+
check(sectionProblems(SECTIONS.zh, 'zh').length === 0 && sectionProblems(SECTIONS.en, 'en').length === 0,
|
|
90
|
+
'RELEASE-CHECK: the canonical section lists pass their own check')
|
|
91
|
+
check(sectionProblems(['## 概述', '## 新增'], 'zh').some((p) => /expected 8 sections/.test(p)),
|
|
92
|
+
'RELEASE-CHECK: a truncated note is caught by its section count')
|
|
93
|
+
check(sectionProblems([...SECTIONS.en].reverse(), 'en').length > 0, 'RELEASE-CHECK: a reordered note is caught')
|
|
94
|
+
|
|
95
|
+
check(manifestProblems(['a.js', 'b.md'], ['a.js', 'b.md', 'package.json']).length === 0, 'RELEASE-CHECK: a fully packed manifest passes')
|
|
96
|
+
check(manifestProblems(['a.js', 'gone.md'], ['a.js']).length === 1, 'RELEASE-CHECK: a declared-but-missing file is caught')
|
|
97
|
+
|
|
98
|
+
check(devOnlyProblems(['README.md'], ['.gitattributes']).length === 0, 'RELEASE-CHECK: repository-only tooling stays out')
|
|
99
|
+
check(devOnlyProblems(['.gitattributes'], ['.gitattributes']).length === 1, 'RELEASE-CHECK: a shipped repository-only file is caught')
|
|
100
|
+
|
|
101
|
+
check(crlfProblems([{ path: 'a.js', crlf: 0 }]).length === 0, 'RELEASE-CHECK: LF-only content passes')
|
|
102
|
+
check(crlfProblems([{ path: 'a.js', crlf: 3 }]).length === 1, 'RELEASE-CHECK: CRLF inside the package is caught')
|
|
103
|
+
|
|
104
|
+
check(compatNoteProblems('v9.9.9 功能版:…', '9.9.9').length === 0, 'RELEASE-CHECK: a compatNote naming the version passes')
|
|
105
|
+
check(compatNoteProblems('v9.9.8 补丁版:…', '9.9.9').length === 1, 'RELEASE-CHECK: a stale compatNote is caught')
|
|
106
|
+
|
|
107
|
+
console.log('release-check self-test: ' + (failures.length ? failures.length + ' failure(s)' : 'all checks passed'))
|
|
108
|
+
process.exit(failures.length ? 1 : 0)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------- the real run
|
|
112
|
+
const argv = process.argv.slice(2)
|
|
113
|
+
const has = (n) => argv.includes('--' + n)
|
|
114
|
+
const problems = []
|
|
115
|
+
const fail = (p) => { for (const x of p) problems.push(x) }
|
|
116
|
+
const readJson = (rel) => JSON.parse(readFileSync(join(REPO, rel), 'utf8'))
|
|
117
|
+
|
|
118
|
+
const pkg = readJson('package.json')
|
|
119
|
+
const lock = readJson('package-lock.json')
|
|
120
|
+
const version = pkg.version
|
|
121
|
+
console.log('release-check: ' + pkg.name + '@' + version)
|
|
122
|
+
|
|
123
|
+
fail(versionProblems({ pkgVersion: version, lockTop: lock.version, lockEmpty: ((lock.packages || {})[''] || {}).version }))
|
|
124
|
+
fail(compatNoteProblems((pkg.dsh || {}).compatNote, version))
|
|
125
|
+
|
|
126
|
+
const notes = { zh: 'docs/release-notes/RELEASE-NOTES-' + version + '.md', en: 'docs/release-notes/RELEASE-NOTES-' + version + '.en.md' }
|
|
127
|
+
for (const [lang, rel] of Object.entries(notes)) {
|
|
128
|
+
if (!existsSync(join(REPO, rel))) { problems.push('missing release note (' + lang + '): ' + rel); continue }
|
|
129
|
+
const text = readFileSync(join(REPO, rel), 'utf8')
|
|
130
|
+
fail(sectionProblems((text.match(/^## .*$/gm) || []), lang).map((p) => rel + ': ' + p))
|
|
131
|
+
if (!(pkg.files || []).includes(rel)) problems.push('release note not listed in package.json#files: ' + rel)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (has('skip-pack')) {
|
|
135
|
+
console.log('release-check: --skip-pack, skipping the tarball checks')
|
|
136
|
+
} else {
|
|
137
|
+
rmSync(TMP, { recursive: true, force: true })
|
|
138
|
+
mkdirSync(TMP, { recursive: true })
|
|
139
|
+
const out = execFileSync('npm', ['pack', '--dry-run', '--json'], { cwd: REPO, encoding: 'utf8', shell: true })
|
|
140
|
+
const packed = JSON.parse(out)[0].files.map((f) => f.path)
|
|
141
|
+
console.log('release-check: tarball would contain ' + packed.length + ' files; declared ' + (pkg.files || []).length)
|
|
142
|
+
fail(manifestProblems(pkg.files, packed))
|
|
143
|
+
fail(devOnlyProblems(packed, ['.gitattributes']))
|
|
144
|
+
|
|
145
|
+
// Real pack + extract, so the EOL check reads the bytes a user would download.
|
|
146
|
+
const tgzName = String(execFileSync('npm', ['pack', '--pack-destination', TMP], { cwd: REPO, encoding: 'utf8', shell: true })).trim().split(/\r?\n/).pop()
|
|
147
|
+
const tgz = join(TMP, tgzName)
|
|
148
|
+
if (!existsSync(tgz)) {
|
|
149
|
+
problems.push('npm pack produced no tarball at ' + tgz)
|
|
150
|
+
} else {
|
|
151
|
+
execFileSync('tar', ['-xzf', tgz, '-C', TMP], { stdio: 'pipe' })
|
|
152
|
+
const root = join(TMP, 'package')
|
|
153
|
+
const eol = []
|
|
154
|
+
const walk = (d, prefix) => {
|
|
155
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
156
|
+
const p = join(d, e.name)
|
|
157
|
+
if (e.isDirectory()) { walk(p, prefix + e.name + '/'); continue }
|
|
158
|
+
if (!/\.(js|mjs|cjs|json|md|yml|yaml|txt|svg)$/.test(e.name)) continue // binaries are not text
|
|
159
|
+
const text = readFileSync(p, 'utf8')
|
|
160
|
+
eol.push({ path: prefix + e.name, crlf: (text.match(/\r\n/g) || []).length })
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
walk(root, '')
|
|
164
|
+
fail(crlfProblems(eol))
|
|
165
|
+
const sha1 = createHash('sha1').update(readFileSync(tgz)).digest('hex')
|
|
166
|
+
console.log('release-check: local tarball sha1 = ' + sha1 + ' (' + eol.length + ' text files scanned for CRLF)')
|
|
167
|
+
if (has('registry')) {
|
|
168
|
+
try {
|
|
169
|
+
const published = String(execFileSync('npm', ['view', pkg.name + '@' + version, 'dist.shasum'], { cwd: REPO, encoding: 'utf8', shell: true })).trim()
|
|
170
|
+
if (published !== sha1) problems.push('registry dist.shasum ' + published + ' != local tarball sha1 ' + sha1)
|
|
171
|
+
else console.log('release-check: registry shasum matches the local tarball (byte-reproducible)')
|
|
172
|
+
} catch (e) { problems.push('registry lookup failed: ' + String((e && e.message) || e).slice(0, 120)) }
|
|
173
|
+
}
|
|
174
|
+
rmSync(TMP, { recursive: true, force: true })
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log('')
|
|
179
|
+
for (const p of problems) console.log('FAIL ' + p)
|
|
180
|
+
console.log(problems.length ? 'release-check: ' + problems.length + ' problem(s)' : 'release-check: ALL CHECKS PASSED')
|
|
181
|
+
process.exit(problems.length ? 1 : 0)
|
|
@@ -1299,6 +1299,13 @@ console.log('\n[24] manual paper, office editor: parts → cross-review → cons
|
|
|
1299
1299
|
console.log(' [task-9] v5 detection evidence: compile=' + String(meta && meta.compile && meta.compile.status) + ' triedPaths=' + JSON.stringify((meta && meta.compile && meta.compile.triedPaths) || null))
|
|
1300
1300
|
assert(Array.isArray(meta && meta.compile && meta.compile.triedPaths) && meta.compile.triedPaths.length > 0,
|
|
1301
1301
|
'★ [task-9] the durable meta records the probed TeX paths (triedPaths)')
|
|
1302
|
+
// task-18 (v4 got this in 2.8.1): the DELIVERED artifacts must state why no PDF was produced — the body is
|
|
1303
|
+
// composed before the compile runs, so the note is written afterwards, exactly once each (idempotent).
|
|
1304
|
+
// The note follows paperLanguage, so the check is language-agnostic and still requires EXACTLY one.
|
|
1305
|
+
const noteHits = (t) => t.split('本机未检测到 LaTeX 引擎:只产出 tex+md').length - 1
|
|
1306
|
+
+ t.split('No LaTeX engine detected: tex+md only').length - 1
|
|
1307
|
+
assert(noteHits(md) === 1 && noteHits(tex) === 1,
|
|
1308
|
+
'★ [task-18/v5] the DELIVERED paper.md and paper.tex state that no engine was detected (once each; md=' + noteHits(md) + ' tex=' + noteHits(tex) + ')')
|
|
1302
1309
|
const warnText = (meta && Array.isArray(meta.warnings) ? meta.warnings : []).join(' | ')
|
|
1303
1310
|
assert(warnText.indexOf('只交付 paper.tex 与 paper.md。') !== -1 && warnText.indexOf('已探测 PATH 与文档化的常见 TeX 根;可用 paperLatexCommand 指定绝对路径') !== -1,
|
|
1304
1311
|
'★ [task-9] the not-detected warning keeps its tail AND says where it looked and how to pin an engine')
|
|
@@ -1437,6 +1444,13 @@ console.log('\n[27] fake LaTeX compiler: repair path and persistent-failure degr
|
|
|
1437
1444
|
const metap = JSON.parse(readFileSync(join(dp, 'paper.meta.json'), 'utf8'))
|
|
1438
1445
|
assert(metap.compile.status === 'not-detected',
|
|
1439
1446
|
'★ [task-10/v5] an unresolvable explicit paperLatexCommand degrades (never falls through to another engine) — got ' + JSON.stringify(metap.compile.status))
|
|
1447
|
+
// task-17 (found on a real host with 2.8.1): the explicit value must be probed ALONE. Probing it as a bare
|
|
1448
|
+
// command name let the known-root stage build impossible paths like `<MiKTeX root>/Z:/…/xelatex.exe`, which
|
|
1449
|
+
// made `triedPaths` — the very field that says WHERE it looked — report paths that cannot exist.
|
|
1450
|
+
const tp = metap.compile.triedPaths || []
|
|
1451
|
+
const joined = tp.filter((p) => /[\\/][A-Za-z]:[\\/]/.test(String(p).replace(/^[A-Za-z]:[\\/]/, '')))
|
|
1452
|
+
assert(tp.length >= 1 && joined.length === 0,
|
|
1453
|
+
'★ [task-17/v5] an ABSOLUTE explicit paperLatexCommand is probed ALONE — triedPaths never joins it onto a known TeX root (triedPaths=' + JSON.stringify(tp) + ')')
|
|
1440
1454
|
}
|
|
1441
1455
|
|
|
1442
1456
|
// ---------- 28. office-only surface + id normalisation ----------
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Falsifiability driver for scripts/release-check.mjs: a SINGLE-SITE edit must turn its own guard
|
|
2
|
+
// (`--self-test`) NAMED-red, while the unmutated control stays green. The mutated copy is written as a
|
|
3
|
+
// dot-sibling inside scripts/ so relative paths still resolve, and it is always removed in `finally`.
|
|
4
|
+
import { spawnSync } from 'node:child_process'
|
|
5
|
+
import { readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
6
|
+
import { dirname, join } from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
const REPO = join(HERE, '..')
|
|
11
|
+
const SRC = join(REPO, 'scripts', 'release-check.mjs')
|
|
12
|
+
|
|
13
|
+
const FAMILIES = [
|
|
14
|
+
{
|
|
15
|
+
// Without this, a lockfile that drifted away from package.json would ship and the market guard would
|
|
16
|
+
// be the only thing that notices.
|
|
17
|
+
name: 'the version-triple check is dropped (a drifted lockfile would ship)',
|
|
18
|
+
from: 'if (lockTop !== pkgVersion) out.push(',
|
|
19
|
+
to: 'if (false) out.push(',
|
|
20
|
+
expect: /drifted lock top-level version is caught/,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
// Without this, a release note missing half its sections would ship.
|
|
24
|
+
name: 'the section-count check is dropped (a truncated release note would ship)',
|
|
25
|
+
from: 'if (got.length !== want.length) out.push(',
|
|
26
|
+
to: 'if (false) out.push(',
|
|
27
|
+
expect: /truncated note is caught by its section count/,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
// Without this, a note whose sections are in the wrong order would ship.
|
|
31
|
+
name: 'the section-order check is dropped (a reordered release note would ship)',
|
|
32
|
+
from: 'if (got[i] !== want[i]) out.push(',
|
|
33
|
+
to: 'if (false) out.push(',
|
|
34
|
+
expect: /reordered note is caught/,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
// Without this, a package containing CRLF would ship - the exact defect this whole line of work fixed.
|
|
38
|
+
name: 'the CRLF scan is dropped (a CRLF package would ship)',
|
|
39
|
+
from: 'if (Number(crlf) > 0) out.push(',
|
|
40
|
+
to: 'if (false) out.push(',
|
|
41
|
+
expect: /CRLF inside the package is caught/,
|
|
42
|
+
},
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
function runSelftest(scriptPath) {
|
|
46
|
+
const r = spawnSync(process.execPath, [scriptPath, '--self-test'], { cwd: REPO, encoding: 'utf8', timeout: 60000 })
|
|
47
|
+
return { code: r.status, out: String(r.stdout || '') + String(r.stderr || '') }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const control = runSelftest(SRC)
|
|
51
|
+
console.log('control (unmutated release-check): exit=' + control.code + (control.code === 0 ? ' green, as required' : ' NOT GREEN'))
|
|
52
|
+
if (control.code !== 0) console.log(control.out.split(/\r?\n/).filter((l) => /FAIL/.test(l)).slice(0, 4).join('\n'))
|
|
53
|
+
|
|
54
|
+
let red = 0
|
|
55
|
+
for (const f of FAMILIES) {
|
|
56
|
+
const dest = join(REPO, 'scripts', '.tmp-release-check-' + Math.random().toString(36).slice(2, 10) + '.mjs')
|
|
57
|
+
let ok = false
|
|
58
|
+
try {
|
|
59
|
+
let text = readFileSync(SRC, 'utf8').replace(/\r\n?/g, '\n')
|
|
60
|
+
if (/\r/.test(text)) { console.error('SETUP-FAIL - the anchor source was not EOL-normalised (a lone CR leaked in)'); process.exit(1) }
|
|
61
|
+
const hits = text.split(f.from).length - 1
|
|
62
|
+
if (hits !== 1) { console.error('SETUP-FAIL - anchor ' + JSON.stringify(f.from) + ' matched ' + hits + ' time(s), expected exactly 1'); process.exit(1) }
|
|
63
|
+
writeFileSync(dest, text.replace(f.from, f.to), 'utf8')
|
|
64
|
+
const r = runSelftest(dest)
|
|
65
|
+
const named = r.out.split(/\r?\n/).filter((l) => /^\s*FAIL - /.test(l) && f.expect.test(l)).map((l) => l.trim())
|
|
66
|
+
ok = r.code !== 0 && named.length > 0
|
|
67
|
+
console.log((ok ? ' ok - ' : ' FAIL - ') + f.name + ' [exit=' + r.code + ']' + (named.length ? ' :: ' + named[0].slice(0, 140) : ' :: no named red (exit=' + r.code + ')'))
|
|
68
|
+
if (ok) red++
|
|
69
|
+
} finally {
|
|
70
|
+
try { rmSync(dest, { force: true }) } catch (e) { /* best effort */ }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log('')
|
|
75
|
+
console.log('mutant families reddening the release-check guard by name: ' + red + '/' + FAMILIES.length)
|
|
76
|
+
console.log('control green: ' + (control.code === 0))
|
|
77
|
+
if (red !== FAMILIES.length || control.code !== 0) process.exit(1)
|
|
78
|
+
console.log('ALL MUTANTS RED AS REQUIRED')
|
|
@@ -95,6 +95,14 @@ const FAMILIES = [
|
|
|
95
95
|
to: 'const order=(prefer?[prefer]:[]).concat(base.filter(x=>x!==prefer))',
|
|
96
96
|
expect: /\[task-10\/v4\]/,
|
|
97
97
|
},
|
|
98
|
+
{
|
|
99
|
+
// The delivered artifacts must state WHY no PDF was produced (the body is composed BEFORE compilation,
|
|
100
|
+
// so this note is written after it). Blunting the zh note must redden that guard BY NAME.
|
|
101
|
+
name: 'deliverable/v4: the delivered paper stops stating that no engine was detected',
|
|
102
|
+
from: "zh?'本机未检测到 LaTeX 引擎:只产出 tex+md'",
|
|
103
|
+
to: "zh?''",
|
|
104
|
+
expect: /\[deliverable\/v4\]/,
|
|
105
|
+
},
|
|
98
106
|
]
|
|
99
107
|
let red = 0
|
|
100
108
|
for (const f of FAMILIES) { const t0 = Date.now(); const ok = runFamily(f); const ms = Date.now() - t0; if (ok) red++ }
|