kld-sdd 2.7.8-5 → 2.7.8-6

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.
@@ -20,7 +20,8 @@
20
20
  const path = require('path');
21
21
  const { listToolProfileIds, buildToolConfigs } = require('../lib/tool-profiles');
22
22
  const { MODE, OP, modeLabel } = require('../lib/uninstall');
23
- const { specSourceLabel } = require('../skywalk-sdd/ontology/spec-detect.cjs');
23
+ const specDetect = require('../skywalk-sdd/ontology/spec-detect.cjs');
24
+ const { specSourceLabel } = specDetect;
24
25
  // spec 候选单选交互(唯一实现,与 lib/init.js 共用)
25
26
  const { selectSpecRoot: selectSpecChoice } = require('../lib/spec-choice');
26
27
 
@@ -125,6 +126,9 @@ function parseInitArgs(argv = cliArgs(), env = process.env) {
125
126
  verbose: isVerbose(argv, env),
126
127
  skipOpenspec: argv.includes('--skip-openspec'),
127
128
  skipTemplate: argv.includes('--skip-template'),
129
+ repairGit: argv[0] === 'repair-git' || argv.includes('--repair-git'),
130
+ applyRepair: argv.includes('--apply'),
131
+ json: argv.includes('--json'),
128
132
  tools,
129
133
  // 显式给了 --tool 即自动化场景:lib 据此抑制 spec 路径的交互询问
130
134
  hasToolArg: tools !== null,
@@ -357,6 +361,7 @@ KLD SDD 项目初始化工具
357
361
  kld-sdd-init [选项]
358
362
  npx kld-sdd [选项]
359
363
  kld-sdd sync-repos # 工作目录下发现新增代码仓并轻量接入(多 spec 候选时单选)
364
+ kld-sdd repair-git # 检查并清理旧 Spec 仓的工具跟踪噪声(默认只预览)
360
365
  kld-sdd uninstall [选项] # 卸载 SDD 产物(渐进式:预览工具范围 → 确认)
361
366
  kld-sdd uninstall --help # 查看工具卸载与保留范围
362
367
 
@@ -366,6 +371,9 @@ KLD SDD 项目初始化工具
366
371
  --verbose 输出每个安装文件/目录的明细日志(默认仅摘要,亦可用 KLD_SDD_VERBOSE=1)
367
372
  --skip-openspec 跳过 openspec init 步骤
368
373
  --skip-template 跳过复制内置模版
374
+ --repair-git 检查 Spec 仓中被 Git 跟踪的安装器工具(默认只预览)
375
+ --apply 与 --repair-git 同用:保留本地文件,仅从 Git 索引移除工具
376
+ --json --repair-git 输出机器可读 JSON,供托盘健康检查使用
369
377
  --tool <name> 指定编辑器,可用值: cursor, claude, codebuddy, qoder, opencode, kunlunzhima, workbuddy, codex, all
370
378
  --spec-path <dir> 指定 spec 包裹包目录(必须是当前目录的子目录)。
371
379
  不指定时(交互终端):自动检测已有 spec 目录(目录名 *-sdd-specs,
@@ -385,6 +393,8 @@ KLD SDD 项目初始化工具
385
393
  kld-sdd-init --skip-openspec # 跳过 openspec,直接部署 skills
386
394
  kld-sdd-init --tool codex # 仅部署 Codex 配置
387
395
  kld-sdd-init --spec-path erp-sdd-specs # 指定 spec 目录(当前目录的子目录)
396
+ kld-sdd repair-git --spec-path=. # 预览旧仓工具跟踪问题
397
+ kld-sdd repair-git --apply --spec-path=. # 一次迁出工具,文件仍留在磁盘
388
398
  kld-sdd sync-repos # 新代码仓加入后同步接入
389
399
  `);
390
400
  }
@@ -393,6 +403,72 @@ KLD SDD 项目初始化工具
393
403
  // 子命令路由:解析参数后调用 lib/ 业务实现
394
404
  // ═══════════════════════════════════════════════════════════════
395
405
 
406
+ function resolveRepairSpecRoot(parsed, cwd = process.cwd()) {
407
+ const root = path.resolve(cwd);
408
+ if (parsed.specPath) {
409
+ const explicit = path.resolve(root, parsed.specPath);
410
+ if (!specDetect.isSpecPackageRoot(explicit)) {
411
+ throw new Error(`指定目录不是有效的 Spec 包裹包: ${explicit}`);
412
+ }
413
+ return explicit;
414
+ }
415
+ if (specDetect.isSpecPackageRoot(root)) return root;
416
+ const candidates = specDetect.detectSpecCandidatesWithGit(root);
417
+ if (candidates.length === 1) return candidates[0].abs;
418
+ if (candidates.length === 0) throw new Error('未发现 Spec 包裹包,请用 --spec-path=<dir> 指定');
419
+ throw new Error(`发现 ${candidates.length} 个 Spec 包裹包,请用 --spec-path=<dir> 指定,不自动猜测`);
420
+ }
421
+
422
+ function runGitRepair(parsed) {
423
+ const init = require('../lib/init');
424
+ const maintenance = require('../lib/maintenance');
425
+ const specRoot = resolveRepairSpecRoot(parsed);
426
+ const plan = maintenance.trackedManagedSpecTools(specRoot);
427
+ if (!plan.ok) throw new Error(plan.error);
428
+
429
+ if (!parsed.applyRepair) {
430
+ const payload = {
431
+ mode: 'preview',
432
+ specRoot,
433
+ trackedToolCount: plan.files.length,
434
+ trackedTools: plan.files.map((file) => file.relative),
435
+ businessFilesAffected: 0,
436
+ };
437
+ if (parsed.json) {
438
+ console.log(JSON.stringify(payload, null, 2));
439
+ return;
440
+ }
441
+ console.log(`🧹 Spec Git 清洁度检查: ${path.basename(specRoot)}`);
442
+ console.log(` 被 Git 跟踪的可再生成工具: ${plan.files.length} 个`);
443
+ for (const file of plan.files.slice(0, 8)) console.log(` - ${file.relative}`);
444
+ if (plan.files.length > 8) console.log(` … 其余 ${plan.files.length - 8} 个`);
445
+ console.log(' 业务 Spec、配置、项目身份与 Hook 参数: 不处理');
446
+ if (plan.files.length) console.log(' 执行 --repair-git --apply 可一次迁出工具;不会删除磁盘文件。');
447
+ return;
448
+ }
449
+
450
+ const existingBefore = plan.files.filter((file) => require('fs').existsSync(path.join(specRoot, file.relative)));
451
+ init.updateGitignore(specRoot, { specPackage: true });
452
+ const result = maintenance.untrackManagedSpecTools(specRoot);
453
+ if (!result.ok) throw new Error(result.error);
454
+ const payload = {
455
+ mode: 'applied',
456
+ specRoot,
457
+ untrackedToolCount: result.files.length,
458
+ filesRemainOnDisk: existingBefore.every((file) => require('fs').existsSync(path.join(specRoot, file.relative))),
459
+ retainedLocalToolCount: existingBefore.length,
460
+ alreadyMissingToolCount: result.files.length - existingBefore.length,
461
+ businessFilesAffected: 0,
462
+ };
463
+ if (parsed.json) {
464
+ console.log(JSON.stringify(payload, null, 2));
465
+ return;
466
+ }
467
+ console.log(`✅ 已将 ${result.files.length} 个安装器工具移出 Git 跟踪,现存的 ${existingBefore.length} 个本地文件全部保留`);
468
+ if (payload.alreadyMissingToolCount) console.log(` ℹ️ 其中 ${payload.alreadyMissingToolCount} 个文件在修复前已从磁盘删除`);
469
+ console.log(' 请审阅一次 git status 并提交迁移;以后升级不再污染业务改动列表。');
470
+ }
471
+
396
472
  async function runInit() {
397
473
  let parsed;
398
474
  try {
@@ -412,6 +488,15 @@ async function runInit() {
412
488
  return;
413
489
  }
414
490
 
491
+ if (parsed.repairGit) {
492
+ try {
493
+ runGitRepair(parsed);
494
+ } catch (error) {
495
+ fail(`Git 清洁度修复失败: ${error.message}`);
496
+ }
497
+ return;
498
+ }
499
+
415
500
  const init = require('../lib/init');
416
501
  init.setVerbose(parsed.verbose);
417
502
 
package/lib/init.js CHANGED
@@ -1282,6 +1282,9 @@ async function populateSpecPackage(specRoot, options = {}) {
1282
1282
  copyTemplatesToProject(root);
1283
1283
  initGlobalOverview(root);
1284
1284
  deploySddGuideManual(root);
1285
+ // Spec 包可能是独立 Git 仓或单仓内的子目录;忽略规则必须落在它自身,
1286
+ // 不能只写工作区根,否则升级会把整套可再生成运行时显示为业务改动。
1287
+ updateGitignore(root, { specPackage: true });
1285
1288
  console.log(`✅ SDD 包裹包已就绪: ${path.basename(root)}`);
1286
1289
  return { ok: true, path: root };
1287
1290
  }
@@ -1376,7 +1379,7 @@ function removeOuterSkywalk(anchorDir) {
1376
1379
  * 创建 / 更新 .gitignore
1377
1380
  * @param {string} [cwd=process.cwd()]
1378
1381
  */
1379
- function updateGitignore(cwd = process.cwd()) {
1382
+ function updateGitignore(cwd = process.cwd(), options = {}) {
1380
1383
  const root = path.resolve(cwd);
1381
1384
  const gitignorePath = path.join(root, '.gitignore');
1382
1385
  const requiredLines = [
@@ -1395,6 +1398,10 @@ function updateGitignore(cwd = process.cwd()) {
1395
1398
  '**/skywalk-sdd/state/',
1396
1399
  '.worktrees/'
1397
1400
  ];
1401
+ const specPackageLines = options.specPackage ? maintenance.specPackageIgnoreLines() : [];
1402
+ if (options.specPackage) {
1403
+ requiredLines.push(...specPackageLines);
1404
+ }
1398
1405
 
1399
1406
  const sddConfig = `
1400
1407
  # KLD SDD AI 编辑器个人配置(请勿提交)
@@ -1418,6 +1425,10 @@ skywalk-sdd/state/
1418
1425
 
1419
1426
  # SDD Apply 隔离 worktree(本地临时目录)
1420
1427
  .worktrees/
1428
+ ${options.specPackage ? `
1429
+ # KLD SDD 安装器工具(可由 init/升级重新生成,不属于业务 Spec)
1430
+ ${specPackageLines.join('\n')}
1431
+ ` : ''}
1421
1432
  `;
1422
1433
 
1423
1434
  const result = deployFile(null, gitignorePath, {
@@ -1868,4 +1879,5 @@ module.exports = {
1868
1879
  promptSpecPath,
1869
1880
  listSpecCandidatesForInit,
1870
1881
  deployCodeRepoHooks,
1882
+ updateGitignore,
1871
1883
  };
@@ -3,11 +3,124 @@
3
3
  // 安装、升级、卸载共用的边界;业务文档和关联配置不属于可清理的工具文件。
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
+ const { spawnSync } = require('child_process');
6
7
  const sdd = require('../skywalk-sdd/ontology/sdd-config.cjs');
7
8
  const layout = require('./workspace-layout');
8
9
  const detect = require('../skywalk-sdd/ontology/spec-detect.cjs');
9
10
  const pkg = path.resolve(__dirname, '..');
10
11
 
12
+ function posix(relative) {
13
+ return String(relative || '').replace(/\\/g, '/').replace(/^\.\//, '');
14
+ }
15
+
16
+ const MANAGED_ROOT_TOOLS = new Set([
17
+ 'apply-worktree-finish.cjs',
18
+ 'context-client.cjs',
19
+ 'kb-sync-identity.cjs',
20
+ 'kb-upload.cjs',
21
+ 'log.cjs',
22
+ 'metrics-v3.cjs',
23
+ 'openspec-shim.cjs',
24
+ 'runtime-metadata.cjs',
25
+ 'spec-root.cjs',
26
+ ]);
27
+
28
+ const MANAGED_HOOK_TOOLS = new Set([
29
+ 'commit-msg-runner.cjs',
30
+ 'commit-msg-sdd-trailer.cjs',
31
+ 'consistency-check-core.cjs',
32
+ 'delivery-check.cjs',
33
+ 'delivery-snapshot.cjs',
34
+ 'pre-commit-consistency-check.cjs',
35
+ 'pre-commit-sdd-check.cjs',
36
+ 'pre-push-consistency-check.cjs',
37
+ 'pre-push-sdd-check.cjs',
38
+ ]);
39
+
40
+ function specPackageIgnoreLines() {
41
+ return [
42
+ ...[...MANAGED_ROOT_TOOLS].sort().map((name) => `/skywalk-sdd/${name}`),
43
+ '/skywalk-sdd/lib/',
44
+ '/skywalk-sdd/ontology/',
45
+ '/skywalk-sdd/reporting/',
46
+ '/skywalk-sdd/ci/github-actions-sdd.yml',
47
+ '/skywalk-sdd/ci/gitlab-ci-sdd.yml',
48
+ ...[...MANAGED_HOOK_TOOLS].sort().map((name) => `/skywalk-sdd/git-hooks/${name}`),
49
+ '/openspec-templates/',
50
+ '/openspec/kld-sdd操作手册.html',
51
+ '/_tmp_*',
52
+ '/*-start.log',
53
+ ];
54
+ }
55
+
56
+ /**
57
+ * 只识别能够由 kld-sdd 安装器重新生成的文件。
58
+ * 业务 Spec、项目身份、Hook 参数、KB 配置和历史数据都不属于工具文件。
59
+ */
60
+ function isManagedSpecToolPath(relative) {
61
+ const rel = posix(relative);
62
+ if (rel === 'openspec/kld-sdd操作手册.html' || rel.startsWith('openspec-templates/')) return true;
63
+ if (rel.startsWith('skywalk-sdd/') && MANAGED_ROOT_TOOLS.has(rel.slice('skywalk-sdd/'.length))) return true;
64
+ if (/^skywalk-sdd\/(?:lib|ontology|reporting)\//.test(rel)) return true;
65
+ if (/^skywalk-sdd\/ci\/(?:github-actions-sdd|gitlab-ci-sdd)\.yml$/.test(rel)) return true;
66
+ const hookPrefix = 'skywalk-sdd/git-hooks/';
67
+ return rel.startsWith(hookPrefix) && MANAGED_HOOK_TOOLS.has(rel.slice(hookPrefix.length));
68
+ }
69
+
70
+ function git(cwd, args) {
71
+ const result = spawnSync('git', args, {
72
+ cwd,
73
+ encoding: 'utf8',
74
+ maxBuffer: 10 * 1024 * 1024,
75
+ });
76
+ if (result.status !== 0) {
77
+ return {
78
+ ok: false,
79
+ error: String(result.stderr || result.stdout || 'git 命令失败').trim(),
80
+ output: '',
81
+ };
82
+ }
83
+ return { ok: true, output: String(result.stdout || '') };
84
+ }
85
+
86
+ /** 列出当前 Spec 包中仍被 Git 跟踪的安装器工具文件。 */
87
+ function trackedManagedSpecTools(specRoot) {
88
+ const requestedRoot = path.resolve(specRoot);
89
+ const root = fs.existsSync(requestedRoot) ? fs.realpathSync(requestedRoot) : requestedRoot;
90
+ const top = git(root, ['rev-parse', '--show-toplevel']);
91
+ if (!top.ok) return { ok: false, specRoot: root, files: [], error: '当前 Spec 目录不在 Git 仓库中' };
92
+ const gitRoot = path.resolve(top.output.trim());
93
+ const rootRelative = posix(path.relative(gitRoot, root));
94
+ if (rootRelative.startsWith('../') || path.isAbsolute(rootRelative)) {
95
+ return { ok: false, specRoot: root, gitRoot, files: [], error: 'Spec 目录不属于检测到的 Git 仓库' };
96
+ }
97
+ const pathspec = rootRelative ? `${rootRelative}/` : '.';
98
+ const listed = git(gitRoot, ['ls-files', '-z', '--', pathspec]);
99
+ if (!listed.ok) return { ok: false, specRoot: root, gitRoot, files: [], error: listed.error };
100
+ const prefix = rootRelative ? `${rootRelative}/` : '';
101
+ const files = listed.output.split('\0').filter(Boolean).map(posix).flatMap((gitPath) => {
102
+ if (prefix && !gitPath.startsWith(prefix)) return [];
103
+ const relative = prefix ? gitPath.slice(prefix.length) : gitPath;
104
+ return isManagedSpecToolPath(relative) ? [{ gitPath, relative }] : [];
105
+ });
106
+ return { ok: true, specRoot: root, gitRoot, files };
107
+ }
108
+
109
+ /**
110
+ * 将安装器工具从 Git 索引移除,文件仍留在磁盘供本地运行。
111
+ * 调用方必须先把对应规则写入 Spec 根 .gitignore。
112
+ */
113
+ function untrackManagedSpecTools(specRoot) {
114
+ const plan = trackedManagedSpecTools(specRoot);
115
+ if (!plan.ok || plan.files.length === 0) return plan;
116
+ for (let offset = 0; offset < plan.files.length; offset += 100) {
117
+ const batch = plan.files.slice(offset, offset + 100).map((file) => file.gitPath);
118
+ const removed = git(plan.gitRoot, ['update-index', '--force-remove', '--', ...batch]);
119
+ if (!removed.ok) return { ...plan, ok: false, error: removed.error };
120
+ }
121
+ return { ...plan, untracked: plan.files.length };
122
+ }
123
+
11
124
  function filesUnder(root, prefix = '') {
12
125
  if (!fs.existsSync(root) || !fs.lstatSync(root).isDirectory()) return [];
13
126
  return fs.readdirSync(root).flatMap(name => {
@@ -93,4 +206,16 @@ function maintenanceConflicts(root, spec, repos) {
93
206
  return failures;
94
207
  }
95
208
 
96
- module.exports = { filesUnder, regularFileWithin, runtimeToolFiles, samePath, associationConflict, workspaceCodeRepos, maintenanceConflicts };
209
+ module.exports = {
210
+ filesUnder,
211
+ regularFileWithin,
212
+ runtimeToolFiles,
213
+ specPackageIgnoreLines,
214
+ isManagedSpecToolPath,
215
+ trackedManagedSpecTools,
216
+ untrackManagedSpecTools,
217
+ samePath,
218
+ associationConflict,
219
+ workspaceCodeRepos,
220
+ maintenanceConflicts,
221
+ };
@@ -40,6 +40,7 @@ const OPSX_SKILL_DIRS = [
40
40
  // Engineering KB 依赖技能(随 init 部署;propose/spec/archive 硬依赖)
41
41
  'opsx-kb-config',
42
42
  'opsx-ontology-query',
43
+ 'opsx-ontology-ingest',
43
44
  'opsx-kb-ingest',
44
45
  // Spec 一致性校验技能
45
46
  'opsx-consistency-check',
@@ -122,6 +123,7 @@ const KUNLUN_SLASH_COMMANDS = [
122
123
  'archive',
123
124
  'explore',
124
125
  'ontology-query',
126
+ 'ontology-ingest',
125
127
  'kb-ingest',
126
128
  'kb-config',
127
129
  'consistency-check',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kld-sdd",
3
- "version": "2.7.8-5",
3
+ "version": "2.7.8-6",
4
4
  "description": "KLD SDD OpenSpec 项目初始化工具 - 一键部署 SDD skills",
5
5
  "main": "lib/init.js",
6
6
  "bin": {
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "scripts": {
11
11
  "bench:check": "node test/benchmark-check.cjs",
12
- "test": "node --test test/delivery-hooks.test.cjs test/execution-friction.test.cjs test/kb-fact-lifecycle.test.cjs test/workflow-usability.test.cjs test/check-efficiency.test.cjs && node test/external-key.cjs && node test/change-key.cjs && node test/modules-and-sdd-config.cjs && node test/active-changes.cjs && node test/hook-layouts.cjs && node test/spec-package-init.cjs && node test/prompt-spec-path.cjs && node test/spec-detect.cjs && node test/workspace-layout.cjs && node test/sync-repos-spec-choice.cjs && node test/ontology-release-blockers.cjs && node test/ontology-semantic-core.cjs && node test/ontology-identity-versioning.cjs && node test/ontology-identity-continuity.cjs && node test/ontology-state-transaction.cjs && node test/ontology-process-concurrency.cjs && node test/ontology-observer-convergence.cjs && node test/ontology-working-runtime.cjs && node test/ontology-stage-materialization.cjs && node test/ontology-template-contract.cjs && node test/ontology-cli-archive.cjs && node test/archive-package-producer.cjs && node --test test/evidence-integrity-oracle.cjs test/evidence-integrity-wiring.cjs test/change-report-correctness.cjs test/report-metrics-correctness.cjs test/change-report-model-ui.cjs && node test/validate-skills-bundle.cjs && node test/tool-profiles.cjs && node test/deploy-strategy.cjs && node test/uninstall.cjs && node --test test/maintenance.test.cjs && node test/settings-merge.cjs && node test/command-bridge.cjs && node test/codebuddy-hooks.cjs && node test/skill-content-contract.cjs && node test/init-agent-profiles.cjs && node test/init-report-runtime.cjs && node test/init-output-verbosity.cjs && node --test test/usage-contract.test.cjs test/user-config.test.cjs test/usage-runtime-removal.test.cjs test/runtime/usage-reporter.test.cjs test/runtime/usage-reporting.test.cjs test/runtime/git-identity.test.cjs test/runtime/kb-state-location.test.cjs && node test/runtime/metrics-v3.test.cjs && node test/runtime/fixback.test.cjs && node test/progress-package-artifact.test.cjs && node --test test/progress-package-install.e2e.cjs && node test/consistency-check-core.cjs"
12
+ "test": "node --test test/delivery-hooks.test.cjs test/execution-friction.test.cjs test/kb-fact-lifecycle.test.cjs test/workflow-usability.test.cjs test/check-efficiency.test.cjs && node test/external-key.cjs && node test/change-key.cjs && node test/modules-and-sdd-config.cjs && node test/active-changes.cjs && node test/hook-layouts.cjs && node test/spec-package-init.cjs && node test/prompt-spec-path.cjs && node test/spec-detect.cjs && node test/workspace-layout.cjs && node test/sync-repos-spec-choice.cjs && node test/ontology-release-blockers.cjs && node test/ontology-semantic-core.cjs && node test/ontology-identity-versioning.cjs && node test/ontology-identity-continuity.cjs && node test/ontology-state-transaction.cjs && node test/ontology-process-concurrency.cjs && node test/ontology-observer-convergence.cjs && node test/ontology-working-runtime.cjs && node test/ontology-stage-materialization.cjs && node test/ontology-template-contract.cjs && node test/ontology-cli-archive.cjs && node test/spec-ontology-compatibility.cjs && node test/archive-package-producer.cjs && node --test test/evidence-integrity-oracle.cjs test/evidence-integrity-wiring.cjs test/change-report-correctness.cjs test/report-metrics-correctness.cjs test/change-report-model-ui.cjs && node test/validate-skills-bundle.cjs && node test/tool-profiles.cjs && node test/deploy-strategy.cjs && node test/uninstall.cjs && node --test test/maintenance.test.cjs && node test/settings-merge.cjs && node test/command-bridge.cjs && node test/codebuddy-hooks.cjs && node test/skill-content-contract.cjs && node test/init-agent-profiles.cjs && node test/init-report-runtime.cjs && node test/init-output-verbosity.cjs && node --test test/usage-contract.test.cjs test/user-config.test.cjs test/usage-runtime-removal.test.cjs test/runtime/usage-reporter.test.cjs test/runtime/usage-reporting.test.cjs test/runtime/git-identity.test.cjs test/runtime/kb-state-location.test.cjs && node test/runtime/metrics-v3.test.cjs && node test/runtime/fixback.test.cjs && node test/progress-package-artifact.test.cjs && node --test test/progress-package-install.e2e.cjs && node test/consistency-check-core.cjs"
13
13
  },
14
14
  "keywords": [
15
15
  "kld",
@@ -37,6 +37,9 @@ function buildEndpoint(apiBase, spaceId, kbId, mode) {
37
37
  if (mode === 'resolve') {
38
38
  return `${base}/v1/spaces/${space}/knowledge-bases/${kb}/entities/resolve`;
39
39
  }
40
+ if (mode === 'spec-generation') {
41
+ return `${base}/v1/spaces/${space}/knowledge-bases/${kb}/context/spec-generation`;
42
+ }
40
43
  return `${base}/v1/spaces/${space}/knowledge-bases/${kb}/context/match-requirement`;
41
44
  }
42
45
 
@@ -61,7 +64,14 @@ function compactContext(data, includeCandidates, mode = 'match') {
61
64
  warnings: data.warnings,
62
65
  clarificationQuestions: data.clarificationQuestions,
63
66
  specGenerationContext: data.specGenerationContext,
67
+ matchedCapabilities: data.matchedCapabilities,
68
+ matchedScenarios: data.matchedScenarios,
69
+ businessObjects: data.businessObjects,
70
+ rules: data.rules,
71
+ specPatterns: data.specPatterns,
72
+ compatibilityMode: data.compatibilityMode,
64
73
  supportEvidence: data.supportEvidence,
74
+ candidateEvidence: data.candidateEvidence,
65
75
  oppositionEvidence: data.oppositionEvidence,
66
76
  };
67
77
  // Continuity resolve 必须透出 candidates;match 默认隐藏内部候选除非 --include-candidates
@@ -171,19 +181,40 @@ async function retrieveContext(args = parseArgs(process.argv), env = process.env
171
181
  return { ...unavailableContext('engineering_kb_not_configured', 'KB 尚未配置,本地工作可以继续'), configured: false };
172
182
  }
173
183
 
174
- const mode = String(args.mode || 'match').trim().toLowerCase() === 'resolve' ? 'resolve' : 'match';
184
+ const requestedMode = String(args.mode || 'match').trim().toLowerCase();
185
+ const mode = requestedMode === 'resolve'
186
+ ? 'resolve'
187
+ : (['spec-generation', 'spec_generation', 'generation'].includes(requestedMode)
188
+ ? 'spec-generation' : 'match');
175
189
  const timeoutMs = contextTimeout(args, env, 5000);
176
190
  const payload = buildPayload(args, mode);
177
- const data = await requestJson(
178
- buildEndpoint(apiBase, spaceId, kbId, mode),
179
- payload,
180
- token,
181
- timeoutMs,
182
- );
191
+ let responseMode = mode;
192
+ let data;
193
+ try {
194
+ data = await requestJson(
195
+ buildEndpoint(apiBase, spaceId, kbId, mode),
196
+ payload,
197
+ token,
198
+ timeoutMs,
199
+ );
200
+ } catch (error) {
201
+ const endpointUnavailable = /Not Found|HTTP 404/i.test(error.message);
202
+ if (mode !== 'spec-generation' || !endpointUnavailable) throw error;
203
+ data = await requestJson(
204
+ buildEndpoint(apiBase, spaceId, kbId, 'match'),
205
+ payload,
206
+ token,
207
+ timeoutMs,
208
+ );
209
+ responseMode = 'match';
210
+ }
183
211
  return {
184
212
  available: true,
185
213
  mode,
214
+ responseMode,
215
+ remoteContextVerified: true,
186
216
  ...compactContext(data, Boolean(args['include-candidates']), mode),
217
+ compatibilityMode: responseMode === mode ? (data.compatibilityMode || 'NATIVE') : 'LEGACY_V2',
187
218
  };
188
219
  }
189
220
 
@@ -50,15 +50,18 @@ function cacheKey(changeName, context) {
50
50
  return JSON.stringify([context.api.replace(/\/+$/, ''), context.spaceId, context.kbId, changeName]);
51
51
  }
52
52
 
53
- function assertIngestionResult(result) {
53
+ function assertIngestionResult(result, options = {}) {
54
54
  if (result?.code != null && result.code !== 0) throw new Error(result.message || 'KB API 拒绝入库');
55
55
  const job = result?.data || result;
56
56
  if (!job || !['projecting', 'succeeded'].includes(job.status) || job.relationStatus !== 'committed') {
57
57
  throw new Error(`INGEST_NOT_COMMITTED: ${job?.errorCode || job?.status || 'unknown'} ${job?.errorMessage || ''} job=${job?.jobId || 'unknown'}`);
58
58
  }
59
- if (Number(job.report?.version_conflicts || 0) > 0 || job.report?.publication_status === 'needs_review') {
59
+ if (Number(job.report?.version_conflicts || 0) > 0) {
60
60
  throw new Error(`VERSION_CONFLICT: 事实包已保存为候选,当前事实未更新;请合并基线后重新发布。job=${job.jobId}`);
61
61
  }
62
+ if (job.report?.publication_status === 'needs_review' && !options.allowNeedsReview) {
63
+ throw new Error(`REVIEW_REQUIRED: 事实包已保存为候选,当前事实未更新;请审核本体后重新发布。job=${job.jobId}`);
64
+ }
62
65
  return job;
63
66
  }
64
67
 
@@ -240,7 +243,7 @@ function preCheckProjectId(projectId, state, selectedTarget) {
240
243
  * 5. materializeArchivePackage(生成 manifest + canonical-facts + conversion-report + zip)
241
244
  * 6. 返回 zip 路径
242
245
  */
243
- function buildPackageFromFolder(specRoot, changeName, options) {
246
+ function buildPackageFromFolder(specRoot, changeName, options = {}) {
244
247
  const runtime = require('./ontology/runtime.cjs');
245
248
  const archivePackage = require('./ontology/archive-package.cjs');
246
249
 
@@ -250,13 +253,18 @@ function buildPackageFromFolder(specRoot, changeName, options) {
250
253
  profile: 'auto',
251
254
  markPending: true,
252
255
  });
256
+ const blockingDiagnostics = semanticResult.diagnostics.filter(d => d.severity === 'error');
257
+ const legacyIdentityOnly = blockingDiagnostics.length > 0
258
+ && blockingDiagnostics.every(d => d.code === 'SEM_UUID_MISSING');
253
259
  if (!semanticResult.valid) {
254
- const errors = semanticResult.diagnostics
255
- .filter(d => d.severity === 'error')
256
- .map(d => `${d.code}: ${d.message}`);
257
- throw new Error(`语义校验失败:\n${errors.join('\n')}`);
260
+ const errors = blockingDiagnostics.map(d => `${d.code}: ${d.message}`);
261
+ if (!legacyIdentityOnly) {
262
+ throw new Error(`语义校验失败:\n${errors.join('\n')}`);
263
+ }
264
+ console.warn('⚠️ 检测到历史产物缺少身份标识,将生成旁路映射和待审核本体包,不改写 Markdown。');
265
+ } else {
266
+ console.log(`✓ 语义校验通过 (profile: ${semanticResult.profile}, revision: ${semanticResult.revision})`);
258
267
  }
259
- console.log(`✓ 语义校验通过 (profile: ${semanticResult.profile}, revision: ${semanticResult.revision})`);
260
268
 
261
269
  // 2. 创建临时 staging 目录
262
270
  const stagingDir = path.join(
@@ -269,28 +277,55 @@ function buildPackageFromFolder(specRoot, changeName, options) {
269
277
  const changeDir = path.join(specRoot, 'openspec', 'changes', changeName);
270
278
  copyDirRecursive(changeDir, stagingDir);
271
279
 
272
- // 4. 创建 archive snapshot(生成 archive-ontology.json)
273
- runtime.createArchiveSnapshot(specRoot, changeName, stagingDir, semanticResult.state);
274
- console.log(' 已生成 archive-ontology.json');
275
-
276
- // 5. 生成 manifest + canonical-facts + conversion-report + zip
277
- const projectIdentity = archivePackage.ensureProjectIdentity(specRoot);
278
- const result = archivePackage.materializeArchivePackage({
279
- projectRoot: specRoot,
280
- archiveDir: stagingDir,
281
- archivePath: stagingDir,
282
- changeName: changeName,
283
- packagePath: zipPath,
284
- reason: options.reason || '随时入库(文件夹上传)',
285
- method: 'kb-upload-folder',
286
- });
280
+ let result;
281
+ if (legacyIdentityOnly) {
282
+ const approvedPath = path.join(changeDir, 'ontology', 'spec-ontology.json');
283
+ const approvedOntology = readJsonSafe(approvedPath);
284
+ const useApproved = approvedOntology?.review_status === 'confirmed'
285
+ && approvedOntology?.publication_eligible === true;
286
+ if (useApproved) {
287
+ console.log('✓ 已发现审核通过的历史 Spec 本体,将校验内容一致性后发布');
288
+ }
289
+ result = archivePackage.materializeCompatibilityPackage({
290
+ projectRoot: specRoot,
291
+ archiveDir: stagingDir,
292
+ changeName,
293
+ facts: semanticResult.state,
294
+ specOntology: useApproved ? approvedOntology : null,
295
+ packagePath: zipPath,
296
+ reason: options.reason || '历史 Spec 兼容入库',
297
+ method: 'kb-upload-legacy-folder',
298
+ });
299
+ console.log(useApproved
300
+ ? '✓ 已生成审核通过的 LEGACY_MATERIALIZED 发布包'
301
+ : '✓ 已生成 LEGACY_MATERIALIZED 本体候选包');
302
+ } else {
303
+ // 严格产物继续生成 confirmed snapshot 和双协议事实包。
304
+ runtime.createArchiveSnapshot(specRoot, changeName, stagingDir, semanticResult.state);
305
+ console.log('✓ 已生成 archive-ontology.json');
306
+ archivePackage.ensureProjectIdentity(specRoot);
307
+ result = archivePackage.materializeArchivePackage({
308
+ projectRoot: specRoot,
309
+ archiveDir: stagingDir,
310
+ archivePath: stagingDir,
311
+ changeName: changeName,
312
+ packagePath: zipPath,
313
+ reason: options.reason || '随时入库(文件夹上传)',
314
+ method: 'kb-upload-folder',
315
+ });
316
+ }
287
317
 
288
318
  console.log(`✓ Archive Package 已生成`);
289
319
  console.log(` archive_id: ${result.manifest.archive_id}`);
290
320
  console.log(` project_id: ${result.manifest.project_id}`);
291
321
  console.log(` files: ${result.manifest.files.length}`);
292
322
 
293
- return { zipPath, manifest: result.manifest, stagingDir };
323
+ return {
324
+ zipPath,
325
+ manifest: result.manifest,
326
+ stagingDir,
327
+ requiresReview: result.conversion_report?.status === 'materialized_pending_review',
328
+ };
294
329
  } catch (err) {
295
330
  // 出错时清理 staging
296
331
  fs.rmSync(stagingDir, { recursive: true, force: true });
@@ -313,7 +348,7 @@ async function uploadZip(zipPath, state, options) {
313
348
 
314
349
  try {
315
350
  const result = await uploadFile(uploadUrl, zipPath, state.apiKey);
316
- assertIngestionResult(result);
351
+ assertIngestionResult(result, options);
317
352
  return result;
318
353
  } catch (err) {
319
354
  throw new Error(`上传失败: ${err.message}`);
@@ -438,7 +473,7 @@ async function main() {
438
473
 
439
474
  // 5. 构建 Archive Package
440
475
  console.log('');
441
- const { zipPath, manifest, stagingDir } = buildPackageFromFolder(specRoot, changeName, {
476
+ const { zipPath, manifest, stagingDir, requiresReview } = buildPackageFromFolder(specRoot, changeName, {
442
477
  reason: args.reason,
443
478
  });
444
479
 
@@ -492,6 +527,7 @@ async function main() {
492
527
  const result = await uploadZip(zipPath, state, {
493
528
  spaceId: target.spaceId,
494
529
  kbId: target.kbId,
530
+ allowNeedsReview: requiresReview,
495
531
  });
496
532
 
497
533
  console.log('\n── 上传结果 ──');
@@ -504,8 +540,17 @@ async function main() {
504
540
  console.log(` change_id: ${manifest.change_id}`);
505
541
  console.log(` files: ${manifest.files.length}`);
506
542
 
507
- // Canonical commit has been verified; a receipt does not imply projection completion.
508
- updateKbStateCache(specRoot, changeName, archiveId, contentHash, cacheContext);
543
+ const publicationStatus = result?.data?.report?.publication_status
544
+ || result?.report?.publication_status
545
+ || '';
546
+ if (publicationStatus === 'needs_review') {
547
+ console.log('\n⚠️ 历史 Spec 已安全保存为候选,尚未进入 current 召回');
548
+ console.log(` → 审核 ${path.join(folderPath, 'ontology', 'spec-ontology.json')}`);
549
+ console.log(' → 运行 approve-spec-ontology 后再次上传即可发布');
550
+ } else {
551
+ // Published commit has been verified; a receipt does not imply projection completion.
552
+ updateKbStateCache(specRoot, changeName, archiveId, contentHash, cacheContext);
553
+ }
509
554
 
510
555
  // === 投影状态提示 ===
511
556
  // 上传成功后,KB 后端异步执行 vector embedding + AGE graph 投影。