acmer-coach 2.3.0 → 2.4.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.
@@ -0,0 +1,38 @@
1
+ import { constants, copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ const PERSONAL_FILES = ['PROGRESS.yaml', 'CURRICULUM.yaml', 'SCHEDULE.yaml', 'TARGET.yaml']
5
+
6
+ function copyMissing(from, to) {
7
+ try { copyFileSync(from, to, constants.COPYFILE_EXCL); return true }
8
+ catch (err) { if (err.code === 'EEXIST') return false; throw err }
9
+ }
10
+
11
+ export function migratePersonalWorkspace(dataDir, accountId) {
12
+ if (!accountId) return []
13
+ const id = String(accountId)
14
+ if (!/^[1-9]\d*$/.test(id)) throw new Error('个人文件迁移需要合法的正整数 VP 账号 id。')
15
+ const sourceDir = join(dataDir, 'vp-accounts', id)
16
+ const marker = join(sourceDir, 'PERSONAL_MIGRATION.json')
17
+ if (!existsSync(sourceDir) || existsSync(marker)) return []
18
+ const migrated = [], files = {}
19
+ for (const file of PERSONAL_FILES) {
20
+ const from = join(sourceDir, file), to = join(dataDir, file)
21
+ if (!existsSync(from)) { files[file] = 'source-missing'; continue }
22
+ if (copyMissing(from, to)) {
23
+ files[file] = 'filled-missing'
24
+ migrated.push(file)
25
+ } else if (readFileSync(from).equals(readFileSync(to))) {
26
+ files[file] = 'identical'
27
+ } else {
28
+ // File modification times cannot tell which version the learner intended to keep.
29
+ const backup = `${to}.from-vp-account-${id}`
30
+ if (copyMissing(from, backup)) migrated.push(`${file}(账号冲突副本已备份,主账本保留)`)
31
+ files[file] = 'conflict-preserved'
32
+ }
33
+ }
34
+ try {
35
+ writeFileSync(marker, JSON.stringify({ version: 1, accountId: id, completedAt: new Date().toISOString(), files }, null, 2) + '\n', { encoding: 'utf8', flag: 'wx' })
36
+ } catch (err) { if (err.code !== 'EEXIST') throw err }
37
+ return migrated
38
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "acmer-coach",
3
3
  "description": "小鲸教练插件:把「位置 + 判断」从小鲸的转述里拿回来。读 MAP/DEPENDS/PROGRESS,给唯一动作、判因、检测。热插拔,删掉即还原。",
4
- "version": "2.3.0",
4
+ "version": "2.4.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/Miantiao1231/acmer-coach.git"
@@ -19,6 +19,12 @@
19
19
  "dependencies": {
20
20
  "yaml": "^2.9.0"
21
21
  },
22
+ "devDependencies": {
23
+ "@deepseek-ai/dsh-tools": "0.2.0-rc.2"
24
+ },
25
+ "scripts": {
26
+ "test": "node --test dev/curriculum-check.mjs dev/curriculum-evidence-check.mjs dev/strategy-check.mjs dev/catalog-check.mjs dev/coaching-contract-check.mjs dev/workspace-migration-check.mjs dev/vp-client-check.mjs dev/vp-sync-check.mjs"
27
+ },
22
28
  "dsh": {
23
29
  "engines": {
24
30
  "dsh": ">=0.1.1-rc.1"
package/verify.mjs CHANGED
@@ -89,7 +89,7 @@ for (const f of readdirSync(KNOWLEDGE)) {
89
89
  }
90
90
  process.env.COACH_DATA_DIR = tmpEarly
91
91
  let stale = []
92
- for (const f of ['index.js', 'package.json', 'lib/setup.js', 'lib/curriculum.js', 'lib/curriculum-evidence.js', 'lib/curriculum-planning.js', 'assets/rules/coach-rules.md']) {
92
+ for (const f of ['index.js', 'package.json', 'lib/setup.js', 'lib/vp-client.js', 'lib/workspace-migration.js', 'lib/strategy.js', 'lib/curriculum.js', 'lib/curriculum-evidence.js', 'lib/curriculum-planning.js', 'assets/knowledge/NODE_SCOPES.yaml', 'assets/knowledge/CONTEST_PROFILES.yaml', 'assets/rules/coach-rules.md']) {
93
93
  let same = true
94
94
  try {
95
95
  same = readFileSync(`${SOURCE_DIR}\\${f}`, 'utf8') === readFileSync(`${INSTALLED_DIR}\\${f}`, 'utf8')