dsh-vibe-math 2.7.2 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.en.md +682 -681
- package/README.md +683 -682
- package/cordis.patch.yml +112 -16
- package/docs/AUDIT-CHECKLIST.md +755 -518
- package/docs/COMPAT-AUDIT-ROUND2.md +354 -328
- package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
- package/docs/audit-methodology.md +93 -0
- package/docs/final-paper.md +172 -172
- package/docs/formal-verification.md +613 -588
- package/docs/generate_framework_diagram_v2.py +114 -114
- package/docs/generate_framework_diagram_v3.py +127 -127
- package/docs/math-computation.md +195 -187
- package/docs/parameter-schema.md +37 -0
- package/docs/release-notes/README.en.md +2 -0
- package/docs/release-notes/README.md +2 -0
- package/docs/release-notes/RELEASE-NOTES-2.0.22.md +112 -112
- package/docs/release-notes/RELEASE-NOTES-2.1.0.md +143 -143
- package/docs/release-notes/RELEASE-NOTES-2.2.0.md +266 -266
- package/docs/release-notes/RELEASE-NOTES-2.2.1.md +43 -43
- package/docs/release-notes/RELEASE-NOTES-2.2.2.md +88 -88
- package/docs/release-notes/RELEASE-NOTES-2.3.0.md +207 -207
- package/docs/release-notes/RELEASE-NOTES-2.3.1.md +134 -134
- package/docs/release-notes/RELEASE-NOTES-2.3.10.md +105 -105
- package/docs/release-notes/RELEASE-NOTES-2.3.11.md +57 -57
- package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -80
- package/docs/release-notes/RELEASE-NOTES-2.3.2.md +145 -145
- package/docs/release-notes/RELEASE-NOTES-2.3.3.md +115 -115
- package/docs/release-notes/RELEASE-NOTES-2.3.4.md +69 -69
- package/docs/release-notes/RELEASE-NOTES-2.3.5.md +63 -63
- package/docs/release-notes/RELEASE-NOTES-2.3.6.md +66 -66
- package/docs/release-notes/RELEASE-NOTES-2.3.7.md +59 -59
- package/docs/release-notes/RELEASE-NOTES-2.3.8.md +45 -45
- package/docs/release-notes/RELEASE-NOTES-2.3.9.md +70 -70
- package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +2 -2
- package/docs/release-notes/RELEASE-NOTES-2.7.2.md +3 -3
- package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.0.en.md +65 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.0.md +65 -0
- package/docs/release-notes/TEMPLATE.md +6 -0
- package/docs/release-playbook.md +103 -0
- package/docs/slv-playbook.md +117 -0
- package/docs/status-report-fields.md +167 -0
- package/docs/test-timing.md +158 -154
- package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
- package/installer.js +793 -783
- package/package.json +260 -203
- package/prompt-corpus-persona/persona-corpus.json +9 -9
- package/prompt-corpus-persona/persona-corpus.md +225 -33
- package/prompt-corpus-v2/formal-verify-v2.json +295 -115
- package/prompt-corpus-v2/formal-verify-v2.md +2290 -282
- package/prompt-corpus-v3/formal-verify-v3.json +112 -112
- package/prompt-corpus-v3/formal-verify-v3.md +227 -143
- package/prompt-corpus-v4/formal-verify-v4.json +18 -18
- package/prompt-corpus-v4/formal-verify-v4.md +49 -44
- package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
- package/prompt-corpus-v5/prompt-corpus-v5.md +573 -207
- package/scripts/clean-temp.mjs +234 -0
- package/scripts/update-doc-counts.mjs +71 -0
- package/tests/audit-artifact-docs.mjs +72 -0
- package/tests/audit-artifact-docs.mutants.mjs +58 -0
- package/tests/audit-engine-faces.mjs +298 -0
- package/tests/audit-formal-sensitivity.mjs +345 -342
- package/tests/audit-installer-assertions.mutants.mjs +63 -0
- package/tests/audit-installer-compat.test.mjs +256 -208
- package/tests/audit-installer-policy.test.mjs +41 -8
- package/tests/audit-math-computation-contract.mjs +64 -8
- package/tests/audit-math-computation-contract.mutants.mjs +106 -0
- package/tests/audit-math-computation-parity.mjs +164 -3
- package/tests/audit-math-computation-sensitivity.mjs +5 -3
- package/tests/audit-package-membership.mjs +57 -0
- package/tests/audit-package-membership.mutants.mjs +28 -0
- package/tests/audit-participant-set-parity.mjs +160 -0
- package/tests/audit-path-discipline.mjs +83 -0
- package/tests/audit-path-discipline.mutants.mjs +51 -0
- package/tests/audit-persona-sensitivity.mjs +323 -309
- package/tests/audit-persona-surface.mutants.mjs +69 -0
- package/tests/audit-persona-surface.test.mjs +431 -390
- package/tests/audit-prompt-duplication.mjs +143 -0
- package/tests/audit-prompt-duplication.mutants.mjs +51 -0
- package/tests/audit-prompt-invariants.mjs +589 -508
- package/tests/audit-readme-bilingual.test.mjs +152 -0
- package/tests/audit-readme-counts.mjs +95 -0
- package/tests/audit-readme-counts.mutants.mjs +52 -0
- package/tests/audit-registration.mjs +78 -0
- package/tests/audit-spec-traceability.mjs +193 -193
- package/tests/audit-status-report-fields.mjs +172 -0
- package/tests/audit-status-report-fields.mutants.mjs +64 -0
- package/tests/audit-v3-registration-parity.mjs +156 -0
- package/tests/audit-v5-integrity.mjs +795 -508
- package/tests/audit-v5-lean-abstention.mjs +392 -0
- package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
- package/tests/audit-v5-prompt-duplication.mjs +88 -0
- package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
- package/tests/audit-v5-sensitivity.mjs +433 -427
- package/tests/e2e-identity-a6.mutants.mjs +65 -0
- package/tests/e2e-v5-round2.test.mjs +2453 -1981
- package/tests/formal-verify-v2.test.mjs +1778 -1724
- package/tests/formal-verify-v3.mutants.mjs +117 -0
- package/tests/formal-verify-v3.test.mjs +1724 -1671
- package/tests/formal-verify-v4.mutants.mjs +152 -0
- package/tests/formal-verify-v4.test.mjs +2114 -1574
- package/tests/formal-verify-v5.test.mjs +790 -724
- package/tests/helpers/doc-counts-format.mjs +41 -0
- package/tests/helpers/math-computation-fake-seam.mjs +4 -0
- package/tests/math-computation-a1.mutants.mjs +72 -0
- package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
- package/tests/math-computation-discovery.mutants.mjs +76 -0
- package/tests/math-computation-shared.test.mjs +392 -6
- package/tests/math-computation-v2.test.mjs +779 -0
- package/tests/math-computation-v3.test.mjs +771 -0
- package/tests/math-computation-v4.mutants.mjs +133 -0
- package/tests/math-computation-v4.test.mjs +739 -0
- package/tests/prompt-v5-integrity.test.mjs +1291 -1280
- package/tests/run-tests.mjs +393 -202
- package/tests/run-tests.mutants.mjs +70 -0
- package/tests/selfdrive-v5.mjs +560 -508
- package/tests/selfdrive-v5.mutants.mjs +132 -0
- package/tests/temp-hygiene.mutants.mjs +80 -0
- package/tests/v2-fix-probes.mutants.mjs +104 -0
- package/tests/v2-fix-probes.test.mjs +1040 -0
- package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
- package/tests/v2v3-interrupt.mutants.mjs +111 -0
- package/tests/v3-fix-probes.mutants.mjs +99 -0
- package/tests/v3-fix-probes.test.mjs +892 -0
- package/tests/v4-final-paper.mutants.mjs +108 -0
- package/tests/v5-institute-fixes.mutants.mjs +218 -0
- package/vibe-math-v2/agent.cordis.yml +378 -322
- package/vibe-math-v2/math-computation.js +144 -53
- package/vibe-math-v2/math-engines.js +12 -12
- package/vibe-math-v2/preset.yml +2 -2
- package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
- package/vibe-math-v2/vibe-math-v2.js +5097 -4755
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +491 -487
- package/vibe-math-v3/agent.cordis.yml +422 -356
- package/vibe-math-v3/math-computation.js +144 -53
- package/vibe-math-v3/math-engines.js +12 -12
- package/vibe-math-v3/preset.yml +2 -2
- package/vibe-math-v3/vibe-math-v3.js +6235 -5873
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
- package/vibe-math-v4/agent.cordis.yml +438 -402
- package/vibe-math-v4/math-computation.js +144 -53
- package/vibe-math-v4/math-engines.js +12 -12
- package/vibe-math-v4/preset.yml +2 -2
- package/vibe-math-v4/vibe-math-v4.js +4490 -4269
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1013
- package/vibe-math-v5/agent.cordis.yml +46 -12
- package/vibe-math-v5/math-computation.js +144 -53
- package/vibe-math-v5/math-engines.js +12 -12
- package/vibe-math-v5/vibe-math-v5.js +1266 -229
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1660
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
|
@@ -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('')
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Registered mutant harness for tests/audit-math-computation-contract.mjs — the audit carries its own proof.
|
|
2
|
+
// Defect shapes (all real, from this project's history):
|
|
3
|
+
// (a) the SINGLE SOURCE (PARAM_PROPS_KEYS) drops a math param ⇒ the derived schema loses it
|
|
4
|
+
// (b) the FIRST (session-layer) registration reverts to a hand-written literal without the math keys
|
|
5
|
+
// (c) the APPLY-LAYER registration does the same — only visible because the audit now checks EVERY site
|
|
6
|
+
// Each family mutates a COPY of the tree (the audit's MC_CONTRACT_ROOT seam) and must make the audit redden
|
|
7
|
+
// BY NAME. Child runs are bounded (a timeout is a HANG); skipped families are printed and counted.
|
|
8
|
+
// Exit criterion: every family reddens by name; ALL MUTANTS RED AS REQUIRED + exit 0 only then.
|
|
9
|
+
// Run: node tests/audit-math-computation-contract.mutants.mjs
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, copyFileSync, rmSync, readdirSync } from 'node:fs'
|
|
11
|
+
import { execFileSync, spawnSync } from 'node:child_process'
|
|
12
|
+
import { dirname, join } from 'node:path'
|
|
13
|
+
import { tmpdir } from 'node:os'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
16
|
+
const REPO = join(HERE, '..')
|
|
17
|
+
const AUDIT = 'tests/audit-math-computation-contract.mjs'
|
|
18
|
+
// Bound every child run: run-tests has no per-suite timeout, so an unbounded child hangs the gate.
|
|
19
|
+
// A timeout is reported as HANG, counts as a failure for that family, and is listed in hangs=[].
|
|
20
|
+
const CHILD_TIMEOUT_MS = Number(process.env.MUTANT_CHILD_TIMEOUT_MS || 120000)
|
|
21
|
+
const TIMES = []
|
|
22
|
+
const hangs = []
|
|
23
|
+
const skipped = []
|
|
24
|
+
function copyTree(from, to) {
|
|
25
|
+
mkdirSync(to, { recursive: true })
|
|
26
|
+
for (const e of readdirSync(from, { withFileTypes: true })) {
|
|
27
|
+
if (e.name === '.git' || e.name === 'node_modules' || e.name === '_oneoff') continue
|
|
28
|
+
if (e.isDirectory()) copyTree(join(from, e.name), join(to, e.name))
|
|
29
|
+
else copyFileSync(join(from, e.name), join(to, e.name))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function runFamily(f) {
|
|
33
|
+
const dest = join(tmpdir(), 'mc-contract-mut-' + Math.random().toString(36).slice(2, 10))
|
|
34
|
+
copyTree(REPO, dest)
|
|
35
|
+
const target = join(dest, f.rel)
|
|
36
|
+
const before = readFileSync(target, 'utf8')
|
|
37
|
+
const n = before.split(f.from).length - 1
|
|
38
|
+
if (f.firstOnly ? n < 1 : n !== 1) {
|
|
39
|
+
skipped.push(f.name + ' (anchor x' + n + ')')
|
|
40
|
+
console.error(' SKIP - ' + f.name + ' (anchor x' + n + ')')
|
|
41
|
+
rmSync(dest, { recursive: true, force: true })
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
const mutated = f.firstOnly
|
|
45
|
+
? (() => { const i = before.indexOf(f.from); return before.slice(0, i) + f.to + before.slice(i + f.from.length) })()
|
|
46
|
+
: before.split(f.from).join(f.to)
|
|
47
|
+
writeFileSync(target, mutated, 'utf8')
|
|
48
|
+
try { execFileSync(process.execPath, ['--check', target], { stdio: 'pipe' }) }
|
|
49
|
+
catch (e) {
|
|
50
|
+
writeFileSync(target, before, 'utf8')
|
|
51
|
+
skipped.push(f.name + ' (mutation did not compile)')
|
|
52
|
+
console.error(' SKIP - ' + f.name + ' (mutation did not compile; restored)')
|
|
53
|
+
rmSync(dest, { recursive: true, force: true })
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
const t0 = Date.now()
|
|
57
|
+
const r = spawnSync(process.execPath, [AUDIT], {
|
|
58
|
+
cwd: REPO, encoding: 'utf8', timeout: CHILD_TIMEOUT_MS, killSignal: 'SIGKILL',
|
|
59
|
+
env: Object.assign({}, process.env, { MC_CONTRACT_ROOT: dest }),
|
|
60
|
+
})
|
|
61
|
+
const ms = Date.now() - t0
|
|
62
|
+
TIMES.push([f.name, ms])
|
|
63
|
+
const hang = !!(r.error && (r.error.code === 'ETIMEDOUT' || r.signal === 'SIGKILL'))
|
|
64
|
+
if (hang) hangs.push(f.name + '(' + Math.round(ms / 1000) + 's)')
|
|
65
|
+
const out = String(r.stdout || '') + String(r.stderr || '')
|
|
66
|
+
// this audit prints failures as ` - <label> — <detail>` (not `FAIL - ...`), so match the label itself
|
|
67
|
+
const named = out.split(/\r?\n/).filter((l) => f.expect.test(l)).map((l) => l.trim())
|
|
68
|
+
const ok = !hang && r.status === 1 && named.length > 0
|
|
69
|
+
console.log((hang ? ' HANG - ' : (ok ? ' ok - ' : ' FAIL - ')) + f.name + ' [' + ms + 'ms]' + (named.length ? ' :: ' + named[0].slice(0, 150) : ' :: no named red (exit=' + r.status + ')'))
|
|
70
|
+
rmSync(dest, { recursive: true, force: true })
|
|
71
|
+
return ok
|
|
72
|
+
}
|
|
73
|
+
const WITHOUT_MATH_KEYS = "objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } })"
|
|
74
|
+
const FAMILIES = [
|
|
75
|
+
{
|
|
76
|
+
name: 'the SINGLE SOURCE drops mathMode (derived key table)',
|
|
77
|
+
rel: 'vibe-math-v3/vibe-math-v3.js',
|
|
78
|
+
from: "'mathMode', 'mathEngines'",
|
|
79
|
+
to: "'mathEngines'",
|
|
80
|
+
expect: /set schema has the property mathMode/,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'the FIRST (session-layer) registration reverts to a literal without the math keys',
|
|
84
|
+
rel: 'vibe-math-v3/vibe-math-v3.js',
|
|
85
|
+
from: 'objParams(paramProps()), async function',
|
|
86
|
+
to: WITHOUT_MATH_KEYS + ', async function',
|
|
87
|
+
firstOnly: true,
|
|
88
|
+
expect: /site#0 set schema has the property mathComputation/,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'the APPLY-LAYER registration reverts to a literal without the math keys',
|
|
92
|
+
rel: 'vibe-math-v3/vibe-math-v3.js',
|
|
93
|
+
from: "objParams(paramProps()), 'vibe_math_set_params')",
|
|
94
|
+
to: WITHOUT_MATH_KEYS + ", 'vibe_math_set_params')",
|
|
95
|
+
expect: /site#1 set schema has the property mathComputation/,
|
|
96
|
+
},
|
|
97
|
+
]
|
|
98
|
+
let red = 0
|
|
99
|
+
for (const f of FAMILIES) { const ok = runFamily(f); if (ok) red++ }
|
|
100
|
+
console.log('')
|
|
101
|
+
console.log('mutant families reddening the math-contract audit by name: ' + red + '/' + FAMILIES.length)
|
|
102
|
+
console.log('timings: ' + TIMES.map((t) => String(t[0]).split('(')[0].trim().slice(0, 18) + '=' + t[1] + 'ms').join(' '))
|
|
103
|
+
console.log('hangs=[' + hangs.join(' | ') + ']')
|
|
104
|
+
console.log('skipped=[' + skipped.join(' | ') + ']')
|
|
105
|
+
if (red !== FAMILIES.length || skipped.length || hangs.length) process.exit(1)
|
|
106
|
+
console.log('ALL MUTANTS RED AS REQUIRED')
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { createHash } from 'node:crypto'
|
|
18
18
|
import { existsSync, readFileSync } from 'node:fs'
|
|
19
19
|
import { join, resolve } from 'node:path'
|
|
20
|
-
import { fileURLToPath } from 'node:url'
|
|
20
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
21
21
|
|
|
22
22
|
const HERE = fileURLToPath(new URL('./', import.meta.url))
|
|
23
23
|
const REPO = resolve(HERE, '..')
|
|
@@ -37,6 +37,101 @@ function ok(cond, label, detail) {
|
|
|
37
37
|
const sha = (p) => createHash('sha256').update(readFileSync(p)).digest('hex')
|
|
38
38
|
const read = (rel) => readFileSync(join(REPO, rel), 'utf8')
|
|
39
39
|
|
|
40
|
+
// ---- extracted predicates (shared by the checks below AND by --self-probe) -----------------------
|
|
41
|
+
// §7/§8 used to compute these inline; extracting them is what lets the self-probe feed the SAME
|
|
42
|
+
// predicates deliberately broken strings. Keep the two users in sync by construction: both call these.
|
|
43
|
+
const SUPPORTED_PLACEHOLDERS = {
|
|
44
|
+
'<script>': "split('<script>').join(",
|
|
45
|
+
'<expr>': "split('<expr>').join(",
|
|
46
|
+
'<pkgs>': "a === '<pkgs>'",
|
|
47
|
+
'<pkgs...>': "a === '<pkgs...>'",
|
|
48
|
+
'<probeCode>': "a === '<probeCode>'",
|
|
49
|
+
'<exe>': ".replace(/<exe>/g, exe)",
|
|
50
|
+
'<pkg>': ".replace(/<pkg>/g, pkg)",
|
|
51
|
+
'__PKGS__': ".replace(/__PKGS__/g, plain)",
|
|
52
|
+
'__QPKGS__': ".replace(/__QPKGS__/g, quoted)",
|
|
53
|
+
'__PKG__': ".replace(/__PKG__/g, pkg)",
|
|
54
|
+
}
|
|
55
|
+
const stringsOfDescriptor = (d) => [].concat(
|
|
56
|
+
Object.values(d.scriptArgv || {}).filter((x) => typeof x === 'string'),
|
|
57
|
+
Object.values(d.evalArgv || {}).filter((x) => typeof x === 'string'),
|
|
58
|
+
((d.packageProbe && d.packageProbe.argv) || []).filter((x) => typeof x === 'string'),
|
|
59
|
+
typeof d.probeCode === 'string' ? [d.probeCode] : [],
|
|
60
|
+
(d.install ? [].concat(d.install.userArgv || [], d.install.systemArgv || [], d.install.uninstallArgv || []) : []).filter((x) => typeof x === 'string'),
|
|
61
|
+
)
|
|
62
|
+
/** Placeholders used by descriptors that the module does not KNOW about. Unit: placeholder occurrences. */
|
|
63
|
+
function unknownPlaceholders(src, all) {
|
|
64
|
+
const out = []
|
|
65
|
+
for (const name of Object.keys(all)) {
|
|
66
|
+
for (const str of stringsOfDescriptor(all[name])) {
|
|
67
|
+
for (const m of (str.match(/<[a-z.]+>|__[A-Z]+__/g) || [])) if (!(m in SUPPORTED_PLACEHOLDERS)) out.push(name + ':' + m)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out
|
|
71
|
+
}
|
|
72
|
+
/** Known placeholders whose implementation is missing from the module. Unit: placeholder occurrences. */
|
|
73
|
+
function unimplementedPlaceholders(src, all) {
|
|
74
|
+
const out = []
|
|
75
|
+
for (const name of Object.keys(all)) {
|
|
76
|
+
for (const str of stringsOfDescriptor(all[name])) {
|
|
77
|
+
for (const m of (str.match(/<[a-z.]+>|__[A-Z]+__/g) || [])) {
|
|
78
|
+
if ((m in SUPPORTED_PLACEHOLDERS) && SUPPORTED_PLACEHOLDERS[m] && src.indexOf(SUPPORTED_PLACEHOLDERS[m]) === -1) out.push(name + ':' + m)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return out
|
|
83
|
+
}
|
|
84
|
+
/** The substitution must be STRING-level so embedded forms (matlab run('<script>')) are covered. */
|
|
85
|
+
const stringLevelSubstitution = (src) => /String\(a\)\.split\('<script>'\)\.join\(/.test(src) && /String\(a\)\.split\('<expr>'\)\.join\(/.test(src)
|
|
86
|
+
/** §8: the descriptor declares the policy / the module consumes it / nothing is hard-coded. */
|
|
87
|
+
const cliDeclaresPolicy = (eng) => /policy:\s*\{\s*requiresMathMode:\s*'typed\+shell',\s*requiresEngineInList:\s*true\s*\}/.test(eng)
|
|
88
|
+
const cliConsumesPolicy = (mod) => (mod.match(/CLI_POLICY\.requiresMathMode/g) || []).length >= 2 && (mod.match(/CLI_POLICY\.requiresEngineInList/g) || []).length >= 1
|
|
89
|
+
const cliNoHardCoded = (mod) => !/mathMode !== 'typed\+shell'/.test(mod)
|
|
90
|
+
/** §8: the shared refusal shape must surface the descriptor value (the OLD literal had only reason). */
|
|
91
|
+
const refusalSurfacesPolicy = (n) => !!n && n.reason === 'policy' && n.requiresMathMode === 'typed+shell'
|
|
92
|
+
|
|
93
|
+
// ---- `--self-probe`: the SAME predicates against deliberately broken inputs (shipped form of the
|
|
94
|
+
// was-dev-only proofs `descriptor-sweep-proof.mjs` + `cli-policy-proof.mjs`). No repo mutation.
|
|
95
|
+
async function selfProbe() {
|
|
96
|
+
const mod = read('vibe-math-v2/math-computation.js')
|
|
97
|
+
const eng = read('vibe-math-v2/math-engines.js')
|
|
98
|
+
const { MATH_ENGINES, MATH_P2_ENGINES } = await import(pathToFileURL(join(REPO, 'vibe-math-v2', 'math-engines.js')).href)
|
|
99
|
+
const all = Object.assign({}, MATH_ENGINES, MATH_P2_ENGINES)
|
|
100
|
+
const LIT = /<script>/
|
|
101
|
+
const matlabTemplate = ['-batch', "run('<script>')"]
|
|
102
|
+
const preFix = (tmpl, abs) => tmpl.map((a) => (a === '<script>' ? abs : a))
|
|
103
|
+
const postFix = (tmpl, abs) => tmpl.map((a) => String(a).split('<script>').join(abs))
|
|
104
|
+
const cases = [
|
|
105
|
+
['§7 substitution is STRING-level (matlab embedding covered)', stringLevelSubstitution(mod),
|
|
106
|
+
stringLevelSubstitution(mod.replace("String(a).split('<script>').join(payload.scriptAbs)", "a === '<script>' ? payload.scriptAbs : a"))],
|
|
107
|
+
['§7 descriptors use only KNOWN placeholders', unknownPlaceholders(mod, all).length === 0,
|
|
108
|
+
unknownPlaceholders(mod, Object.assign({}, all, { probe: { scriptArgv: { x: '<mysterySlot>' } } })).length > 0],
|
|
109
|
+
['§7 known placeholders are IMPLEMENTED in the module', unimplementedPlaceholders(mod, all).length === 0,
|
|
110
|
+
unimplementedPlaceholders('/* no implementation */', all).length === 0],
|
|
111
|
+
['§7 the guard predicate catches what the OLD substitution produced', postFix(matlabTemplate, 'ABS').every((a) => !LIT.test(a)),
|
|
112
|
+
preFix(matlabTemplate, 'ABS').every((a) => !LIT.test(a))],
|
|
113
|
+
['§8 the module CONSUMES the descriptor policy', cliConsumesPolicy(mod),
|
|
114
|
+
cliConsumesPolicy(mod.replace(/CLI_POLICY\./g, 'HARDCODED.'))],
|
|
115
|
+
['§8 no hard-coded policy comparison remains', cliNoHardCoded(mod),
|
|
116
|
+
cliNoHardCoded(mod + "\\n if (params.mathMode !== 'typed+shell') { /* hard-coded revert */ }")],
|
|
117
|
+
['§8 descriptor DECLARES the policy', cliDeclaresPolicy(eng),
|
|
118
|
+
cliDeclaresPolicy(eng.replace("policy: { requiresMathMode: 'typed+shell', requiresEngineInList: true }", 'policy: null'))],
|
|
119
|
+
['§8 the shared refusal surfaces the descriptor value (OLD literal caught)',
|
|
120
|
+
refusalSurfacesPolicy({ reason: 'policy', requiresMathMode: 'typed+shell' }),
|
|
121
|
+
refusalSurfacesPolicy({ reason: 'policy' })],
|
|
122
|
+
]
|
|
123
|
+
let bad = 0
|
|
124
|
+
for (const [name, greenNow, redWhenBroken] of cases) {
|
|
125
|
+
const good = greenNow === true && redWhenBroken === false
|
|
126
|
+
if (!good) bad++
|
|
127
|
+
console.log((good ? ' ok ' : ' FAIL ') + name + ' :: green-now=' + greenNow + ' broken-goes-red=' + !redWhenBroken)
|
|
128
|
+
}
|
|
129
|
+
console.log('=== MATH PARITY SELF-PROBE: ' + (cases.length - bad) + '/' + cases.length + ' as required ===')
|
|
130
|
+
return bad === 0
|
|
131
|
+
}
|
|
132
|
+
if (process.argv.includes('--self-probe')) process.exit(await selfProbe() ? 0 : 1)
|
|
133
|
+
|
|
134
|
+
|
|
40
135
|
console.log('-- math_computation parity --')
|
|
41
136
|
|
|
42
137
|
// ---- 1/2. the four copies are identical, and match the canonical source ----
|
|
@@ -106,7 +201,12 @@ for (const f of MODULES) {
|
|
|
106
201
|
: new URL('../vibe-math-v2/math-engines.js', import.meta.url).href)
|
|
107
202
|
const order = E.MATH_ENGINE_ORDER
|
|
108
203
|
ok(JSON.stringify(order) === JSON.stringify(['python', 'r', 'octave', 'julia', 'matlab', 'maple', 'wolfram', 'cli']), 'MATH_ENGINE_ORDER is exactly the P1 set')
|
|
109
|
-
|
|
204
|
+
// Item-3 ruling: `defaultOn` was declared but never consumed - cli's default-on property is
|
|
205
|
+
// already pinned above through the REAL single source (MATH_PARAM_DEFAULTS.mathEngines).
|
|
206
|
+
const zombie = ['winPrefix', 'stdinArgv', 'defaultOn']
|
|
207
|
+
.filter((f) => JSON.stringify(E.MATH_ENGINES).indexOf('"' + f + '"') !== -1 || JSON.stringify(E.MATH_P2_ENGINES).indexOf('"' + f + '"') !== -1)
|
|
208
|
+
ok(zombie.length === 0, 'no zombie descriptor field is declared (winPrefix/stdinArgv/defaultOn were removed)', JSON.stringify(zombie))
|
|
209
|
+
ok(!/<cliArgv/.test(readFileSync(join(REPO, 'vibe-math-v2', 'math-engines.js'), 'utf8')), 'the decorative <cliArgv...> marker is gone from the descriptor table')
|
|
110
210
|
ok(E.MATH_ENGINES.cli.policy && E.MATH_ENGINES.cli.policy.requiresMathMode === 'typed+shell' && E.MATH_ENGINES.cli.policy.requiresEngineInList === true, 'cli policy gate declared')
|
|
111
211
|
ok(E.MATH_ENGINES.maple.verify === true && !!E.MATH_ENGINES.maple.verifyReason, 'Maple template is flagged VERIFY with a reason')
|
|
112
212
|
for (const name of ['matlab', 'maple', 'wolfram']) {
|
|
@@ -192,7 +292,68 @@ for (const f of MODULES) {
|
|
|
192
292
|
for (const r of reasons) ok(doc.indexOf('`' + r + '`') !== -1, 'docs/math-computation.md names next.reason ' + r)
|
|
193
293
|
}
|
|
194
294
|
|
|
195
|
-
|
|
295
|
+
// ── 7. descriptor sweep: every argv placeholder dialect must be one the module IMPLEMENTS
|
|
296
|
+
// Sweep finding (HIGH, fixed): substitution was exact-element (`a === '<script>'`), so matlab's embedded
|
|
297
|
+
// `run('<script>')` reached the engine literally. The fix is string-level; this guard keeps the two
|
|
298
|
+
// sides in sync: no descriptor may use a placeholder the module does not substitute.
|
|
299
|
+
{
|
|
300
|
+
const E = await import(pathToFileURL(join(REPO, 'vibe-math-v2', 'math-engines.js')).href)
|
|
301
|
+
const modPath = join(REPO, 'vibe-math-v2', 'math-computation.js')
|
|
302
|
+
const src = existsSync(modPath) ? readFileSync(modPath, 'utf8') : ''
|
|
303
|
+
const all = Object.assign({}, E.MATH_ENGINES, E.MATH_P2_ENGINES)
|
|
304
|
+
const unknown = unknownPlaceholders(src, all)
|
|
305
|
+
const unimplemented = unimplementedPlaceholders(src, all)
|
|
306
|
+
ok(stringLevelSubstitution(src),
|
|
307
|
+
'substitution is STRING-level for <script>/<expr> (embedded forms like matlab run(\'<script>\') are covered)')
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── 8. the cli policy is declared ONCE (descriptor) and consumed, never hard-coded
|
|
311
|
+
// Sweep ruling: `cli.policy.{requiresMathMode,requiresEngineInList}` were declared but never consumed,
|
|
312
|
+
// while the same rules were hard-coded twice in the module (so they could drift). The module now reads
|
|
313
|
+
// the descriptor; this guard makes a hard-coded revert impossible to land.
|
|
314
|
+
{
|
|
315
|
+
const engPath = join(REPO, 'vibe-math-v2', 'math-engines.js')
|
|
316
|
+
const modPath = join(REPO, 'vibe-math-v2', 'math-computation.js')
|
|
317
|
+
const eng = existsSync(engPath) ? readFileSync(engPath, 'utf8') : ''
|
|
318
|
+
const mod = existsSync(modPath) ? readFileSync(modPath, 'utf8') : ''
|
|
319
|
+
ok(cliDeclaresPolicy(eng),
|
|
320
|
+
'the cli descriptor DECLARES its policy (requiresMathMode + requiresEngineInList)')
|
|
321
|
+
ok(cliConsumesPolicy(mod),
|
|
322
|
+
'the module CONSUMES the descriptor policy (both refusal sites read CLI_POLICY)')
|
|
323
|
+
ok(cliNoHardCoded(mod),
|
|
324
|
+
'no hard-coded policy comparison is left in the module (the rule lives only in the descriptor)')
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── 9. doc-sync: the v5 editor closed set and v2's single-source status/report helper are documented
|
|
328
|
+
{
|
|
329
|
+
const v5 = existsSync(join(REPO, 'vibe-math-v5', 'vibe-math-v5.js')) ? readFileSync(join(REPO, 'vibe-math-v5', 'vibe-math-v5.js'), 'utf8') : ''
|
|
330
|
+
const fp = existsSync(join(REPO, 'docs', 'final-paper.md')) ? readFileSync(join(REPO, 'docs', 'final-paper.md'), 'utf8') : ''
|
|
331
|
+
// The closed set appears twice in v5 (the coerce call and the tool schema enum); the separator may be
|
|
332
|
+
// written with or without a space, so accept both rather than pinning one spelling.
|
|
333
|
+
ok(/\[\s*'office'\s*,\s*'academician'\s*\]/.test(v5) && /V5_INVALID_ARGUMENT/.test(v5), 'v5 validates the paper editor against the closed set and returns V5_INVALID_ARGUMENT')
|
|
334
|
+
ok(fp.indexOf('office|academician') !== -1 && fp.indexOf('V5_INVALID_ARGUMENT') !== -1, 'docs/final-paper.md documents the v5 editor closed set + its error code')
|
|
335
|
+
const v2src = existsSync(join(REPO, 'vibe-math-v2', 'vibe-math-v2.js')) ? readFileSync(join(REPO, 'vibe-math-v2', 'vibe-math-v2.js'), 'utf8') : ''
|
|
336
|
+
const v2doc = existsSync(join(REPO, 'vibe-math-v2', '实现方案.md')) ? readFileSync(join(REPO, 'vibe-math-v2', '实现方案.md'), 'utf8') : ''
|
|
337
|
+
ok((v2src.match(/verifyTasksView\(/g) || []).length >= 3, 'v2 produces status+report through ONE verifyTasksView helper')
|
|
338
|
+
ok(v2doc.indexOf('verifyTasksView') !== -1, 'the v2 design doc states the single-source status/report helper')
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// S4/S5 (install-failure discrimination) is asserted in tests/math-computation-shared.test.mjs §33, NOT
|
|
342
|
+
// here: parity reads the shipped copy unconditionally, so a mutant cannot reach what it reads (the
|
|
343
|
+
// assertion would exist but could never redden). What parity keeps is the canonical<->copy byte identity,
|
|
344
|
+
// which makes a canonical-only change impossible to ship unnoticed - a stronger defence than a text grep.
|
|
345
|
+
// ── 11. S6: the version policy has ONE wording (constant + helper), not one per path.
|
|
346
|
+
{
|
|
347
|
+
const mc = readFileSync(join(REPO, 'vibe-math-v2', 'math-computation.js'), 'utf8')
|
|
348
|
+
ok((mc.match(/version-constraints-are-existence-only: /g) || []).length === 1, '★ S6: the policy wording appears exactly ONCE (in MATH_VERSION_POLICY)')
|
|
349
|
+
ok(/function mathVersionPolicy\(/.test(mc), 'S6: the shared helper mathVersionPolicy() exists')
|
|
350
|
+
ok(mc.indexOf('版本求解交给包管理器(本工具只检查是否已安装') === -1, '★ S6: the old divergent install-plan wording is gone')
|
|
351
|
+
ok(/versionPolicy: mathVersionPolicy\(args\.packages/.test(mc), 'S6: the install plan builds its wording through the helper')
|
|
352
|
+
ok((mc.match(/只检查是否存在;版本求解交给包管理器/g) || []).length === 1, '★ S6: the policy clause literal appears exactly ONCE (MATH_VERSION_POLICY_CLAUSE)')
|
|
353
|
+
ok(/const MATH_VERSION_POLICY = .*MATH_VERSION_POLICY_CLAUSE/.test(mc), 'S6: the long policy sentence is DERIVED from the clause')
|
|
354
|
+
ok(/MATH_MISSING_PACKAGES[\s\S]{0,120}MATH_VERSION_POLICY_CLAUSE/.test(mc), 'S6: the missing-package detail derives the clause')
|
|
355
|
+
}
|
|
356
|
+
|
|
196
357
|
console.log('=== MATH COMPUTATION PARITY: ' + passed + ' passed, ' + failed + ' failed ===')
|
|
197
358
|
if (failures.length) for (const f of failures) console.error(' - ' + f)
|
|
198
359
|
process.exit(failed === 0 ? 0 : 1)
|
|
@@ -34,8 +34,10 @@ const PROBES = [
|
|
|
34
34
|
{
|
|
35
35
|
name: 'cli-policy-ignored',
|
|
36
36
|
file: 'math-computation.js',
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Re-anchored after the single-source ruling: the policy now comes from the DESCRIPTOR (CLI_POLICY),
|
|
38
|
+
// so the probe disables the descriptor-driven guard. Ignoring it must turn the shared suite RED.
|
|
39
|
+
from: 'if (CLI_POLICY.requiresMathMode && p.mathMode !== CLI_POLICY.requiresMathMode)',
|
|
40
|
+
to: 'if (false && CLI_POLICY.requiresMathMode)',
|
|
39
41
|
expect: 'cli refused while mathMode=typed',
|
|
40
42
|
},
|
|
41
43
|
{
|
|
@@ -208,7 +210,7 @@ function runSuite(modulePath) {
|
|
|
208
210
|
for (const p of PROBES) {
|
|
209
211
|
const dir = mkdtempSync(join(tmpdir(), 'mc-sens-' + p.name + '-'))
|
|
210
212
|
copyFileSync(join(SRC, 'math-engines.js'), join(dir, 'math-engines.js'))
|
|
211
|
-
const src = readFileSync(join(SRC, p.file), 'utf8')
|
|
213
|
+
const src = readFileSync(join(SRC, p.file), 'utf8').replace(/\r\n/g, '\n') // anchors are LF: normalize CRLF checkouts
|
|
212
214
|
if (src.indexOf(p.from) === -1) {
|
|
213
215
|
bad++
|
|
214
216
|
console.log(' FAIL ' + p.name + ' — mutation anchor no longer applies (drift): ' + JSON.stringify(p.from.slice(0, 60)))
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PACKAGE MEMBERSHIP - every guard/harness meant to ship is listed in package.json#files.
|
|
4
|
+
*
|
|
5
|
+
* DEFECT CLASS: "shipped guard whose proving harness is not in the tarball" (the ship rule: a guard
|
|
6
|
+
* ships together with its harness). A package reader could not reproduce the proof.
|
|
7
|
+
*
|
|
8
|
+
* The intended shipped set is stated EXPLICITLY, not globbed blindly: every tests/*.mutants.mjs on
|
|
9
|
+
* disk must be listed, and every tests/*.mjs cited by the guard index must be listed unless it is in
|
|
10
|
+
* EXCLUDED (documented, deliberate exclusions - e.g. audit-market-metadata.test.mjs asserts its own
|
|
11
|
+
* absence because it checks repo-only image artifacts).
|
|
12
|
+
*
|
|
13
|
+
* Run: node tests/audit-package-membership.mjs
|
|
14
|
+
* Env: PACKAGE_JSON=<path> (used by the mutants harness to point at a mutated copy)
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs'
|
|
17
|
+
import { dirname, join } from 'node:path'
|
|
18
|
+
import { fileURLToPath } from 'node:url'
|
|
19
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
20
|
+
const REPO = join(HERE, '..')
|
|
21
|
+
const PKG = process.env.PACKAGE_JSON || join(REPO, 'package.json')
|
|
22
|
+
const EXCLUDED = new Set(['tests/audit-market-metadata.test.mjs', 'tests/audit-diagram-assets.test.mjs'])
|
|
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 pkg = JSON.parse(readFileSync(PKG, 'utf8'))
|
|
27
|
+
const listed = new Set(pkg.files)
|
|
28
|
+
const disk = readdirSync(join(REPO, 'tests')).filter((f) => f.endsWith('.mjs'))
|
|
29
|
+
const mutants = disk.filter((f) => /\.mutants\.mjs$/.test(f)).map((f) => 'tests/' + f).sort()
|
|
30
|
+
const unlistedMutants = mutants.filter((f) => !listed.has(f) && !EXCLUDED.has(f))
|
|
31
|
+
ok(mutants.length > 0, 'there is at least one proving harness on disk')
|
|
32
|
+
ok(unlistedMutants.length === 0, 'every tests/*.mutants.mjs on disk is listed in package.json#files', JSON.stringify(unlistedMutants))
|
|
33
|
+
// guard-index citations: tests/*.mjs paths mentioned in the checklist
|
|
34
|
+
const idx = readFileSync(join(REPO, 'docs', 'AUDIT-CHECKLIST.md'), 'utf8')
|
|
35
|
+
const cited = [...new Set((idx.match(/tests\/[A-Za-z0-9._-]+\.mjs/g) || []))]
|
|
36
|
+
const unlistedCited = cited.filter((f) => !listed.has(f) && !EXCLUDED.has(f))
|
|
37
|
+
ok(cited.length > 0, 'the guard index cites at least one test file')
|
|
38
|
+
ok(unlistedCited.length === 0, 'every guard-index-cited test file is listed (or explicitly excluded)', JSON.stringify(unlistedCited))
|
|
39
|
+
ok(listed.has('tests/audit-package-membership.mjs'), 'this audit itself ships')
|
|
40
|
+
ok(!listed.has('tests/audit-market-metadata.test.mjs'), 'the self-excluded repo-only guard is NOT listed (its own assertion)')
|
|
41
|
+
const missingPaths = [...listed].filter((f) => !existsSync(join(REPO, f)))
|
|
42
|
+
// docs sibling rule: a doc cited by a SHIPPED file must ship too (docs/** was previously uncovered).
|
|
43
|
+
const citers = ['README.md', 'README.en.md', 'docs/AUDIT-CHECKLIST.md', 'docs/COMPAT-AUDIT-ROUND2.md']
|
|
44
|
+
const docRefs = new Set()
|
|
45
|
+
for (const c of citers) {
|
|
46
|
+
const cp = join(REPO, c)
|
|
47
|
+
if (!existsSync(cp)) continue
|
|
48
|
+
for (const m of readFileSync(cp, 'utf8').matchAll(/docs\/[A-Za-z0-9._-]+\.md/g)) docRefs.add(m[0])
|
|
49
|
+
}
|
|
50
|
+
ok(docRefs.size > 0, 'shipped files cite at least one docs/*.md')
|
|
51
|
+
const unlistedDocs = [...docRefs].filter((f) => !listed.has(f))
|
|
52
|
+
ok(unlistedDocs.length === 0, 'every docs/*.md cited by a shipped file is listed in package.json#files', JSON.stringify(unlistedDocs))
|
|
53
|
+
ok(missingPaths.length === 0, 'every listed path exists', JSON.stringify(missingPaths))
|
|
54
|
+
console.log('')
|
|
55
|
+
console.log('=== PACKAGE MEMBERSHIP: ' + passed + ' passed, ' + failed + ' failed ===')
|
|
56
|
+
for (const f of failures) console.error(' - ' + f)
|
|
57
|
+
process.exit(failed === 0 ? 0 : 1)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Mutant harness for the package-membership audit: drop one listed entry from a COPY of package.json
|
|
4
|
+
* and require the audit to go named-red. Run: node tests/audit-package-membership.mutants.mjs
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs'
|
|
7
|
+
import { tmpdir } from 'node:os'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
import { spawnSync } from 'node:child_process'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
const HERE = fileURLToPath(new URL('./', import.meta.url))
|
|
12
|
+
const REPO = join(HERE, '..')
|
|
13
|
+
let passed = 0, failed = 0
|
|
14
|
+
const ok = (c, l, d) => { if (c) { passed++; console.log(' ok ' + l) } else { failed++; console.log(' FAIL ' + l + (d ? ' — ' + d : '')) } }
|
|
15
|
+
const base = spawnSync(process.execPath, [join(HERE, 'audit-package-membership.mjs')], { cwd: REPO, encoding: 'utf8' })
|
|
16
|
+
ok(base.status === 0, 'baseline: the membership audit is green', String(base.stdout || '').trim().split('\n').slice(-1)[0])
|
|
17
|
+
const dir = mkdtempSync(join(tmpdir(), 'pkgmem-'))
|
|
18
|
+
const pkg = JSON.parse(readFileSync(join(REPO, 'package.json'), 'utf8'))
|
|
19
|
+
const victim = 'tests/audit-package-membership.mjs'
|
|
20
|
+
pkg.files = pkg.files.filter((f) => f !== victim)
|
|
21
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify(pkg, null, 2))
|
|
22
|
+
const mut = spawnSync(process.execPath, [join(HERE, 'audit-package-membership.mjs')], { cwd: REPO, encoding: 'utf8', env: Object.assign({}, process.env, { PACKAGE_JSON: join(dir, 'package.json') }) })
|
|
23
|
+
const out = String(mut.stdout || '') + String(mut.stderr || '')
|
|
24
|
+
ok(mut.status !== 0 && /this audit itself ships/.test(out), '★ removing a listed entry makes the membership audit RED (named assertion)', 'exit=' + mut.status)
|
|
25
|
+
rmSync(dir, { recursive: true, force: true })
|
|
26
|
+
console.log('')
|
|
27
|
+
console.log('=== PACKAGE MEMBERSHIP MUTANTS: ' + passed + ' passed, ' + failed + ' failed ===')
|
|
28
|
+
process.exit(failed === 0 ? 0 : 1)
|