tanmi-dock 1.0.3 → 1.0.5-beta

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 (116) hide show
  1. package/README.md +18 -0
  2. package/dist/commands/calibrate.d.ts +10 -0
  3. package/dist/commands/calibrate.d.ts.map +1 -0
  4. package/dist/commands/calibrate.js +158 -0
  5. package/dist/commands/calibrate.js.map +1 -0
  6. package/dist/commands/check.d.ts +38 -4
  7. package/dist/commands/check.d.ts.map +1 -1
  8. package/dist/commands/check.js +704 -168
  9. package/dist/commands/check.js.map +1 -1
  10. package/dist/commands/clean.d.ts +2 -6
  11. package/dist/commands/clean.d.ts.map +1 -1
  12. package/dist/commands/clean.js +224 -209
  13. package/dist/commands/clean.js.map +1 -1
  14. package/dist/commands/dashboard.d.ts.map +1 -1
  15. package/dist/commands/dashboard.js +56 -28
  16. package/dist/commands/dashboard.js.map +1 -1
  17. package/dist/commands/link.d.ts +6 -6
  18. package/dist/commands/link.d.ts.map +1 -1
  19. package/dist/commands/link.js +417 -196
  20. package/dist/commands/link.js.map +1 -1
  21. package/dist/commands/migrate.d.ts +11 -0
  22. package/dist/commands/migrate.d.ts.map +1 -1
  23. package/dist/commands/migrate.js +277 -57
  24. package/dist/commands/migrate.js.map +1 -1
  25. package/dist/commands/reset.d.ts.map +1 -1
  26. package/dist/commands/reset.js +19 -24
  27. package/dist/commands/reset.js.map +1 -1
  28. package/dist/commands/status.d.ts +4 -0
  29. package/dist/commands/status.d.ts.map +1 -1
  30. package/dist/commands/status.js +37 -20
  31. package/dist/commands/status.js.map +1 -1
  32. package/dist/commands/unavailable.js.map +1 -1
  33. package/dist/commands/unlink.d.ts.map +1 -1
  34. package/dist/commands/unlink.js +43 -39
  35. package/dist/commands/unlink.js.map +1 -1
  36. package/dist/core/config.d.ts +3 -1
  37. package/dist/core/config.d.ts.map +1 -1
  38. package/dist/core/config.js +10 -6
  39. package/dist/core/config.js.map +1 -1
  40. package/dist/core/parser.js +1 -1
  41. package/dist/core/parser.js.map +1 -1
  42. package/dist/core/registry.d.ts +11 -29
  43. package/dist/core/registry.d.ts.map +1 -1
  44. package/dist/core/registry.js +46 -113
  45. package/dist/core/registry.js.map +1 -1
  46. package/dist/core/space/calibration-executor.d.ts +17 -0
  47. package/dist/core/space/calibration-executor.d.ts.map +1 -0
  48. package/dist/core/space/calibration-executor.js +144 -0
  49. package/dist/core/space/calibration-executor.js.map +1 -0
  50. package/dist/core/space/calibration-planner.d.ts +5 -0
  51. package/dist/core/space/calibration-planner.d.ts.map +1 -0
  52. package/dist/core/space/calibration-planner.js +157 -0
  53. package/dist/core/space/calibration-planner.js.map +1 -0
  54. package/dist/core/space/cleanup-executor.d.ts +15 -0
  55. package/dist/core/space/cleanup-executor.d.ts.map +1 -0
  56. package/dist/core/space/cleanup-executor.js +188 -0
  57. package/dist/core/space/cleanup-executor.js.map +1 -0
  58. package/dist/core/space/cleanup-journal.d.ts +43 -0
  59. package/dist/core/space/cleanup-journal.d.ts.map +1 -0
  60. package/dist/core/space/cleanup-journal.js +96 -0
  61. package/dist/core/space/cleanup-journal.js.map +1 -0
  62. package/dist/core/space/cleanup-planner.d.ts +14 -0
  63. package/dist/core/space/cleanup-planner.d.ts.map +1 -0
  64. package/dist/core/space/cleanup-planner.js +166 -0
  65. package/dist/core/space/cleanup-planner.js.map +1 -0
  66. package/dist/core/space/cleanup-selection.d.ts +10 -0
  67. package/dist/core/space/cleanup-selection.d.ts.map +1 -0
  68. package/dist/core/space/cleanup-selection.js +84 -0
  69. package/dist/core/space/cleanup-selection.js.map +1 -0
  70. package/dist/core/space/estimator.d.ts +11 -0
  71. package/dist/core/space/estimator.d.ts.map +1 -0
  72. package/dist/core/space/estimator.js +142 -0
  73. package/dist/core/space/estimator.js.map +1 -0
  74. package/dist/core/space/inventory.d.ts +16 -0
  75. package/dist/core/space/inventory.d.ts.map +1 -0
  76. package/dist/core/space/inventory.js +610 -0
  77. package/dist/core/space/inventory.js.map +1 -0
  78. package/dist/core/space/meter.d.ts +5 -0
  79. package/dist/core/space/meter.d.ts.map +1 -0
  80. package/dist/core/space/meter.js +87 -0
  81. package/dist/core/space/meter.js.map +1 -0
  82. package/dist/core/space/migration-journal.d.ts +31 -0
  83. package/dist/core/space/migration-journal.d.ts.map +1 -0
  84. package/dist/core/space/migration-journal.js +121 -0
  85. package/dist/core/space/migration-journal.js.map +1 -0
  86. package/dist/core/space/service.d.ts +26 -0
  87. package/dist/core/space/service.d.ts.map +1 -0
  88. package/dist/core/space/service.js +54 -0
  89. package/dist/core/space/service.js.map +1 -0
  90. package/dist/core/space/types.d.ts +248 -0
  91. package/dist/core/space/types.d.ts.map +1 -0
  92. package/dist/core/space/types.js +13 -0
  93. package/dist/core/space/types.js.map +1 -0
  94. package/dist/core/store.d.ts.map +1 -1
  95. package/dist/core/store.js +12 -6
  96. package/dist/core/store.js.map +1 -1
  97. package/dist/index.js +2 -0
  98. package/dist/index.js.map +1 -1
  99. package/dist/types/index.d.ts +2 -0
  100. package/dist/types/index.d.ts.map +1 -1
  101. package/dist/utils/atomic-file.d.ts +6 -0
  102. package/dist/utils/atomic-file.d.ts.map +1 -0
  103. package/dist/utils/atomic-file.js +37 -0
  104. package/dist/utils/atomic-file.js.map +1 -0
  105. package/dist/utils/fs-utils.d.ts +18 -1
  106. package/dist/utils/fs-utils.d.ts.map +1 -1
  107. package/dist/utils/fs-utils.js +150 -2
  108. package/dist/utils/fs-utils.js.map +1 -1
  109. package/dist/utils/logger.d.ts +7 -1
  110. package/dist/utils/logger.d.ts.map +1 -1
  111. package/dist/utils/logger.js +45 -5
  112. package/dist/utils/logger.js.map +1 -1
  113. package/dist/utils/prompt.d.ts.map +1 -1
  114. package/dist/utils/prompt.js +4 -4
  115. package/dist/utils/prompt.js.map +1 -1
  116. package/package.json +1 -1
@@ -9,14 +9,91 @@ import { getDiskInfo, formatSize } from '../utils/disk.js';
9
9
  import * as config from '../core/config.js';
10
10
  import { getRegistry } from '../core/registry.js';
11
11
  import * as store from '../core/store.js';
12
- import { KNOWN_PLATFORM_VALUES } from '../core/platform.js';
13
- import { info, warn, success, error, hint, blank, title, separator, colorize, debug, } from '../utils/logger.js';
12
+ import { GENERAL_PLATFORM, KNOWN_PLATFORM_VALUES, SHARED_PLATFORM } from '../core/platform.js';
13
+ import { SpaceService } from '../core/space/service.js';
14
+ import { CleanupJournal } from '../core/space/cleanup-journal.js';
15
+ import { MigrationJournal } from '../core/space/migration-journal.js';
16
+ import { withGlobalLock } from '../utils/global-lock.js';
17
+ import { info, warn, success, error, hint, blank, title, separator, colorize, debug, withStdoutLogsSuppressed, } from '../utils/logger.js';
14
18
  import { selectWithCancel, checkboxWithCancel, confirmWithCancel, PROMPT_CANCELLED, } from '../utils/prompt.js';
19
+ function createSpaceProgressRenderer(label) {
20
+ let lineWidth = 0;
21
+ let lastReported = -1;
22
+ const close = () => {
23
+ if (lineWidth > 0 && process.stdout.isTTY) {
24
+ process.stdout.write(`\r${' '.repeat(lineWidth)}\r`);
25
+ }
26
+ lineWidth = 0;
27
+ };
28
+ return {
29
+ update(progress) {
30
+ if (progress.phase === 'completed') {
31
+ close();
32
+ return;
33
+ }
34
+ const message = progress.phase === 'measure-store'
35
+ ? `${label}: 正在汇总 Store 总占用...`
36
+ : `${label}: ${progress.completedCommits}/${progress.totalCommits} 个版本`;
37
+ if (process.stdout.isTTY) {
38
+ const padded = message.padEnd(Math.max(lineWidth, message.length));
39
+ lineWidth = padded.length;
40
+ process.stdout.write(`\r${padded}`);
41
+ return;
42
+ }
43
+ const interval = Math.max(1, Math.ceil(progress.totalCommits / 10));
44
+ const shouldReport = progress.phase === 'measure-store' ||
45
+ progress.completedCommits === 0 ||
46
+ progress.completedCommits === progress.totalCommits ||
47
+ progress.completedCommits - lastReported >= interval;
48
+ if (shouldReport && progress.completedCommits !== lastReported) {
49
+ info(message);
50
+ lastReported = progress.completedCommits;
51
+ }
52
+ },
53
+ close,
54
+ };
55
+ }
56
+ function createCheckProgressRenderer() {
57
+ const spaceProgress = createSpaceProgressRenderer('[3/3] 正在审计 Store 空间');
58
+ return (progress) => {
59
+ if (progress.stage === 'space' && progress.state === 'progress') {
60
+ spaceProgress.update(progress.progress);
61
+ return;
62
+ }
63
+ if (progress.stage === 'space')
64
+ spaceProgress.close();
65
+ const indexes = { environment: 1, integrity: 2, space: 3 };
66
+ if (progress.state === 'started') {
67
+ const messages = {
68
+ environment: '正在检查运行环境...',
69
+ integrity: '正在核对项目与依赖...',
70
+ space: '正在审计 Store 空间...',
71
+ };
72
+ info(`[${indexes[progress.stage]}/3] ${messages[progress.stage]}`);
73
+ }
74
+ else if (progress.state === 'completed') {
75
+ const messages = {
76
+ environment: '运行环境检查完成',
77
+ integrity: '项目与依赖核对完成',
78
+ space: 'Store 空间审计完成',
79
+ };
80
+ info(`[${indexes[progress.stage]}/3] ${messages[progress.stage]}`);
81
+ }
82
+ else if (progress.state === 'failed') {
83
+ const labels = {
84
+ environment: '运行环境',
85
+ integrity: '项目与依赖',
86
+ space: 'Store 空间',
87
+ };
88
+ warn(`[${indexes[progress.stage]}/3] ${labels[progress.stage]}检查失败: ${progress.error ?? '未知错误'}`);
89
+ }
90
+ };
91
+ }
15
92
  // ============ 命令创建 ============
16
93
  export function createCheckCommand() {
17
94
  return new Command('check')
18
95
  .description('健康检查(环境诊断 + 数据一致性验证)')
19
- .option('--fix', '直接修复所有问题')
96
+ .option('--fix', '直接修复所有可修复问题')
20
97
  .option('--dry-run', '只显示问题,不修复')
21
98
  .option('--json', '输出 JSON 格式')
22
99
  .option('--force', '跳过确认')
@@ -27,7 +104,26 @@ export function createCheckCommand() {
27
104
  }
28
105
  // ============ 主流程 ============
29
106
  async function runCheck(options) {
30
- const result = await collectAllIssues({ integrity: options.integrity });
107
+ if (options.json) {
108
+ await withStdoutLogsSuppressed(() => runCheckInternal(options));
109
+ return;
110
+ }
111
+ await runCheckInternal(options);
112
+ }
113
+ async function runCheckInternal(options) {
114
+ debug(`[check] 命令输入: fix=${options.fix}, dryRun=${options.dryRun}, json=${options.json}, force=${options.force}, integrity=${options.integrity}`);
115
+ if (options.dryRun) {
116
+ const cfg = await config.load({ migrate: false });
117
+ if (cfg && config.checkConfigVersion(cfg) === 'migrate') {
118
+ error('配置需要迁移;dry-run 不会修改配置,请先运行一次普通命令完成迁移');
119
+ return;
120
+ }
121
+ }
122
+ const result = await collectAllIssues({
123
+ integrity: options.integrity,
124
+ readOnly: options.dryRun,
125
+ onProgress: options.json ? undefined : createCheckProgressRenderer(),
126
+ });
31
127
  // JSON 输出
32
128
  if (options.json) {
33
129
  console.log(JSON.stringify(result, null, 2));
@@ -35,13 +131,15 @@ async function runCheck(options) {
35
131
  }
36
132
  // 渲染报告
37
133
  renderReport(result);
38
- const hasIssues = result.summary.integrityIssues > 0;
134
+ const hasIntegrityIssues = result.summary.integrityIssues > 0;
135
+ const hasReportedIssues = hasIntegrityIssues || result.summary.spaceIssues > 0;
136
+ const hasRepairableIssues = hasIntegrityIssues || hasSpaceCalibrationChoice(result.space);
39
137
  const hasEnvErrors = result.summary.envErrors > 0;
40
138
  // dry-run 模式
41
139
  if (options.dryRun) {
42
- if (hasIssues) {
140
+ if (hasRepairableIssues) {
43
141
  blank();
44
- hint('运行 td check --fix 修复问题');
142
+ hint('运行 td check --fix 修复可修复问题');
45
143
  }
46
144
  return;
47
145
  }
@@ -52,21 +150,90 @@ async function runCheck(options) {
52
150
  return;
53
151
  }
54
152
  // 无问题
55
- if (!hasIssues) {
153
+ if (!hasRepairableIssues) {
154
+ if (hasReportedIssues) {
155
+ debug('[check] 检查结果只包含无自动修复动作的问题');
156
+ }
56
157
  return;
57
158
  }
58
159
  // --fix 模式:直接修复
59
160
  if (options.fix) {
60
- await fixAllIssues(result.integrity, { force: options.force });
161
+ await fixAllIssues(result.integrity, result.space, {
162
+ force: options.force,
163
+ calibrateSpace: hasSpaceCalibrationChoice(result.space),
164
+ deleteUnregistered: false,
165
+ verifyStoreIntegrity: options.integrity,
166
+ });
167
+ renderUncachedDependencyHint(result.integrity);
61
168
  return;
62
169
  }
63
170
  // 交互模式
64
- await interactiveCheck(result.integrity, options);
171
+ await interactiveCheck(result, options);
65
172
  }
66
173
  // ============ 数据收集 ============
67
- async function collectAllIssues(options) {
68
- const environment = await checkEnvironment();
174
+ async function collectAllIssues(options = {}) {
175
+ debug(`[check] 健康检查开始: integrity=${options.integrity ?? false}, readOnly=${options.readOnly ?? false}, progress=${options.onProgress ? 'enabled' : 'disabled'}`);
176
+ options.onProgress?.({ stage: 'environment', state: 'started' });
177
+ const environment = await checkEnvironment(options?.readOnly);
178
+ options.onProgress?.({ stage: 'environment', state: 'completed' });
179
+ options.onProgress?.({ stage: 'integrity', state: 'started' });
69
180
  const integrity = await checkIntegrity(options);
181
+ options.onProgress?.({ stage: 'integrity', state: 'completed' });
182
+ const space = { issues: [] };
183
+ options.onProgress?.({ stage: 'space', state: 'started' });
184
+ try {
185
+ const registry = getRegistry();
186
+ await registry.load({ migrate: !options?.readOnly });
187
+ const storePath = await store.getStorePath();
188
+ debug(`[space] audit 开始: store=${storePath}, source=td-check`);
189
+ const spaceService = new SpaceService(registry, storePath);
190
+ const snapshot = await spaceService.buildSnapshot('audit', undefined, (progress) => options.onProgress?.({ stage: 'space', state: 'progress', progress }));
191
+ space.snapshotId = snapshot.id;
192
+ space.totals = snapshot.totals;
193
+ space.issues = snapshot.issues;
194
+ space.calibrationPlan = spaceService.buildCalibrationPlan(snapshot);
195
+ const cleanupLogs = await CleanupJournal.inspectPending();
196
+ for (const journal of cleanupLogs.journals) {
197
+ const data = journal.getData();
198
+ space.issues.push({
199
+ kind: 'recovery-log',
200
+ id: { libName: 'cleanup-journal', commit: data.planId },
201
+ message: `存在待恢复清理日志,组状态: ${data.groups.map((group) => `${group.index}:${group.state}`).join(', ')}`,
202
+ });
203
+ }
204
+ for (const filePath of cleanupLogs.corruptedFiles) {
205
+ space.issues.push({
206
+ kind: 'recovery-log',
207
+ id: { libName: 'cleanup-journal', commit: path.basename(filePath) },
208
+ message: `清理日志损坏,已保留原文件: ${filePath}`,
209
+ });
210
+ }
211
+ try {
212
+ const migrations = await MigrationJournal.listPending();
213
+ for (const migration of migrations) {
214
+ const data = migration.getData();
215
+ space.issues.push({
216
+ kind: 'recovery-log',
217
+ id: { libName: 'migration-journal', commit: data.id },
218
+ message: `存在待恢复迁移: ${data.sourcePath} -> ${data.destinationPath},阶段 ${data.phase}`,
219
+ });
220
+ }
221
+ }
222
+ catch (migrationError) {
223
+ space.issues.push({
224
+ kind: 'recovery-log',
225
+ id: { libName: 'migration-journal', commit: 'corrupted' },
226
+ message: migrationError.message,
227
+ });
228
+ }
229
+ debug(`[space] audit 完成: snapshot=${snapshot.id}, physical=${snapshot.totals.physical.bytes}, registered=${snapshot.totals.registered.bytes}, difference=${snapshot.totals.differenceBytes}, issues=${space.issues.length}, pendingCleanupLogs=${cleanupLogs.journals.length}, corruptedCleanupLogs=${cleanupLogs.corruptedFiles.length}`);
230
+ options.onProgress?.({ stage: 'space', state: 'completed' });
231
+ }
232
+ catch (err) {
233
+ space.error = err.message;
234
+ debug(`[space] audit 失败: error=${space.error}`);
235
+ options.onProgress?.({ stage: 'space', state: 'failed', error: space.error });
236
+ }
70
237
  // 计算汇总
71
238
  // 错误:检查项失败(ok === false),排除仅作为警告的项
72
239
  // 警告:目前只有 disk 有 warn 字段(空间 < 5GB 但不影响功能)
@@ -80,22 +247,27 @@ async function collectAllIssues(options) {
80
247
  const integrityIssues = integrity.invalidProjects.length +
81
248
  integrity.danglingLinks.length +
82
249
  integrity.orphanLibraries.length +
83
- integrity.missingLibraries.length +
84
250
  integrity.staleReferences.length +
85
251
  integrity.corruptedStores.length;
86
- const reclaimableSize = integrity.orphanLibraries.reduce((sum, lib) => sum + lib.size, 0);
87
- return {
252
+ const reclaimableSize = space.totals?.reclaimable.bytes
253
+ ?? integrity.orphanLibraries.reduce((sum, lib) => sum + lib.size, 0);
254
+ const result = {
88
255
  environment,
89
256
  integrity,
257
+ space,
90
258
  summary: {
91
259
  envErrors,
92
260
  envWarnings,
93
261
  integrityIssues,
262
+ uncachedDependencies: integrity.missingLibraries.length,
94
263
  reclaimableSize,
264
+ spaceIssues: space.issues.length,
95
265
  },
96
266
  };
267
+ debug(`[check] 健康检查完成: envErrors=${envErrors}, envWarnings=${envWarnings}, integrityIssues=${integrityIssues}, uncachedDependencies=${integrity.missingLibraries.length}, spaceIssues=${space.issues.length}, spaceSnapshot=${space.snapshotId ?? 'none'}, spaceError=${space.error ?? 'none'}`);
268
+ return result;
97
269
  }
98
- async function checkEnvironment() {
270
+ async function checkEnvironment(readOnly = false) {
99
271
  const codepacEnvironment = await checkCodepacEnvironment();
100
272
  const result = {
101
273
  codepac: { ok: false, message: '', details: codepacEnvironment },
@@ -111,7 +283,7 @@ async function checkEnvironment() {
111
283
  details: codepacEnvironment,
112
284
  };
113
285
  // 2. 配置
114
- const cfg = await config.load();
286
+ const cfg = await config.load({ migrate: !readOnly });
115
287
  result.config = {
116
288
  ok: !!cfg,
117
289
  message: cfg ? '已初始化' : '未初始化,运行 td init',
@@ -179,7 +351,7 @@ async function checkIntegrity(options) {
179
351
  let storePath;
180
352
  try {
181
353
  registry = getRegistry();
182
- await registry.load();
354
+ await registry.load({ migrate: !options?.readOnly });
183
355
  storePath = await store.getStorePath();
184
356
  }
185
357
  catch (err) {
@@ -188,6 +360,7 @@ async function checkIntegrity(options) {
188
360
  return result;
189
361
  }
190
362
  const projects = registry.listProjects();
363
+ debug(`[check] 数据一致性检查开始: store=${storePath}, projects=${projects.length}, integrity=${options?.integrity ?? false}, readOnly=${options?.readOnly ?? false}`);
191
364
  // 1. 检查项目和链接
192
365
  for (const project of projects) {
193
366
  const projectHash = registry.hashPath(project.path);
@@ -207,6 +380,25 @@ async function checkIntegrity(options) {
207
380
  if (!verifyPlatform) {
208
381
  continue;
209
382
  }
383
+ const libraryKey = registry.getLibraryKey(dep.libName, dep.commit);
384
+ const library = registry.getLibrary(libraryKey);
385
+ const hasGeneralStoreEntry = registry
386
+ .getLibraryStoreKeys(dep.libName, dep.commit)
387
+ .some((storeKey) => registry.getStore(storeKey)?.platform === GENERAL_PLATFORM);
388
+ const isGeneral = library?.isGeneral === true ||
389
+ library?.platforms.includes(GENERAL_PLATFORM) === true ||
390
+ hasGeneralStoreEntry;
391
+ const cachePlatform = isGeneral ? GENERAL_PLATFORM : verifyPlatform;
392
+ const cacheExists = await store.exists(dep.libName, dep.commit, cachePlatform);
393
+ if (!cacheExists) {
394
+ result.missingLibraries.push({
395
+ libName: dep.libName,
396
+ commit: dep.commit,
397
+ project: project.path,
398
+ });
399
+ debug(`[check] 本地依赖缓存缺失: project=${project.path}, lib=${dep.libName}, commit=${dep.commit}, platform=${cachePlatform}, link=${linkPath}`);
400
+ continue;
401
+ }
210
402
  try {
211
403
  const stat = await fs.lstat(linkPath);
212
404
  if (stat.isSymbolicLink()) {
@@ -219,21 +411,19 @@ async function checkIntegrity(options) {
219
411
  result.danglingLinks.push({
220
412
  path: linkPath,
221
413
  projectHash,
222
- dep: { libName: dep.libName, commit: dep.commit },
414
+ dep: {
415
+ libName: dep.libName,
416
+ commit: dep.commit,
417
+ platform: dep.platform,
418
+ linkedPath: dep.linkedPath,
419
+ ...(dep.scope ? { scope: dep.scope } : {}),
420
+ },
223
421
  });
224
422
  }
225
423
  }
226
424
  }
227
425
  catch {
228
- // 链接不存在,检查库是否在 Store
229
- const exists = await store.exists(dep.libName, dep.commit, verifyPlatform);
230
- if (!exists) {
231
- result.missingLibraries.push({
232
- libName: dep.libName,
233
- commit: dep.commit,
234
- project: project.path,
235
- });
236
- }
426
+ // Store 缓存存在但项目链接缺失,后续引用检查会报告失效引用。
237
427
  }
238
428
  }
239
429
  }
@@ -348,7 +538,7 @@ async function checkIntegrity(options) {
348
538
  if (options?.integrity) {
349
539
  const stores = registry.listStores();
350
540
  for (const entry of stores) {
351
- if (entry.fileCount != null) {
541
+ if (entry.fileCount != null && entry.contentHash) {
352
542
  // 有 fileCount 的做 fullVerify
353
543
  const expectedData = {
354
544
  size: entry.size,
@@ -368,24 +558,19 @@ async function checkIntegrity(options) {
368
558
  }
369
559
  }
370
560
  else {
371
- // 无 fileCount 的做回填(captureIntegrity)
372
- try {
373
- const integrity = await store.captureIntegrity(entry.libName, entry.commit, entry.platform);
374
- const storeKey = registry.getStoreKey(entry.libName, entry.commit, entry.platform);
375
- registry.updateStore(storeKey, {
376
- size: integrity.size,
377
- fileCount: integrity.fileCount,
378
- contentHash: integrity.contentHash,
379
- });
380
- }
381
- catch {
382
- debug(`[check] 回填完整性数据失败: ${entry.libName}:${entry.commit}:${entry.platform}`);
383
- }
561
+ result.corruptedStores.push({
562
+ libName: entry.libName,
563
+ commit: entry.commit,
564
+ platform: entry.platform,
565
+ kind: 'missing_integrity_record',
566
+ reason: 'missing integrity record',
567
+ expected: { size: entry.size },
568
+ actual: { fileCount: 0, size: 0 },
569
+ });
384
570
  }
385
571
  }
386
- // 保存回填的数据
387
- await registry.save();
388
572
  }
573
+ debug(`[check] 数据一致性检查完成: invalidProjects=${result.invalidProjects.length}, danglingLinks=${result.danglingLinks.length}, orphanLibraries=${result.orphanLibraries.length}, uncachedDependencies=${result.missingLibraries.length}, staleReferences=${result.staleReferences.length}, corruptedStores=${result.corruptedStores.length}`);
389
574
  return result;
390
575
  }
391
576
  // ============ 报告渲染 ============
@@ -415,10 +600,9 @@ function renderReport(result) {
415
600
  renderCheck('孤立库', integ.orphanLibraries.length === 0, integ.orphanLibraries.length === 0
416
601
  ? '无'
417
602
  : `${integ.orphanLibraries.length} 个未登记 (${formatSize(result.summary.reclaimableSize)})`, false);
418
- renderCheck('缺失库', integ.missingLibraries.length === 0, integ.missingLibraries.length === 0
603
+ renderCheck('本地未缓存依赖', integ.missingLibraries.length === 0, integ.missingLibraries.length === 0
419
604
  ? '无'
420
- : `${integ.missingLibraries.length} (需 td link 下载)`, true // 显示为警告而非错误,因为需要用户手动下载
421
- );
605
+ : `${integ.missingLibraries.length} 项(项目需要时可运行 td link)`, integ.missingLibraries.length > 0);
422
606
  renderCheck('引用关系', integ.staleReferences.length === 0, integ.staleReferences.length === 0
423
607
  ? '一致'
424
608
  : `${integ.staleReferences.length} 个失效引用`, false);
@@ -426,10 +610,32 @@ function renderReport(result) {
426
610
  ? '完整'
427
611
  : `${integ.corruptedStores.length} 个损坏的 Store 条目`, false);
428
612
  blank();
613
+ info('空间一致性');
614
+ separator();
615
+ if (result.space.error) {
616
+ renderCheck('空间审计', false, result.space.error, false);
617
+ }
618
+ else if (result.space.totals) {
619
+ const totals = result.space.totals;
620
+ renderCheck('空间审计', result.space.issues.length === 0, result.space.issues.length === 0
621
+ ? `实际占用 ${formatSize(totals.physical.bytes)},登记容量一致`
622
+ : `${result.space.issues.length} 个差异,实际 ${formatSize(totals.physical.bytes)},登记 ${formatSize(totals.registered.bytes)}`, false);
623
+ for (const issue of result.space.issues.slice(0, 20)) {
624
+ debug(`[space] audit 差异: kind=${issue.kind}, lib=${issue.id.libName}, commit=${issue.id.commit}, platform=${issue.platform ?? 'all'}, registered=${issue.registeredBytes ?? 'unknown'}, measured=${issue.measuredBytes ?? 'unknown'}, message=${issue.message}`);
625
+ }
626
+ if (result.space.issues.length > 20) {
627
+ debug(`[space] audit 差异省略: remaining=${result.space.issues.length - 20}`);
628
+ }
629
+ }
630
+ blank();
429
631
  separator();
430
632
  // 汇总
431
- const { envErrors, envWarnings, integrityIssues, reclaimableSize } = result.summary;
432
- if (envErrors === 0 && envWarnings === 0 && integrityIssues === 0) {
633
+ const { envErrors, envWarnings, integrityIssues, uncachedDependencies, reclaimableSize, spaceIssues, } = result.summary;
634
+ if (envErrors === 0 &&
635
+ envWarnings === 0 &&
636
+ integrityIssues === 0 &&
637
+ uncachedDependencies === 0 &&
638
+ spaceIssues === 0) {
433
639
  success('系统健康,无问题');
434
640
  }
435
641
  else {
@@ -440,6 +646,10 @@ function renderReport(result) {
440
646
  parts.push(`${envWarnings} 个警告`);
441
647
  if (integrityIssues > 0)
442
648
  parts.push(`${integrityIssues} 个数据问题`);
649
+ if (uncachedDependencies > 0)
650
+ parts.push(`${uncachedDependencies} 项本地未缓存依赖`);
651
+ if (spaceIssues > 0)
652
+ parts.push(`${spaceIssues} 个空间差异`);
443
653
  if (reclaimableSize > 0)
444
654
  parts.push(`可回收 ${formatSize(reclaimableSize)}`);
445
655
  warn(`发现问题: ${parts.join(', ')}`);
@@ -499,43 +709,55 @@ function renderCheck(name, ok, message, isWarning) {
499
709
  }
500
710
  }
501
711
  // ============ 交互式修复 ============
502
- async function interactiveCheck(issues, options) {
712
+ function hasSpaceCalibrationChoice(space) {
713
+ const plan = space.calibrationPlan;
714
+ return Boolean(plan && (plan.actions.length > 0 || plan.unregistered.deletableCount > 0));
715
+ }
716
+ function renderUncachedDependencyHint(issues) {
717
+ if (issues.missingLibraries.length === 0)
718
+ return;
503
719
  blank();
504
- const action = await selectWithCancel({
505
- message: '选择操作',
506
- choices: [
507
- { name: '修复所有问题', value: 'fix-all' },
508
- { name: '选择性修复', value: 'select' },
509
- { name: '查看详情', value: 'detail' },
510
- { name: colorize('← 退出', 'dim'), value: 'exit' },
511
- ],
512
- });
513
- // ESC 取消 = 退出
514
- if (action === PROMPT_CANCELLED) {
515
- info('已取消');
720
+ hint(`${issues.missingLibraries.length} 项本地未缓存依赖已保留;项目需要这些依赖时可运行 td link`);
721
+ }
722
+ async function interactiveCheck(result, options) {
723
+ while (true) {
724
+ blank();
725
+ const action = await selectWithCancel({
726
+ message: '选择操作',
727
+ choices: [
728
+ { name: '修复所有可修复问题', value: 'fix-all' },
729
+ { name: '选择性修复', value: 'select' },
730
+ { name: '查看详情', value: 'detail' },
731
+ { name: colorize('← 退出', 'dim'), value: 'exit' },
732
+ ],
733
+ });
734
+ if (action === PROMPT_CANCELLED) {
735
+ info('已取消');
736
+ return;
737
+ }
738
+ debug(`[check] 交互操作: action=${action}`);
739
+ if (action === 'detail') {
740
+ showDetails(result);
741
+ continue;
742
+ }
743
+ if (action === 'exit')
744
+ return;
745
+ if (action === 'select') {
746
+ await selectiveFix(result, options);
747
+ return;
748
+ }
749
+ await fixAllIssues(result.integrity, result.space, {
750
+ force: true,
751
+ calibrateSpace: hasSpaceCalibrationChoice(result.space),
752
+ deleteUnregistered: false,
753
+ verifyStoreIntegrity: options.integrity,
754
+ });
755
+ renderUncachedDependencyHint(result.integrity);
516
756
  return;
517
757
  }
518
- switch (action) {
519
- case 'fix-all':
520
- await fixAllIssues(issues, { force: true });
521
- // 缺失库无法自动修复,需要提示用户
522
- if (issues.missingLibraries.length > 0) {
523
- blank();
524
- hint(`缺失库 (${issues.missingLibraries.length} 个) 需要通过 td link 重新下载`);
525
- }
526
- break;
527
- case 'select':
528
- await selectiveFix(issues, options);
529
- break;
530
- case 'detail':
531
- showDetails(issues);
532
- await interactiveCheck(issues, options);
533
- break;
534
- case 'exit':
535
- break;
536
- }
537
- }
538
- async function selectiveFix(issues, options) {
758
+ }
759
+ async function selectiveFix(result, options) {
760
+ const issues = result.integrity;
539
761
  // 构建选项
540
762
  const choices = [];
541
763
  if (issues.invalidProjects.length > 0) {
@@ -574,6 +796,13 @@ async function selectiveFix(issues, options) {
574
796
  checked: true,
575
797
  });
576
798
  }
799
+ if (hasSpaceCalibrationChoice(result.space)) {
800
+ choices.push({
801
+ name: `校准 ${result.space.issues.length} 个空间差异`,
802
+ value: 'spaceCalibration',
803
+ checked: true,
804
+ });
805
+ }
577
806
  if (choices.length === 0) {
578
807
  success('没有可修复的问题');
579
808
  return;
@@ -601,7 +830,8 @@ async function selectiveFix(issues, options) {
601
830
  orphanLibraries: selected.includes('orphanLibraries')
602
831
  ? issues.orphanLibraries
603
832
  : [],
604
- missingLibraries: [], // 缺失库无法修复
833
+ // 本地未缓存依赖是用户允许的缓存状态,修复流程不下载也不移除依赖记录。
834
+ missingLibraries: [],
605
835
  staleReferences: selected.includes('staleReferences')
606
836
  ? issues.staleReferences
607
837
  : [],
@@ -609,14 +839,76 @@ async function selectiveFix(issues, options) {
609
839
  ? issues.corruptedStores
610
840
  : [],
611
841
  };
612
- await fixAllIssues(toFix, { force: options.force });
613
- // 缺失库无法自动修复,需要提示用户
614
- if (issues.missingLibraries.length > 0) {
842
+ const calibrateSpace = selected.includes('spaceCalibration');
843
+ let deleteUnregistered = false;
844
+ const spacePlan = result.space.calibrationPlan;
845
+ if (calibrateSpace &&
846
+ spacePlan &&
847
+ spacePlan.unregistered.totalCount > 0 &&
848
+ spacePlan.unregistered.deletableCount > 0) {
849
+ blank();
850
+ const unregisteredAction = await selectWithCancel({
851
+ message: '未登记平台如何处理?',
852
+ choices: [
853
+ {
854
+ name: '暂不处理',
855
+ value: 'keep',
856
+ description: `保留全部 ${spacePlan.unregistered.totalCount} 项内容`,
857
+ },
858
+ {
859
+ name: '删除无项目引用的内容',
860
+ value: 'delete-unreferenced',
861
+ description: `删除 ${spacePlan.unregistered.deletableCount} 项,保留 ${spacePlan.unregistered.protectedCount} 项`,
862
+ },
863
+ ],
864
+ });
865
+ if (unregisteredAction === PROMPT_CANCELLED) {
866
+ info('已取消');
867
+ return;
868
+ }
869
+ deleteUnregistered = unregisteredAction === 'delete-unreferenced';
870
+ }
871
+ debug(`[check] 选择性修复输入: selected=${selected.join(',')}, calibrateSpace=${calibrateSpace}, unregisteredAction=${deleteUnregistered ? 'delete-unreferenced' : 'keep'}`);
872
+ await fixAllIssues(toFix, result.space, {
873
+ force: options.force,
874
+ calibrateSpace,
875
+ deleteUnregistered,
876
+ verifyStoreIntegrity: options.integrity,
877
+ });
878
+ renderUncachedDependencyHint(issues);
879
+ }
880
+ function formatSignedSize(bytes) {
881
+ const sign = bytes > 0 ? '+' : bytes < 0 ? '-' : '';
882
+ return `${sign}${formatSize(Math.abs(bytes))}`;
883
+ }
884
+ function showSpaceDetails(space) {
885
+ if (space.error) {
886
+ info('空间审计:');
887
+ warn(` ${space.error}`);
615
888
  blank();
616
- hint(`缺失库 (${issues.missingLibraries.length} 个) 需要通过 td link 重新下载`);
889
+ return;
890
+ }
891
+ const plan = space.calibrationPlan;
892
+ if (!plan || space.issues.length === 0)
893
+ return;
894
+ info(`空间差异 (${space.issues.length}):`);
895
+ info(` 登记容量变化 ${plan.summary.changedCount} 项,合计 ${formatSignedSize(plan.summary.changedDifferenceBytes)}`);
896
+ info(` 缺失登记 ${plan.summary.missingCount} 项,登记容量 ${formatSize(plan.summary.missingRegisteredBytes)}`);
897
+ info(` 重复共享登记 ${plan.summary.duplicateSharedCount} 项`);
898
+ info(` 未登记平台 ${plan.unregistered.totalCount} 项,合计 ${formatSize(plan.unregistered.totalBytes)}`);
899
+ if (plan.unregistered.protectedCount > 0) {
900
+ info(` 引用保护 ${plan.unregistered.protectedCount} 项`);
901
+ }
902
+ const otherIssues = space.issues.filter((issue) => !['changed', 'missing', 'duplicate-record', 'unregistered'].includes(issue.kind));
903
+ if (otherIssues.length > 0)
904
+ info(` 其他空间异常 ${otherIssues.length} 项`);
905
+ if (plan.summary.ignoredArtifactCount > 0) {
906
+ info(` 系统文件 ${plan.summary.ignoredArtifactCount} 项,将忽略并保留`);
617
907
  }
908
+ blank();
618
909
  }
619
- function showDetails(issues) {
910
+ function showDetails(result) {
911
+ const issues = result.integrity;
620
912
  blank();
621
913
  title('问题详情');
622
914
  blank();
@@ -642,7 +934,7 @@ function showDetails(issues) {
642
934
  blank();
643
935
  }
644
936
  if (issues.missingLibraries.length > 0) {
645
- info(`缺失库 (${issues.missingLibraries.length}):`);
937
+ info(`本地未缓存依赖 (${issues.missingLibraries.length}):`);
646
938
  for (const lib of issues.missingLibraries) {
647
939
  hint(` - ${lib.libName}/${lib.commit.slice(0, 7)} (引用自 ${lib.project})`);
648
940
  }
@@ -669,22 +961,61 @@ function showDetails(issues) {
669
961
  }
670
962
  blank();
671
963
  }
964
+ showSpaceDetails(result.space);
672
965
  }
673
- async function fixAllIssues(issues, options) {
674
- const totalIssues = issues.invalidProjects.length +
966
+ function integrityRepairCount(issues) {
967
+ return (issues.invalidProjects.length +
675
968
  issues.danglingLinks.length +
676
969
  issues.orphanLibraries.length +
677
970
  issues.staleReferences.length +
678
- issues.corruptedStores.length;
679
- if (totalIssues === 0) {
971
+ issues.corruptedStores.length);
972
+ }
973
+ function retainCurrentIssues(selected, current, keyOf) {
974
+ const selectedKeys = new Set(selected.map(keyOf));
975
+ return current.filter((issue) => selectedKeys.has(keyOf(issue)));
976
+ }
977
+ /**
978
+ * 只保留用户确认过且在取得全局锁后仍然存在的问题。
979
+ * 新出现的问题不会被本次修复附带处理。
980
+ */
981
+ async function revalidateIntegritySelection(selected, verifyStoreIntegrity) {
982
+ const current = await checkIntegrity({ integrity: verifyStoreIntegrity });
983
+ const revalidated = {
984
+ invalidProjects: retainCurrentIssues(selected.invalidProjects, current.invalidProjects, (issue) => JSON.stringify([issue.hash, issue.path])),
985
+ danglingLinks: retainCurrentIssues(selected.danglingLinks, current.danglingLinks, (issue) => JSON.stringify([
986
+ issue.path,
987
+ issue.projectHash,
988
+ issue.dep.libName,
989
+ issue.dep.commit,
990
+ issue.dep.platform,
991
+ issue.dep.linkedPath,
992
+ issue.dep.scope ?? '',
993
+ ])),
994
+ orphanLibraries: retainCurrentIssues(selected.orphanLibraries, current.orphanLibraries, (issue) => JSON.stringify([issue.libName, issue.commit, issue.path])),
995
+ missingLibraries: [],
996
+ staleReferences: retainCurrentIssues(selected.staleReferences, current.staleReferences, (issue) => JSON.stringify([issue.libKey, issue.projectHash])),
997
+ corruptedStores: retainCurrentIssues(selected.corruptedStores, current.corruptedStores, (issue) => JSON.stringify([issue.libName, issue.commit, issue.platform, issue.kind ?? 'integrity'])),
998
+ };
999
+ debug(`[check] 锁内重新检查完成: selected=${integrityRepairCount(selected)}, retained=${integrityRepairCount(revalidated)}, currentUncachedDependencies=${current.missingLibraries.length}, invalidProjects=${revalidated.invalidProjects.length}, danglingLinks=${revalidated.danglingLinks.length}, orphanLibraries=${revalidated.orphanLibraries.length}, staleReferences=${revalidated.staleReferences.length}, corruptedStores=${revalidated.corruptedStores.length}`);
1000
+ return revalidated;
1001
+ }
1002
+ async function fixAllIssues(issues, space, options) {
1003
+ const totalIssues = integrityRepairCount(issues);
1004
+ const spaceIssueCount = options.calibrateSpace ? space.issues.length : 0;
1005
+ if (totalIssues === 0 && spaceIssueCount === 0) {
680
1006
  success('没有需要修复的问题');
681
1007
  return;
682
1008
  }
1009
+ debug(`[check] 修复请求: integrityIssues=${totalIssues}, calibrateSpace=${options.calibrateSpace}, reportedSpaceIssues=${spaceIssueCount}, unregisteredAction=${options.deleteUnregistered ? 'delete-unreferenced' : 'keep'}, force=${options.force}, sourceSnapshot=${space.snapshotId ?? 'none'}`);
683
1010
  // 确认
684
1011
  if (!options.force) {
685
1012
  blank();
1013
+ const targets = [
1014
+ ...(totalIssues > 0 ? [`${totalIssues} 个数据问题`] : []),
1015
+ ...(spaceIssueCount > 0 ? [`${spaceIssueCount} 个空间差异`] : []),
1016
+ ];
686
1017
  const confirmed = await confirmWithCancel({
687
- message: `确认修复以上 ${totalIssues} 个问题?`,
1018
+ message: `确认处理${targets.join('和')}?`,
688
1019
  default: false,
689
1020
  });
690
1021
  if (confirmed === PROMPT_CANCELLED || !confirmed) {
@@ -696,19 +1027,167 @@ async function fixAllIssues(issues, options) {
696
1027
  separator();
697
1028
  info('正在修复...');
698
1029
  blank();
1030
+ await withGlobalLock(async () => {
1031
+ if (totalIssues > 0) {
1032
+ info('正在重新确认待修复的数据问题...');
1033
+ const revalidated = await revalidateIntegritySelection(issues, options.verifyStoreIntegrity);
1034
+ if (integrityRepairCount(revalidated) === 0) {
1035
+ success('待修复的数据问题状态已变化,本次无需处理');
1036
+ }
1037
+ else {
1038
+ const repairResult = await applyAllIssueFixes(revalidated);
1039
+ if (repairResult.failed > 0)
1040
+ process.exitCode = 1;
1041
+ }
1042
+ }
1043
+ if (options.calibrateSpace) {
1044
+ try {
1045
+ await applySpaceCalibration(space, options.deleteUnregistered);
1046
+ }
1047
+ catch (err) {
1048
+ const message = err.message;
1049
+ error(`空间校准失败: ${message}`);
1050
+ debug(`[check] 空间校准异常: sourceSnapshot=${space.snapshotId ?? 'none'}, deleteUnregistered=${options.deleteUnregistered}, error=${message}`);
1051
+ process.exitCode = 1;
1052
+ }
1053
+ }
1054
+ });
1055
+ }
1056
+ async function applySpaceCalibration(sourceSpace, deleteUnregistered) {
1057
+ const registry = getRegistry();
1058
+ await registry.load();
1059
+ const storePath = await store.getStorePath();
1060
+ const service = new SpaceService(registry, storePath);
1061
+ const cleanupExecutor = service.createCleanupExecutor();
1062
+ const recovered = await cleanupExecutor.recoverPending();
1063
+ debug(`[check] 空间校准开始: store=${storePath}, sourceSnapshot=${sourceSpace.snapshotId ?? 'none'}, sourceIssues=${sourceSpace.issues.length}, unregisteredAction=${deleteUnregistered ? 'delete-unreferenced' : 'keep'}, recovered=${recovered}`);
1064
+ if (recovered > 0)
1065
+ info(`恢复了 ${recovered} 个中断清理记录`);
1066
+ const planningProgress = createSpaceProgressRenderer('正在重新核对空间差异');
1067
+ let snapshot;
1068
+ try {
1069
+ snapshot = await service.buildSnapshot('audit', undefined, planningProgress.update);
1070
+ }
1071
+ finally {
1072
+ planningProgress.close();
1073
+ }
1074
+ const plan = service.buildCalibrationPlan(snapshot);
1075
+ const willDelete = deleteUnregistered && plan.unregistered.deletableCount > 0;
1076
+ debug(`[check] 空间校准计划: plan=${plan.id}, snapshot=${snapshot.id}, metadataActions=${plan.actions.length}, changed=${plan.summary.changedCount}, missing=${plan.summary.missingCount}, duplicates=${plan.summary.duplicateSharedCount}, unregistered=${plan.unregistered.totalCount}, deletable=${plan.unregistered.deletableCount}, protected=${plan.unregistered.protectedCount}, willDelete=${willDelete}`);
1077
+ if (plan.actions.length === 0 && !willDelete) {
1078
+ if (plan.unregistered.totalCount > 0) {
1079
+ success(`登记数据无需调整,保留 ${plan.unregistered.totalCount} 项未登记平台`);
1080
+ }
1081
+ else {
1082
+ success('空间登记与 Store 实际状态一致');
1083
+ }
1084
+ return;
1085
+ }
1086
+ const stageProgress = {
1087
+ 'validate-plan': createSpaceProgressRenderer('正在验证校准计划'),
1088
+ 'verify-result': createSpaceProgressRenderer('正在验证校准结果'),
1089
+ };
1090
+ let calibrationResult;
1091
+ try {
1092
+ calibrationResult = await service.createCalibrationExecutor().execute(plan, {
1093
+ deleteUnregistered,
1094
+ onProgress: (stage, progress) => stageProgress[stage].update(progress),
1095
+ });
1096
+ }
1097
+ finally {
1098
+ stageProgress['validate-plan'].close();
1099
+ stageProgress['verify-result'].close();
1100
+ }
1101
+ const remainingPlan = service.buildCalibrationPlan(calibrationResult.finalSnapshot);
1102
+ const cleanupFailed = calibrationResult.cleanup != null && calibrationResult.cleanup.status !== 'completed';
1103
+ if (cleanupFailed) {
1104
+ process.exitCode = 1;
1105
+ warn(`空间校准部分完成: 更新 ${calibrationResult.metadataActionCount} 项登记数据,未登记平台删除成功 ${calibrationResult.cleanup.succeeded.length} 项、失败 ${calibrationResult.cleanup.failed.length} 项`);
1106
+ for (const failure of calibrationResult.cleanup.failed) {
1107
+ error(`未登记平台删除失败: ${failure.id.libName}/${failure.id.commit.slice(0, 7)}/${failure.platform ?? 'all'} - ${failure.message}`);
1108
+ }
1109
+ }
1110
+ else {
1111
+ success(`空间校准完成: 更新 ${calibrationResult.metadataActionCount} 项登记数据`);
1112
+ }
1113
+ if (calibrationResult.cleanup && !cleanupFailed) {
1114
+ info(`未登记平台清理: 删除 ${calibrationResult.cleanup.succeeded.length} 项,释放 ${formatSize(calibrationResult.cleanup.released.bytes)}`);
1115
+ }
1116
+ if (remainingPlan.unregistered.totalCount > 0) {
1117
+ info(`保留未登记平台: ${remainingPlan.unregistered.totalCount} 项`);
1118
+ }
1119
+ if (remainingPlan.actions.length > 0) {
1120
+ process.exitCode = 1;
1121
+ warn(`重新检查仍发现 ${remainingPlan.actions.length} 项登记差异,请查看详细日志`);
1122
+ }
1123
+ debug(`[check] 空间校准完成: plan=${plan.id}, finalSnapshot=${calibrationResult.finalSnapshot.id}, metadataActions=${calibrationResult.metadataActionCount}, cleanupStatus=${calibrationResult.cleanup?.status ?? 'skipped'}, cleanupSucceeded=${calibrationResult.cleanup?.succeeded.length ?? 0}, cleanupFailed=${calibrationResult.cleanup?.failed.length ?? 0}, remainingMetadata=${remainingPlan.actions.length}, remainingUnregistered=${remainingPlan.unregistered.totalCount}, finalDifference=${calibrationResult.finalSnapshot.totals.differenceBytes}`);
1124
+ }
1125
+ function commitKey(libName, commit) {
1126
+ return JSON.stringify([libName, commit]);
1127
+ }
1128
+ function buildCorruptedSelections(corrupted, includeShared) {
1129
+ const grouped = new Map();
1130
+ for (const item of corrupted) {
1131
+ const key = commitKey(item.libName, item.commit);
1132
+ const group = grouped.get(key) ?? {
1133
+ id: { libName: item.libName, commit: item.commit },
1134
+ platforms: new Set(),
1135
+ };
1136
+ if (!includeShared)
1137
+ group.platforms.add(item.platform);
1138
+ grouped.set(key, group);
1139
+ }
1140
+ return [...grouped.values()].map((group) => ({
1141
+ id: group.id,
1142
+ platforms: includeShared ? 'all' : [...group.platforms],
1143
+ ...(includeShared ? { includeShared: true } : {}),
1144
+ }));
1145
+ }
1146
+ async function executeRepairPlan(spaceService, executor, selections, options) {
1147
+ if (selections.length === 0)
1148
+ return null;
1149
+ const plan = await spaceService.buildCleanupPlan({
1150
+ reason: 'repair',
1151
+ selections,
1152
+ requireUnreferenced: options.requireUnreferenced,
1153
+ allowUnknownContent: options.allowUnknownContent,
1154
+ });
1155
+ for (const warningMessage of plan.warnings)
1156
+ warn(`[space] ${warningMessage}`);
1157
+ debug(`[check] 调用统一清理执行器: category=${options.category}, plan=${plan.id}, selections=${selections.length}, groups=${plan.groups.length}, expected=${plan.expectedRelease.bytes}`);
1158
+ if (plan.groups.length === 0)
1159
+ return null;
1160
+ const result = await executor.execute(plan);
1161
+ debug(`[check] 清理执行返回: category=${options.category}, plan=${plan.id}, status=${result.status}, succeeded=${result.succeeded.length}, failed=${result.failed.length}, released=${result.released.bytes}`);
1162
+ return result;
1163
+ }
1164
+ function resolvedIssueCount(result, issues) {
1165
+ if (!result)
1166
+ return 0;
1167
+ return issues.filter((issue) => result.succeeded.some((group) => group.id.libName === issue.libName
1168
+ && group.id.commit === issue.commit
1169
+ && (group.platform == null || issue.platform == null || group.platform === issue.platform))).length;
1170
+ }
1171
+ async function applyAllIssueFixes(issues) {
699
1172
  const registry = getRegistry();
700
1173
  await registry.load();
701
1174
  const storePath = await store.getStorePath();
1175
+ const spaceService = new SpaceService(registry, storePath);
1176
+ const executor = spaceService.createCleanupExecutor();
1177
+ const recovered = await executor.recoverPending();
1178
+ debug(`[check] 修复开始: invalidProjects=${issues.invalidProjects.length}, danglingLinks=${issues.danglingLinks.length}, orphanLibraries=${issues.orphanLibraries.length}, staleReferences=${issues.staleReferences.length}, corruptedStores=${issues.corruptedStores.length}, recovered=${recovered}`);
702
1179
  let fixed = 0;
1180
+ let failed = 0;
703
1181
  // 1. 清理无效项目
704
1182
  for (const p of issues.invalidProjects) {
705
1183
  try {
706
1184
  registry.removeProject(p.hash);
707
- success(`[ok] 清理项目: ${p.path}`);
1185
+ success(`清理项目: ${p.path}`);
708
1186
  fixed++;
709
1187
  }
710
1188
  catch (err) {
711
1189
  error(`[err] 清理项目失败: ${p.path} - ${err.message}`);
1190
+ failed++;
712
1191
  }
713
1192
  }
714
1193
  // 2. 移除悬挂链接
@@ -717,7 +1196,11 @@ async function fixAllIssues(issues, options) {
717
1196
  await fs.unlink(link.path);
718
1197
  const project = registry.getProject(link.projectHash);
719
1198
  if (project) {
720
- project.dependencies = project.dependencies.filter((d) => !(d.libName === link.dep.libName && d.commit === link.dep.commit));
1199
+ project.dependencies = project.dependencies.filter((dependency) => !(dependency.libName === link.dep.libName &&
1200
+ dependency.commit === link.dep.commit &&
1201
+ dependency.platform === link.dep.platform &&
1202
+ dependency.linkedPath === link.dep.linkedPath &&
1203
+ (dependency.scope ?? '') === (link.dep.scope ?? '')));
721
1204
  registry.updateProject(link.projectHash, {
722
1205
  dependencies: project.dependencies,
723
1206
  });
@@ -727,24 +1210,32 @@ async function fixAllIssues(issues, options) {
727
1210
  }
728
1211
  catch (err) {
729
1212
  error(`[err] 移除链接失败: ${link.path} - ${err.message}`);
1213
+ failed++;
730
1214
  }
731
1215
  }
732
- // 3. 删除孤立库
733
- for (const lib of issues.orphanLibraries) {
734
- try {
735
- await fs.rm(lib.path, { recursive: true, force: true });
736
- // 如果删除后 libName 目录为空,也一并删除
737
- const libDir = path.dirname(lib.path);
738
- const remaining = await fs.readdir(libDir).catch(() => ['placeholder']);
739
- if (remaining.length === 0) {
740
- await fs.rm(libDir, { recursive: true, force: true }).catch(() => { });
741
- }
742
- success(`[ok] 删除孤立库: ${lib.libName}/${lib.commit.slice(0, 7)}`);
743
- fixed++;
1216
+ // 3. 删除孤立库。显式修复允许清理该版本中的未登记内容。
1217
+ const orphanSelections = [...new Map(issues.orphanLibraries.map((lib) => [
1218
+ commitKey(lib.libName, lib.commit),
1219
+ { id: { libName: lib.libName, commit: lib.commit }, platforms: 'all' },
1220
+ ])).values()];
1221
+ try {
1222
+ const result = await executeRepairPlan(spaceService, executor, orphanSelections, {
1223
+ requireUnreferenced: true,
1224
+ allowUnknownContent: true,
1225
+ category: 'orphan-library',
1226
+ });
1227
+ fixed += resolvedIssueCount(result, issues.orphanLibraries);
1228
+ for (const group of result?.succeeded ?? []) {
1229
+ success(`[ok] 删除孤立库: ${group.id.libName}/${group.id.commit.slice(0, 7)}`);
744
1230
  }
745
- catch (err) {
746
- error(`[err] 删除孤立库失败: ${lib.libName}/${lib.commit.slice(0, 7)} - ${err.message}`);
1231
+ for (const failure of result?.failed ?? []) {
1232
+ error(`[err] 删除孤立库失败: ${failure.id.libName}/${failure.id.commit.slice(0, 7)} - ${failure.message}`);
747
1233
  }
1234
+ failed += result?.failed.length ?? (orphanSelections.length > 0 ? 1 : 0);
1235
+ }
1236
+ catch (err) {
1237
+ error(`[err] 删除孤立库失败: ${err.message}`);
1238
+ failed++;
748
1239
  }
749
1240
  // 4. 移除失效引用
750
1241
  for (const ref of issues.staleReferences) {
@@ -754,6 +1245,7 @@ async function fixAllIssues(issues, options) {
754
1245
  const colonIndex = ref.libKey.lastIndexOf(':');
755
1246
  if (colonIndex === -1) {
756
1247
  error(`[err] 无效的 libKey: ${ref.libKey}`);
1248
+ failed++;
757
1249
  continue;
758
1250
  }
759
1251
  const libName = ref.libKey.slice(0, colonIndex);
@@ -767,37 +1259,62 @@ async function fixAllIssues(issues, options) {
767
1259
  }
768
1260
  catch (err) {
769
1261
  error(`[err] 移除引用失败: ${ref.libKey} - ${err.message}`);
1262
+ failed++;
770
1263
  }
771
1264
  }
772
- // 5. 清理损坏的 Store 条目
773
- for (const item of issues.corruptedStores) {
1265
+ // 5. 清理损坏的 Store 条目。共享冲突整版本处理,其他问题按平台处理。
1266
+ const orphanKeys = new Set(issues.orphanLibraries.map((item) => commitKey(item.libName, item.commit)));
1267
+ const sharedCorruptions = issues.corruptedStores.filter((item) => (item.kind === 'shared_platform_conflict' || item.platform === SHARED_PLATFORM)
1268
+ && !orphanKeys.has(commitKey(item.libName, item.commit)));
1269
+ const sharedCorruptionKeys = new Set(sharedCorruptions.map((item) => commitKey(item.libName, item.commit)));
1270
+ const platformCorruptions = issues.corruptedStores.filter((item) => !orphanKeys.has(commitKey(item.libName, item.commit))
1271
+ && !sharedCorruptionKeys.has(commitKey(item.libName, item.commit)));
1272
+ for (const cleanup of [
1273
+ {
1274
+ items: issues.corruptedStores.filter((item) => sharedCorruptionKeys.has(commitKey(item.libName, item.commit))),
1275
+ selections: buildCorruptedSelections(sharedCorruptions, true),
1276
+ allowUnknownContent: false,
1277
+ category: 'shared-corruption',
1278
+ },
1279
+ {
1280
+ items: platformCorruptions,
1281
+ selections: buildCorruptedSelections(platformCorruptions, false),
1282
+ allowUnknownContent: false,
1283
+ category: 'corrupted-platform',
1284
+ },
1285
+ ]) {
774
1286
  try {
775
- if (item.kind === 'shared_platform_conflict') {
776
- const storeKeys = registry.getLibraryStoreKeys(item.libName, item.commit);
777
- for (const storeKey of storeKeys) {
778
- registry.removeStore(storeKey);
779
- }
780
- const commitPath = path.join(storePath, item.libName, item.commit);
781
- await fs.rm(commitPath, { recursive: true, force: true });
782
- success(`[ok] 清理损坏 Store: ${item.libName}/${item.commit.slice(0, 7)} (移除整个 commit 缓存)`);
1287
+ const result = await executeRepairPlan(spaceService, executor, cleanup.selections, {
1288
+ requireUnreferenced: false,
1289
+ allowUnknownContent: cleanup.allowUnknownContent,
1290
+ category: cleanup.category,
1291
+ });
1292
+ fixed += resolvedIssueCount(result, cleanup.items);
1293
+ for (const group of result?.succeeded ?? []) {
1294
+ const suffix = group.platform ? `/${group.platform}` : ' (移除整个 commit 缓存)';
1295
+ success(`清理损坏 Store: ${group.id.libName}/${group.id.commit.slice(0, 7)}${suffix}`);
783
1296
  }
784
- else {
785
- const storeKey = registry.getStoreKey(item.libName, item.commit, item.platform);
786
- registry.removeStore(storeKey);
787
- const platformPath = path.join(storePath, item.libName, item.commit, item.platform);
788
- await fs.rm(platformPath, { recursive: true, force: true });
789
- success(`[ok] 清理损坏 Store: ${item.libName}/${item.commit.slice(0, 7)}/${item.platform}`);
1297
+ for (const failure of result?.failed ?? []) {
1298
+ error(`清理损坏 Store 失败: ${failure.id.libName}/${failure.id.commit.slice(0, 7)}/${failure.platform ?? 'all'} - ${failure.message}`);
790
1299
  }
791
- fixed++;
1300
+ failed += result?.failed.length ?? (cleanup.selections.length > 0 ? 1 : 0);
792
1301
  }
793
1302
  catch (err) {
794
- error(`[err] 清理损坏 Store 失败: ${item.libName}/${item.commit.slice(0, 7)}/${item.platform} - ${err.message}`);
1303
+ error(`清理损坏 Store 失败: category=${cleanup.category} - ${err.message}`);
1304
+ failed++;
795
1305
  }
796
1306
  }
797
1307
  await registry.save();
798
1308
  blank();
799
1309
  separator();
800
- success(`修复完成: ${fixed} 个问题已解决`);
1310
+ if (failed > 0) {
1311
+ warn(`修复部分完成: 解决 ${fixed} 个问题,失败 ${failed} 项操作`);
1312
+ }
1313
+ else {
1314
+ success(`修复完成: ${fixed} 个问题已解决`);
1315
+ }
1316
+ debug(`[check] 修复完成: fixed=${fixed}, failed=${failed}`);
1317
+ return { fixed, failed };
801
1318
  }
802
1319
  // ============ 工具函数 ============
803
1320
  async function getDirSizeRecursive(dirPath) {
@@ -845,6 +1362,12 @@ export async function verifyIntegrity() {
845
1362
  export async function collectCheckResultForTest(options) {
846
1363
  return collectAllIssues(options);
847
1364
  }
1365
+ export async function runCheckForTest(options) {
1366
+ await runCheck(options);
1367
+ }
1368
+ export async function interactiveCheckForTest(result, options) {
1369
+ await interactiveCheck(result, options);
1370
+ }
848
1371
  /**
849
1372
  * 修复问题(兼容旧 repair 命令)
850
1373
  */
@@ -852,16 +1375,22 @@ export async function repairIssues(options) {
852
1375
  const result = await collectAllIssues();
853
1376
  // 渲染报告
854
1377
  renderReport(result);
855
- const hasIssues = result.summary.integrityIssues > 0;
1378
+ const hasIssues = result.summary.integrityIssues > 0 || hasSpaceCalibrationChoice(result.space);
856
1379
  if (!hasIssues) {
857
1380
  return;
858
1381
  }
859
1382
  if (options.dryRun) {
860
1383
  blank();
861
- hint('运行 td check --fix 修复问题');
1384
+ hint('运行 td check --fix 修复可修复问题');
862
1385
  return;
863
1386
  }
864
- await fixAllIssues(result.integrity, { force: options.force });
1387
+ await fixAllIssues(result.integrity, result.space, {
1388
+ force: options.force,
1389
+ calibrateSpace: hasSpaceCalibrationChoice(result.space),
1390
+ deleteUnregistered: false,
1391
+ verifyStoreIntegrity: false,
1392
+ });
1393
+ renderUncachedDependencyHint(result.integrity);
865
1394
  }
866
1395
  /**
867
1396
  * 校验所有 Store 条目的文件完整性
@@ -902,7 +1431,7 @@ export async function checkStoreIntegrity() {
902
1431
  });
903
1432
  }
904
1433
  }
905
- if (entry.fileCount != null) {
1434
+ if (entry.fileCount != null && entry.contentHash) {
906
1435
  const expectedData = {
907
1436
  size: entry.size,
908
1437
  fileCount: entry.fileCount,
@@ -921,22 +1450,17 @@ export async function checkStoreIntegrity() {
921
1450
  }
922
1451
  }
923
1452
  else {
924
- // 无 fileCount 的做回填
925
- try {
926
- const integrity = await store.captureIntegrity(entry.libName, entry.commit, entry.platform);
927
- const storeKey = registry.getStoreKey(entry.libName, entry.commit, entry.platform);
928
- registry.updateStore(storeKey, {
929
- size: integrity.size,
930
- fileCount: integrity.fileCount,
931
- contentHash: integrity.contentHash,
932
- });
933
- }
934
- catch {
935
- debug(`[check] 回填完整性数据失败: ${entry.libName}:${entry.commit}:${entry.platform}`);
936
- }
1453
+ corrupted.push({
1454
+ libName: entry.libName,
1455
+ commit: entry.commit,
1456
+ platform: entry.platform,
1457
+ kind: 'missing_integrity_record',
1458
+ reason: 'missing integrity record',
1459
+ expected: { size: entry.size },
1460
+ actual: { fileCount: 0, size: 0 },
1461
+ });
937
1462
  }
938
1463
  }
939
- await registry.save();
940
1464
  return corrupted;
941
1465
  }
942
1466
  /**
@@ -948,30 +1472,42 @@ export async function checkStoreIntegrity() {
948
1472
  export async function fixCorruptedStores(corrupted) {
949
1473
  if (corrupted.length === 0)
950
1474
  return;
1475
+ await withGlobalLock(() => applyCorruptedStoreFixes(corrupted));
1476
+ }
1477
+ async function applyCorruptedStoreFixes(corrupted) {
951
1478
  const registry = getRegistry();
952
1479
  await registry.load();
953
1480
  const storePath = await store.getStorePath();
954
- for (const item of corrupted) {
955
- try {
956
- if (item.kind === 'shared_platform_conflict') {
957
- const storeKeys = registry.getLibraryStoreKeys(item.libName, item.commit);
958
- for (const storeKey of storeKeys) {
959
- registry.removeStore(storeKey);
960
- }
961
- const commitPath = path.join(storePath, item.libName, item.commit);
962
- await fs.rm(commitPath, { recursive: true, force: true });
963
- success(`[ok] 清理损坏 Store: ${item.libName}/${item.commit.slice(0, 7)} (移除整个 commit 缓存)`);
964
- }
965
- else {
966
- const storeKey = registry.getStoreKey(item.libName, item.commit, item.platform);
967
- registry.removeStore(storeKey);
968
- const platformPath = path.join(storePath, item.libName, item.commit, item.platform);
969
- await fs.rm(platformPath, { recursive: true, force: true });
970
- success(`[ok] 清理损坏 Store: ${item.libName}/${item.commit.slice(0, 7)}/${item.platform}`);
971
- }
1481
+ const spaceService = new SpaceService(registry, storePath);
1482
+ const executor = spaceService.createCleanupExecutor();
1483
+ const recovered = await executor.recoverPending();
1484
+ debug(`[check] 兼容修复入口开始: corruptedStores=${corrupted.length}, recovered=${recovered}`);
1485
+ const sharedCorruptions = corrupted.filter((item) => item.kind === 'shared_platform_conflict' || item.platform === SHARED_PLATFORM);
1486
+ const sharedCorruptionKeys = new Set(sharedCorruptions.map((item) => commitKey(item.libName, item.commit)));
1487
+ const platformCorruptions = corrupted.filter((item) => !sharedCorruptionKeys.has(commitKey(item.libName, item.commit)));
1488
+ for (const cleanup of [
1489
+ {
1490
+ selections: buildCorruptedSelections(sharedCorruptions, true),
1491
+ allowUnknownContent: false,
1492
+ category: 'shared-corruption',
1493
+ },
1494
+ {
1495
+ selections: buildCorruptedSelections(platformCorruptions, false),
1496
+ allowUnknownContent: false,
1497
+ category: 'corrupted-platform',
1498
+ },
1499
+ ]) {
1500
+ const result = await executeRepairPlan(spaceService, executor, cleanup.selections, {
1501
+ requireUnreferenced: false,
1502
+ allowUnknownContent: cleanup.allowUnknownContent,
1503
+ category: cleanup.category,
1504
+ });
1505
+ for (const group of result?.succeeded ?? []) {
1506
+ const suffix = group.platform ? `/${group.platform}` : ' (移除整个 commit 缓存)';
1507
+ success(`清理损坏 Store: ${group.id.libName}/${group.id.commit.slice(0, 7)}${suffix}`);
972
1508
  }
973
- catch (err) {
974
- error(`[err] 清理损坏 Store 失败: ${item.libName}/${item.commit.slice(0, 7)}/${item.platform} - ${err.message}`);
1509
+ for (const failure of result?.failed ?? []) {
1510
+ error(`清理损坏 Store 失败: ${failure.id.libName}/${failure.id.commit.slice(0, 7)}/${failure.platform ?? 'all'} - ${failure.message}`);
975
1511
  }
976
1512
  }
977
1513
  await registry.save();