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.
Files changed (153) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +682 -681
  3. package/README.md +683 -682
  4. package/cordis.patch.yml +112 -16
  5. package/docs/AUDIT-CHECKLIST.md +755 -518
  6. package/docs/COMPAT-AUDIT-ROUND2.md +354 -328
  7. package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
  8. package/docs/audit-methodology.md +93 -0
  9. package/docs/final-paper.md +172 -172
  10. package/docs/formal-verification.md +613 -588
  11. package/docs/generate_framework_diagram_v2.py +114 -114
  12. package/docs/generate_framework_diagram_v3.py +127 -127
  13. package/docs/math-computation.md +195 -187
  14. package/docs/parameter-schema.md +37 -0
  15. package/docs/release-notes/README.en.md +2 -0
  16. package/docs/release-notes/README.md +2 -0
  17. package/docs/release-notes/RELEASE-NOTES-2.0.22.md +112 -112
  18. package/docs/release-notes/RELEASE-NOTES-2.1.0.md +143 -143
  19. package/docs/release-notes/RELEASE-NOTES-2.2.0.md +266 -266
  20. package/docs/release-notes/RELEASE-NOTES-2.2.1.md +43 -43
  21. package/docs/release-notes/RELEASE-NOTES-2.2.2.md +88 -88
  22. package/docs/release-notes/RELEASE-NOTES-2.3.0.md +207 -207
  23. package/docs/release-notes/RELEASE-NOTES-2.3.1.md +134 -134
  24. package/docs/release-notes/RELEASE-NOTES-2.3.10.md +105 -105
  25. package/docs/release-notes/RELEASE-NOTES-2.3.11.md +57 -57
  26. package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -80
  27. package/docs/release-notes/RELEASE-NOTES-2.3.2.md +145 -145
  28. package/docs/release-notes/RELEASE-NOTES-2.3.3.md +115 -115
  29. package/docs/release-notes/RELEASE-NOTES-2.3.4.md +69 -69
  30. package/docs/release-notes/RELEASE-NOTES-2.3.5.md +63 -63
  31. package/docs/release-notes/RELEASE-NOTES-2.3.6.md +66 -66
  32. package/docs/release-notes/RELEASE-NOTES-2.3.7.md +59 -59
  33. package/docs/release-notes/RELEASE-NOTES-2.3.8.md +45 -45
  34. package/docs/release-notes/RELEASE-NOTES-2.3.9.md +70 -70
  35. package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +2 -2
  36. package/docs/release-notes/RELEASE-NOTES-2.7.2.md +3 -3
  37. package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
  38. package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
  39. package/docs/release-notes/RELEASE-NOTES-2.8.0.en.md +65 -0
  40. package/docs/release-notes/RELEASE-NOTES-2.8.0.md +65 -0
  41. package/docs/release-notes/TEMPLATE.md +6 -0
  42. package/docs/release-playbook.md +103 -0
  43. package/docs/slv-playbook.md +117 -0
  44. package/docs/status-report-fields.md +167 -0
  45. package/docs/test-timing.md +158 -154
  46. package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
  47. package/installer.js +793 -783
  48. package/package.json +260 -203
  49. package/prompt-corpus-persona/persona-corpus.json +9 -9
  50. package/prompt-corpus-persona/persona-corpus.md +225 -33
  51. package/prompt-corpus-v2/formal-verify-v2.json +295 -115
  52. package/prompt-corpus-v2/formal-verify-v2.md +2290 -282
  53. package/prompt-corpus-v3/formal-verify-v3.json +112 -112
  54. package/prompt-corpus-v3/formal-verify-v3.md +227 -143
  55. package/prompt-corpus-v4/formal-verify-v4.json +18 -18
  56. package/prompt-corpus-v4/formal-verify-v4.md +49 -44
  57. package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
  58. package/prompt-corpus-v5/prompt-corpus-v5.md +573 -207
  59. package/scripts/clean-temp.mjs +234 -0
  60. package/scripts/update-doc-counts.mjs +71 -0
  61. package/tests/audit-artifact-docs.mjs +72 -0
  62. package/tests/audit-artifact-docs.mutants.mjs +58 -0
  63. package/tests/audit-engine-faces.mjs +298 -0
  64. package/tests/audit-formal-sensitivity.mjs +345 -342
  65. package/tests/audit-installer-assertions.mutants.mjs +63 -0
  66. package/tests/audit-installer-compat.test.mjs +256 -208
  67. package/tests/audit-installer-policy.test.mjs +41 -8
  68. package/tests/audit-math-computation-contract.mjs +64 -8
  69. package/tests/audit-math-computation-contract.mutants.mjs +106 -0
  70. package/tests/audit-math-computation-parity.mjs +164 -3
  71. package/tests/audit-math-computation-sensitivity.mjs +5 -3
  72. package/tests/audit-package-membership.mjs +57 -0
  73. package/tests/audit-package-membership.mutants.mjs +28 -0
  74. package/tests/audit-participant-set-parity.mjs +160 -0
  75. package/tests/audit-path-discipline.mjs +83 -0
  76. package/tests/audit-path-discipline.mutants.mjs +51 -0
  77. package/tests/audit-persona-sensitivity.mjs +323 -309
  78. package/tests/audit-persona-surface.mutants.mjs +69 -0
  79. package/tests/audit-persona-surface.test.mjs +431 -390
  80. package/tests/audit-prompt-duplication.mjs +143 -0
  81. package/tests/audit-prompt-duplication.mutants.mjs +51 -0
  82. package/tests/audit-prompt-invariants.mjs +589 -508
  83. package/tests/audit-readme-bilingual.test.mjs +152 -0
  84. package/tests/audit-readme-counts.mjs +95 -0
  85. package/tests/audit-readme-counts.mutants.mjs +52 -0
  86. package/tests/audit-registration.mjs +78 -0
  87. package/tests/audit-spec-traceability.mjs +193 -193
  88. package/tests/audit-status-report-fields.mjs +172 -0
  89. package/tests/audit-status-report-fields.mutants.mjs +64 -0
  90. package/tests/audit-v3-registration-parity.mjs +156 -0
  91. package/tests/audit-v5-integrity.mjs +795 -508
  92. package/tests/audit-v5-lean-abstention.mjs +392 -0
  93. package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
  94. package/tests/audit-v5-prompt-duplication.mjs +88 -0
  95. package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
  96. package/tests/audit-v5-sensitivity.mjs +433 -427
  97. package/tests/e2e-identity-a6.mutants.mjs +65 -0
  98. package/tests/e2e-v5-round2.test.mjs +2453 -1981
  99. package/tests/formal-verify-v2.test.mjs +1778 -1724
  100. package/tests/formal-verify-v3.mutants.mjs +117 -0
  101. package/tests/formal-verify-v3.test.mjs +1724 -1671
  102. package/tests/formal-verify-v4.mutants.mjs +152 -0
  103. package/tests/formal-verify-v4.test.mjs +2114 -1574
  104. package/tests/formal-verify-v5.test.mjs +790 -724
  105. package/tests/helpers/doc-counts-format.mjs +41 -0
  106. package/tests/helpers/math-computation-fake-seam.mjs +4 -0
  107. package/tests/math-computation-a1.mutants.mjs +72 -0
  108. package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
  109. package/tests/math-computation-discovery.mutants.mjs +76 -0
  110. package/tests/math-computation-shared.test.mjs +392 -6
  111. package/tests/math-computation-v2.test.mjs +779 -0
  112. package/tests/math-computation-v3.test.mjs +771 -0
  113. package/tests/math-computation-v4.mutants.mjs +133 -0
  114. package/tests/math-computation-v4.test.mjs +739 -0
  115. package/tests/prompt-v5-integrity.test.mjs +1291 -1280
  116. package/tests/run-tests.mjs +393 -202
  117. package/tests/run-tests.mutants.mjs +70 -0
  118. package/tests/selfdrive-v5.mjs +560 -508
  119. package/tests/selfdrive-v5.mutants.mjs +132 -0
  120. package/tests/temp-hygiene.mutants.mjs +80 -0
  121. package/tests/v2-fix-probes.mutants.mjs +104 -0
  122. package/tests/v2-fix-probes.test.mjs +1040 -0
  123. package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
  124. package/tests/v2v3-interrupt.mutants.mjs +111 -0
  125. package/tests/v3-fix-probes.mutants.mjs +99 -0
  126. package/tests/v3-fix-probes.test.mjs +892 -0
  127. package/tests/v4-final-paper.mutants.mjs +108 -0
  128. package/tests/v5-institute-fixes.mutants.mjs +218 -0
  129. package/vibe-math-v2/agent.cordis.yml +378 -322
  130. package/vibe-math-v2/math-computation.js +144 -53
  131. package/vibe-math-v2/math-engines.js +12 -12
  132. package/vibe-math-v2/preset.yml +2 -2
  133. package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
  134. package/vibe-math-v2/vibe-math-v2.js +5097 -4755
  135. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +491 -487
  136. package/vibe-math-v3/agent.cordis.yml +422 -356
  137. package/vibe-math-v3/math-computation.js +144 -53
  138. package/vibe-math-v3/math-engines.js +12 -12
  139. package/vibe-math-v3/preset.yml +2 -2
  140. package/vibe-math-v3/vibe-math-v3.js +6235 -5873
  141. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
  142. package/vibe-math-v4/agent.cordis.yml +438 -402
  143. package/vibe-math-v4/math-computation.js +144 -53
  144. package/vibe-math-v4/math-engines.js +12 -12
  145. package/vibe-math-v4/preset.yml +2 -2
  146. package/vibe-math-v4/vibe-math-v4.js +4490 -4269
  147. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1013
  148. package/vibe-math-v5/agent.cordis.yml +46 -12
  149. package/vibe-math-v5/math-computation.js +144 -53
  150. package/vibe-math-v5/math-engines.js +12 -12
  151. package/vibe-math-v5/vibe-math-v5.js +1266 -229
  152. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1660
  153. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Mutant harness for the installer ASSERTION-QUALITY work (ships: `tests/*.mutants.mjs`).
4
+ *
5
+ * It replaces the dev-only scratch proofs (`_oneoff/auditR2/a78-mutants.mjs`, `a8-mutant.mjs`) so a package
6
+ * reader can reproduce them from the tarball. Every case is a SINGLE-SITE edit to a COPY of installer.js,
7
+ * injected through the guards' `INSTALLER_JS` env seam, and must redden the NAMED assertion:
8
+ *
9
+ * A7-version perturb the structured version the probe returns -> compat: "STRUCTURED field: version"
10
+ * A7-source make the structured source stop naming the service -> compat: "structured field naming the service"
11
+ * A8-failure inject one failure line in the backup path -> policy: "NOT reported as a failure (report-level"
12
+ * DL-label revert one label to English-only -> policy: "English-only label"
13
+ * DL-duplicate report the cleanup action a second time -> policy: "reported EXACTLY ONCE"
14
+ *
15
+ * Run: node tests/audit-installer-assertions.mutants.mjs
16
+ */
17
+ import { mkdtempSync, copyFileSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
18
+ import { tmpdir } from 'node:os'
19
+ import { join } from 'node:path'
20
+ import { spawnSync } from 'node:child_process'
21
+ import { fileURLToPath } from 'node:url'
22
+
23
+ const HERE = fileURLToPath(new URL('./', import.meta.url))
24
+ const REPO = join(HERE, '..')
25
+ const SRC = join(REPO, 'installer.js')
26
+ const COMPAT = join(HERE, 'audit-installer-compat.test.mjs')
27
+ const POLICY = join(HERE, 'audit-installer-policy.test.mjs')
28
+ let passed = 0, failed = 0
29
+ const ok = (c, l, d) => { if (c) { passed++; console.log(' ok ' + l) } else { failed++; console.log(' FAIL ' + l + (d ? ' — ' + d : '')) } }
30
+
31
+ const ORIGIN = readFileSync(SRC, 'utf8')
32
+ const OUT_LINE = 'const out = { version: first.version, source: first.source, probes: probes.slice() }'
33
+ const CLEANUP_IF = 'if (removedFiles > 0 || removedDirs.length > 0 || cleanupNotes.length > 0) {'
34
+ const CASES = [
35
+ { name: 'A7-version: the structured version is perturbed', guard: COMPAT, expect: 'STRUCTURED field: version',
36
+ edit: (s) => s.replace(OUT_LINE, "const out = { version: String(first.version) + '-mutated', source: first.source, probes: probes.slice() }") },
37
+ { name: 'A7-source: the structured source stops naming the service', guard: COMPAT, expect: 'structured field naming the service',
38
+ edit: (s) => s.replace(OUT_LINE, "const out = { version: first.version, source: 'pm', probes: probes.slice() }") },
39
+ { name: 'A8-failure: a failure line is injected in the backup path', guard: POLICY, expect: 'NOT reported as a failure (report-level',
40
+ edit: (s) => s.replace('const backupFailures = []', "const backupFailures = []\n logger?.warn?.('[dsh-vibe-math] 预设安装/更新失败:mutant injected failure line')") },
41
+ { name: 'DL-label: one label is reverted to English-only', guard: POLICY, expect: 'English-only label',
42
+ edit: (s) => s.replace("'[dsh-vibe-math] 预设清理:", "'[dsh-vibe-math] preset cleanup: ") },
43
+ { name: 'DL-duplicate: the cleanup action is reported twice', guard: POLICY, expect: 'reported EXACTLY ONCE',
44
+ edit: (s) => s.replace(CLEANUP_IF, CLEANUP_IF + "\n logger?.info?.('[dsh-vibe-math] 预设清理:再次报告(删除 ' + removedFiles + ' 个文件)')") },
45
+ ]
46
+ for (const c of CASES) {
47
+ const mutated = c.edit(ORIGIN)
48
+ if (mutated === ORIGIN) { ok(false, c.name + ' — the single-site anchor applies', 'ANCHOR MISS'); continue }
49
+ const dir = mkdtempSync(join(tmpdir(), 'installer-assert-mut-'))
50
+ copyFileSync(SRC, join(dir, 'installer.js'))
51
+ writeFileSync(join(dir, 'installer.js'), mutated)
52
+ const r = spawnSync(process.execPath, [c.guard], { cwd: REPO, encoding: 'utf8', maxBuffer: 1 << 26, env: Object.assign({}, process.env, { INSTALLER_JS: join(dir, 'installer.js') }) })
53
+ const o = String(r.stdout || '') + String(r.stderr || '')
54
+ const failLines = o.split('\n').filter((l) => /^ - /.test(l))
55
+ const named = failLines.some((l) => l.indexOf(c.expect) !== -1)
56
+ const sum = (o.split('\n').filter((l) => /passed, \d+ failed/.test(l)).slice(-1)[0] || '').trim()
57
+ ok(r.status !== 0 && named, '★ ' + c.name + ' -> ' + sum, r.status === 0 ? 'the guard stayed GREEN' : (named ? '' : 'named red not found'))
58
+ if (!named) for (const l of failLines.slice(0, 3)) console.log(' ' + l.trim().slice(0, 140))
59
+ rmSync(dir, { recursive: true, force: true })
60
+ }
61
+ console.log('')
62
+ console.log('=== INSTALLER ASSERTION MUTANTS: ' + passed + ' red as required, ' + failed + ' problem(s) ===')
63
+ process.exit(failed === 0 ? 0 : 1)
@@ -1,208 +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, 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
- ok(quick.logs.some((l) => l.includes('0.2.0-rc.2')),
200
- 'a prompt listBundles() still supplies the version (the timeout is a fallback, not a replacement)',
201
- quick.logs.filter((l) => l.includes('source')).join(' | ').slice(0, 160))
202
- rmSync(tmp, { recursive: true, force: true })
203
- }
204
-
205
- console.log('')
206
- console.log('=== INSTALLER COMPAT: ' + passed + ' passed, ' + failed + ' failed ===')
207
- if (failed) { for (const f of failures) console.error(' - ' + f); process.exit(1) }
208
- 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')