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.
- package/README.md +12 -5
- package/USABILITY.md +84 -0
- package/kld-sdd-guide.html +16 -4
- package/lib/hook-gate-core.js +327 -0
- package/lib/init.js +221 -37
- package/lib/scale-thresholds.json +19 -0
- package/lib/skills-bundle.js +2 -1
- package/package.json +5 -3
- package/skywalk-sdd/context-client.cjs +50 -30
- package/skywalk-sdd/index.cjs +36 -16
- package/skywalk-sdd/kb-sync-identity.cjs +99 -451
- package/skywalk-sdd/kb-upload.cjs +67 -44
- package/skywalk-sdd/lib/usage-contract.cjs +3 -2
- package/skywalk-sdd/lib/usage-reporter.cjs +27 -2
- package/skywalk-sdd/metrics-v3.cjs +2 -2
- package/skywalk-sdd/ontology/active-changes.cjs +2 -1
- package/skywalk-sdd/ontology/archive-package.cjs +1 -1
- package/skywalk-sdd/ontology/artifact-parser.cjs +7 -4
- package/skywalk-sdd/ontology/id.cjs +5 -4
- package/skywalk-sdd/ontology/identity-index.cjs +4 -3
- package/skywalk-sdd/ontology/list-changes.cjs +1 -1
- package/skywalk-sdd/ontology/runtime.cjs +7 -3
- package/skywalk-sdd/ontology/schema.cjs +2 -0
- package/skywalk-sdd/ontology/traceability-validator.cjs +74 -4
- package/skywalk-sdd/ontology/workspace-layout.cjs +25 -5
- package/skywalk-sdd/reporting/change-report-model.cjs +4 -3
- package/templates/git-hooks/commit-msg +22 -21
- package/templates/git-hooks/consistency-check-core.cjs +1087 -0
- package/templates/git-hooks/hooks.config +20 -1
- package/templates/git-hooks/pre-commit +22 -21
- package/templates/git-hooks/pre-commit-consistency-check.cjs +29 -332
- package/templates/git-hooks/pre-commit-sdd-check.cjs +98 -0
- package/templates/git-hooks/pre-push +22 -21
- package/templates/git-hooks/pre-push-consistency-check.cjs +58 -406
- package/templates/hooks/codebuddy/hooks/sdd-tdd-rhythm-gate.cjs +1 -1
- package/templates/openspec/tasks.md +3 -3
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +43 -6
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +1 -1
- package/templates/skills/kld-sdd/opsx-apply/reference.md +1 -1
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +9 -13
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +54 -328
- package/templates/skills/kld-sdd/opsx-check/checklist.md +7 -4
- package/templates/skills/kld-sdd/opsx-check/reference.md +58 -0
- package/templates/skills/kld-sdd/opsx-check/result-template.json +52 -0
- package/templates/skills/kld-sdd/opsx-check/reviewer.md +100 -0
- package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +82 -451
- package/templates/skills/kld-sdd/opsx-consistency-check/{reference.md → references/reference.md} +0 -1
- package/templates/skills/kld-sdd/opsx-consistency-check/scripts/scripts.cjs +517 -0
- package/templates/skills/kld-sdd/opsx-design/SKILL.md +10 -30
- package/templates/skills/kld-sdd/opsx-design/checklist.md +3 -4
- package/templates/skills/kld-sdd/opsx-design/reference.md +1 -1
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +12 -74
- package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +7 -5
- package/templates/skills/kld-sdd/opsx-ontology-query/phase-1-prechange.md +2 -2
- package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +1 -1
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +28 -73
- package/templates/skills/kld-sdd/opsx-propose/checklist.md +8 -8
- package/templates/skills/kld-sdd/opsx-propose/interaction-policy.md +28 -0
- package/templates/skills/kld-sdd/opsx-propose/reference.md +12 -46
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +14 -61
- package/templates/skills/kld-sdd/opsx-spec/checklist.md +3 -3
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +38 -32
- package/templates/skills/kld-sdd/opsx-task/checklist.md +5 -6
- package/templates/skills/kld-sdd/opsx-test/SKILL.md +2 -0
- 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
|
-
|
|
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(` 代码仓: ${
|
|
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 (
|
|
1700
|
-
layout.specRepo
|
|
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
|
|
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
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
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
|
+
}
|
package/lib/skills-bundle.js
CHANGED
|
@@ -132,7 +132,8 @@ function transformKunlunSlashCommands(content) {
|
|
|
132
132
|
|
|
133
133
|
let result = content;
|
|
134
134
|
for (const cmd of KUNLUN_SLASH_COMMANDS) {
|
|
135
|
-
|
|
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
|
+
"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
|
-
"
|
|
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 =
|
|
137
|
-
const kbId =
|
|
143
|
+
const spaceId = explicitSpace || kbSpaceId;
|
|
144
|
+
const kbId = explicitKb || kbKbId;
|
|
138
145
|
|
|
139
|
-
// --check-only
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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 =
|
|
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
|
-
|
|
214
|
+
result = await retrieveContext(args);
|
|
191
215
|
} catch (error) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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) {
|