kld-sdd 2.6.16 → 2.6.21

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 (70) hide show
  1. package/bin/kld-sdd-init.js +10 -0
  2. package/kld-sdd-guide.html +22 -0
  3. package/lib/device-auth-cli.js +130 -0
  4. package/lib/device-auth.js +345 -0
  5. package/lib/init-account-binding.js +108 -0
  6. package/lib/init.js +238 -55
  7. package/lib/skills-bundle.js +20 -1
  8. package/lib/tool-profiles.js +8 -0
  9. package/package.json +7 -3
  10. package/skywalk-sdd/apply-worktree-finish.cjs +2 -23
  11. package/skywalk-sdd/context-client.cjs +38 -87
  12. package/skywalk-sdd/index.cjs +860 -132
  13. package/skywalk-sdd/kb-sync-identity.cjs +780 -0
  14. package/skywalk-sdd/kb-upload.cjs +505 -0
  15. package/skywalk-sdd/lib/shared.cjs +811 -0
  16. package/skywalk-sdd/lib/usage-contract.cjs +276 -0
  17. package/skywalk-sdd/lib/usage-reporter.cjs +354 -0
  18. package/skywalk-sdd/lib/user-config.cjs +157 -0
  19. package/skywalk-sdd/metrics-v3.cjs +138 -8
  20. package/skywalk-sdd/ontology/archive-package.cjs +19 -34
  21. package/skywalk-sdd/ontology/change-lock.cjs +3 -7
  22. package/skywalk-sdd/ontology/external-key.cjs +18 -4
  23. package/skywalk-sdd/ontology/id.cjs +26 -5
  24. package/skywalk-sdd/ontology/identity-index.cjs +3 -7
  25. package/skywalk-sdd/ontology/resolve-spec-root.cjs +20 -6
  26. package/skywalk-sdd/ontology/runtime.cjs +16 -12
  27. package/skywalk-sdd/ontology/traceability-validator.cjs +7 -4
  28. package/skywalk-sdd/reporting/change-report-markdown.cjs +137 -19
  29. package/skywalk-sdd/reporting/change-report-model.cjs +993 -14
  30. package/skywalk-sdd/reporting/change-report-renderer.cjs +106 -41
  31. package/skywalk-sdd/reporting/change-report-view-model.cjs +272 -43
  32. package/skywalk-sdd/reporting/core-metric-definitions.cjs +192 -0
  33. package/skywalk-sdd/spec-root.cjs +31 -0
  34. package/templates/git-hooks/pre-commit-consistency-check.cjs +271 -116
  35. package/templates/git-hooks/pre-push-consistency-check.cjs +252 -123
  36. package/templates/hooks/codebuddy/hooks/hook-gate-core.cjs +327 -0
  37. package/templates/hooks/codebuddy/hooks/sdd-apply-test-gate.cjs +54 -9
  38. package/templates/hooks/codebuddy/hooks/sdd-mid-checkpoint.cjs +63 -6
  39. package/templates/hooks/codebuddy/hooks/sdd-tdd-rhythm-gate.cjs +113 -65
  40. package/templates/openspec/proposal.md +7 -3
  41. package/templates/openspec/spec.md +3 -3
  42. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +8 -6
  43. package/templates/skills/kld-sdd/opsx-apply/checklist.md +2 -0
  44. package/templates/skills/kld-sdd/opsx-apply/reference.md +29 -7
  45. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +6 -5
  46. package/templates/skills/kld-sdd/opsx-check/SKILL.md +49 -17
  47. package/templates/skills/kld-sdd/opsx-check/checklist.md +4 -2
  48. package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +187 -257
  49. package/templates/skills/kld-sdd/opsx-consistency-check/reference.md +129 -0
  50. package/templates/skills/kld-sdd/opsx-design/SKILL.md +2 -2
  51. package/templates/skills/kld-sdd/opsx-explore/SKILL.md +2 -2
  52. package/templates/skills/kld-sdd/opsx-kb-config/SKILL.md +185 -0
  53. package/templates/skills/kld-sdd/opsx-kb-config/reference.md +127 -0
  54. package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +218 -53
  55. package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +51 -9
  56. package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +12 -50
  57. package/templates/skills/kld-sdd/opsx-ontology-query/phase-3-postchange.md +2 -2
  58. package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +1 -1
  59. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +35 -23
  60. package/templates/skills/kld-sdd/opsx-propose/checklist.md +2 -0
  61. package/templates/skills/kld-sdd/opsx-propose/reference.md +22 -17
  62. package/templates/skills/kld-sdd/opsx-rules/SKILL.md +2 -2
  63. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +19 -15
  64. package/templates/skills/kld-sdd/opsx-spec/checklist.md +2 -0
  65. package/templates/skills/kld-sdd/opsx-task/SKILL.md +2 -4
  66. package/templates/skills/kld-sdd/opsx-test/SKILL.md +2 -2
  67. package/templates/skills/kld-sdd/tdd-core/reference.md +1 -1
  68. package/templates/skills/kld-sdd/tdd-rules/rules/test-skeleton-telemetry.md +1 -1
  69. package/templates/skills/kld-sdd/opsx-kb-ingest/state.example.json +0 -7
  70. package/templates/skills/kld-sdd/opsx-ontology-query/state.example.json +0 -7
@@ -0,0 +1,505 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * kb-upload.cjs — 知识库上传脚本
6
+ *
7
+ * 支持两种上传模式:
8
+ * 1. 文件夹上传:自动跑语义管线 → 生成元数据 → 打包 zip → 上传
9
+ * 2. Zip 上传:直接上传已有 zip 包
10
+ *
11
+ * 用法:
12
+ * node skywalk-sdd/kb-upload.cjs --folder=<path> [--minimum-level=propose-spec] [--validate-only]
13
+ * node skywalk-sdd/kb-upload.cjs --package=<zip-path>
14
+ * node skywalk-sdd/kb-upload.cjs --folder=<path> --project=<spec-root> --change=<name>
15
+ *
16
+ * project_id 解析:
17
+ * 1. $SPEC_ROOT/skywalk-sdd/project-identity.json 的 project_id
18
+ * 2. 都没有 → 报错:请运行 /opsx-kb-config 配置知识库
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const {
24
+ parseArgs,
25
+ readJson,
26
+ readJsonSafe,
27
+ writeJsonAtomic,
28
+ ensureDir,
29
+ copyDirRecursive,
30
+ resolveSpecRootFast,
31
+ loadKbState,
32
+ uploadFile,
33
+ httpGetJson,
34
+ } = require('./lib/shared.cjs');
35
+
36
+ /**
37
+ * 读取 kb-state.json 中指定 change 的 archive 缓存。
38
+ * @param {string} specRoot
39
+ * @param {string} changeName
40
+ * @returns {object|null} { archive_id, content_hash, uploaded_at }
41
+ */
42
+ function readArchiveCache(specRoot, changeName) {
43
+ const directPath = path.join(specRoot, 'kb-state.json');
44
+ const legacyPath = path.join(specRoot, '..', '.shared', 'kb-state.json');
45
+ const statePath = fs.existsSync(directPath) ? directPath : legacyPath;
46
+ const state = readJsonSafe(statePath);
47
+ if (!state || !state.archiveCache) return null;
48
+ return state.archiveCache[changeName] || null;
49
+ }
50
+
51
+ /**
52
+ * 更新 kb-state.json 中的 archiveCache(原子写入)。
53
+ * @param {string} specRoot
54
+ * @param {string} changeName
55
+ * @param {string} archiveId
56
+ * @param {string} contentHash
57
+ */
58
+ function updateKbStateCache(specRoot, changeName, archiveId, contentHash) {
59
+ const directPath = path.join(specRoot, 'kb-state.json');
60
+ const legacyPath = path.join(specRoot, '..', '.shared', 'kb-state.json');
61
+ const statePath = fs.existsSync(directPath) ? directPath : legacyPath;
62
+ const state = readJsonSafe(statePath) || {};
63
+ if (!state.archiveCache) state.archiveCache = {};
64
+ state.archiveCache[changeName] = {
65
+ archive_id: archiveId,
66
+ content_hash: contentHash,
67
+ uploaded_at: new Date().toISOString(),
68
+ };
69
+ writeJsonAtomic(statePath, state);
70
+ console.log(`✓ 已更新本地缓存: ${changeName} → ${archiveId.slice(0, 24)}...`);
71
+ }
72
+
73
+ // ── 常量 ──────────────────────────────────────────────
74
+
75
+ const MINIMUM_LEVELS = {
76
+ 'propose-spec': ['proposal.md', 'spec.md'],
77
+ 'design': ['proposal.md', 'spec.md', 'design.md'],
78
+ 'task': ['proposal.md', 'spec.md', 'design.md', 'tasks.md'],
79
+ 'full': ['proposal.md', 'spec.md', 'design.md', 'tasks.md'],
80
+ };
81
+
82
+ const DEFAULT_LEVEL = 'propose-spec';
83
+
84
+ // ── 工具函数 ──────────────────────────────────────────
85
+
86
+ function resolveSpecRoot(explicit) {
87
+ if (explicit) return path.resolve(explicit);
88
+ return resolveSpecRootFast(__dirname);
89
+ }
90
+
91
+ /**
92
+ * 解析 project_id
93
+ * 优先级:project-identity.json → 报错
94
+ */
95
+ function resolveProjectId(specRoot) {
96
+ const identityPath = path.join(specRoot, 'skywalk-sdd', 'project-identity.json');
97
+ if (fs.existsSync(identityPath)) {
98
+ const identity = readJson(identityPath);
99
+ if (identity.project_id) {
100
+ return identity.project_id;
101
+ }
102
+ }
103
+ throw new Error(
104
+ '无法确定 project_id。project-identity.json 不存在或无 project_id 字段。\n' +
105
+ '请运行 /opsx-kb-config 配置知识库。'
106
+ );
107
+ }
108
+
109
+ /**
110
+ * 读取 KB 配置(api, apiKey, tenantKey, targets)
111
+ * 使用 shared.cjs 的 loadKbState,支持 BOM 剥离和多路径搜索
112
+ */
113
+ function readKbState(specRoot) {
114
+ // 优先尝试 specRoot 根目录下的 kb-state.json(新路径)
115
+ const directPath = path.join(specRoot, 'kb-state.json');
116
+ // 兼容旧路径:specRoot 同级 .shared/kb-state.json
117
+ const legacyPath = path.join(specRoot, '..', '.shared', 'kb-state.json');
118
+ if (fs.existsSync(directPath)) {
119
+ try {
120
+ return readJson(directPath);
121
+ } catch (e) {
122
+ throw new Error(
123
+ `KB 配置解析失败 (${directPath}): ${e.message}\n` +
124
+ '请运行 /opsx-kb-config 重新配置知识库。'
125
+ );
126
+ }
127
+ }
128
+ // 退回到多路径搜索(从 specRoot 向上查找)
129
+ const { config, path: foundPath, warnings } = loadKbState(specRoot);
130
+ if (warnings.length > 0) {
131
+ for (const w of warnings) console.warn(`⚠️ ${w}`);
132
+ }
133
+ if (!config) {
134
+ throw new Error(
135
+ 'KB 配置不存在(kb-state.json 缺失)。\n' +
136
+ '请运行 /opsx-kb-config 配置知识库。'
137
+ );
138
+ }
139
+ return config;
140
+ }
141
+
142
+ /**
143
+ * 校验文件夹必需文件
144
+ */
145
+ function validateRequiredFiles(folderPath, minimumLevel) {
146
+ const required = MINIMUM_LEVELS[minimumLevel] || MINIMUM_LEVELS[DEFAULT_LEVEL];
147
+ const missing = required.filter(f => !fs.existsSync(path.join(folderPath, f)));
148
+ if (missing.length > 0) {
149
+ throw new Error(
150
+ `[${minimumLevel}] 缺少必需文件: ${missing.join(', ')}\n` +
151
+ `目录: ${folderPath}\n` +
152
+ `等级说明:\n` +
153
+ ` propose-spec: proposal.md + spec.md\n` +
154
+ ` design: + design.md\n` +
155
+ ` task: + tasks.md\n` +
156
+ ` full: 全部 4 个`
157
+ );
158
+ }
159
+ console.log(`✓ 文件校验通过 [${minimumLevel}]: ${required.join(', ')}`);
160
+ }
161
+
162
+ /**
163
+ * 本地预检 project_id === spaceKey
164
+ */
165
+ function preCheckProjectId(projectId, state) {
166
+ if (!state.targets || !state.targets[0]) {
167
+ throw new Error('KB 配置中无 targets,请运行 /opsx-kb-config 配置知识库。');
168
+ }
169
+ const spaceKey = state.targets[0].spaceKey;
170
+ if (!spaceKey) {
171
+ console.warn('⚠️ kb-state.json targets[0].spaceKey 为空,跳过本地预检');
172
+ return;
173
+ }
174
+ if (projectId !== spaceKey) {
175
+ throw new Error(
176
+ `PROJECT_SPACE_MISMATCH 预检失败:\n` +
177
+ ` project_id (project-identity.json): ${projectId}\n` +
178
+ ` spaceKey (kb-state.json): ${spaceKey}\n` +
179
+ `请运行 /opsx-kb-config 更新配置。`
180
+ );
181
+ }
182
+ console.log(`✓ project_id 预检通过: ${projectId} === ${spaceKey}`);
183
+ }
184
+
185
+ /**
186
+ * 文件夹模式:构建 Archive Package(生成元数据 + zip)
187
+ *
188
+ * 流程:
189
+ * 1. 运行 semantic-reconcile(生成 working-ontology.json)
190
+ * 2. 创建临时 staging 目录
191
+ * 3. 复制源文件到 staging
192
+ * 4. createArchiveSnapshot(生成 archive-ontology.json)
193
+ * 5. materializeArchivePackage(生成 manifest + canonical-facts + conversion-report + zip)
194
+ * 6. 返回 zip 路径
195
+ */
196
+ function buildPackageFromFolder(specRoot, changeName, options) {
197
+ const runtime = require('./ontology/runtime.cjs');
198
+ const archivePackage = require('./ontology/archive-package.cjs');
199
+
200
+ // 1. 语义管线
201
+ console.log('→ 运行语义管线 (semantic-reconcile)...');
202
+ const semanticResult = runtime.reconcileChange(specRoot, changeName, {
203
+ profile: 'auto',
204
+ markPending: true,
205
+ });
206
+ if (!semanticResult.valid) {
207
+ const errors = semanticResult.diagnostics
208
+ .filter(d => d.severity === 'error')
209
+ .map(d => `${d.code}: ${d.message}`);
210
+ throw new Error(`语义校验失败:\n${errors.join('\n')}`);
211
+ }
212
+ console.log(`✓ 语义校验通过 (profile: ${semanticResult.profile}, revision: ${semanticResult.revision})`);
213
+
214
+ // 2. 创建临时 staging 目录
215
+ const stagingDir = path.join(
216
+ fs.mkdtempSync(path.join(require('os').tmpdir(), 'kb-upload-')),
217
+ );
218
+ const zipPath = path.join(stagingDir, `${changeName}.zip`);
219
+
220
+ try {
221
+ // 3. 复制源文件到 staging
222
+ const changeDir = path.join(specRoot, 'openspec', 'changes', changeName);
223
+ copyDirRecursive(changeDir, stagingDir);
224
+
225
+ // 4. 创建 archive snapshot(生成 archive-ontology.json)
226
+ runtime.createArchiveSnapshot(specRoot, changeName, stagingDir, semanticResult.state);
227
+ console.log('✓ 已生成 archive-ontology.json');
228
+
229
+ // 5. 生成 manifest + canonical-facts + conversion-report + zip
230
+ const projectIdentity = archivePackage.ensureProjectIdentity(specRoot);
231
+ const result = archivePackage.materializeArchivePackage({
232
+ projectRoot: specRoot,
233
+ archiveDir: stagingDir,
234
+ archivePath: stagingDir,
235
+ changeName: changeName,
236
+ packagePath: zipPath,
237
+ reason: options.reason || '随时入库(文件夹上传)',
238
+ method: 'kb-upload-folder',
239
+ });
240
+
241
+ console.log(`✓ Archive Package 已生成`);
242
+ console.log(` archive_id: ${result.manifest.archive_id}`);
243
+ console.log(` project_id: ${result.manifest.project_id}`);
244
+ console.log(` files: ${result.manifest.files.length}`);
245
+
246
+ return { zipPath, manifest: result.manifest, stagingDir };
247
+ } catch (err) {
248
+ // 出错时清理 staging
249
+ fs.rmSync(stagingDir, { recursive: true, force: true });
250
+ throw err;
251
+ }
252
+ }
253
+
254
+ /**
255
+ * 上传 zip 到 KB(使用 Node.js HTTP,替代 curl)
256
+ */
257
+ async function uploadZip(zipPath, state, options) {
258
+ const target = options.spaceId && options.kbId
259
+ ? { spaceId: options.spaceId, kbId: options.kbId }
260
+ : state.targets[0];
261
+
262
+ if (!target || !target.spaceId || !target.kbId) {
263
+ throw new Error('未指定目标 Space/KB,请运行 /opsx-kb-config 配置知识库。');
264
+ }
265
+
266
+ const apiUrl = state.api || 'http://localhost:8090/api';
267
+ const uploadUrl = `${apiUrl}/v1/spaces/${target.spaceId}/knowledge-bases/${target.kbId}/ingestions`;
268
+
269
+ console.log(`→ 上传到 KB: ${target.spaceName || target.spaceId}/${target.kbName || target.kbId}`);
270
+ console.log(` URL: ${uploadUrl}`);
271
+ console.log(` ZIP: ${zipPath} (${(fs.statSync(zipPath).size / 1024).toFixed(1)} KB)`);
272
+
273
+ try {
274
+ const result = await uploadFile(uploadUrl, zipPath, state.apiKey);
275
+ return result;
276
+ } catch (err) {
277
+ throw new Error(`上传失败: ${err.message}`);
278
+ }
279
+ }
280
+
281
+ // ── 主流程 ────────────────────────────────────────────
282
+
283
+ async function main() {
284
+ const args = parseArgs(process.argv.slice(2));
285
+
286
+ if (!args.folder && !args.package) {
287
+ console.error('用法:');
288
+ console.error(' node skywalk-sdd/kb-upload.cjs --folder=<path> [--minimum-level=propose-spec] [--validate-only]');
289
+ console.error(' node skywalk-sdd/kb-upload.cjs --package=<zip-path>');
290
+ console.error(' node skywalk-sdd/kb-upload.cjs --folder=<path> --project=<spec-root> --change=<name>');
291
+ process.exit(1);
292
+ }
293
+
294
+ const minimumLevel = args['minimum-level'] || DEFAULT_LEVEL;
295
+ if (!MINIMUM_LEVELS[minimumLevel]) {
296
+ console.error(`无效的 --minimum-level: ${minimumLevel}`);
297
+ console.error(`可选值: ${Object.keys(MINIMUM_LEVELS).join(', ')}`);
298
+ process.exit(1);
299
+ }
300
+
301
+ try {
302
+ // ── Zip 模式 ──
303
+ if (args.package) {
304
+ const zipPath = path.resolve(args.package);
305
+ if (!fs.existsSync(zipPath)) {
306
+ throw new Error(`zip 文件不存在: ${zipPath}`);
307
+ }
308
+ console.log(`模式: Zip 上传`);
309
+ console.log(` ZIP: ${zipPath}`);
310
+
311
+ const specRoot = resolveSpecRoot(args.project);
312
+ const state = readKbState(specRoot);
313
+
314
+ if (!state.apiKey) {
315
+ throw new Error('KB 配置中无 apiKey,请运行 /opsx-kb-config 配置知识库。');
316
+ }
317
+
318
+ const result = await uploadZip(zipPath, state, {
319
+ spaceId: args['space-id'],
320
+ kbId: args['kb-id'],
321
+ });
322
+
323
+ console.log('\n── 上传结果 ──');
324
+ console.log(JSON.stringify(result, null, 2));
325
+ return;
326
+ }
327
+
328
+ // ── 文件夹模式 ──
329
+ const specRoot = resolveSpecRoot(args.project);
330
+
331
+ // 解析 changeName
332
+ let changeName;
333
+ let folderPath;
334
+ if (args.change) {
335
+ changeName = args.change;
336
+ folderPath = path.join(specRoot, 'openspec', 'changes', changeName);
337
+ } else {
338
+ folderPath = path.resolve(args.folder);
339
+ changeName = path.basename(folderPath);
340
+ }
341
+
342
+ if (!fs.existsSync(folderPath)) {
343
+ throw new Error(`变更目录不存在: ${folderPath}`);
344
+ }
345
+
346
+ console.log(`模式: 文件夹上传`);
347
+ console.log(` 目录: ${folderPath}`);
348
+ console.log(` 变更: ${changeName}`);
349
+ console.log(` 等级: ${minimumLevel}`);
350
+ console.log(` Spec Root: ${specRoot}`);
351
+ console.log('');
352
+
353
+ // 1. 校验必需文件
354
+ validateRequiredFiles(folderPath, minimumLevel);
355
+
356
+ // 2. 解析 project_id
357
+ const projectId = resolveProjectId(specRoot);
358
+ console.log(`✓ project_id: ${projectId}`);
359
+
360
+ // 3. 读取 KB 配置 + 预检
361
+ const state = readKbState(specRoot);
362
+ if (!state.apiKey) {
363
+ throw new Error('KB 配置中无 apiKey,请运行 /opsx-kb-config 配置知识库。');
364
+ }
365
+ preCheckProjectId(projectId, state);
366
+
367
+ // 4. 仅校验模式(含语义校验)
368
+ if (args['validate-only']) {
369
+ const runtime = require('./ontology/runtime.cjs');
370
+ console.log('→ 运行语义管线 (semantic-reconcile)...');
371
+ const semanticResult = runtime.reconcileChange(specRoot, changeName, {
372
+ profile: 'auto',
373
+ markPending: true,
374
+ });
375
+ if (!semanticResult.valid) {
376
+ const errors = semanticResult.diagnostics
377
+ .filter(d => d.severity === 'error')
378
+ .map(d => `${d.code}: ${d.message}`);
379
+ throw new Error(`语义校验失败:\n${errors.join('\n')}`);
380
+ }
381
+ console.log(`✓ 语义校验通过 (profile: ${semanticResult.profile}, revision: ${semanticResult.revision})`);
382
+ console.log('\n✓ 校验完成(--validate-only,未上传)');
383
+ return;
384
+ }
385
+
386
+ // 5. 构建 Archive Package
387
+ console.log('');
388
+ const { zipPath, manifest, stagingDir } = buildPackageFromFolder(specRoot, changeName, {
389
+ reason: args.reason,
390
+ });
391
+
392
+ try {
393
+ // === Step 5.1: 本地缓存快速检查 ===
394
+ const cache = readArchiveCache(specRoot, changeName);
395
+ if (cache && cache.archive_id === manifest.archive_id) {
396
+ console.log('\n✓ 内容无变更(archive_id 与上次成功上传一致),跳过上传');
397
+ console.log(` cached archive_id: ${cache.archive_id}`);
398
+ console.log(` uploaded_at: ${cache.uploaded_at}`);
399
+ console.log('\n✓ 完成(本地缓存命中)');
400
+ return;
401
+ }
402
+
403
+ // 解析上传目标
404
+ const target = (args['space-id'] && args['kb-id'])
405
+ ? { spaceId: args['space-id'], kbId: args['kb-id'] }
406
+ : state.targets[0];
407
+
408
+ if (!target || !target.spaceId || !target.kbId) {
409
+ throw new Error('未指定目标 Space/KB,请运行 /opsx-kb-config 配置知识库。');
410
+ }
411
+
412
+ const apiUrl = state.api || 'http://localhost:8090/api';
413
+
414
+ // === Step 5.5: KB 预检(网络级幂等确认)===
415
+ const archiveId = manifest.archive_id;
416
+ const contentHash = manifest.content_hash;
417
+ console.log(`→ Pre-check: archive_id=${archiveId}`);
418
+ console.log(` content_hash=${contentHash}`);
419
+
420
+ const checkUrl = `${apiUrl}/v1/spaces/${target.spaceId}/knowledge-bases/${target.kbId}/ingestions/check`
421
+ + `?archiveId=${encodeURIComponent(archiveId)}`
422
+ + `&contentHash=${encodeURIComponent(contentHash)}`;
423
+
424
+ try {
425
+ const checkResult = await httpGetJson(checkUrl, state.apiKey);
426
+
427
+ if (checkResult && checkResult.data && checkResult.data.idempotent) {
428
+ console.log('\n✓ KB 中已存在相同 archive(幂等命中),跳过上传');
429
+ console.log(` snapshot_id: ${checkResult.data.snapshotId}`);
430
+ updateKbStateCache(specRoot, changeName, archiveId, contentHash);
431
+ console.log('\n✓ 完成(KB 预检命中)');
432
+ return;
433
+ }
434
+
435
+ if (checkResult && checkResult.data && checkResult.data.exists && !checkResult.data.idempotent) {
436
+ throw new Error(
437
+ `ARCHIVE_IMMUTABILITY_VIOLATION: archive_id 已存在但 content_hash 不同\n` +
438
+ ` archive_id: ${archiveId}\n` +
439
+ ` 如果内容确实变更,请确认实体身份块已正确更新`
440
+ );
441
+ }
442
+
443
+ console.log('✓ Pre-check 通过(KB 中无此 archive,继续上传)');
444
+ } catch (checkErr) {
445
+ // 降级:KB 后端未部署 /check 接口(404)→ 继续上传
446
+ if (checkErr.message && checkErr.message.includes('HTTP 404')) {
447
+ console.log('⚠️ KB 后端未部署 /check 接口,降级为直接上传');
448
+ } else {
449
+ throw checkErr;
450
+ }
451
+ }
452
+
453
+ // 6. 上传
454
+ console.log('');
455
+ const result = await uploadZip(zipPath, state, {
456
+ spaceId: target.spaceId,
457
+ kbId: target.kbId,
458
+ });
459
+
460
+ console.log('\n── 上传结果 ──');
461
+ console.log(JSON.stringify(result, null, 2));
462
+
463
+ // 7. 输出 manifest 摘要
464
+ console.log('\n── Archive Package 摘要 ──');
465
+ console.log(` archive_id: ${manifest.archive_id}`);
466
+ console.log(` project_id: ${manifest.project_id}`);
467
+ console.log(` change_id: ${manifest.change_id}`);
468
+ console.log(` files: ${manifest.files.length}`);
469
+
470
+ // === 上传成功后更新本地缓存 ===
471
+ updateKbStateCache(specRoot, changeName, archiveId, contentHash);
472
+
473
+ // === 投影状态提示 ===
474
+ // 上传成功后,KB 后端异步执行 vector embedding + AGE graph 投影。
475
+ // 正常 SDD 流程中,Spec→Design 等阶段间有用户手动触发的时间间隔,投影通常在此期间完成。
476
+ const projStatus = result?.data?.projectionStatus || result?.projectionStatus || '';
477
+ const jobStatus = result?.data?.status || result?.status || '';
478
+
479
+ if (projStatus === 'complete') {
480
+ console.log('\n✓ 投影已完成,向量/图检索可用');
481
+ } else if (projStatus === 'degraded') {
482
+ console.log('\n⚠️ 投影完成但有降级(部分通道不可用,FTS 仍可用)');
483
+ } else if (projStatus === 'disabled') {
484
+ console.log('\nℹ️ 投影已禁用(FTS-only 模式)');
485
+ } else if (projStatus && projStatus !== 'complete') {
486
+ console.log(`\nℹ️ 投影进行中 (${projStatus}),向量/图检索暂不可用`);
487
+ console.log(' → 正常 SDD 流程中阶段间间隔通常足够投影完成');
488
+ console.log(' → 如后续查询 degraded,可用 `context-client.cjs --check-only` 确认 KB 可用性');
489
+ } else if (jobStatus === 'succeeded') {
490
+ console.log('\n✓ 入库成功');
491
+ }
492
+
493
+ } finally {
494
+ // 清理临时目录
495
+ fs.rmSync(stagingDir, { recursive: true, force: true });
496
+ console.log('\n✓ 临时文件已清理');
497
+ }
498
+
499
+ } catch (err) {
500
+ console.error(`\n❌ ${err.message}`);
501
+ process.exit(1);
502
+ }
503
+ }
504
+
505
+ main();