kld-sdd 2.7.3 → 2.7.8

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 (65) hide show
  1. package/README.md +12 -5
  2. package/USABILITY.md +84 -0
  3. package/kld-sdd-guide.html +16 -4
  4. package/lib/hook-gate-core.js +327 -0
  5. package/lib/init.js +221 -37
  6. package/lib/scale-thresholds.json +19 -0
  7. package/lib/skills-bundle.js +2 -1
  8. package/package.json +5 -3
  9. package/skywalk-sdd/context-client.cjs +50 -30
  10. package/skywalk-sdd/index.cjs +36 -16
  11. package/skywalk-sdd/kb-sync-identity.cjs +99 -451
  12. package/skywalk-sdd/kb-upload.cjs +67 -44
  13. package/skywalk-sdd/lib/usage-contract.cjs +3 -2
  14. package/skywalk-sdd/lib/usage-reporter.cjs +27 -2
  15. package/skywalk-sdd/metrics-v3.cjs +2 -2
  16. package/skywalk-sdd/ontology/active-changes.cjs +2 -1
  17. package/skywalk-sdd/ontology/archive-package.cjs +1 -1
  18. package/skywalk-sdd/ontology/artifact-parser.cjs +7 -4
  19. package/skywalk-sdd/ontology/id.cjs +5 -4
  20. package/skywalk-sdd/ontology/identity-index.cjs +4 -3
  21. package/skywalk-sdd/ontology/list-changes.cjs +1 -1
  22. package/skywalk-sdd/ontology/runtime.cjs +7 -3
  23. package/skywalk-sdd/ontology/schema.cjs +2 -0
  24. package/skywalk-sdd/ontology/traceability-validator.cjs +74 -4
  25. package/skywalk-sdd/ontology/workspace-layout.cjs +25 -5
  26. package/skywalk-sdd/reporting/change-report-model.cjs +4 -3
  27. package/templates/git-hooks/commit-msg +22 -21
  28. package/templates/git-hooks/consistency-check-core.cjs +1087 -0
  29. package/templates/git-hooks/hooks.config +20 -1
  30. package/templates/git-hooks/pre-commit +22 -21
  31. package/templates/git-hooks/pre-commit-consistency-check.cjs +29 -332
  32. package/templates/git-hooks/pre-commit-sdd-check.cjs +98 -0
  33. package/templates/git-hooks/pre-push +22 -21
  34. package/templates/git-hooks/pre-push-consistency-check.cjs +58 -406
  35. package/templates/hooks/codebuddy/hooks/sdd-tdd-rhythm-gate.cjs +1 -1
  36. package/templates/openspec/tasks.md +3 -3
  37. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +43 -6
  38. package/templates/skills/kld-sdd/opsx-apply/checklist.md +1 -1
  39. package/templates/skills/kld-sdd/opsx-apply/reference.md +1 -1
  40. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +9 -13
  41. package/templates/skills/kld-sdd/opsx-check/SKILL.md +54 -328
  42. package/templates/skills/kld-sdd/opsx-check/checklist.md +7 -4
  43. package/templates/skills/kld-sdd/opsx-check/reference.md +58 -0
  44. package/templates/skills/kld-sdd/opsx-check/result-template.json +52 -0
  45. package/templates/skills/kld-sdd/opsx-check/reviewer.md +100 -0
  46. package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +82 -451
  47. package/templates/skills/kld-sdd/opsx-consistency-check/{reference.md → references/reference.md} +0 -1
  48. package/templates/skills/kld-sdd/opsx-consistency-check/scripts/scripts.cjs +517 -0
  49. package/templates/skills/kld-sdd/opsx-design/SKILL.md +10 -30
  50. package/templates/skills/kld-sdd/opsx-design/checklist.md +3 -4
  51. package/templates/skills/kld-sdd/opsx-design/reference.md +1 -1
  52. package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +12 -74
  53. package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +7 -5
  54. package/templates/skills/kld-sdd/opsx-ontology-query/phase-1-prechange.md +2 -2
  55. package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +1 -1
  56. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +28 -73
  57. package/templates/skills/kld-sdd/opsx-propose/checklist.md +8 -8
  58. package/templates/skills/kld-sdd/opsx-propose/interaction-policy.md +28 -0
  59. package/templates/skills/kld-sdd/opsx-propose/reference.md +12 -46
  60. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +14 -61
  61. package/templates/skills/kld-sdd/opsx-spec/checklist.md +3 -3
  62. package/templates/skills/kld-sdd/opsx-task/SKILL.md +38 -32
  63. package/templates/skills/kld-sdd/opsx-task/checklist.md +5 -6
  64. package/templates/skills/kld-sdd/opsx-test/SKILL.md +2 -0
  65. package/templates/skills/kld-sdd/tdd-rules/rules/tdd-strategy-selection.md +1 -1
package/lib/init.js CHANGED
@@ -112,6 +112,139 @@ function parseSelectedTools(args) {
112
112
  return selected;
113
113
  }
114
114
 
115
+ /**
116
+ * 从命令行参数读取自定义 spec 目录
117
+ * 支持:--spec-path erp-sdd-specs、--spec-path=./specs、--spec-path=/abs/path/specs
118
+ * 返回相对/绝对路径字符串,未指定时返回 null。
119
+ */
120
+ function parseSpecPathArg(args) {
121
+ const index = args.findIndex(
122
+ (arg) => arg === '--spec-path' || arg.startsWith('--spec-path='),
123
+ );
124
+ if (index === -1) return null;
125
+
126
+ const rawValue = args[index] === '--spec-path'
127
+ ? args[index + 1]
128
+ : args[index].slice('--spec-path='.length);
129
+
130
+ if (!rawValue || rawValue.startsWith('--')) {
131
+ throw new Error('缺少 --spec-path 参数值,请指定 spec 仓库目录(相对或绝对路径)');
132
+ }
133
+ return rawValue.trim();
134
+ }
135
+
136
+ /**
137
+ * 将 --spec-path 解析为 { name, abs }。
138
+ * - 绝对路径:直接使用
139
+ * - 相对路径:相对 workspaceRoot 解析
140
+ * 仅取末段目录名作为包裹包名,abs 落在 workspaceRoot 下(保持包裹包在工作区/仓内的定位)。
141
+ */
142
+ function resolveSpecPathOption(specPathArg, workspaceRoot) {
143
+ if (!specPathArg) return null;
144
+ const root = path.resolve(workspaceRoot);
145
+ const abs = path.resolve(root, specPathArg);
146
+ const rel = path.relative(root, abs);
147
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) {
148
+ throw new Error(
149
+ `--spec-path 必须落在当前目录内(相对包裹包定位):${specPathArg}\n` +
150
+ ` 当前目录: ${root}\n 解析结果: ${abs}`,
151
+ );
152
+ }
153
+ const relPosix = rel.replace(/\\/g, '/');
154
+ // name 用相对路径(支持嵌套),供 ensureSpecPackage 与 .sdd.yaml spec_path 使用
155
+ return { name: relPosix, abs, rel: relPosix, display: path.basename(abs) };
156
+ }
157
+
158
+ /**
159
+ * 交互提示前的 spec 目录兜底检测:
160
+ * detectWorkspaceLayout 的 specRepo 只覆盖「子目录是 Git 仓」的场景,
161
+ * 这里补充两种常见形态,避免已有 spec 产物被漏检:
162
+ * 1. .sdd-spec-root 指针文件(历史初始化产物)
163
+ * 2. 非 Git 子目录的 spec 产物(findNestedSpecPackage 不要求子目录是 Git 仓)
164
+ */
165
+ function detectSpecCandidateForPrompt(workspaceRoot) {
166
+ try {
167
+ const hintPath = path.join(workspaceRoot, '.sdd-spec-root');
168
+ if (fs.existsSync(hintPath)) {
169
+ const rel = String(fs.readFileSync(hintPath, 'utf8') || '')
170
+ .trim()
171
+ .split(/\r?\n/)[0]
172
+ .trim();
173
+ if (rel) {
174
+ const abs = path.resolve(workspaceRoot, rel);
175
+ if (fs.existsSync(abs) && workspaceLayout.looksLikeSpecRepo(abs)) {
176
+ return { name: path.basename(abs), abs };
177
+ }
178
+ }
179
+ }
180
+ } catch { /* 指针读取异常时继续兜底 */ }
181
+
182
+ const nested = workspaceLayout.findNestedSpecPackage(workspaceRoot);
183
+ return nested ? { name: nested.name, abs: nested.abs } : null;
184
+ }
185
+
186
+ /**
187
+ * 交互式确定 spec 包裹包目录(--spec-path 未显式指定时):
188
+ * 1. 自动检测到已有 spec 产物(modules.yaml / openspec/changes / *-sdd-specs)→ 提醒用户确认是否使用
189
+ * 2. 未检测到 → 要求用户输入相对路径指定(回车则用默认推导名 <当前目录名>-sdd-specs)
190
+ *
191
+ * 非交互环境(CI / 管道 / --tool 自动化)不询问,保持原自动推导行为。
192
+ *
193
+ * @param {object} [options] - 测试注入:{ askFn, interactive }
194
+ * @returns {{ name: string, abs: string, rel: string, display: string } | null}
195
+ * null 表示未选择自定义目录,走默认推导逻辑
196
+ */
197
+ async function promptSpecPath(workspaceRoot, detectedSpec, options = {}) {
198
+ const askUser = options.askFn || ask;
199
+ let interactive = options.interactive;
200
+ if (typeof interactive !== 'boolean') {
201
+ // 非交互环境:无 TTY 或显式给了 --tool(自动化场景),不询问
202
+ const isTTY = process.stdin && process.stdin.isTTY;
203
+ const hasToolArg = process.argv.slice(2).some((arg) => arg === '--tool' || arg.startsWith('--tool='));
204
+ interactive = Boolean(isTTY) && !hasToolArg;
205
+ }
206
+ if (!interactive) return null;
207
+
208
+ if (detectedSpec) {
209
+ console.log(`\n🔍 检测到已有 spec 目录(命名约定或完整结构标记): ${detectedSpec.name}`);
210
+ console.log(` 位置: ${detectedSpec.abs}`);
211
+ while (true) {
212
+ const answer = await askUser(`是否使用该 spec 仓库?(Y=使用 / n=重新指定路径,默认 Y): `);
213
+ const normalized = (answer || '').trim().toLowerCase();
214
+ if (!normalized || normalized === 'y' || normalized === 'yes') {
215
+ const relPosix = path.relative(workspaceRoot, detectedSpec.abs).replace(/\\/g, '/') || detectedSpec.name;
216
+ return { name: relPosix, abs: detectedSpec.abs, rel: relPosix, display: detectedSpec.name };
217
+ }
218
+ if (normalized === 'n' || normalized === 'no') break;
219
+ console.log(`❌ 无效输入 "${answer}",请输入 Y 或 n`);
220
+ }
221
+ } else {
222
+ console.log('\n🔍 未检测到已有 spec 目录(目录名 *-sdd-specs,或 modules.yaml + sdd.config.yaml + openspec/changes + openspec/specs/overview.md 齐全)');
223
+ }
224
+
225
+ // 未确认使用检测目录(或未检测到):要求输入相对路径
226
+ const defaultName = workspaceLayout.deriveSpecPackageName(workspaceRoot);
227
+ while (true) {
228
+ const input = await askUser(`请输入 spec 目录的相对路径(直接回车使用默认 ${defaultName}): `);
229
+ const trimmed = (input || '').trim();
230
+ if (!trimmed) {
231
+ // 回车 = 使用默认推导名。
232
+ // 若用户刚用 n 拒绝了检测到的目录,必须显式返回默认目录:
233
+ // 返回 null 会让主流程走自动探测,重新命中刚被拒绝的目录。
234
+ if (detectedSpec) {
235
+ return resolveSpecPathOption(defaultName, workspaceRoot);
236
+ }
237
+ return null; // 未检测到场景:走主流程默认创建
238
+ }
239
+ try {
240
+ return resolveSpecPathOption(trimmed, workspaceRoot);
241
+ } catch (error) {
242
+ console.log(`❌ ${error.message}`);
243
+ // 循环重新输入
244
+ }
245
+ }
246
+ }
247
+
115
248
  /**
116
249
  * 让用户选择编辑器
117
250
  * 无效输入时重新询问,不允许默认
@@ -918,6 +1051,17 @@ function deployTelemetryDataDir(targetCwd = process.cwd()) {
918
1051
  }
919
1052
  }
920
1053
 
1054
+ // 部署 scale-thresholds.json(Simple/Full 模式判定阈值,opsx-propose §6.8 自动规模判定依赖)
1055
+ const thresholdsSrc = path.join(pkgPath, 'lib', 'scale-thresholds.json');
1056
+ const thresholdsDst = path.join(sharedLibDir, 'scale-thresholds.json');
1057
+ if (fs.existsSync(thresholdsSrc)) {
1058
+ fs.copyFileSync(thresholdsSrc, thresholdsDst);
1059
+ deployedModules += 1;
1060
+ vlog(' ✓ 部署 skywalk-sdd/lib/scale-thresholds.json(Simple/Full 判定阈值)');
1061
+ } else {
1062
+ console.log(' ⚠️ scale-thresholds.json 源文件缺失,propose 自动规模判定将使用内置默认值');
1063
+ }
1064
+
921
1065
  const packageVersion = require(path.join(pkgPath, 'package.json')).version;
922
1066
  const runtimeMetadataDst = path.join(dataDir, 'runtime-metadata.cjs');
923
1067
  fs.writeFileSync(
@@ -1043,7 +1187,8 @@ function installSddShellHook(cwd, hookName, pkgPath = getPackagePath()) {
1043
1187
  // 已是 SDD hook:允许覆盖升级
1044
1188
  fs.writeFileSync(hookPath, content, 'utf8');
1045
1189
  try {
1046
- fs.chmodSync(hookPath, 0o755);
1190
+ // 0o700:仅属主可执行(git 以属主身份执行 hook),避免 S2612 对组/其他用户放权的告警
1191
+ fs.chmodSync(hookPath, 0o700);
1047
1192
  } catch {
1048
1193
  // ignore
1049
1194
  }
@@ -1563,6 +1708,11 @@ KLD SDD 项目初始化工具
1563
1708
  --skip-openspec 跳过 openspec init 步骤
1564
1709
  --skip-template 跳过复制内置模版
1565
1710
  --tool <name> 指定编辑器,可用值: cursor, claude, codebuddy, qoder, opencode, kunlunzhima, workbuddy, codex, all
1711
+ --spec-path <dir> 指定 spec 包裹包目录(必须是当前目录的子目录)。
1712
+ 不指定时(交互终端):自动检测已有 spec 目录(目录名 *-sdd-specs,
1713
+ 或 modules.yaml + sdd.config.yaml + openspec/changes + openspec/specs/overview.md 齐全),
1714
+ 检测到则询问是否使用;未检测到则要求输入相对路径(回车用默认 <当前目录名>-sdd-specs)。
1715
+ 例: 在 erp-klny-all 下 --spec-path erp-sdd-specs
1566
1716
 
1567
1717
  工作区模式(在个人工作目录执行):
1568
1718
  - skills / 编辑器配置只部署一次到当前工作目录
@@ -1575,6 +1725,7 @@ KLD SDD 项目初始化工具
1575
1725
  kld-sdd-init # 完整初始化流程
1576
1726
  kld-sdd-init --skip-openspec # 跳过 openspec,直接部署 skills
1577
1727
  kld-sdd-init --tool codex # 仅部署 Codex 配置
1728
+ kld-sdd-init --spec-path erp-sdd-specs # 指定 spec 目录(当前目录的子目录)
1578
1729
  kld-sdd sync-repos # 新代码仓加入后同步接入
1579
1730
  `);
1580
1731
  }
@@ -1642,6 +1793,17 @@ async function main() {
1642
1793
  const skipOpenspec = args.includes('--skip-openspec');
1643
1794
  const skipTemplate = args.includes('--skip-template');
1644
1795
 
1796
+ // 自定义 spec 目录(--spec-path):未指定时交互询问(自动检测确认 / 输入相对路径)
1797
+ let customSpec = null;
1798
+ try {
1799
+ const specPathArg = parseSpecPathArg(args);
1800
+ if (specPathArg) customSpec = resolveSpecPathOption(specPathArg, process.cwd());
1801
+ } catch (error) {
1802
+ console.error(`❌ ${error.message}`);
1803
+ closeRl();
1804
+ process.exit(1);
1805
+ }
1806
+
1645
1807
  console.log('╔════════════════════════════════════════╗');
1646
1808
  console.log('║ KLD SDD 项目初始化工具 ║');
1647
1809
  console.log('║ 内置 openSpec 标准模版 ║');
@@ -1664,10 +1826,47 @@ async function main() {
1664
1826
  ? { name: layout.specRepo.name, abs: layout.specRepo.abs, created: false, isGit: true }
1665
1827
  : null;
1666
1828
 
1829
+ // 统一在此确定 spec 包裹包目录,后续所有分支直接复用 specPackage,不再各自 ensureSpecPackage。
1830
+ // - 显式 --spec-path:以指定目录为准(复用或创建),优先于同级自动探测
1831
+ // - 未显式指定:交互环境先自动检测已有 spec 产物,检测到则询问确认,未检测到则要求输入相对路径
1832
+ // (非交互环境 / 用户回车跳过:保持原默认推导行为)
1833
+ // - 否则未探测到 spec 仓:创建默认包裹包(--skip-template + --skip-openspec 时不创建)
1834
+ // initGit:多仓工作区的包裹包是独立 Git 仓;单仓/非 Git 则共用外层 Git。
1835
+ const specInitGit = layout.isWorkspace;
1836
+ if (customSpec) {
1837
+ specPackage = workspaceLayout.ensureSpecPackage(workspaceRoot, {
1838
+ initGit: specInitGit,
1839
+ name: customSpec.name,
1840
+ });
1841
+ console.log(`📌 使用自定义 spec 目录: ${customSpec.rel}`);
1842
+ console.log();
1843
+ } else {
1844
+ // 交互提示前兜底检测:Git 仓探测(layout.specRepo)漏掉的非 Git spec 产物
1845
+ const promptCandidate = layout.specRepo
1846
+ ? { name: layout.specRepo.name, abs: layout.specRepo.abs }
1847
+ : detectSpecCandidateForPrompt(workspaceRoot);
1848
+ const interactiveSpec = await promptSpecPath(workspaceRoot, promptCandidate);
1849
+ if (interactiveSpec) {
1850
+ customSpec = interactiveSpec;
1851
+ specPackage = workspaceLayout.ensureSpecPackage(workspaceRoot, {
1852
+ initGit: specInitGit,
1853
+ name: customSpec.name,
1854
+ });
1855
+ console.log(`📌 使用 spec 目录: ${customSpec.rel}`);
1856
+ console.log();
1857
+ } else if (!specPackage && !(skipTemplate && skipOpenspec)) {
1858
+ specPackage = workspaceLayout.ensureSpecPackage(workspaceRoot, { initGit: specInitGit });
1859
+ }
1860
+ }
1861
+
1667
1862
  if (layout.isWorkspace) {
1863
+ // 显示层同步过滤:用户交互选择的 spec 目录可能原被分类为代码仓
1864
+ const displayCodeRepos = specPackage
1865
+ ? layout.codeRepos.filter((c) => path.resolve(c.abs) !== path.resolve(specPackage.abs))
1866
+ : layout.codeRepos;
1668
1867
  console.log('📂 检测到个人工作目录布局(子目录 Git 仓库)');
1669
1868
  console.log(` Spec: ${specPackage ? specPackage.name : '(未发现,将自动创建包裹包)'}`);
1670
- console.log(` 代码仓: ${layout.codeRepos.map((c) => c.name).join(', ') || '(无)'}`);
1869
+ console.log(` 代码仓: ${displayCodeRepos.map((c) => c.name).join(', ') || '(无)'}`);
1671
1870
  console.log(' → skills 只部署到本工作目录;SDD 文档统一进 *-sdd-specs 包裹包');
1672
1871
  console.log();
1673
1872
  }
@@ -1681,12 +1880,6 @@ async function main() {
1681
1880
  deployProfileArtifacts(selectedTools, workspaceRoot, getPackagePath());
1682
1881
 
1683
1882
  if (layout.isWorkspace) {
1684
- if (!specPackage) {
1685
- console.log('📦 未发现 spec 仓,正在创建统一 SDD 包裹包(独立 Git)...');
1686
- specPackage = workspaceLayout.ensureSpecPackage(workspaceRoot, { initGit: true });
1687
- console.log(` ✓ ${specPackage.created ? '已创建' : '已复用'}: ${specPackage.name}`);
1688
- }
1689
-
1690
1883
  await populateSpecPackage(specPackage.abs, {
1691
1884
  skipOpenspec,
1692
1885
  asIndependentGit: true,
@@ -1694,10 +1887,17 @@ async function main() {
1694
1887
  writeSddSpecRootHint(workspaceRoot, specPackage.abs);
1695
1888
  removeOuterSkywalk(workspaceRoot);
1696
1889
 
1697
- // 重新探测,刷新 layout.specRepo / codeRepos
1890
+ // 重新探测,刷新 layout.specRepo / codeRepos
1891
+ // 用户交互中可能指定了与自动探测不同的 spec 目录(旧目录仍在盘上),
1892
+ // 此时以用户选择为准,避免重探测把旧目录选为 specRepo、把新目录误分类为代码仓。
1698
1893
  layout = workspaceLayout.detectWorkspaceLayout(workspaceRoot);
1699
- if (!layout.specRepo) {
1700
- layout.specRepo = { name: specPackage.name, abs: specPackage.abs };
1894
+ if (specPackage) {
1895
+ if (!layout.specRepo || path.resolve(layout.specRepo.abs) !== path.resolve(specPackage.abs)) {
1896
+ layout.specRepo = { name: specPackage.name, abs: specPackage.abs };
1897
+ }
1898
+ layout.codeRepos = layout.codeRepos.filter(
1899
+ (c) => path.resolve(c.abs) !== path.resolve(specPackage.abs),
1900
+ );
1701
1901
  }
1702
1902
 
1703
1903
  console.log();
@@ -1712,10 +1912,6 @@ async function main() {
1712
1912
  const isGit = workspaceLayout.isGitRepo(workspaceRoot);
1713
1913
 
1714
1914
  if (isGit) {
1715
- console.log('📦 单仓模式:创建/复用仓内统一 SDD 包裹目录...');
1716
- specPackage = workspaceLayout.ensureSpecPackage(workspaceRoot, { initGit: false });
1717
- console.log(` ✓ ${specPackage.created ? '已创建' : '已复用'}: ${specPackage.name}`);
1718
-
1719
1915
  await populateSpecPackage(specPackage.abs, {
1720
1916
  skipOpenspec,
1721
1917
  asIndependentGit: false,
@@ -1737,8 +1933,7 @@ async function main() {
1737
1933
  }
1738
1934
  } else {
1739
1935
  // 非 Git 目录:仍创建包裹包,便于随后 git init
1740
- console.log('📦 当前目录不是 Git 仓:仍创建 SDD 包裹包(请稍后 git init)...');
1741
- specPackage = workspaceLayout.ensureSpecPackage(workspaceRoot, { initGit: false });
1936
+ console.log('📦 当前目录不是 Git 仓:包裹包已就绪(请稍后 git init)...');
1742
1937
  await populateSpecPackage(specPackage.abs, {
1743
1938
  skipOpenspec,
1744
1939
  asIndependentGit: false,
@@ -1750,26 +1945,13 @@ async function main() {
1750
1945
  }
1751
1946
  } else if (!skipOpenspec) {
1752
1947
  // 仅 openspec、跳过模版时:仍尊重包裹包,避免在工作区根平铺
1753
- if (layout.isWorkspace) {
1754
- if (!specPackage) {
1755
- specPackage = workspaceLayout.ensureSpecPackage(workspaceRoot, { initGit: true });
1756
- }
1757
- const prev = process.cwd();
1758
- try {
1759
- process.chdir(specPackage.abs);
1760
- await runOpenspecInit([]);
1761
- } finally {
1762
- process.chdir(prev);
1763
- }
1764
- } else {
1765
- specPackage = workspaceLayout.ensureSpecPackage(workspaceRoot, { initGit: false });
1766
- const prev = process.cwd();
1767
- try {
1768
- process.chdir(specPackage.abs);
1769
- await runOpenspecInit([]);
1770
- } finally {
1771
- process.chdir(prev);
1772
- }
1948
+ // specPackage 已在顶部统一 ensureSpecPackage 创建/复用
1949
+ const prev = process.cwd();
1950
+ try {
1951
+ process.chdir(specPackage.abs);
1952
+ await runOpenspecInit([]);
1953
+ } finally {
1954
+ process.chdir(prev);
1773
1955
  }
1774
1956
  }
1775
1957
 
@@ -1912,6 +2094,8 @@ module.exports = {
1912
2094
  syncWorkspaceRepos,
1913
2095
  copyDirRendered,
1914
2096
  parseSelectedTools,
2097
+ promptSpecPath,
2098
+ detectSpecCandidateForPrompt,
1915
2099
  installSddShellHook,
1916
2100
  deployCodeRepoHooks,
1917
2101
  TOOL_CONFIGS,
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://kld-sdd.dev/schemas/scale-thresholds/v1",
3
+ "description": "SDD Simple 模式自动规模判定阈值。临界时优先选 Full(保守策略)。",
4
+ "version": "1.0.0",
5
+ "simple": {
6
+ "max_files": 5,
7
+ "max_lines": 200,
8
+ "max_modules": 1,
9
+ "allow_new_table": false,
10
+ "allow_new_api": false
11
+ },
12
+ "fallback": {
13
+ "max_files": 5,
14
+ "max_lines": 200,
15
+ "max_modules": 1,
16
+ "allow_new_table": false,
17
+ "allow_new_api": false
18
+ }
19
+ }
@@ -132,7 +132,8 @@ function transformKunlunSlashCommands(content) {
132
132
 
133
133
  let result = content;
134
134
  for (const cmd of KUNLUN_SLASH_COMMANDS) {
135
- result = result.replace(new RegExp(`/opsx-${cmd}(?![a-z-])`, 'g'), `/opsx:${cmd}`);
135
+ // Command syntax changes for Kunlun; skill directory paths remain portable.
136
+ result = result.replace(new RegExp(`/opsx-${cmd}(?![a-z-/])`, 'g'), `/opsx:${cmd}`);
136
137
  }
137
138
  return result;
138
139
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kld-sdd",
3
- "version": "2.7.3",
3
+ "version": "2.7.8",
4
4
  "description": "KLD SDD OpenSpec 项目初始化工具 - 一键部署 SDD skills",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  "kld-sdd-init": "bin/kld-sdd-init.js"
9
9
  },
10
10
  "scripts": {
11
- "test": "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/workspace-layout.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/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 skywalk-sdd/usage-reporter.test.cjs skywalk-sdd/usage-reporting.test.cjs && node test/progress-package-artifact.test.cjs && node --test test/progress-package-install.e2e.cjs"
11
+ "bench:check": "node test/benchmark-check.cjs",
12
+ "test": "node --test 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/workspace-layout.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/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 skywalk-sdd/usage-reporter.test.cjs skywalk-sdd/usage-reporting.test.cjs && node test/progress-package-artifact.test.cjs && node --test test/progress-package-install.e2e.cjs && node test/consistency-check-core.cjs"
12
13
  },
13
14
  "keywords": [
14
15
  "kld",
@@ -43,6 +44,7 @@
43
44
  "skywalk-sdd/lib/",
44
45
  "skywalk-sdd/kb-upload.cjs",
45
46
  "skywalk-sdd/kb-sync-identity.cjs",
46
- "README.md"
47
+ "README.md",
48
+ "USABILITY.md"
47
49
  ]
48
50
  }
@@ -12,6 +12,7 @@ const {
12
12
  loadKbState,
13
13
  resolveKbParams,
14
14
  stripBom,
15
+ httpGetJson,
15
16
  } = require('./lib/shared.cjs');
16
17
 
17
18
  // Re-export requestJson for external callers (API compatibility)
@@ -105,6 +106,7 @@ function buildPayload(args, mode) {
105
106
  }
106
107
 
107
108
  async function retrieveContext(args = parseArgs(process.argv), env = process.env) {
109
+ if (args.offline) return unavailableContext('engineering_kb_offline', '本次使用本地上下文,未验证远程基线');
108
110
  // 优先从共享 state 文件读取配置(与 opsx-ontology-query / opsx-kb-ingest 共用)
109
111
  let stateConfig = {};
110
112
  const stateFile = args['state-file'] || env.SDD_KB_STATE_FILE;
@@ -131,12 +133,17 @@ async function retrieveContext(args = parseArgs(process.argv), env = process.env
131
133
 
132
134
  const { apiBase: kbApiBase, token: kbToken, spaceId: kbSpaceId, kbId: kbKbId, targets: kbTargets } = resolveKbParams(stateConfig, env);
133
135
 
136
+ if (Boolean(args['space-id']) !== Boolean(args['kb-id'])) throw new Error('请同时指定 --space-id 和 --kb-id');
137
+ const explicitSpace = args['space-id'] || env.ENGINEERING_KB_SPACE_ID;
138
+ const explicitKb = args['kb-id'] || env.ENGINEERING_KB_KB_ID;
139
+ if (Boolean(explicitSpace) !== Boolean(explicitKb)) throw new Error('请同时配置目标 spaceId 和 kbId');
140
+ if (kbTargets.length > 1 && !explicitSpace) throw new Error('配置了多个 KB,请用 --space-id 和 --kb-id 明确本次查询目标');
134
141
  const apiBase = args['api-base'] || kbApiBase;
135
142
  const token = args.token || kbToken;
136
- const spaceId = args['space-id'] || kbSpaceId;
137
- const kbId = args['kb-id'] || kbKbId;
143
+ const spaceId = explicitSpace || kbSpaceId;
144
+ const kbId = explicitKb || kbKbId;
138
145
 
139
- // --check-only: 只检查 KB 配置是否就绪,不发起 API 请求
146
+ // --check-only performs an authenticated check against the selected KB.
140
147
  // 用于 SDD 各阶段(propose/spec/design/task/check)统一检测 KB 可用性,消除相对路径歧义
141
148
  if (args['check-only']) {
142
149
  const missing = [];
@@ -145,31 +152,27 @@ async function retrieveContext(args = parseArgs(process.argv), env = process.env
145
152
  if (!kbId) missing.push('kbId');
146
153
  if (missing.length > 0) {
147
154
  return {
148
- available: false,
149
- reason: 'engineering_kb_not_configured',
155
+ ...unavailableContext('engineering_kb_not_configured', 'KB 尚未配置,本地工作可以继续'),
156
+ configured: false,
150
157
  missing,
151
- hint: 'Run /opsx-kb-config to configure kb-state.json (in spec root)',
152
158
  };
153
159
  }
154
- return {
155
- available: true,
156
- api: apiBase,
157
- targets: kbTargets,
158
- };
160
+ const endpoint = `${String(apiBase).replace(/\/+$/, '')}/v1/spaces/${encodeURIComponent(spaceId)}`
161
+ + `/knowledge-bases/${encodeURIComponent(kbId)}/context/readiness`;
162
+ const response = await httpGetJson(endpoint, token, contextTimeout(args, env, 3000));
163
+ if (response?.code != null && response.code !== 0) throw new Error(response.message || 'KB readiness failed');
164
+ const readiness = response?.data || response;
165
+ if (readiness?.available !== true || readiness?.authorized !== true) throw new Error('KB 未返回有效的权限与就绪状态');
166
+ return { ...readiness, configured: true, api: apiBase, spaceId, kbId };
167
+
159
168
  }
160
169
 
161
- if (!spaceId || !kbId) {
162
- return {
163
- available: false,
164
- advisory: true,
165
- degraded: true,
166
- reason: 'engineering_kb_not_configured',
167
- hint: 'set ENGINEERING_KB_SPACE_ID and ENGINEERING_KB_KB_ID; do not fall back to local archive/',
168
- };
170
+ if (!spaceId || !kbId || !token) {
171
+ return { ...unavailableContext('engineering_kb_not_configured', 'KB 尚未配置,本地工作可以继续'), configured: false };
169
172
  }
170
173
 
171
174
  const mode = String(args.mode || 'match').trim().toLowerCase() === 'resolve' ? 'resolve' : 'match';
172
- const timeoutMs = Number(args.timeout || env.ENGINEERING_KB_TIMEOUT_MS || 30000);
175
+ const timeoutMs = contextTimeout(args, env, 5000);
173
176
  const payload = buildPayload(args, mode);
174
177
  const data = await requestJson(
175
178
  buildEndpoint(apiBase, spaceId, kbId, mode),
@@ -184,21 +187,38 @@ async function retrieveContext(args = parseArgs(process.argv), env = process.env
184
187
  };
185
188
  }
186
189
 
190
+ function contextTimeout(args, env, fallback) {
191
+ const timeout = Number(args.timeout || env.ENGINEERING_KB_TIMEOUT_MS || fallback);
192
+ if (!Number.isFinite(timeout) || timeout <= 0) throw new Error('timeout 必须是大于 0 的毫秒数');
193
+ return timeout;
194
+ }
195
+
196
+ function unavailableContext(reason, message) {
197
+ return {
198
+ available: false,
199
+ advisory: true,
200
+ degraded: true,
201
+ localWorkAllowed: true,
202
+ baselineVerified: false,
203
+ inheritanceAllowed: false,
204
+ reason,
205
+ message,
206
+ hint: '继续本地编写、检查和测试;保留已有身份与真实基线。发布前重新在线校验,不把历史参考当作 KB current。',
207
+ };
208
+ }
209
+
187
210
  async function main() {
188
211
  const args = parseArgs(process.argv);
212
+ let result;
189
213
  try {
190
- console.log(JSON.stringify(await retrieveContext(args), null, 2));
214
+ result = await retrieveContext(args);
191
215
  } catch (error) {
192
- console.log(JSON.stringify({
193
- available: false,
194
- advisory: true,
195
- degraded: true,
196
- reason: 'engineering_kb_unavailable',
197
- message: error.message,
198
- hint: 'KB unavailable: continue degraded; never scan local archive/ for inheritance UUIDs',
199
- }, null, 2));
200
- if (args.strict) process.exitCode = 1;
216
+ const reason = /\b(401|403|Unauthorized|Forbidden)\b/.test(error.message)
217
+ ? 'engineering_kb_access_denied' : 'engineering_kb_unavailable';
218
+ result = unavailableContext(reason, error.message);
201
219
  }
220
+ console.log(JSON.stringify(result, null, 2));
221
+ if (args.strict && result.available !== true) process.exitCode = 1;
202
222
  }
203
223
 
204
224
  if (require.main === module) {