dsh-vibe-math 2.3.11 → 2.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -40
- package/{AUDIT-CHECKLIST.md → docs/AUDIT-CHECKLIST.md} +323 -304
- package/docs/COMPAT-AUDIT-ROUND2.md +325 -0
- package/docs/generate_framework_diagram_v2.py +114 -0
- package/docs/generate_framework_diagram_v3.py +127 -0
- package/{RELEASE-NOTES-2.1.0.md → docs/release-notes/RELEASE-NOTES-2.1.0.md} +143 -143
- package/{RELEASE-NOTES-2.2.0.md → docs/release-notes/RELEASE-NOTES-2.2.0.md} +266 -266
- package/{RELEASE-NOTES-2.2.1.md → docs/release-notes/RELEASE-NOTES-2.2.1.md} +43 -43
- package/{RELEASE-NOTES-2.2.2.md → docs/release-notes/RELEASE-NOTES-2.2.2.md} +88 -88
- package/{RELEASE-NOTES-2.3.0.md → docs/release-notes/RELEASE-NOTES-2.3.0.md} +207 -207
- package/{RELEASE-NOTES-2.3.1.md → docs/release-notes/RELEASE-NOTES-2.3.1.md} +134 -134
- package/{RELEASE-NOTES-2.3.10.md → docs/release-notes/RELEASE-NOTES-2.3.10.md} +105 -105
- package/{RELEASE-NOTES-2.3.11.md → docs/release-notes/RELEASE-NOTES-2.3.11.md} +57 -57
- package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -0
- package/docs/release-notes/RELEASE-NOTES-2.3.13.md +137 -0
- package/{RELEASE-NOTES-2.3.2.md → docs/release-notes/RELEASE-NOTES-2.3.2.md} +145 -145
- package/{RELEASE-NOTES-2.3.3.md → docs/release-notes/RELEASE-NOTES-2.3.3.md} +115 -115
- package/{RELEASE-NOTES-2.3.4.md → docs/release-notes/RELEASE-NOTES-2.3.4.md} +69 -69
- package/{RELEASE-NOTES-2.3.5.md → docs/release-notes/RELEASE-NOTES-2.3.5.md} +63 -63
- package/{RELEASE-NOTES-2.3.6.md → docs/release-notes/RELEASE-NOTES-2.3.6.md} +66 -66
- package/{RELEASE-NOTES-2.3.7.md → docs/release-notes/RELEASE-NOTES-2.3.7.md} +59 -59
- package/{RELEASE-NOTES-2.3.8.md → docs/release-notes/RELEASE-NOTES-2.3.8.md} +45 -45
- package/{RELEASE-NOTES-2.3.9.md → docs/release-notes/RELEASE-NOTES-2.3.9.md} +70 -70
- package/docs/test-timing.md +18 -17
- package/installer.js +258 -53
- package/package.json +50 -40
- package/{audit-formal-sensitivity.mjs → tests/audit-formal-sensitivity.mjs} +342 -342
- package/tests/audit-installer-compat.test.mjs +136 -0
- package/tests/audit-installer-policy.test.mjs +245 -0
- package/{audit-persona-sensitivity.mjs → tests/audit-persona-sensitivity.mjs} +249 -249
- package/{audit-persona-surface.test.mjs → tests/audit-persona-surface.test.mjs} +349 -349
- package/{audit-prompt-invariants.mjs → tests/audit-prompt-invariants.mjs} +508 -508
- package/{audit-spec-traceability.mjs → tests/audit-spec-traceability.mjs} +193 -193
- package/{audit-v5-integrity.mjs → tests/audit-v5-integrity.mjs} +448 -448
- package/{audit-v5-sensitivity.mjs → tests/audit-v5-sensitivity.mjs} +384 -384
- package/{e2e-v5-round2.test.mjs → tests/e2e-v5-round2.test.mjs} +521 -521
- package/{formal-verify-v2.test.mjs → tests/formal-verify-v2.test.mjs} +1315 -1315
- package/{formal-verify-v3.test.mjs → tests/formal-verify-v3.test.mjs} +1257 -1257
- package/{formal-verify-v4.test.mjs → tests/formal-verify-v4.test.mjs} +1082 -1082
- package/{formal-verify-v5.test.mjs → tests/formal-verify-v5.test.mjs} +708 -708
- package/{prompt-v5-integrity.test.mjs → tests/prompt-v5-integrity.test.mjs} +3 -3
- package/{run-tests.mjs → tests/run-tests.mjs} +121 -118
- package/{selfdrive-v5.mjs → tests/selfdrive-v5.mjs} +470 -470
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1 -1
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1 -1
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +1 -1
- package//347/244/272/344/276/213/345/233/276//345/256/236/351/231/205/344/275/277/347/224/250/347/244/272/344/276/213-/351/225/277/346/210/252/345/233/276.png +0 -0
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v2.png +0 -0
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v3.png +0 -0
- /package/{RELEASE-NOTES-2.0.22.md → docs/release-notes/RELEASE-NOTES-2.0.22.md} +0 -0
package/installer.js
CHANGED
|
@@ -1,28 +1,36 @@
|
|
|
1
1
|
// dsh-vibe-math merged bundle installer — VERSIONED AUTO-UPDATE.
|
|
2
2
|
// When this bundle is installed (e.g. `dsh plugin add dsh-vibe-math` or from the
|
|
3
|
-
// dsh-market), this plugin copies ALL
|
|
4
|
-
// the DSH preset root, so the user immediately gets
|
|
3
|
+
// dsh-market), this plugin copies ALL FOUR agent presets out of the package into
|
|
4
|
+
// the DSH preset root, so the user immediately gets four presets in the picker:
|
|
5
5
|
// vibe-math-v2/ (probability-driven architecture)
|
|
6
6
|
// vibe-math-v3/ (THIRD-generation: paper-style Markdown knowledge base +
|
|
7
7
|
// planner-agent scheduling + universal theory/method library)
|
|
8
8
|
// vibe-math-v4/ (FOURTH-generation: persistent self-organizing resident
|
|
9
9
|
// subagents — message bus / meetings / unanimous-consensus
|
|
10
10
|
// verification / per-resident libraries)
|
|
11
|
+
// vibe-math-v5/ (FIFTH-generation: research institute — academician who
|
|
12
|
+
// assigns work, permanent researchers who vote, temp workers,
|
|
13
|
+
// group chat + meetings + m-vote consensus)
|
|
11
14
|
//
|
|
12
15
|
// (vibe-math-v1 — the classic pipeline — was removed at v2.0.0; this bundle now
|
|
13
|
-
// ships v2/v3/v4
|
|
16
|
+
// ships v2/v3/v4/v5.)
|
|
14
17
|
//
|
|
15
18
|
// UPDATE POLICY (state recorded in <presetRoot>/.vibe-math-installed.json):
|
|
16
|
-
// -
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
19
|
+
// - FORCE-REPLACE ON VERSION CHANGE. When the recorded version differs from this package's
|
|
20
|
+
// version — or there is no record at all (an install made by an older installer) — every
|
|
21
|
+
// managed file is overwritten with the shipped bytes. This is deliberately NOT conditional on
|
|
22
|
+
// the file being unmodified. Two reasons:
|
|
23
|
+
// · a preset assembled from two different versions (the old policy updated a file's
|
|
24
|
+
// neighbours and kept the file the user had touched) is exactly the state that fails to
|
|
25
|
+
// mount or misbehaves subtly, and the user has no way to see that from the outside;
|
|
26
|
+
// · editing a shipped preset in place is not the supported way to customize one — DSH
|
|
27
|
+
// provides a real one (copy the preset: the picker's copy action, or a new directory
|
|
28
|
+
// under <presetRoot>), which leaves the managed set updatable.
|
|
29
|
+
// - Nothing is destroyed silently: before a file whose bytes are not what the installer last
|
|
30
|
+
// wrote is replaced, the user's copy is kept under
|
|
31
|
+
// <presetRoot>/.vibe-math-backup/<fromVersion>/<preset>/<file> and named in the log.
|
|
32
|
+
// - same version: no-op (idempotent) — restarting DSH never rewrites a file or churns the
|
|
33
|
+
// preset's generation stamp. Missing files are ALWAYS restored, at any version.
|
|
26
34
|
// - force a full refresh at any time: delete the preset dirs and restart DSH.
|
|
27
35
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, unlinkSync, rmdirSync } from 'node:fs'
|
|
28
36
|
import { createRequire } from 'node:module'
|
|
@@ -33,7 +41,10 @@ import { fileURLToPath } from 'node:url'
|
|
|
33
41
|
|
|
34
42
|
export const name = 'vibe-math-preset-installer'
|
|
35
43
|
|
|
36
|
-
|
|
44
|
+
// Exported so the shipped policy suite can prove the managed list still covers what each preset
|
|
45
|
+
// needs at runtime (a file present in the preset directory but missing here is copied by nobody,
|
|
46
|
+
// and the installed preset then cannot mount).
|
|
47
|
+
export const PRESETS = [
|
|
37
48
|
{
|
|
38
49
|
src: 'vibe-math-v2',
|
|
39
50
|
dst: 'vibe-math-v2',
|
|
@@ -59,9 +70,34 @@ const PRESETS = [
|
|
|
59
70
|
]
|
|
60
71
|
|
|
61
72
|
const STATE_FILE = '.vibe-math-installed.json'
|
|
73
|
+
// where a replaced user edit is preserved; a leading dot keeps DSH's preset discovery from ever
|
|
74
|
+
// treating it as a preset directory (ids must match [a-z0-9][a-z0-9-]*)
|
|
75
|
+
const BACKUP_DIR = '.vibe-math-backup'
|
|
62
76
|
|
|
63
77
|
function sha256(buf) { return createHash('sha256').update(buf).digest('hex') }
|
|
64
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Preserve one file that is about to be replaced by the shipped version, under
|
|
81
|
+
* `<presetRoot>/.vibe-math-backup/<fromVersion>/<preset>/<file>`.
|
|
82
|
+
*
|
|
83
|
+
* Returns `'written'` (a copy was made now), `'kept'` (a copy for this version was already there —
|
|
84
|
+
* the earliest edit is the one worth keeping, and a re-run must not overwrite it with an
|
|
85
|
+
* already-replaced file) or `'failed'`. A caller must not report `'kept'` as a failure: the user's
|
|
86
|
+
* bytes are preserved, just from an earlier run.
|
|
87
|
+
*/
|
|
88
|
+
function backupReplacedFile(presetRoot, fromVersion, presetDir, fileName, buf) {
|
|
89
|
+
try {
|
|
90
|
+
const dir = join(presetRoot, BACKUP_DIR, String(fromVersion || 'unversioned'), presetDir)
|
|
91
|
+
const dst = join(dir, fileName)
|
|
92
|
+
if (existsSync(dst)) return 'kept' // the earliest copy for this version is the one worth keeping
|
|
93
|
+
mkdirSync(dir, { recursive: true })
|
|
94
|
+
writeFileSync(dst, buf)
|
|
95
|
+
return 'written'
|
|
96
|
+
} catch (e) {
|
|
97
|
+
return 'failed' // a backup failure must never stop the update; it is reported by the caller
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
65
101
|
function readState(path) {
|
|
66
102
|
try {
|
|
67
103
|
const raw = readFileSync(path, 'utf8')
|
|
@@ -99,22 +135,160 @@ function detectDshVersion() {
|
|
|
99
135
|
return undefined
|
|
100
136
|
}
|
|
101
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Minimal semver-range matcher for the host requirement.
|
|
140
|
+
*
|
|
141
|
+
* WHY THIS EXISTS: the DSH requirement a host is judged against lives in `package.json`
|
|
142
|
+
* (`engines.dsh` / `dsh.engines.dsh`) — the same declaration dsh-market shows on a plugin's card.
|
|
143
|
+
* Judging the host by a *second* source (the `dsh.compatibility.dshReleases` map) let the two
|
|
144
|
+
* disagree: the card could say "compatible" while this self-check warned, or the reverse. So the
|
|
145
|
+
* range is evaluated here, and the map remains only a fallback for manifests that declare no range.
|
|
146
|
+
*
|
|
147
|
+
* Supported: `*`, exact, `^`, `~`, `>=`, `>`, `<=`, `<`, whitespace-separated sets, `||`
|
|
148
|
+
* alternatives — everything the ecosystem publishes (see `dshmarket`'s own
|
|
149
|
+
* `^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2` shape). Anything else returns null = unknown
|
|
150
|
+
* (reported, never asserted), never a silent "incompatible".
|
|
151
|
+
*
|
|
152
|
+
* PRERELEASE RULE (npm's, applied per comparator SET — one `||` alternative is one set): a version
|
|
153
|
+
* carrying a prerelease tag satisfies a set only when at least one comparator in that set shares its
|
|
154
|
+
* [major, minor, patch] tuple AND carries a prerelease of its own. This is why
|
|
155
|
+
* `>=0.1.2-rc.1 <0.2.0` does NOT match `0.1.5-rc.2`, and why this package declares explicit
|
|
156
|
+
* per-tuple branches instead. Callers pass `includePrerelease: true` for host checks because every
|
|
157
|
+
* published DSH release line is itself a prerelease.
|
|
158
|
+
*/
|
|
159
|
+
export function satisfiesDshRange(version, range, options = {}) {
|
|
160
|
+
const v = parseSemver(version)
|
|
161
|
+
if (v === null || typeof range !== 'string' || range.trim() === '') return null
|
|
162
|
+
const versionHasPre = v.pre.length > 0
|
|
163
|
+
let sawUnknown = false
|
|
164
|
+
for (const set of range.split('||')) {
|
|
165
|
+
const parts = set.trim().split(/\s+/).filter((p) => p !== '')
|
|
166
|
+
if (parts.length === 0) return true // an empty alternative is `*`
|
|
167
|
+
const parsed = parts.map(comparator)
|
|
168
|
+
if (parsed.some((p) => p === null)) { sawUnknown = true; continue }
|
|
169
|
+
if (versionHasPre && options.includePrerelease !== true) {
|
|
170
|
+
const admitted = parsed.some((p) => p.target !== null && p.target.pre.length > 0 &&
|
|
171
|
+
p.target.major === v.major && p.target.minor === v.minor && p.target.patch === v.patch)
|
|
172
|
+
if (!admitted) continue
|
|
173
|
+
} if (parsed.every((p) => matchesComparator(v, p))) return true
|
|
174
|
+
}
|
|
175
|
+
return sawUnknown ? null : false
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseSemver(value) {
|
|
179
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(String(value == null ? '' : value).trim())
|
|
180
|
+
if (m === null) return null
|
|
181
|
+
const pre = m[4] === undefined ? [] : m[4].split('.')
|
|
182
|
+
for (const id of pre) if (/^\d+$/.test(id) && id.length > 1 && id[0] === '0') return null
|
|
183
|
+
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]), pre }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function compareSemver(a, b) {
|
|
187
|
+
if (a.major !== b.major) return a.major < b.major ? -1 : 1
|
|
188
|
+
if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1
|
|
189
|
+
if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1
|
|
190
|
+
if (a.pre.length === 0 && b.pre.length === 0) return 0
|
|
191
|
+
if (a.pre.length === 0) return 1 // a release outranks its prereleases
|
|
192
|
+
if (b.pre.length === 0) return -1
|
|
193
|
+
for (let i = 0; i < Math.max(a.pre.length, b.pre.length); i++) {
|
|
194
|
+
const x = a.pre[i], y = b.pre[i]
|
|
195
|
+
if (x === undefined) return -1
|
|
196
|
+
if (y === undefined) return 1
|
|
197
|
+
const xn = /^\d+$/.test(x), yn = /^\d+$/.test(y)
|
|
198
|
+
if (xn && yn) { const d = Number(x) - Number(y); if (d !== 0) return d < 0 ? -1 : 1; continue }
|
|
199
|
+
if (xn !== yn) return xn ? -1 : 1 // numeric identifiers sort below alphanumeric ones
|
|
200
|
+
if (x !== y) return x < y ? -1 : 1
|
|
201
|
+
}
|
|
202
|
+
return 0
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** One comparator such as `^0.1.0-rc.7`. Returns null when the target is not a version. */
|
|
206
|
+
function comparator(part) {
|
|
207
|
+
const p = part.trim()
|
|
208
|
+
// `*`, `x`, `X` and an empty token mean "any version" (npm: still not a prerelease unless the
|
|
209
|
+
// caller opts into includePrerelease — the set-level gate below is what enforces that).
|
|
210
|
+
if (p === '' || p === '*' || p === 'x' || p === 'X') return { op: 'any', target: null }
|
|
211
|
+
const m = /^(\^|~|>=|<=|>|<)?(.*)$/.exec(p)
|
|
212
|
+
const op = m === null || m[1] === undefined ? '' : m[1]
|
|
213
|
+
const target = parseSemver(m === null ? '' : m[2])
|
|
214
|
+
return target === null ? null : { op, target }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function matchesComparator(v, { op, target }) {
|
|
218
|
+
if (op === 'any') return true
|
|
219
|
+
const c = compareSemver(v, target)
|
|
220
|
+
switch (op) {
|
|
221
|
+
case '': return c === 0
|
|
222
|
+
case '>=': return c >= 0
|
|
223
|
+
case '>': return c > 0
|
|
224
|
+
case '<=': return c <= 0
|
|
225
|
+
case '<': return c < 0
|
|
226
|
+
case '^': {
|
|
227
|
+
const upper = target.major > 0
|
|
228
|
+
? { major: target.major + 1, minor: 0, patch: 0, pre: [ '0' ] }
|
|
229
|
+
: target.minor > 0
|
|
230
|
+
? { major: 0, minor: target.minor + 1, patch: 0, pre: [ '0' ] }
|
|
231
|
+
: { major: 0, minor: 0, patch: target.patch + 1, pre: [ '0' ] }
|
|
232
|
+
return c >= 0 && compareSemver(v, upper) < 0
|
|
233
|
+
}
|
|
234
|
+
case '~': {
|
|
235
|
+
const upper = { major: target.major, minor: target.minor + 1, patch: 0, pre: [ '0' ] }
|
|
236
|
+
return c >= 0 && compareSemver(v, upper) < 0
|
|
237
|
+
}
|
|
238
|
+
default: return false
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The host verdict for a DSH version against a package manifest.
|
|
244
|
+
* Prefers the declared range (`engines.dsh`, then `dsh.engines.dsh`); falls back to the
|
|
245
|
+
* `dsh.compatibility.dshReleases` map so packages that declare only that keep working.
|
|
246
|
+
*/
|
|
247
|
+
export function dshVersionVerdict(version, manifest) {
|
|
248
|
+
const pkg = manifest && typeof manifest === 'object' ? manifest : {}
|
|
249
|
+
const declared = (pkg.engines && typeof pkg.engines.dsh === 'string' && pkg.engines.dsh.trim() !== '')
|
|
250
|
+
? pkg.engines.dsh
|
|
251
|
+
: (pkg.dsh && pkg.dsh.engines && typeof pkg.dsh.engines.dsh === 'string' && pkg.dsh.engines.dsh.trim() !== '')
|
|
252
|
+
? pkg.dsh.engines.dsh
|
|
253
|
+
: null
|
|
254
|
+
if (declared !== null) {
|
|
255
|
+
// includePrerelease: the whole published DSH line is prerelease builds.
|
|
256
|
+
const sat = satisfiesDshRange(version, declared, { includePrerelease: true })
|
|
257
|
+
return {
|
|
258
|
+
basis: 'engines',
|
|
259
|
+
requirement: declared,
|
|
260
|
+
status: sat === true ? 'compatible' : sat === false ? 'incompatible' : 'unknown',
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const rel = (pkg.dsh && pkg.dsh.compatibility && pkg.dsh.compatibility.dshReleases) || {}
|
|
264
|
+
const status = rel[version]
|
|
265
|
+
if (status === 'compatible' || status === 'incompatible') {
|
|
266
|
+
return { basis: 'dshReleases', requirement: null, status }
|
|
267
|
+
}
|
|
268
|
+
return { basis: 'dshReleases', requirement: null, status: status === 'unknown' ? 'unknown' : 'undeclared' }
|
|
269
|
+
}
|
|
270
|
+
|
|
102
271
|
async function checkHostCapabilities(ctx, logger) {
|
|
103
272
|
const problems = []
|
|
104
273
|
// 1) DSH version compatibility (best-effort, only when the version is detectable).
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
let
|
|
109
|
-
try {
|
|
274
|
+
// The AUTHORITATIVE source is the declared range `engines.dsh` / `dsh.engines.dsh` — the same
|
|
275
|
+
// field dsh-market renders on the plugin card, so the two verdicts cannot disagree. The
|
|
276
|
+
// `dsh.compatibility.dshReleases` map is the fallback for manifests without a range.
|
|
277
|
+
let manifest = {}
|
|
278
|
+
try { manifest = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'package.json'), 'utf8')) } catch (e) {}
|
|
279
|
+
const dshRel = (manifest.dsh && manifest.dsh.compatibility && manifest.dsh.compatibility.dshReleases) || {}
|
|
110
280
|
const supported = Object.keys(dshRel).sort()
|
|
111
281
|
const dshVersion = detectDshVersion()
|
|
112
282
|
if (dshVersion) {
|
|
113
|
-
const
|
|
114
|
-
if (status === 'incompatible') {
|
|
115
|
-
problems.push(
|
|
116
|
-
|
|
283
|
+
const verdict = dshVersionVerdict(dshVersion, manifest)
|
|
284
|
+
if (verdict.status === 'incompatible') {
|
|
285
|
+
problems.push(verdict.basis === 'engines'
|
|
286
|
+
? '当前 DSH 版本 v' + dshVersion + ' 不满足本包声明的宿主版本要求(engines.dsh = ' + verdict.requirement + ')。'
|
|
287
|
+
: '当前 DSH 版本 v' + dshVersion + ' 被本包声明为 incompatible;请使用 ' + supported.join(' / ') + '。')
|
|
288
|
+
} else if (verdict.status === 'undeclared' || (verdict.status === 'unknown' && verdict.basis === 'dshReleases')) {
|
|
117
289
|
problems.push('当前 DSH 版本 v' + dshVersion + ' 尚未被本包声明为兼容(dshReleases 仅声明 ' + supported.join(' / ') + ');建议使用 ' + supported.join(' / ') + ',或将该版本在 dshReleases 中标注后再自行验证。')
|
|
290
|
+
} else if (verdict.status === 'unknown' && verdict.basis === 'engines') {
|
|
291
|
+
problems.push('当前 DSH 版本 v' + dshVersion + ' 无法与 engines.dsh 的范围比对(声明值 ' + verdict.requirement + ' 不是本安装器能解析的范围);请按该范围自行确认。')
|
|
118
292
|
}
|
|
119
293
|
}
|
|
120
294
|
// 2) capability self-check (the authoritative mounting gate; also covers hosts whose version
|
|
@@ -209,11 +383,23 @@ export async function apply(ctx) {
|
|
|
209
383
|
const state = readState(stateFile)
|
|
210
384
|
const prevFiles = (state && state.files) || {}
|
|
211
385
|
const isUpgrade = state !== null && pkgVersion !== '' && state.version !== pkgVersion
|
|
212
|
-
const isBaseline = state === null // no recorded history → refresh everything
|
|
386
|
+
const isBaseline = state === null // no recorded history → refresh everything
|
|
387
|
+
// A version change (or a first sighting) REPLACES the managed files; only a same-version boot
|
|
388
|
+
// leaves the working tree alone. See UPDATE POLICY at the top of this file.
|
|
389
|
+
const refresh = isBaseline || isUpgrade
|
|
390
|
+
const fromVersion = (state && state.version) || '(unversioned)'
|
|
391
|
+
if (pkgVersion === '') {
|
|
392
|
+
// Without a version there is nothing to compare against, so this run must NOT replace
|
|
393
|
+
// anything (a wrong guess would overwrite files for no reason); it still restores missing
|
|
394
|
+
// ones and keeps the previously recorded version, so the next readable run reports the
|
|
395
|
+
// right "from" version.
|
|
396
|
+
logger?.warn?.('[dsh-vibe-math] 读不到本包版本(package.json 缺失或损坏):本次不做版本比对,只补回缺失的 preset 文件。')
|
|
397
|
+
}
|
|
213
398
|
|
|
214
399
|
const nextFiles = {}
|
|
215
|
-
let installed = 0, updated = 0, kept = 0
|
|
216
|
-
const
|
|
400
|
+
let installed = 0, updated = 0, kept = 0, backedUp = 0
|
|
401
|
+
const replacedEdits = []
|
|
402
|
+
const backupFailures = []
|
|
217
403
|
|
|
218
404
|
for (const p of PRESETS) {
|
|
219
405
|
const srcDir = join(here, p.src)
|
|
@@ -234,32 +420,35 @@ export async function apply(ctx) {
|
|
|
234
420
|
nextFiles[key] = { hash: curHash, provenance: 'package' }
|
|
235
421
|
continue
|
|
236
422
|
}
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
// no recorded history: refresh to the current package (one-time; edits
|
|
240
|
-
// made before this mechanism are overwritten, later edits are protected)
|
|
241
|
-
if (destHash === curHash) { nextFiles[key] = { hash: curHash, provenance: 'package' } }
|
|
242
|
-
else { writeFileSync(d, cur); updated += 1; nextFiles[key] = { hash: curHash, provenance: 'package' } }
|
|
243
|
-
continue
|
|
244
|
-
}
|
|
423
|
+
const destBuf = readFileSync(d)
|
|
424
|
+
const destHash = sha256(destBuf)
|
|
245
425
|
const prev = prevFiles[key]
|
|
246
426
|
const prevRec = (prev && typeof prev === 'object') ? prev : { hash: prev, provenance: 'package' }
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
//
|
|
250
|
-
if (destHash !== curHash) { writeFileSync(d, cur); updated += 1 }
|
|
427
|
+
if (destHash === curHash) {
|
|
428
|
+
// already the shipped bytes: never rewrite, so the file's mtime (which keys the preset's
|
|
429
|
+
// DSH generation) stays put
|
|
251
430
|
nextFiles[key] = { hash: curHash, provenance: 'package' }
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
// 包文件但自上次安装后已被用户改动
|
|
431
|
+
continue
|
|
432
|
+
}
|
|
433
|
+
if (!refresh) {
|
|
434
|
+
// same version: nothing is being updated, so a file that differs from the package is left
|
|
435
|
+
// exactly as it is. The recorded hash stays "what this installer last wrote" (or unknown),
|
|
436
|
+
// so the drift is still recognised — and backed up — at the next version change.
|
|
259
437
|
kept += 1
|
|
260
|
-
|
|
261
|
-
|
|
438
|
+
nextFiles[key] = typeof prevRec.hash === 'string' ? { hash: prevRec.hash, provenance: 'package' } : { provenance: 'package' }
|
|
439
|
+
continue
|
|
440
|
+
}
|
|
441
|
+
// replacing: preserve the user's bytes when they are not what this installer last wrote
|
|
442
|
+
// (a legacy state without a hash cannot tell, so it backs the file up rather than risk it)
|
|
443
|
+
if (typeof prevRec.hash !== 'string' || destHash !== prevRec.hash) {
|
|
444
|
+
const backupResult = backupReplacedFile(presetRoot, fromVersion, p.dst, f, destBuf)
|
|
445
|
+
if (backupResult === 'failed') backupFailures.push(key)
|
|
446
|
+
else backedUp += 1
|
|
447
|
+
replacedEdits.push(key)
|
|
262
448
|
}
|
|
449
|
+
writeFileSync(d, cur)
|
|
450
|
+
updated += 1
|
|
451
|
+
nextFiles[key] = { hash: curHash, provenance: 'package' }
|
|
263
452
|
}
|
|
264
453
|
}
|
|
265
454
|
|
|
@@ -293,22 +482,38 @@ export async function apply(ctx) {
|
|
|
293
482
|
if (dirEmpty && existsSync(dir)) { try { rmdirSync(dir); removedDirs.push(dir) } catch (e) {} }
|
|
294
483
|
}
|
|
295
484
|
|
|
296
|
-
writeState(stateFile, { version: pkgVersion, files: nextFiles, updatedAt: Date.now() })
|
|
485
|
+
writeState(stateFile, { version: pkgVersion || (state && state.version) || '', files: nextFiles, updatedAt: Date.now() })
|
|
297
486
|
|
|
298
487
|
if (removedFiles > 0 || removedDirs.length > 0) {
|
|
299
488
|
logger?.info?.('[dsh-vibe-math] preset cleanup: removed ' + removedFiles + ' file(s) from ' + removedDirs.length + ' stale preset dir(s) (' + removedDirs.map(d => d.split(/[\\/]/).pop()).join(', ') + ') that are no longer shipped.')
|
|
300
489
|
}
|
|
301
490
|
|
|
302
491
|
if (isUpgrade) {
|
|
303
|
-
logger?.info?.('[dsh-vibe-math] preset auto-update: version ' +
|
|
304
|
-
' — 新增 ' + installed + ' 个文件,更新 ' + updated + '
|
|
305
|
-
(
|
|
306
|
-
|
|
492
|
+
logger?.info?.('[dsh-vibe-math] preset auto-update: version ' + fromVersion + ' → ' + pkgVersion +
|
|
493
|
+
' — 新增 ' + installed + ' 个文件,更新 ' + updated + ' 个文件。' +
|
|
494
|
+
(replacedEdits.length > 0
|
|
495
|
+
? '其中 ' + replacedEdits.length + ' 个文件与上一次安装的字节不同(被改过),已按版本一致化覆盖' +
|
|
496
|
+
(backupFailures.length === 0
|
|
497
|
+
? ',原文备份在 ' + join(presetRoot, BACKUP_DIR, String(fromVersion)) + ':' + replacedEdits.join(', ')
|
|
498
|
+
: ';这 ' + backupFailures.length + ' 个文件**备份失败**(原文未保留):' + backupFailures.join(', ')) +
|
|
499
|
+
'。要自定义 preset,请复制一份而不是改这几个文件——被管理的文件在下一次版本变更时一定会被替换。'
|
|
500
|
+
: '') +
|
|
501
|
+
'新版本 preset 将在新会话生效。')
|
|
307
502
|
} else if (isBaseline) {
|
|
308
503
|
logger?.info?.('[dsh-vibe-math] preset baseline: refreshed ' + (installed + updated) + ' file(s) to v' + pkgVersion +
|
|
309
|
-
|
|
504
|
+
(replacedEdits.length > 0
|
|
505
|
+
? ',其中 ' + replacedEdits.length + ' 个原有文件与随包版本不同' +
|
|
506
|
+
(backupFailures.length === 0 ? ',原文已备份在 ' + join(presetRoot, BACKUP_DIR, String(fromVersion))
|
|
507
|
+
: ',但有 ' + backupFailures.length + ' 个备份失败(原文未保留):' + backupFailures.join(', '))
|
|
508
|
+
: '') +
|
|
509
|
+
' — 已启用版本化自动更新(后续版本变更会直接替换被管理的 preset 文件)。')
|
|
310
510
|
} else if (installed > 0) {
|
|
311
511
|
logger?.info?.('[dsh-vibe-math] restored ' + installed + ' missing preset file(s)')
|
|
512
|
+
} else if (kept > 0) {
|
|
513
|
+
// same version, and some managed file on disk differs from the package: reported, never
|
|
514
|
+
// rewritten mid-version (it is replaced, with a backup, at the next version change)
|
|
515
|
+
logger?.info?.('[dsh-vibe-math] preset files untouched (v' + pkgVersion + ' unchanged): ' + kept +
|
|
516
|
+
' file(s) differ from the shipped copy; they will be replaced on the next version change (原件会先备份)')
|
|
312
517
|
}
|
|
313
518
|
await checkHostCapabilities(ctx, logger)
|
|
314
519
|
} catch (err) {
|