kld-sdd 2.7.8-2 → 2.7.8-3

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 CHANGED
@@ -4,6 +4,8 @@ KLD SDD OpenSpec 工程增强工具:一键初始化 AI 编辑器技能、语
4
4
 
5
5
  本分支已调整使用方式:模式与测试策略自动选择,按已有授权连续处理,KB 断连时继续本地工作,发布时核对真实基线。新规则、设计依据与尚未完成的结构调整见 [使用摩擦分析](USABILITY.md)。旧安装副本需要更新后才会应用这些规则。
6
6
 
7
+ 日常提交默认只保留本地关联 Hook;Apply 自动执行代码/Spec 检查、真实测试与交付验证。旧项目升级、备份回退、多仓和 CI 边界见 [Git Hook 调整说明](USABILITY.md#2026-09-10git-hook-收缩与交付验证)。
8
+
7
9
  ## 这是什么?
8
10
 
9
11
  **SDD(Specification-Driven Development)** 是一种以文档链驱动 AI 编码的研发方法:先写清楚"要做什么",再让 AI 去实现,避免 AI 乱猜、反复返工。
package/USABILITY.md CHANGED
@@ -102,3 +102,55 @@ SDD 应帮助团队留下三类东西:明确的业务约定、能够证明实
102
102
  本机独立后端联调确认七种 Skill 的使用事件均成功写入,断网事件保留并恢复补报,服务端统计为 8 次、覆盖 7 阶段。使用统计是独立的 usage 事件,不等于测试通过、归档完成或 Spec 已入库;当前服务端按提交次数计数,网络响应丢失后的重复提交不提供 exactly-once 保证。
103
103
 
104
104
  配置 Skill 与参考同步去除默认首目标、截短 UUID 和旧监控项目绑定步骤,修正 KB API 字段映射;切换查询目标不再引导改写团队项目身份,权限失败也不清空其他目标可能仍有效的密钥。
105
+
106
+ ## 2026-09-10:Git Hook 收缩与交付验证
107
+
108
+ 日常 commit 负责保存工作,Apply 收尾负责检查交付,Spec 入库负责发布知识。新安装只自动接入一个本地 `commit-msg`,它不请求 KB、不调用模型、不扫描整个工作区,也不重复执行文档 Check。
109
+
110
+ | 场景 | 当前行为 |
111
+ |---|---|
112
+ | 本次明确指定 Change | `KLD_SDD_CHANGE` 支持逗号分隔的多个 Change,校验目录后写关联 |
113
+ | 未显式指定 Change | 只复用能与暂存文件内容逐项匹配的检查关联;多个候选或没有证据时提示待关联 |
114
+ | Spec 仓尚未提交 | 允许保存代码,不写旧 HEAD 冒充当前 `Spec-Revision` |
115
+ | 独立 Spec 仓干净且远端关系匹配 | 写完整不可变 Spec SHA;单仓不写自指 Revision |
116
+ | 路径失效、Node 缺失或关联脚本超时 | 本次提交继续,显示待关联,不表示交付检查通过 |
117
+ | 实施收尾 | Agent 自动进行代码/Spec 检查、真实测试和 `delivery-check verify` |
118
+
119
+ 关联 worker 设置 1.5 秒超时上限。这个上限不是性能测试结果;用户自有 Hook 的耗时不受 SDD 控制。
120
+
121
+ ### 一个验证入口,四个内部步骤
122
+
123
+ 入口是 `<spec-root>/skywalk-sdd/git-hooks/delivery-check.cjs`。Apply 自动串联,用户不需要记忆四条命令:
124
+
125
+ 1. `prepare --project=<代码仓Git根> --change=<Change> --base=<实施前SHA>`:记录输入,返回本次需要检查的文件和是否可复用旧回执。
126
+ 2. `record-review --project=... --change=... --input=<检查JSON>`:收录实际检查结果;输入改变、漏覆盖、失败、未检查都会明确显示。
127
+ 3. `test --project=... --change=... --session-id=... --task-ids=... --command-file=<命令数组JSON>`:内部复用原 `test-run`,保留严格测试事件、命令退出码及原始输出。
128
+ 4. `verify --project=... --change=...`:判断当前快照的证据是否完整。`ready` 与 `pending` 分开,不把 KB 入库或合并状态混入测试结果。
129
+
130
+ 机器回执绑定代码仓身份、Spec 仓身份、明确 Change、代码和 Spec 内容、规则版本与配置。当前保守地纳入本仓依赖文件及全局规格;相同指纹可复用,变化后再定向取证。它尚未实现自动跨仓依赖图或逐规则的最小重查。mtime 不再作为内容一致的证据。UTF-8 文本统一 LF 以兼容 Git autocrlf,二进制和符号链接目标按原始字节检查。
131
+
132
+ 检查结果分别记录接口、业务、校验和错误处理;`pass` 需要证据,`na` 需要理由,`not_checked` 与 `fail` 都不能算通过。回执记录 Agent 做过的检查及其输入,不单靠模型自报置信度判断正确性。
133
+
134
+ 测试计数支持 Node TAP 与 Maven Surefire/Failsafe 的结果摘要;未知测试格式保留实际输出并显示待验证,不编造计数。只有无业务代码变更、四个维度都经检查判为不适用,且回执有明确测试不适用理由时,才允许 `testStatus=not_applicable`,不会显示“测试通过”。
135
+
136
+ ### 多仓与正式提交
137
+
138
+ 每个代码仓分别存回执,同一 Change 的多仓结果互不覆盖。Spec 可以先于代码完成并入库;“规格已发布”和“实现已验证”是两个状态。
139
+
140
+ 需要核对已提交代码时使用 `verify --scope=commit --ref=<目标提交> --base=<实施前提交>`;独立 Spec 仓额外提供 `--spec-ref=<完整Spec SHA>`。命令读取指定 Git 对象,可以验证非当前 HEAD,不会用工作区替代。冲突解决改变内容后需重新检查和测试。
141
+
142
+ 本次没有自动启用团队 CI。现有 CI 样例不等于已接入的合并门禁。团队接入时需要在受信任的执行环境生成或提供相应回执与原始测试日志;当前测试回执包含本地路径,复制到另一机器不能直接当作可移植的验证包。可以在目标 checkout 上重新执行同一入口。
143
+
144
+ ### 已有项目升级与回退
145
+
146
+ 重新运行安装器会更新运行时与 Skills,并迁移已识别的原版 SDD Hook。已退役或更新的 Hook 先备份到 Git 公共目录的 `kld-sdd-hook-backups/`;回退时把对应 `.bak` 复制回 hooks 同名路径并恢复执行权限。
147
+
148
+ 只含 marker 不足以证明文件受管:追加过用户逻辑、Hook 符号链接、修改过插件列表、显式旧门禁设置都会保留并提示。`core.hooksPath`(包括 Husky)交给现有管理器维护;安装器不会接管其目录。普通 worktree 使用 Git 解析出的公共 hooks 目录。`hooks.config` 保留用户内容,重新安装不重置选项。
149
+
150
+ 空目录首次安装声明正确的 mono 布局;此前生成的原样多仓占位配置可备份后自动迁移,真实团队配置不会被猜测改写。只有一个无效 `.git` 目录时明确提示 Hook 未安装,本地文档与运行时仍可部署。
151
+
152
+ ### 上报与知识链路的边界
153
+
154
+ 原 Skill 使用上报、严格测试事件和 KB 上传/查询 API 保留。默认移除旧 pre-commit 后,不再采集那条仅凭“本次提交有代码而没有文档”推断的 bypass 事件;这类旧统计与真实 Skill 使用量不能混为一谈。
155
+
156
+ `kb-upload --validate-only` 在未连接 KB 时可做本地语义校验,并明确说明未验证权限、发布基线及入库状态;正式上传仍严格检查项目与目标配置,连接或身份异常不能伪装为入库成功。UUID 继续完整保存,仅修正帮助中残留的 8 位说明。
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const crypto = require('crypto');
5
+ const { execFileSync } = require('child_process');
6
+ const known = require('./managed-hook-hashes.json');
7
+ const hash = text => crypto.createHash('sha256').update(text).digest('hex');
8
+
9
+ function section(text, name) {
10
+ let active = false;
11
+ const result = [];
12
+ for (const raw of text.split(/\r?\n/)) {
13
+ const line = raw.trim();
14
+ if (line.startsWith('[')) active = line === `[${name}]`;
15
+ else if (active && line && !line.startsWith('#')) result.push(line);
16
+ }
17
+ return result.join('\n');
18
+ }
19
+
20
+ function updateHook(root, name, pkgPath, configFile) {
21
+ const git = args => execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
22
+ let configured = '';
23
+ try { configured = git(['config', '--get', 'core.hooksPath']); } catch { /* default path */ }
24
+ if (configured) {
25
+ console.log(` ℹ️ 保留 core.hooksPath=${configured};由现有 Hook 管理器接入,SDD 不覆盖它`);
26
+ return;
27
+ }
28
+ let hookPath;
29
+ try { hookPath = path.resolve(root, git(['rev-parse', '--git-path', `hooks/${name}`])); }
30
+ catch { console.log(` ⚠️ ${root} 不是有效 Git 仓库,未安装 ${name} Hook`); return; }
31
+ const templateDir = path.join(pkgPath, 'templates/git-hooks');
32
+ const content = fs.readFileSync(path.join(templateDir, name));
33
+ const stat = fs.lstatSync(hookPath, { throwIfNoEntry: false });
34
+ if (stat && !stat.isFile()) { console.log(` ℹ️ 保留 Hook 链接或特殊文件:${hookPath}`); return; }
35
+ const existing = stat ? fs.readFileSync(hookPath) : null;
36
+ const customizedConfig = configFile && fs.existsSync(configFile)
37
+ && section(fs.readFileSync(configFile, 'utf8'), name) !== section(fs.readFileSync(path.join(templateDir, 'hooks.config'), 'utf8'), name);
38
+ const explicitGate = name !== 'commit-msg' && configFile && fs.existsSync(configFile)
39
+ && /^(?:strict|skip|warn_only)\s*=\s*(?:1|true|yes)$/m.test(section(fs.readFileSync(configFile, 'utf8'), 'consistency'));
40
+ if (existing && (explicitGate || customizedConfig ||
41
+ (!known[name].includes(hash(existing)) && hash(existing) !== hash(content)))) {
42
+ console.log(` ℹ️ 保留自定义 Hook:${hookPath};可审阅后自行移除旧 SDD 调用`);
43
+ return;
44
+ }
45
+ if (existing && hash(existing) === hash(content) && name === 'commit-msg') return;
46
+ if (existing) {
47
+ const common = path.resolve(root, git(['rev-parse', '--git-common-dir']));
48
+ const backup = path.join(common, 'kld-sdd-hook-backups', `${name}.${hash(existing)}.bak`);
49
+ fs.mkdirSync(path.dirname(backup), { recursive: true });
50
+ if (!fs.existsSync(backup)) fs.copyFileSync(hookPath, backup);
51
+ console.log(` ✓ 旧 Hook 已备份:${backup}`);
52
+ }
53
+ if (name !== 'commit-msg') {
54
+ if (existing) fs.unlinkSync(hookPath);
55
+ return;
56
+ }
57
+ fs.mkdirSync(path.dirname(hookPath), { recursive: true });
58
+ fs.writeFileSync(hookPath, content, { mode: 0o755 });
59
+ fs.chmodSync(hookPath, 0o755);
60
+ console.log(' ✓ commit-msg 已更新为本地关联提示;交付验证由 Apply 收尾执行');
61
+ }
62
+
63
+ function deployRuntime(specRoot, pkgPath) {
64
+ const dir = path.join(specRoot, 'skywalk-sdd/git-hooks');
65
+ const source = path.join(pkgPath, 'templates/git-hooks');
66
+ fs.mkdirSync(dir, { recursive: true });
67
+ for (const file of fs.readdirSync(source)) {
68
+ const target = path.join(dir, file);
69
+ const stat = fs.lstatSync(target, { throwIfNoEntry: false });
70
+ if (stat && !stat.isFile()) { console.log(` ℹ️ 保留运行时链接:${target}`); continue; }
71
+ if (file.endsWith('.cjs') || (file === 'hooks.config' && !stat)) fs.copyFileSync(path.join(source, file), target);
72
+ }
73
+ return path.join(dir, 'hooks.config');
74
+ }
75
+ module.exports = { updateHook, deployRuntime };
package/lib/init.js CHANGED
@@ -1151,6 +1151,8 @@ function deployTelemetryDataDir(targetCwd = process.cwd()) {
1151
1151
  console.log(' ⚠️ kb-sync-identity.cjs 源文件缺失,重新入库身份同步不可用');
1152
1152
  }
1153
1153
 
1154
+ require('./hook-install.js').deployRuntime(cwd, pkgPath);
1155
+
1154
1156
  console.log(` ✓ 部署 skywalk-sdd/ 运行时(events/state 数据目录 + ${deployedModules} 个模块)`);
1155
1157
  vlog(' ✓ 调用方式: node skywalk-sdd/log.cjs start|end|metrics|semantic-identity|semantic-reconcile|semantic-check');
1156
1158
  vlog(' ✓ openspec: node skywalk-sdd/openspec-shim.cjs list(自动 cd 到 *-sdd-specs)');
@@ -1161,76 +1163,12 @@ function deployTelemetryDataDir(targetCwd = process.cwd()) {
1161
1163
  return true;
1162
1164
  }
1163
1165
 
1164
- /**
1165
- * 安装 SDD shell hook(多脚本调用型 hook,如 pre-commit / pre-push / commit-msg)。
1166
- * 直接部署完整的 shell 脚本到 .git/hooks/,脚本内部远程引用 spec 仓的 .cjs 脚本。
1167
- *
1168
- * shell hook 运行时通过 git config sdd.specPath / .sdd-spec-root / 向上搜索
1169
- * 定位 spec 仓的 skywalk-sdd/git-hooks/ 目录,兼容单仓/多仓/mono 布局。
1170
- *
1171
- * @param {string} cwd - Git 仓库根目录
1172
- * @param {string} hookName - hook 名称(如 'pre-commit'、'pre-push')
1173
- * @param {string} pkgPath - kld-sdd 包路径(用于读取模板)
1174
- */
1175
- function installSddShellHook(cwd, hookName, pkgPath = getPackagePath()) {
1176
- const hooksDir = path.join(cwd, '.git', 'hooks');
1177
- if (!fs.existsSync(hooksDir)) {
1178
- return;
1179
- }
1180
-
1181
- const templatePath = path.join(pkgPath, 'templates', 'git-hooks', hookName);
1182
- if (!fs.existsSync(templatePath)) {
1183
- console.log(` ⚠️ Hook 模板不存在: ${templatePath}`);
1184
- return;
1185
- }
1186
-
1187
- const hookPath = path.join(hooksDir, hookName);
1188
- const marker = 'KLD SDD quality gate';
1189
- const content = fs.readFileSync(templatePath, 'utf8');
1190
-
1191
- if (fs.existsSync(hookPath)) {
1192
- const existing = fs.readFileSync(hookPath, 'utf8');
1193
- if (existing.includes(marker)) {
1194
- // 已是 SDD hook:允许覆盖升级
1195
- fs.writeFileSync(hookPath, content, 'utf8');
1196
- try {
1197
- // 0o700:仅属主可执行(git 以属主身份执行 hook),避免 S2612 对组/其他用户放权的告警
1198
- fs.chmodSync(hookPath, 0o700);
1199
- } catch {
1200
- // ignore
1201
- }
1202
- console.log(` ✓ 更新 .git/hooks/${hookName} SDD 质量门禁(多脚本)`);
1203
- return;
1204
- }
1205
- console.log(` ℹ️ .git/hooks/${hookName} 已存在且非 SDD 受管 hook,已保留不覆盖`);
1206
- return;
1207
- }
1208
-
1209
- fs.writeFileSync(hookPath, content, 'utf8');
1210
- try {
1211
- fs.chmodSync(hookPath, 0o755);
1212
- } catch {
1213
- // Windows 上 chmod 可能没有实际效果
1214
- }
1215
- console.log(` ✓ 安装 .git/hooks/${hookName} SDD 质量门禁(多脚本)`);
1166
+ /** 安全升级 commit-msg、备份并退役受管旧门禁,保留用户 Hook 与管理器。 */
1167
+ function installSddShellHook(cwd, hookName, pkgPath = getPackagePath(), configFile) {
1168
+ return require('./hook-install.js').updateHook(cwd, hookName, pkgPath, configFile);
1216
1169
  }
1217
1170
 
1218
- /**
1219
- * 部署代码仓全部 Git hooks(commit-msg + pre-commit + pre-push)。
1220
- *
1221
- * 统一入口,封装三种 hook 的部署逻辑。代码仓**不落 skywalk-sdd/**,
1222
- * 只在 .git/hooks/ 安装 shell hook 模板,远程引用 spec 仓的 .cjs 脚本:
1223
- *
1224
- * - **spec 仓**:确保 skywalk-sdd/git-hooks/ 下有全部 .cjs 脚本
1225
- * (commit-msg-sdd-trailer + pre-commit-* + pre-push-* + ontology)。
1226
- * - **代码仓 .git/hooks/**:仅安装 3 个 shell hook 模板,运行时通过
1227
- * git config sdd.specPath / .sdd-spec-root / 向上搜索定位 spec 仓的脚本。
1228
- *
1229
- * @param {string} codeRepoRoot - 代码仓根目录
1230
- * @param {string} specRepoRoot - spec 包裹包根目录
1231
- * @param {object} options - { pkgPath }
1232
- * @returns {{ ok: boolean, message?: string }}
1233
- */
1171
+ /** 代码仓只安装 commit-msg;验证脚本集中部署在 Spec 包。 */
1234
1172
  function deployCodeRepoHooks(codeRepoRoot, specRepoRoot, options = {}) {
1235
1173
  const cwd = path.resolve(codeRepoRoot);
1236
1174
  const specRoot = path.resolve(specRepoRoot);
@@ -1240,32 +1178,11 @@ function deployCodeRepoHooks(codeRepoRoot, specRepoRoot, options = {}) {
1240
1178
  return { ok: false, message: '不是 Git 仓库' };
1241
1179
  }
1242
1180
 
1243
- // ── 1. 确保 spec 仓有全部 hook 脚本 + ontology ──
1244
- // commit-msg .cjs + ontology(ensureSpecCommitMsgScript 已处理)
1245
1181
  ensureSpecCommitMsgScript(specRoot, pkgPath);
1246
- // pre-commit / pre-push .cjs 脚本
1247
- const specHooksDir = path.join(specRoot, 'skywalk-sdd', 'git-hooks');
1248
- const gitHooksTemplateDir = path.join(pkgPath, 'templates', 'git-hooks');
1249
- if (!fs.existsSync(specHooksDir)) {
1250
- fs.mkdirSync(specHooksDir, { recursive: true });
1251
- }
1252
- // 复制全部 .cjs 脚本 + hooks.config 配置文件
1253
- const hookScripts = fs.existsSync(gitHooksTemplateDir)
1254
- ? fs.readdirSync(gitHooksTemplateDir).filter(f => f.endsWith('.cjs') || f === 'hooks.config')
1255
- : [];
1256
- for (const script of hookScripts) {
1257
- const src = path.join(gitHooksTemplateDir, script);
1258
- const dst = path.join(specHooksDir, script);
1259
- // 始终覆盖,与 shell hook 的 marker 覆盖策略保持一致
1260
- fs.copyFileSync(src, dst);
1261
- }
1262
- console.log(` ✓ 确保 spec 仓 skywalk-sdd/git-hooks/ 脚本齐全(${hookScripts.length} 个文件)`);
1263
-
1264
- // ── 2. 安装三个 shell hooks 到代码仓 .git/hooks/ ──
1265
- // 模板 hook 运行时自动定位 spec 仓的 .cjs 脚本
1266
- installSddShellHook(cwd, 'commit-msg', pkgPath);
1267
- installSddShellHook(cwd, 'pre-commit', pkgPath);
1268
- installSddShellHook(cwd, 'pre-push', pkgPath);
1182
+ const configFile = require('./hook-install.js').deployRuntime(specRoot, pkgPath);
1183
+ for (const name of ['commit-msg', 'pre-commit', 'pre-push']) {
1184
+ installSddShellHook(cwd, name, pkgPath, configFile);
1185
+ }
1269
1186
 
1270
1187
  return { ok: true };
1271
1188
  }
@@ -1292,64 +1209,21 @@ function ensureSpecCommitMsgScript(specRepoRoot, pkgPath = getPackagePath()) {
1292
1209
  return { ok: true, scriptPath, deployed: true };
1293
1210
  }
1294
1211
 
1295
- /**
1296
- * 部署 Git hooks / CI 兜底模板
1297
- * 这些脚本只做质量门禁和 CI 补充记录,不替代 OPSX 主采集链路。
1298
- *
1299
- * @param {string} [targetCwd]
1300
- * @param {{ mode?: 'full'|'commit-msg-only' }} [options]
1301
- * full: spec 包裹包(pre-commit/pre-push/commit-msg + CI)
1302
- * commit-msg-only: 仅安装 commit-msg hook(跳过 pre-commit/pre-push)
1303
- */
1212
+ /** 部署验证运行时与轻量 Hook;不自动启用 CI。options 为旧调用兼容保留。 */
1304
1213
  function deployQualityGateTemplates(targetCwd = process.cwd(), options = {}) {
1305
1214
  const pkgPath = getPackagePath();
1306
1215
  const cwd = path.resolve(targetCwd);
1307
- const mode = options.mode || 'full';
1308
- // U4: Git 项目跳过 Git hooks / CI 兜底模板(按需部署,不污染非 Git 项目根)
1309
- if (!fs.existsSync(path.join(cwd, '.git'))) {
1310
- console.log('🧰 Git 项目,跳过 Git hooks / CI 兜底模板部署');
1311
- return false;
1216
+ const configFile = require('./hook-install.js').deployRuntime(cwd, pkgPath);
1217
+ if (!workspaceLayout.isGitRepo(cwd)) return false;
1218
+ for (const name of ['commit-msg', 'pre-commit', 'pre-push']) {
1219
+ installSddShellHook(cwd, name, pkgPath, configFile);
1312
1220
  }
1313
- console.log(`🧰 正在部署 SDD Git hooks(${mode}): ${cwd}`);
1314
- const dataDir = path.join(cwd, 'skywalk-sdd');
1315
- const gitHooksTemplateDir = path.join(pkgPath, 'templates', 'git-hooks');
1316
- const ciTemplateDir = path.join(pkgPath, 'templates', 'ci');
1317
- const targetGitHooksDir = path.join(dataDir, 'git-hooks');
1318
- const targetCiDir = path.join(dataDir, 'ci');
1319
-
1320
- if (fs.existsSync(gitHooksTemplateDir)) {
1321
- // 复制 .cjs 脚本 + hooks.config 到 skywalk-sdd/git-hooks/
1322
- // shell hook 模板(commit-msg/pre-commit/pre-push)不落此目录
1323
- if (!fs.existsSync(targetGitHooksDir)) {
1324
- fs.mkdirSync(targetGitHooksDir, { recursive: true });
1325
- }
1326
- for (const file of fs.readdirSync(gitHooksTemplateDir)) {
1327
- if (file.endsWith('.cjs') || file === 'hooks.config') {
1328
- fs.copyFileSync(path.join(gitHooksTemplateDir, file), path.join(targetGitHooksDir, file));
1329
- }
1330
- }
1331
- console.log(' ✓ 部署 skywalk-sdd/git-hooks/(.cjs 脚本 + hooks.config)');
1332
- } else {
1333
- console.log(` ⚠️ Git hooks 模板缺失: ${gitHooksTemplateDir}`);
1334
- }
1335
-
1336
- if (mode === 'full' && fs.existsSync(ciTemplateDir)) {
1337
- copyDir(ciTemplateDir, targetCiDir);
1338
- console.log(' ✓ 部署 skywalk-sdd/ci/');
1339
- }
1340
-
1341
- if (mode === 'full') {
1342
- installSddShellHook(cwd, 'pre-commit', pkgPath);
1343
- installSddShellHook(cwd, 'pre-push', pkgPath);
1344
- }
1345
- installSddShellHook(cwd, 'commit-msg', pkgPath);
1346
-
1347
- console.log('✅ Git hooks 已就绪');
1221
+ // CI is an explicit team integration; installing SDD does not enable a merge gate.
1348
1222
  return true;
1349
1223
  }
1350
1224
 
1351
1225
  /**
1352
- * 代码仓轻量接入:写 .sdd.yaml + sdd.specPath + .git/hooks/(commit-msg + pre-commit + pre-push)。
1226
+ * 代码仓轻量接入:写 .sdd.yaml + sdd.specPath + 轻量 commit-msg
1353
1227
  * 代码仓不落 skywalk-sdd/,shell hook 远程引用 spec 仓 skywalk-sdd/git-hooks/ 下的 .cjs 脚本。
1354
1228
  */
1355
1229
  function attachCodeRepoLite(codeRepoRoot, specRepoRoot, options = {}) {
@@ -1364,7 +1238,7 @@ function attachCodeRepoLite(codeRepoRoot, specRepoRoot, options = {}) {
1364
1238
  return { ok: false, repo: label, message: `spec 路径不是 Git 仓库: ${specRoot}` };
1365
1239
  }
1366
1240
 
1367
- console.log(`🔗 轻量接入代码仓(commit-msg + pre-commit + pre-push): ${label}`);
1241
+ console.log(`🔗 轻量接入代码仓(本地关联 Hook): ${label}`);
1368
1242
 
1369
1243
  const remote = sddConfig.gitRemoteUrl(specRoot) || 'git@gitlab.example.com:biz/xxx-sdd-specs.git';
1370
1244
  const sddYamlPath = path.join(cwd, '.sdd.yaml');
@@ -1390,7 +1264,7 @@ function attachCodeRepoLite(codeRepoRoot, specRepoRoot, options = {}) {
1390
1264
  }
1391
1265
  console.log(` ✓ sdd.specPath = ${linked.path}`);
1392
1266
 
1393
- // 部署代码仓全部 Git hooks(commit-msg + pre-commit + pre-push)
1267
+ // 部署轻量关联 Hook,并迁移可识别的旧门禁
1394
1268
  deployCodeRepoHooks(cwd, specRoot, options);
1395
1269
 
1396
1270
  return { ok: true, repo: label, path: cwd, specPath: linked.path };
@@ -1470,7 +1344,13 @@ function deployMonoSddYaml(cwd = process.cwd(), options = {}) {
1470
1344
  : '';
1471
1345
 
1472
1346
  if (fs.existsSync(target)) {
1473
- const existing = fs.readFileSync(target, 'utf8');
1347
+ let existing = fs.readFileSync(target, 'utf8');
1348
+ const placeholder = fs.readFileSync(path.join(getPackagePath(), 'templates/dot-sdd.yaml'), 'utf8');
1349
+ if (existing === placeholder && specRel) {
1350
+ fs.copyFileSync(target, target + '.pre-mono.bak');
1351
+ existing = `version: 1\nlayout: mono\n`;
1352
+ fs.writeFileSync(target, existing, 'utf8');
1353
+ }
1474
1354
  if (/^layout:\s*mono\s*$/m.test(existing)) {
1475
1355
  if (specRel && !new RegExp(`^spec_path:\\s*${specRel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, 'm').test(existing)) {
1476
1356
  // 已有 mono 但缺/错 spec_path:补写相对路径
@@ -1927,7 +1807,7 @@ async function main() {
1927
1807
  removeOuterSkywalk(workspaceRoot);
1928
1808
 
1929
1809
  deployMonoSddYaml(workspaceRoot, { specPathRelative: specPackage.name });
1930
- // 部署代码仓全部 Git hooks(commit-msg + pre-commit + pre-push)
1810
+ // 部署轻量关联 Hook,并迁移可识别的旧门禁
1931
1811
  // 单仓 mono:hook 通过 .sdd-spec-root 定位包裹包的 skywalk-sdd/git-hooks/
1932
1812
  deployCodeRepoHooks(workspaceRoot, specPackage.abs);
1933
1813
  const linked = sddConfig.setSpecPath(workspaceRoot, specPackage.abs, {
@@ -1947,7 +1827,7 @@ async function main() {
1947
1827
  });
1948
1828
  writeSddSpecRootHint(workspaceRoot, specPackage.abs);
1949
1829
  removeOuterSkywalk(workspaceRoot);
1950
- deployDotSddYaml(workspaceRoot);
1830
+ deployMonoSddYaml(workspaceRoot, { specPathRelative: specPackage.name });
1951
1831
  }
1952
1832
  }
1953
1833
  } else if (!skipOpenspec) {
@@ -0,0 +1,24 @@
1
+ {
2
+ "pre-commit": [
3
+ "497f012da027df5c26af3c501126e9a1c26276b038ee3b1edaaf5ab4401aa95c",
4
+ "7322d9aecafb497a5f40171a6bd7daeaadd7d2056b4a253cb0ee1a128faea36e",
5
+ "8fcdd74a3d2afaa823f9038c477c9dfd6fe53e2bccbe48cecd01535794862e4a",
6
+ "c8546c6c96adc3ecc32a15a689c59189543c2454f47998c46fbd5a0bf810cf30",
7
+ "e3b6776af51df76ff04b528152d0049a6c45d94d96a19bd2bc5ca2cc9786b22c"
8
+ ],
9
+ "pre-push": [
10
+ "405debb5e6f9186e95b727e6013b06ef3e06c598b6d2b96678d34554d3530e8c",
11
+ "461abd16663757c5a092103ac2ecae8453891c0c817d1c763369e0cdd0a8ec7b",
12
+ "80f710ff647ce8b216fb1c081fb3e22f6ee8be25847b6d02921668ba83fb1965",
13
+ "d49eb8794fde4b0d7847a876cc7083d161b290a44990ff8a036c2ebb8a00ec21",
14
+ "e6e125cbf9d7424c01b249ce03197cef9e1923ed2974a53656084488a0de60c4",
15
+ "ebf628481ae93ac0c4c6649b204449f6825291dc6b6550a71563bebec1f3ad92"
16
+ ],
17
+ "commit-msg": [
18
+ "15083d5165bff965ab2feba9a55dfedcecd54cb577c06fd11f4d1b9672d978b6",
19
+ "3b0ee15f5a126d159e14c5a3681cb7820cbf9c5e0c134ee6812053bc34330b85",
20
+ "6db7adbf17905bf14915d7e57a3c5ac46030ee3282fea5d5b211c80065406d48",
21
+ "9c5a86761b36d71b655f804a822eff675a244175799736c803e1f7379c638609",
22
+ "fc390776720393f100712fa02d5e143c2927fbb0d605a4a0242824ff7ea84574"
23
+ ]
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kld-sdd",
3
- "version": "2.7.8-2",
3
+ "version": "2.7.8-3",
4
4
  "description": "KLD SDD OpenSpec 项目初始化工具 - 一键部署 SDD skills",
5
5
  "main": "index.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/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/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
+ "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/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"
13
13
  },
14
14
  "keywords": [
15
15
  "kld",
@@ -8610,7 +8610,7 @@ SDD Telemetry CLI - 流程度量采集工具
8610
8610
  check-task 扫描变更目录 tasks.md 并勾选指定 task_id
8611
8611
  archive-docs 将 Simple/Full spec 变更真实移动到 openspec/changes/archive/,并可结束 archive 阶段生成报告
8612
8612
  kb-upload 知识库上传(文件夹自动打包 / zip 直传),支持随时入库
8613
- semantic-identity 生成或复用实体/版本 ID(8 位十六进制);无需网络和中央 ID 服务
8613
+ semantic-identity 生成或复用实体/版本 ID(完整 UUID,界面可显示短标识);无需网络和中央 ID 服务
8614
8614
  semantic-scan 只读解析并校验当前 Change
8615
8615
  semantic-reconcile 全量对账文件与本地工作态本体实例
8616
8616
  semantic-check 全量对账并在通过时标记 pending;--include-tasks 同时返回任务状态、评审输入清单和程序耗时
@@ -332,6 +332,7 @@ async function main() {
332
332
  try {
333
333
  // ── Zip 模式 ──
334
334
  if (args.package) {
335
+ if (args['validate-only']) throw new Error('--validate-only 仅支持 --folder/--change 本地语义校验;未执行归档上传');
335
336
  const zipPath = path.resolve(args.package);
336
337
  if (!fs.existsSync(zipPath)) {
337
338
  throw new Error(`zip 文件不存在: ${zipPath}`);
@@ -370,9 +371,12 @@ async function main() {
370
371
  changeName = path.basename(folderPath);
371
372
  }
372
373
 
373
- const expectedFolder = path.resolve(specRoot, 'openspec', 'changes', changeName);
374
- if (path.resolve(folderPath) !== expectedFolder || !expectedFolder.startsWith(path.resolve(specRoot, 'openspec', 'changes') + path.sep)) {
375
- throw new Error('文件夹上传仅接受当前 Spec openspec/changes/<change>;已归档内容请用 --zip 指定归档包');
374
+ const changesRoot = fs.realpathSync(path.resolve(specRoot, 'openspec', 'changes'));
375
+ const expectedPath = path.resolve(changesRoot, changeName);
376
+ const expectedFolder = fs.existsSync(expectedPath) ? fs.realpathSync(expectedPath) : expectedPath;
377
+ const suppliedFolder = fs.existsSync(folderPath) ? fs.realpathSync(folderPath) : path.resolve(folderPath);
378
+ if (suppliedFolder !== expectedFolder || !expectedFolder.startsWith(changesRoot + path.sep)) {
379
+ throw new Error('文件夹上传仅接受当前 Spec 仓 openspec/changes/<change>;已归档内容请用 --package 指定归档包');
376
380
  }
377
381
 
378
382
  if (!fs.existsSync(folderPath)) {
@@ -389,19 +393,6 @@ async function main() {
389
393
  // 1. 校验必需文件
390
394
  validateRequiredFiles(folderPath, minimumLevel);
391
395
 
392
- // 2. 解析 project_id
393
- const projectId = resolveProjectId(specRoot);
394
- console.log(`✓ project_id: ${projectId}`);
395
-
396
- // 3. 读取 KB 配置 + 预检
397
- const state = readKbState(specRoot);
398
- if (!state.apiKey) {
399
- throw new Error('KB 配置中无 apiKey,请运行 /opsx-kb-config 配置知识库。');
400
- }
401
- const target = resolveUploadTarget(state, { spaceId: args['space-id'], kbId: args['kb-id'] });
402
- preCheckProjectId(projectId, state, target);
403
- const cacheContext = { api: state.api || 'http://localhost:8090/api', spaceId: target.spaceId, kbId: target.kbId };
404
-
405
396
  // 4. 仅校验模式(含语义校验)
406
397
  if (args['validate-only']) {
407
398
  const runtime = require('./ontology/runtime.cjs');
@@ -417,10 +408,23 @@ async function main() {
417
408
  throw new Error(`语义校验失败:\n${errors.join('\n')}`);
418
409
  }
419
410
  console.log(`✓ 语义校验通过 (profile: ${semanticResult.profile}, revision: ${semanticResult.revision})`);
420
- console.log('\n✓ 校验完成(--validate-only,未上传)');
411
+ console.log('\n✓ 本地语义校验完成(--validate-only,未联网;不代表权限、发布基线或入库已验证)');
421
412
  return;
422
413
  }
423
414
 
415
+ // 2. 解析 project_id
416
+ const projectId = resolveProjectId(specRoot);
417
+ console.log(`✓ project_id: ${projectId}`);
418
+
419
+ // 3. 读取 KB 配置 + 预检
420
+ const state = readKbState(specRoot);
421
+ if (!state.apiKey) {
422
+ throw new Error('KB 配置中无 apiKey,请运行 /opsx-kb-config 配置知识库。');
423
+ }
424
+ const target = resolveUploadTarget(state, { spaceId: args['space-id'], kbId: args['kb-id'] });
425
+ preCheckProjectId(projectId, state, target);
426
+ const cacheContext = { api: state.api || 'http://localhost:8090/api', spaceId: target.spaceId, kbId: target.kbId };
427
+
424
428
  // 5. 构建 Archive Package
425
429
  console.log('');
426
430
  const { zipPath, manifest, stagingDir } = buildPackageFromFolder(specRoot, changeName, {
@@ -17,6 +17,26 @@ function parseCounts(output) {
17
17
  && total === passed + failed + (skipped || 0) + (todo || 0)) {
18
18
  return { counts_known: true, passed, failed, skipped: (skipped || 0) + (todo || 0) };
19
19
  }
20
+ // Surefire/Failsafe print per-class counts and a Results section. Count only
21
+ // the latter, once per module/execution, to avoid double-counting classes.
22
+ const summaries = [];
23
+ let inResults = false;
24
+ for (const raw of output.replace(/\u001b\[[0-9;]*m/g, '').split(/\r?\n/)) {
25
+ const line = raw.replace(/^\[(?:INFO|WARNING|ERROR)\]\s*/, '').trim();
26
+ if (line === 'Results:') { inResults = true; continue; }
27
+ if (!inResults) continue;
28
+ const match = /^Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+),\s*Skipped:\s*(\d+)\b/.exec(line);
29
+ if (!match) continue;
30
+ const [, count, failures, errors, ignored] = match.map(Number);
31
+ if (count < failures + errors + ignored) return { counts_known: false };
32
+ summaries.push({ passed: count - failures - errors - ignored, failed: failures + errors, skipped: ignored });
33
+ inResults = false;
34
+ }
35
+ if (summaries.length) {
36
+ return summaries.reduce((sum, item) => ({ counts_known: true, passed: sum.passed + item.passed,
37
+ failed: sum.failed + item.failed, skipped: sum.skipped + item.skipped }),
38
+ { counts_known: true, passed: 0, failed: 0, skipped: 0 });
39
+ }
20
40
  return { counts_known: false };
21
41
  }
22
42