dsh-vibe-math 2.7.3 → 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.
Files changed (101) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +682 -682
  3. package/README.md +683 -683
  4. package/docs/AUDIT-CHECKLIST.md +755 -744
  5. package/docs/COMPAT-AUDIT-ROUND2.md +351 -351
  6. package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
  7. package/docs/audit-methodology.md +93 -0
  8. package/docs/final-paper.md +172 -172
  9. package/docs/formal-verification.md +613 -588
  10. package/docs/generate_framework_diagram_v2.py +114 -114
  11. package/docs/generate_framework_diagram_v3.py +127 -127
  12. package/docs/math-computation.md +195 -194
  13. package/docs/release-notes/README.en.md +1 -0
  14. package/docs/release-notes/README.md +1 -0
  15. package/docs/release-notes/RELEASE-NOTES-2.0.22.md +112 -112
  16. package/docs/release-notes/RELEASE-NOTES-2.1.0.md +143 -143
  17. package/docs/release-notes/RELEASE-NOTES-2.2.0.md +266 -266
  18. package/docs/release-notes/RELEASE-NOTES-2.2.1.md +43 -43
  19. package/docs/release-notes/RELEASE-NOTES-2.2.2.md +88 -88
  20. package/docs/release-notes/RELEASE-NOTES-2.3.0.md +207 -207
  21. package/docs/release-notes/RELEASE-NOTES-2.3.1.md +134 -134
  22. package/docs/release-notes/RELEASE-NOTES-2.3.10.md +105 -105
  23. package/docs/release-notes/RELEASE-NOTES-2.3.11.md +57 -57
  24. package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -80
  25. package/docs/release-notes/RELEASE-NOTES-2.3.2.md +145 -145
  26. package/docs/release-notes/RELEASE-NOTES-2.3.3.md +115 -115
  27. package/docs/release-notes/RELEASE-NOTES-2.3.4.md +69 -69
  28. package/docs/release-notes/RELEASE-NOTES-2.3.5.md +63 -63
  29. package/docs/release-notes/RELEASE-NOTES-2.3.6.md +66 -66
  30. package/docs/release-notes/RELEASE-NOTES-2.3.7.md +59 -59
  31. package/docs/release-notes/RELEASE-NOTES-2.3.8.md +45 -45
  32. package/docs/release-notes/RELEASE-NOTES-2.3.9.md +70 -70
  33. package/docs/release-notes/RELEASE-NOTES-2.8.0.en.md +65 -0
  34. package/docs/release-notes/RELEASE-NOTES-2.8.0.md +65 -0
  35. package/docs/release-playbook.md +103 -0
  36. package/docs/slv-playbook.md +117 -0
  37. package/docs/test-timing.md +158 -157
  38. package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
  39. package/installer.js +792 -792
  40. package/package.json +13 -3
  41. package/prompt-corpus-v2/formal-verify-v2.json +2 -2
  42. package/prompt-corpus-v2/formal-verify-v2.md +4 -0
  43. package/prompt-corpus-v3/formal-verify-v3.json +7 -7
  44. package/prompt-corpus-v3/formal-verify-v3.md +14 -0
  45. package/prompt-corpus-v5/prompt-corpus-v5.json +61 -61
  46. package/prompt-corpus-v5/prompt-corpus-v5.md +122 -0
  47. package/scripts/clean-temp.mjs +234 -0
  48. package/tests/audit-engine-faces.mjs +298 -0
  49. package/tests/audit-formal-sensitivity.mjs +345 -342
  50. package/tests/audit-installer-compat.test.mjs +256 -256
  51. package/tests/audit-math-computation-sensitivity.mjs +1 -1
  52. package/tests/audit-persona-sensitivity.mjs +323 -323
  53. package/tests/audit-persona-surface.test.mjs +431 -431
  54. package/tests/audit-prompt-duplication.mutants.mjs +3 -2
  55. package/tests/audit-prompt-invariants.mjs +589 -546
  56. package/tests/audit-registration.mjs +78 -78
  57. package/tests/audit-spec-traceability.mjs +193 -193
  58. package/tests/audit-status-report-fields.mjs +172 -172
  59. package/tests/audit-v5-integrity.mjs +795 -795
  60. package/tests/audit-v5-sensitivity.mjs +433 -432
  61. package/tests/e2e-v5-round2.test.mjs +2453 -2426
  62. package/tests/formal-verify-v2.test.mjs +1806 -1800
  63. package/tests/formal-verify-v3.test.mjs +1757 -1750
  64. package/tests/formal-verify-v4.test.mjs +2114 -2106
  65. package/tests/formal-verify-v5.test.mjs +774 -767
  66. package/tests/math-computation-discovery.mutants.mjs +3 -3
  67. package/tests/math-computation-shared.test.mjs +122 -5
  68. package/tests/math-computation-v2.test.mjs +14 -2
  69. package/tests/math-computation-v3.test.mjs +14 -2
  70. package/tests/math-computation-v4.test.mjs +739 -739
  71. package/tests/prompt-v5-integrity.test.mjs +1291 -1291
  72. package/tests/run-tests.mjs +393 -332
  73. package/tests/selfdrive-v5.mjs +560 -560
  74. package/tests/temp-hygiene.mutants.mjs +80 -0
  75. package/tests/v2-fix-probes.mutants.mjs +4 -0
  76. package/tests/v2-fix-probes.test.mjs +1034 -1033
  77. package/tests/v2v3-interrupt.mutants.mjs +111 -0
  78. package/tests/v3-fix-probes.mutants.mjs +99 -95
  79. package/tests/v3-fix-probes.test.mjs +891 -890
  80. package/tests/v4-final-paper.mutants.mjs +108 -0
  81. package/tests/v5-institute-fixes.mutants.mjs +218 -190
  82. package/vibe-math-v2/agent.cordis.yml +376 -376
  83. package/vibe-math-v2/math-computation.js +17 -15
  84. package/vibe-math-v2/preset.yml +2 -2
  85. package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
  86. package/vibe-math-v2/vibe-math-v2.js +5078 -5011
  87. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +486 -486
  88. package/vibe-math-v3/agent.cordis.yml +420 -420
  89. package/vibe-math-v3/math-computation.js +17 -15
  90. package/vibe-math-v3/preset.yml +2 -2
  91. package/vibe-math-v3/vibe-math-v3.js +6215 -6149
  92. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
  93. package/vibe-math-v4/agent.cordis.yml +438 -438
  94. package/vibe-math-v4/math-computation.js +17 -15
  95. package/vibe-math-v4/preset.yml +2 -2
  96. package/vibe-math-v4/vibe-math-v4.js +4490 -4422
  97. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1015
  98. package/vibe-math-v5/math-computation.js +17 -15
  99. package/vibe-math-v5/vibe-math-v5.js +93 -11
  100. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1728
  101. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -1,256 +1,256 @@
1
- // ============================================================================================
2
- // INSTALLER HOST-COMPATIBILITY AUDIT
3
- //
4
- // The installer's host self-check warns when the running DSH is outside the range this package
5
- // declares. That verdict must come from the SAME declaration dsh-market renders on the plugin card
6
- // (`engines.dsh` / `dsh.engines.dsh`) and not from a second source, or the two disagree: the card
7
- // says "compatible" while the log warns, or the reverse. This suite pins that:
8
- //
9
- // · the range matcher reproduces semver 7.8.5 exactly — the expectations below were generated with
10
- // npm's own bundled semver (`require('…/npm/node_modules/semver')`) and re-checked against the
11
- // shipped matcher over 320 (range, version, includePrerelease) pairs, 0 mismatches;
12
- // · `dshVersionVerdict` prefers the declared range and falls back to `dsh.compatibility`;
13
- // · every release our own compatibility matrix calls `compatible` is NOT judged incompatible by
14
- // the declared range — the two sources cannot drift apart.
15
- //
16
- // Run: node tests/audit-installer-compat.test.mjs (part of `node tests/run-tests.mjs`)
17
- // ============================================================================================
18
- import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync, cpSync } from 'node:fs'
19
- import { tmpdir } from 'node:os'
20
- import { fileURLToPath, pathToFileURL } from 'node:url'
21
- import { join, resolve } from 'node:path'
22
- import { satisfiesDshRange, dshVersionVerdict, detectDshVersion, PRESETS } from '../installer.js'
23
-
24
- const HERE = fileURLToPath(new URL('../', import.meta.url))
25
- const pkg = JSON.parse(readFileSync(join(HERE, 'package.json'), 'utf8'))
26
- // §4 drives the real apply(); INSTALLER_JS lets the sensitivity probe run it against a reverted copy
27
- const INSTALLER_SRC = process.env.INSTALLER_JS ? resolve(process.env.INSTALLER_JS) : join(HERE, 'installer.js')
28
-
29
- let passed = 0, failed = 0
30
- const failures = []
31
- const ok = (cond, label, detail) => {
32
- if (cond) { passed++; console.log(' ok ' + label); return true }
33
- failed++; failures.push(label + (detail ? ' — ' + detail : ''))
34
- console.error(' FAIL ' + label + (detail ? ' — ' + detail : ''))
35
- return false
36
- }
37
- const eq = (actual, expected, label) => ok(actual === expected, label, 'got ' + JSON.stringify(actual) + ', expected ' + JSON.stringify(expected))
38
-
39
- // ---------------------------------------------------------------------------------------------
40
- // 1. the matcher reproduces semver 7.8.5 (expectations generated with npm's bundled semver)
41
- // ---------------------------------------------------------------------------------------------
42
- const CASES = [
43
- // [range, version, includePrerelease, expected]
44
- ['*', '1.2.3', false, true],
45
- ['*', '0.1.5-rc.2', false, false], // npm: `*` excludes prereleases unless opted in
46
- ['*', '0.1.5-rc.2', true, true],
47
- ['1.2.3', '1.2.3', false, true],
48
- ['1.2.3', '1.2.4', false, false],
49
- ['^0.1.0', '0.1.4', false, true],
50
- ['^0.1.0', '0.2.0-rc.1', true, false], // 0.x caret never reaches the next minor
51
- ['^0.1.0', '0.1.0-rc.5', true, false], // ...nor a prerelease below its own floor
52
- ['^0.1.0-rc.7', '0.1.0-rc.7', false, true],
53
- ['^0.1.0-rc.7', '0.1.5-rc.2', true, true],
54
- ['^0.0.1', '0.0.1', false, true],
55
- ['^0.0.1', '0.0.2', false, false],
56
- ['~0.1.2', '0.1.4', false, true],
57
- ['~0.1.2', '0.2.0', false, false],
58
- ['>0.1.0 <=0.1.5', '0.1.5', false, true],
59
- ['>=0.1.5-rc.1', '0.1.5-rc.2', false, true],
60
- ['>=0.1.5-rc.1', '0.2.0', false, true], // a floor-only range keeps admitting newer hosts
61
- // the trap contributing.md warns about: no comparator on the 0.1.5 tuple carrying a prerelease
62
- ['>=0.1.2-rc.1 <0.2.0', '0.1.5-rc.2', false, false],
63
- ['>=0.1.2-rc.1 <0.2.0', '0.1.5-rc.2', true, true],
64
- // the market-style chain
65
- ['^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2', '0.1.2-rc.1', false, true],
66
- ['^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2', '0.1.3-alpha.2', false, false],
67
- ['^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2', '0.1.3-alpha.2', true, true],
68
- ]
69
- for (const [range, version, pre, expected] of CASES) {
70
- eq(satisfiesDshRange(version, range, { includePrerelease: pre }), expected,
71
- 'matcher: ' + JSON.stringify(range) + ' vs ' + version + (pre ? ' (includePrerelease)' : ''))
72
- }
73
- // our own declaration, both semantics (the exact chain the market and the installer share)
74
- // The window is per-tuple: every supported line has its own prerelease branch, because npm's
75
- // prerelease rule only admits a version when a comparator in the SAME [major,minor,patch] tuple also
76
- // carries a prerelease. 0.2.0 joined the window with the DSH 0.2.0 adaptation.
77
- for (const v of ['0.1.2-alpha.4', '0.1.2-rc.1', '0.1.3-alpha.2', '0.1.5-rc.1', '0.1.5-rc.2', '0.1.6-alpha.2', '0.1.7-rc.2', '0.2.0-rc.1', '0.2.0-rc.2']) {
78
- eq(satisfiesDshRange(v, pkg.engines.dsh), true, 'own range admits ' + v + ' (default semantics)')
79
- eq(satisfiesDshRange(v, pkg.engines.dsh, { includePrerelease: true }), true, 'own range admits ' + v + ' (includePrerelease)')
80
- }
81
- for (const v of ['0.2.0']) {
82
- eq(satisfiesDshRange(v, pkg.engines.dsh, { includePrerelease: true }), true, 'own range admits the released ' + v)
83
- }
84
- for (const v of ['0.1.1-rc.1', '0.3.0-rc.1', '0.3.0', '1.0.0']) {
85
- eq(satisfiesDshRange(v, pkg.engines.dsh, { includePrerelease: true }), false, 'own range rejects ' + v)
86
- }
87
- // unsupported forms are UNKNOWN (null) — reported, never asserted incompatible
88
- for (const r of ['not-a-range', '>=1.2', '1.2.x', '']) {
89
- eq(satisfiesDshRange('1.2.5', r, { includePrerelease: true }), null, 'unsupported range is unknown: ' + JSON.stringify(r))
90
- }
91
- eq(satisfiesDshRange('not-a-version', pkg.engines.dsh), null, 'an unparseable host version is unknown, not incompatible')
92
-
93
- // ---------------------------------------------------------------------------------------------
94
- // 2. the verdict prefers the declared range, falls back to the matrix
95
- // ---------------------------------------------------------------------------------------------
96
- {
97
- const a = dshVersionVerdict('0.1.5-rc.2', pkg)
98
- ok(a.status === 'compatible' && a.basis === 'engines', 'our own host line is compatible via the declared range', JSON.stringify(a))
99
- eq(a.requirement, pkg.engines.dsh, 'the verdict surfaces the declared range (so the log and the market card say the same thing)')
100
- const b = dshVersionVerdict('0.3.0', pkg)
101
- ok(b.status === 'incompatible' && b.basis === 'engines', 'a host outside the declared range is incompatible via the range', JSON.stringify(b))
102
- const c = dshVersionVerdict('0.1.0-rc.6', pkg)
103
- ok(c.status === 'incompatible', 'a host below the declared floor is incompatible', JSON.stringify(c))
104
- // dsh.engines.dsh is the second position the ecosystem (and the market) reads
105
- const onlyNested = { dsh: { engines: { dsh: '>=0.1.0-rc.6' } } }
106
- const d = dshVersionVerdict('0.1.0-rc.6', onlyNested)
107
- ok(d.status === 'compatible' && d.basis === 'engines', 'dsh.engines.dsh is honoured when engines.dsh is absent', JSON.stringify(d))
108
- // top-level wins when both are present (mirrors dsh-market's manifestFacts)
109
- const both = { engines: { dsh: '>=0.2.0' }, dsh: { engines: { dsh: '>=0.1.0' } } }
110
- eq(dshVersionVerdict('0.1.5-rc.2', both).status, 'incompatible', 'engines.dsh wins when both positions are declared')
111
- // fallback: a manifest that declares no range keeps the dshReleases behaviour
112
- const mapOnly = {
113
- dsh: { compatibility: { dshReleases: { '0.1.5-rc.2': 'compatible', '0.1.0-rc.1': 'incompatible', '0.1.9': 'unknown' } } },
114
- }
115
- const e = dshVersionVerdict('0.1.5-rc.2', mapOnly)
116
- ok(e.status === 'compatible' && e.basis === 'dshReleases', 'fallback: the matrix still decides when no range is declared', JSON.stringify(e))
117
- eq(dshVersionVerdict('0.1.0-rc.1', mapOnly).status, 'incompatible', 'fallback: an explicitly incompatible release')
118
- eq(dshVersionVerdict('0.1.7', mapOnly).status, 'undeclared', 'fallback: an unlisted release is undeclared (soft warning)')
119
- eq(dshVersionVerdict('0.1.9', mapOnly).status, 'unknown', 'fallback: an explicitly unknown release')
120
- // a range the matcher cannot parse must not become "incompatible"
121
- const unparsable = { engines: { dsh: 'not a range' } }
122
- const f = dshVersionVerdict('0.1.5-rc.2', unparsable)
123
- ok(f.status === 'unknown' && f.basis === 'engines', 'an unparseable declared range is unknown, never incompatible', JSON.stringify(f))
124
- }
125
-
126
- // ---------------------------------------------------------------------------------------------
127
- // 3. the two sources cannot drift: every release our matrix calls compatible must not be judged
128
- // incompatible by the declared range (the installer and the market must agree).
129
- // ---------------------------------------------------------------------------------------------
130
- {
131
- const matrix = Object.entries((pkg.dsh.compatibility || {}).dshReleases || {})
132
- const contradicted = matrix.filter(([ver, status]) => {
133
- const v = dshVersionVerdict(ver, pkg)
134
- return status === 'compatible' && v.status === 'incompatible'
135
- }).map(([ver]) => ver)
136
- ok(contradicted.length === 0, 'no release marked compatible in dsh.compatibility is rejected by engines.dsh',
137
- contradicted.join(', '))
138
- const missedByRange = matrix.filter(([ver]) => dshVersionVerdict(ver, pkg).status === 'unknown').map(([ver]) => ver)
139
- ok(missedByRange.length === 0, 'the declared range can be evaluated for every release in the matrix', missedByRange.join(', '))
140
- }
141
-
142
- // ---------------------------------------------------------------------------------------------
143
- // 4. the version probe must not be able to hold the installer ROW's activation
144
- //
145
- // `detectDshVersion` asks `pluginManager.listBundles()` for the runtime version, and it runs at the
146
- // top of apply(). The result is diagnostic only — the preset mechanism is decided by the loader tree
147
- // / the service's `register`, never by the version — so a slow or stuck plugin manager must fall
148
- // through to the next source instead of stalling the boot. (Round B, machinery audit.)
149
- // ---------------------------------------------------------------------------------------------
150
- console.log('=== 4. a pluginManager that never answers must not hold the row ===')
151
- {
152
- const tmp = mkdtempSync(join(tmpdir(), 'vibe-installer-timeout-'))
153
- const pkg = join(tmp, 'pkg')
154
- mkdirSync(pkg, { recursive: true })
155
- cpSync(INSTALLER_SRC, join(pkg, 'installer.js'))
156
- writeFileSync(join(pkg, 'package.json'), JSON.stringify({ name: 'dsh-vibe-math', version: '9.9.9', type: 'module' }, null, 2))
157
- for (const p of PRESETS) {
158
- mkdirSync(join(pkg, p.src), { recursive: true })
159
- for (const f of p.files) cpSync(join(HERE, p.src, f), join(pkg, p.src, f))
160
- }
161
- const home = join(tmp, 'dshhome')
162
- mkdirSync(home, { recursive: true })
163
- process.env.DSH_HOME = home
164
- delete process.env.DSH_VERSION // leave the pluginManager as the only source
165
- const presetRoot = join(home, '.agent-presets')
166
- // import the COPY, so `here` inside installer.js is the throwaway package (manifest + preset dirs)
167
- const mod = await import(pathToFileURL(join(pkg, 'installer.js')).href + '?t=' + Date.now())
168
-
169
- const run = (services) => {
170
- const logs = []
171
- const ctx = {
172
- get: (n) => (n === 'pluginManager' ? services : undefined),
173
- logger: { info: (m) => logs.push('info: ' + m), warn: (m) => logs.push('warn: ' + m), error: (m) => logs.push('error: ' + m) },
174
- }
175
- return { ctx, logs }
176
- }
177
-
178
- // a promise that never settles: without a timeout apply() would await it forever
179
- const HARD_LIMIT_MS = 8000
180
- const { ctx, logs } = run({ listBundles: () => new Promise(() => {}) })
181
- let verdict = 'hung'
182
- const t0 = Date.now()
183
- let hardTimer
184
- await Promise.race([
185
- mod.apply(ctx).then(() => { verdict = 'returned' }, (e) => { verdict = 'threw: ' + String(e && e.message) }),
186
- new Promise((resolve) => { hardTimer = setTimeout(resolve, HARD_LIMIT_MS) }),
187
- ])
188
- clearTimeout(hardTimer) // the shipped installer wins the race in ~1.5 s; do not hold the suite open
189
- const elapsed = Date.now() - t0
190
- ok(verdict === 'returned', 'apply() returns instead of waiting on a pluginManager that never answers', verdict)
191
- ok(elapsed < 6000, '...within a short timeout, not at the hard test limit', elapsed + 'ms')
192
- const copied = PRESETS.every((p) => p.files.every((f) => existsSync(join(presetRoot, p.dst, f))))
193
- ok(copied, '...and the preset copy still happens (the version probe gates nothing)')
194
- ok(!logs.some((l) => l.includes('preset install/update failed')), 'the timeout is handled as a fallback, not as an error')
195
-
196
- // the timeout must NOT replace a working probe: a prompt service still supplies the version
197
- const quick = run({ listBundles: async () => [{ name: '@deepseek-ai/dsh-base', version: '0.2.0-rc.2' }] })
198
- await mod.apply(quick.ctx)
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)')
211
- rmSync(tmp, { recursive: true, force: true })
212
- }
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
-
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
-
254
- console.log('=== INSTALLER COMPAT: ' + passed + ' passed, ' + failed + ' failed ===')
255
- if (failed) { for (const f of failures) console.error(' - ' + f); process.exit(1) }
256
- console.log('ALL GREEN')
1
+ // ============================================================================================
2
+ // INSTALLER HOST-COMPATIBILITY AUDIT
3
+ //
4
+ // The installer's host self-check warns when the running DSH is outside the range this package
5
+ // declares. That verdict must come from the SAME declaration dsh-market renders on the plugin card
6
+ // (`engines.dsh` / `dsh.engines.dsh`) and not from a second source, or the two disagree: the card
7
+ // says "compatible" while the log warns, or the reverse. This suite pins that:
8
+ //
9
+ // · the range matcher reproduces semver 7.8.5 exactly — the expectations below were generated with
10
+ // npm's own bundled semver (`require('…/npm/node_modules/semver')`) and re-checked against the
11
+ // shipped matcher over 320 (range, version, includePrerelease) pairs, 0 mismatches;
12
+ // · `dshVersionVerdict` prefers the declared range and falls back to `dsh.compatibility`;
13
+ // · every release our own compatibility matrix calls `compatible` is NOT judged incompatible by
14
+ // the declared range — the two sources cannot drift apart.
15
+ //
16
+ // Run: node tests/audit-installer-compat.test.mjs (part of `node tests/run-tests.mjs`)
17
+ // ============================================================================================
18
+ import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync, cpSync } from 'node:fs'
19
+ import { tmpdir } from 'node:os'
20
+ import { fileURLToPath, pathToFileURL } from 'node:url'
21
+ import { join, resolve } from 'node:path'
22
+ import { satisfiesDshRange, dshVersionVerdict, detectDshVersion, PRESETS } from '../installer.js'
23
+
24
+ const HERE = fileURLToPath(new URL('../', import.meta.url))
25
+ const pkg = JSON.parse(readFileSync(join(HERE, 'package.json'), 'utf8'))
26
+ // §4 drives the real apply(); INSTALLER_JS lets the sensitivity probe run it against a reverted copy
27
+ const INSTALLER_SRC = process.env.INSTALLER_JS ? resolve(process.env.INSTALLER_JS) : join(HERE, 'installer.js')
28
+
29
+ let passed = 0, failed = 0
30
+ const failures = []
31
+ const ok = (cond, label, detail) => {
32
+ if (cond) { passed++; console.log(' ok ' + label); return true }
33
+ failed++; failures.push(label + (detail ? ' — ' + detail : ''))
34
+ console.error(' FAIL ' + label + (detail ? ' — ' + detail : ''))
35
+ return false
36
+ }
37
+ const eq = (actual, expected, label) => ok(actual === expected, label, 'got ' + JSON.stringify(actual) + ', expected ' + JSON.stringify(expected))
38
+
39
+ // ---------------------------------------------------------------------------------------------
40
+ // 1. the matcher reproduces semver 7.8.5 (expectations generated with npm's bundled semver)
41
+ // ---------------------------------------------------------------------------------------------
42
+ const CASES = [
43
+ // [range, version, includePrerelease, expected]
44
+ ['*', '1.2.3', false, true],
45
+ ['*', '0.1.5-rc.2', false, false], // npm: `*` excludes prereleases unless opted in
46
+ ['*', '0.1.5-rc.2', true, true],
47
+ ['1.2.3', '1.2.3', false, true],
48
+ ['1.2.3', '1.2.4', false, false],
49
+ ['^0.1.0', '0.1.4', false, true],
50
+ ['^0.1.0', '0.2.0-rc.1', true, false], // 0.x caret never reaches the next minor
51
+ ['^0.1.0', '0.1.0-rc.5', true, false], // ...nor a prerelease below its own floor
52
+ ['^0.1.0-rc.7', '0.1.0-rc.7', false, true],
53
+ ['^0.1.0-rc.7', '0.1.5-rc.2', true, true],
54
+ ['^0.0.1', '0.0.1', false, true],
55
+ ['^0.0.1', '0.0.2', false, false],
56
+ ['~0.1.2', '0.1.4', false, true],
57
+ ['~0.1.2', '0.2.0', false, false],
58
+ ['>0.1.0 <=0.1.5', '0.1.5', false, true],
59
+ ['>=0.1.5-rc.1', '0.1.5-rc.2', false, true],
60
+ ['>=0.1.5-rc.1', '0.2.0', false, true], // a floor-only range keeps admitting newer hosts
61
+ // the trap contributing.md warns about: no comparator on the 0.1.5 tuple carrying a prerelease
62
+ ['>=0.1.2-rc.1 <0.2.0', '0.1.5-rc.2', false, false],
63
+ ['>=0.1.2-rc.1 <0.2.0', '0.1.5-rc.2', true, true],
64
+ // the market-style chain
65
+ ['^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2', '0.1.2-rc.1', false, true],
66
+ ['^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2', '0.1.3-alpha.2', false, false],
67
+ ['^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2', '0.1.3-alpha.2', true, true],
68
+ ]
69
+ for (const [range, version, pre, expected] of CASES) {
70
+ eq(satisfiesDshRange(version, range, { includePrerelease: pre }), expected,
71
+ 'matcher: ' + JSON.stringify(range) + ' vs ' + version + (pre ? ' (includePrerelease)' : ''))
72
+ }
73
+ // our own declaration, both semantics (the exact chain the market and the installer share)
74
+ // The window is per-tuple: every supported line has its own prerelease branch, because npm's
75
+ // prerelease rule only admits a version when a comparator in the SAME [major,minor,patch] tuple also
76
+ // carries a prerelease. 0.2.0 joined the window with the DSH 0.2.0 adaptation.
77
+ for (const v of ['0.1.2-alpha.4', '0.1.2-rc.1', '0.1.3-alpha.2', '0.1.5-rc.1', '0.1.5-rc.2', '0.1.6-alpha.2', '0.1.7-rc.2', '0.2.0-rc.1', '0.2.0-rc.2']) {
78
+ eq(satisfiesDshRange(v, pkg.engines.dsh), true, 'own range admits ' + v + ' (default semantics)')
79
+ eq(satisfiesDshRange(v, pkg.engines.dsh, { includePrerelease: true }), true, 'own range admits ' + v + ' (includePrerelease)')
80
+ }
81
+ for (const v of ['0.2.0']) {
82
+ eq(satisfiesDshRange(v, pkg.engines.dsh, { includePrerelease: true }), true, 'own range admits the released ' + v)
83
+ }
84
+ for (const v of ['0.1.1-rc.1', '0.3.0-rc.1', '0.3.0', '1.0.0']) {
85
+ eq(satisfiesDshRange(v, pkg.engines.dsh, { includePrerelease: true }), false, 'own range rejects ' + v)
86
+ }
87
+ // unsupported forms are UNKNOWN (null) — reported, never asserted incompatible
88
+ for (const r of ['not-a-range', '>=1.2', '1.2.x', '']) {
89
+ eq(satisfiesDshRange('1.2.5', r, { includePrerelease: true }), null, 'unsupported range is unknown: ' + JSON.stringify(r))
90
+ }
91
+ eq(satisfiesDshRange('not-a-version', pkg.engines.dsh), null, 'an unparseable host version is unknown, not incompatible')
92
+
93
+ // ---------------------------------------------------------------------------------------------
94
+ // 2. the verdict prefers the declared range, falls back to the matrix
95
+ // ---------------------------------------------------------------------------------------------
96
+ {
97
+ const a = dshVersionVerdict('0.1.5-rc.2', pkg)
98
+ ok(a.status === 'compatible' && a.basis === 'engines', 'our own host line is compatible via the declared range', JSON.stringify(a))
99
+ eq(a.requirement, pkg.engines.dsh, 'the verdict surfaces the declared range (so the log and the market card say the same thing)')
100
+ const b = dshVersionVerdict('0.3.0', pkg)
101
+ ok(b.status === 'incompatible' && b.basis === 'engines', 'a host outside the declared range is incompatible via the range', JSON.stringify(b))
102
+ const c = dshVersionVerdict('0.1.0-rc.6', pkg)
103
+ ok(c.status === 'incompatible', 'a host below the declared floor is incompatible', JSON.stringify(c))
104
+ // dsh.engines.dsh is the second position the ecosystem (and the market) reads
105
+ const onlyNested = { dsh: { engines: { dsh: '>=0.1.0-rc.6' } } }
106
+ const d = dshVersionVerdict('0.1.0-rc.6', onlyNested)
107
+ ok(d.status === 'compatible' && d.basis === 'engines', 'dsh.engines.dsh is honoured when engines.dsh is absent', JSON.stringify(d))
108
+ // top-level wins when both are present (mirrors dsh-market's manifestFacts)
109
+ const both = { engines: { dsh: '>=0.2.0' }, dsh: { engines: { dsh: '>=0.1.0' } } }
110
+ eq(dshVersionVerdict('0.1.5-rc.2', both).status, 'incompatible', 'engines.dsh wins when both positions are declared')
111
+ // fallback: a manifest that declares no range keeps the dshReleases behaviour
112
+ const mapOnly = {
113
+ dsh: { compatibility: { dshReleases: { '0.1.5-rc.2': 'compatible', '0.1.0-rc.1': 'incompatible', '0.1.9': 'unknown' } } },
114
+ }
115
+ const e = dshVersionVerdict('0.1.5-rc.2', mapOnly)
116
+ ok(e.status === 'compatible' && e.basis === 'dshReleases', 'fallback: the matrix still decides when no range is declared', JSON.stringify(e))
117
+ eq(dshVersionVerdict('0.1.0-rc.1', mapOnly).status, 'incompatible', 'fallback: an explicitly incompatible release')
118
+ eq(dshVersionVerdict('0.1.7', mapOnly).status, 'undeclared', 'fallback: an unlisted release is undeclared (soft warning)')
119
+ eq(dshVersionVerdict('0.1.9', mapOnly).status, 'unknown', 'fallback: an explicitly unknown release')
120
+ // a range the matcher cannot parse must not become "incompatible"
121
+ const unparsable = { engines: { dsh: 'not a range' } }
122
+ const f = dshVersionVerdict('0.1.5-rc.2', unparsable)
123
+ ok(f.status === 'unknown' && f.basis === 'engines', 'an unparseable declared range is unknown, never incompatible', JSON.stringify(f))
124
+ }
125
+
126
+ // ---------------------------------------------------------------------------------------------
127
+ // 3. the two sources cannot drift: every release our matrix calls compatible must not be judged
128
+ // incompatible by the declared range (the installer and the market must agree).
129
+ // ---------------------------------------------------------------------------------------------
130
+ {
131
+ const matrix = Object.entries((pkg.dsh.compatibility || {}).dshReleases || {})
132
+ const contradicted = matrix.filter(([ver, status]) => {
133
+ const v = dshVersionVerdict(ver, pkg)
134
+ return status === 'compatible' && v.status === 'incompatible'
135
+ }).map(([ver]) => ver)
136
+ ok(contradicted.length === 0, 'no release marked compatible in dsh.compatibility is rejected by engines.dsh',
137
+ contradicted.join(', '))
138
+ const missedByRange = matrix.filter(([ver]) => dshVersionVerdict(ver, pkg).status === 'unknown').map(([ver]) => ver)
139
+ ok(missedByRange.length === 0, 'the declared range can be evaluated for every release in the matrix', missedByRange.join(', '))
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------------------------
143
+ // 4. the version probe must not be able to hold the installer ROW's activation
144
+ //
145
+ // `detectDshVersion` asks `pluginManager.listBundles()` for the runtime version, and it runs at the
146
+ // top of apply(). The result is diagnostic only — the preset mechanism is decided by the loader tree
147
+ // / the service's `register`, never by the version — so a slow or stuck plugin manager must fall
148
+ // through to the next source instead of stalling the boot. (Round B, machinery audit.)
149
+ // ---------------------------------------------------------------------------------------------
150
+ console.log('=== 4. a pluginManager that never answers must not hold the row ===')
151
+ {
152
+ const tmp = mkdtempSync(join(tmpdir(), 'vibe-installer-timeout-'))
153
+ const pkg = join(tmp, 'pkg')
154
+ mkdirSync(pkg, { recursive: true })
155
+ cpSync(INSTALLER_SRC, join(pkg, 'installer.js'))
156
+ writeFileSync(join(pkg, 'package.json'), JSON.stringify({ name: 'dsh-vibe-math', version: '9.9.9', type: 'module' }, null, 2))
157
+ for (const p of PRESETS) {
158
+ mkdirSync(join(pkg, p.src), { recursive: true })
159
+ for (const f of p.files) cpSync(join(HERE, p.src, f), join(pkg, p.src, f))
160
+ }
161
+ const home = join(tmp, 'dshhome')
162
+ mkdirSync(home, { recursive: true })
163
+ process.env.DSH_HOME = home
164
+ delete process.env.DSH_VERSION // leave the pluginManager as the only source
165
+ const presetRoot = join(home, '.agent-presets')
166
+ // import the COPY, so `here` inside installer.js is the throwaway package (manifest + preset dirs)
167
+ const mod = await import(pathToFileURL(join(pkg, 'installer.js')).href + '?t=' + Date.now())
168
+
169
+ const run = (services) => {
170
+ const logs = []
171
+ const ctx = {
172
+ get: (n) => (n === 'pluginManager' ? services : undefined),
173
+ logger: { info: (m) => logs.push('info: ' + m), warn: (m) => logs.push('warn: ' + m), error: (m) => logs.push('error: ' + m) },
174
+ }
175
+ return { ctx, logs }
176
+ }
177
+
178
+ // a promise that never settles: without a timeout apply() would await it forever
179
+ const HARD_LIMIT_MS = 8000
180
+ const { ctx, logs } = run({ listBundles: () => new Promise(() => {}) })
181
+ let verdict = 'hung'
182
+ const t0 = Date.now()
183
+ let hardTimer
184
+ await Promise.race([
185
+ mod.apply(ctx).then(() => { verdict = 'returned' }, (e) => { verdict = 'threw: ' + String(e && e.message) }),
186
+ new Promise((resolve) => { hardTimer = setTimeout(resolve, HARD_LIMIT_MS) }),
187
+ ])
188
+ clearTimeout(hardTimer) // the shipped installer wins the race in ~1.5 s; do not hold the suite open
189
+ const elapsed = Date.now() - t0
190
+ ok(verdict === 'returned', 'apply() returns instead of waiting on a pluginManager that never answers', verdict)
191
+ ok(elapsed < 6000, '...within a short timeout, not at the hard test limit', elapsed + 'ms')
192
+ const copied = PRESETS.every((p) => p.files.every((f) => existsSync(join(presetRoot, p.dst, f))))
193
+ ok(copied, '...and the preset copy still happens (the version probe gates nothing)')
194
+ ok(!logs.some((l) => l.includes('preset install/update failed')), 'the timeout is handled as a fallback, not as an error')
195
+
196
+ // the timeout must NOT replace a working probe: a prompt service still supplies the version
197
+ const quick = run({ listBundles: async () => [{ name: '@deepseek-ai/dsh-base', version: '0.2.0-rc.2' }] })
198
+ await mod.apply(quick.ctx)
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)')
211
+ rmSync(tmp, { recursive: true, force: true })
212
+ }
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
+
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
+
254
+ console.log('=== INSTALLER COMPAT: ' + passed + ' passed, ' + failed + ' failed ===')
255
+ if (failed) { for (const f of failures) console.error(' - ' + f); process.exit(1) }
256
+ console.log('ALL GREEN')
@@ -210,7 +210,7 @@ function runSuite(modulePath) {
210
210
  for (const p of PROBES) {
211
211
  const dir = mkdtempSync(join(tmpdir(), 'mc-sens-' + p.name + '-'))
212
212
  copyFileSync(join(SRC, 'math-engines.js'), join(dir, 'math-engines.js'))
213
- 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
214
214
  if (src.indexOf(p.from) === -1) {
215
215
  bad++
216
216
  console.log(' FAIL ' + p.name + ' — mutation anchor no longer applies (drift): ' + JSON.stringify(p.from.slice(0, 60)))