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.
@@ -0,0 +1,181 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+ const { execFileSync } = require('child_process');
7
+
8
+ const RULE_VERSION = 'sdd-delivery-v1';
9
+ const digest = value => crypto.createHash('sha256').update(value).digest('hex');
10
+ const hashJson = value => digest(JSON.stringify(value));
11
+ const slash = value => value.replace(/\\/g, '/');
12
+
13
+ // Git exports these during hooks. A different cwd alone does not switch repositories.
14
+ function gitEnv() {
15
+ const env = { ...process.env };
16
+ for (const key of Object.keys(env)) {
17
+ if (/^GIT_(DIR|WORK_TREE|INDEX_FILE|COMMON_DIR|OBJECT_DIRECTORY|ALTERNATE_OBJECT_DIRECTORIES|PREFIX)$/.test(key)) delete env[key];
18
+ }
19
+ return env;
20
+ }
21
+
22
+ function git(root, args, options = {}) {
23
+ return execFileSync('git', args, {
24
+ cwd: root, env: { ...gitEnv(), ...(fs.realpathSync(root) === fs.realpathSync(process.cwd()) && process.env.GIT_INDEX_FILE ? { GIT_INDEX_FILE: path.resolve(process.env.GIT_INDEX_FILE) } : {}) }, encoding: 'utf8', timeout: 10000,
25
+ maxBuffer: 64 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], ...options,
26
+ });
27
+ }
28
+
29
+ function repoIdentity(root) {
30
+ let remote = '';
31
+ try { remote = git(root, ['remote', 'get-url', 'origin']).trim(); } catch { /* local repo */ }
32
+ // Do not persist credentials from a remote URL.
33
+ remote = remote.replace(/(https?:\/\/)[^/@]+@/i, '$1').replace(/\.git$/, '').replace(/\/$/, '');
34
+ remote = remote.replace(/^git@([^:]+):/, '$1/').replace(/^https?:\/\//i, '');
35
+ const common = git(root, ['rev-parse', '--git-common-dir']).trim();
36
+ return remote || `local:${fs.realpathSync(path.resolve(root, common))}`;
37
+ }
38
+
39
+ function entriesAt(root, scope = 'worktree', ref = 'HEAD') {
40
+ if (scope === 'commit') {
41
+ const sha = git(root, ['rev-parse', '--verify', '--end-of-options', `${ref}^{commit}`]).trim();
42
+ return git(root, ['ls-tree', '-r', '-z', '--full-tree', sha]).split('\0').filter(Boolean).map(row => {
43
+ const tab = row.indexOf('\t'), [mode, type, oid] = row.slice(0, tab).split(' ');
44
+ return { path: row.slice(tab + 1), mode, oid, type };
45
+ });
46
+ }
47
+ const entries = git(root, ['ls-files', '--stage', '-z']).split('\0').filter(Boolean).map(row => {
48
+ const tab = row.indexOf('\t'), [mode, oid, stage] = row.slice(0, tab).split(' ');
49
+ if (stage !== '0') throw new Error('UNMERGED_INDEX: 请先解决 Git 冲突');
50
+ return { path: row.slice(tab + 1), mode, oid, type: mode === '160000' ? 'commit' : 'blob' };
51
+ });
52
+ if (scope === 'worktree') {
53
+ for (const file of git(root, ['ls-files', '--others', '--exclude-standard', '-z']).split('\0').filter(Boolean)) {
54
+ entries.push({ path: file, mode: '100644', type: 'blob' });
55
+ }
56
+ }
57
+ return entries;
58
+ }
59
+
60
+ function readBlobs(root, entries) {
61
+ const blobs = entries.filter(e => e.type === 'blob');
62
+ const result = new Map();
63
+ if (!blobs.length) return result;
64
+ const out = git(root, ['cat-file', '--batch'], { input: blobs.map(e => e.oid).join('\n') + '\n', encoding: null });
65
+ let offset = 0;
66
+ for (const entry of blobs) {
67
+ const end = out.indexOf(10, offset);
68
+ const header = out.subarray(offset, end).toString('ascii').split(' ');
69
+ if (end < 0 || header[1] !== 'blob' || !/^\d+$/.test(header[2])) throw new Error('GIT_BLOB_UNREADABLE');
70
+ const size = Number(header[2]);
71
+ offset = end + 1;
72
+ if (offset + size >= out.length) throw new Error('GIT_BLOB_TRUNCATED');
73
+ result.set(entry.path, out.subarray(offset, offset + size));
74
+ offset += size + 1;
75
+ }
76
+ return result;
77
+ }
78
+
79
+ function fileMeta(file, content, mode = '100644') {
80
+ // Git autocrlf changes text line endings between the worktree and index.
81
+ // Normalize valid UTF-8 text only; binary bytes and symlink targets stay exact.
82
+ const text = content.toString('utf8');
83
+ const isText = mode !== '120000' && !content.includes(0) && Buffer.from(text, 'utf8').equals(content);
84
+ const bytes = isText ? Buffer.from(text.replace(/\r\n/g, '\n'), 'utf8') : content;
85
+ return { path: file, mode, normalization: isText ? 'utf8-lf' : 'bytes', size: bytes.length, sha256: digest(bytes) };
86
+ }
87
+
88
+ function excluded(file, specRelative) {
89
+ if (specRelative && specRelative !== '.' && (file === specRelative || file.startsWith(specRelative + '/'))) return true;
90
+ return /(^|\/)(\.git|node_modules|target|dist|build|coverage|skywalk-sdd)(\/|$)/.test(file)
91
+ || /(^|\/)(consistency-report(?:-result)?\.(md|json)|consistency-reviews)(\/|$)/.test(file)
92
+ || /^(openspec|\.claude|\.cursor|\.codex|\.agents|\.qoder)\//.test(file);
93
+ }
94
+
95
+ function codeSnapshot(root, specRoot, scope = 'worktree', ref = 'HEAD') {
96
+ if (!['worktree', 'index', 'commit'].includes(scope)) throw new Error('SCOPE_INVALID');
97
+ const relative = slash(path.relative(root, specRoot));
98
+ const entries = entriesAt(root, scope, ref).filter(e => !excluded(e.path, relative.startsWith('..') ? '' : relative));
99
+ const blobs = scope === 'worktree' ? null : readBlobs(root, entries);
100
+ let trustFileMode = true;
101
+ try { trustFileMode = git(root, ['config', '--bool', 'core.filemode']).trim() !== 'false'; } catch { /* Git default */ }
102
+ const files = [];
103
+ for (const entry of entries) {
104
+ if (entry.type === 'commit') {
105
+ // Submodules require a separate delivery check in that repository.
106
+ files.push({ path: entry.path, mode: entry.mode, gitlink: entry.oid });
107
+ continue;
108
+ }
109
+ let content, mode = entry.mode;
110
+ if (scope === 'worktree') {
111
+ const fp = path.join(root, entry.path);
112
+ if (!fs.existsSync(fp) && !fs.lstatSync(fp, { throwIfNoEntry: false })) continue;
113
+ const stat = fs.lstatSync(fp);
114
+ if (stat.isSymbolicLink()) { content = Buffer.from(fs.readlinkSync(fp)); mode = '120000'; }
115
+ else { content = fs.readFileSync(fp); mode = trustFileMode ? (stat.mode & 0o111 ? '100755' : '100644') : entry.mode; }
116
+ } else content = blobs.get(entry.path);
117
+ if (!content) throw new Error(`UNREADABLE: ${entry.path}`);
118
+ files.push(fileMeta(entry.path, content, mode));
119
+ }
120
+ return files.sort((a, b) => a.path.localeCompare(b.path, 'en'));
121
+ }
122
+
123
+ function isSpecInput(file, change) {
124
+ if (file.startsWith(`openspec/changes/${change}/logs/`) || file.startsWith(`openspec/changes/${change}/reports/`)) return false;
125
+ if (/(^|\/)(consistency-report(?:-result)?\.(json|md)|consistency-reviews)(\/|$)/.test(file)) return false;
126
+ return file === 'modules.yaml' || file === 'sdd.config.yaml' || file === 'openspec/config.yaml'
127
+ || file.startsWith('openspec/specs/') || file.startsWith(`openspec/changes/${change}/`);
128
+ }
129
+
130
+ function specSnapshot(specRoot, change, options = {}) {
131
+ if (!/^[a-z0-9][a-z0-9-]+$/.test(change)) throw new Error('CHANGE_REQUIRED: 必须明确指定 change');
132
+ const files = [];
133
+ if (options.scope === 'commit' || options.scope === 'index') {
134
+ const root = options.root || specRoot;
135
+ const prefix = slash(path.relative(root, specRoot));
136
+ const start = prefix ? prefix + '/' : '';
137
+ const entries = entriesAt(root, options.scope, options.ref).filter(e => e.path.startsWith(start) && isSpecInput(e.path.slice(start.length), change));
138
+ const blobs = readBlobs(root, entries);
139
+ for (const entry of entries) {
140
+ if (entry.mode === '120000' || entry.type !== 'blob') throw new Error('SPEC_INPUT_SYMLINK_OR_SUBMODULE');
141
+ files.push(fileMeta(entry.path.slice(start.length), blobs.get(entry.path), entry.mode));
142
+ }
143
+ } else {
144
+ const walk = (relative) => {
145
+ const fp = path.join(specRoot, relative);
146
+ if (!fs.existsSync(fp)) return;
147
+ const stat = fs.lstatSync(fp);
148
+ if (stat.isSymbolicLink()) throw new Error(`SPEC_INPUT_SYMLINK: ${relative}`);
149
+ if (stat.isDirectory()) {
150
+ for (const name of fs.readdirSync(fp)) walk(relative ? `${relative}/${name}` : name);
151
+ } else if (isSpecInput(relative, change)) files.push(fileMeta(relative, fs.readFileSync(fp)));
152
+ };
153
+ for (const relative of ['modules.yaml', 'sdd.config.yaml', 'openspec/config.yaml', 'openspec/specs', `openspec/changes/${change}`]) walk(relative);
154
+ }
155
+ if (!files.some(f => f.path === `openspec/changes/${change}/proposal.md`)) throw new Error(`CHANGE_MISSING: ${change}`);
156
+ files.sort((a, b) => a.path.localeCompare(b.path, 'en'));
157
+ return { files, sha256: hashJson(files) };
158
+ }
159
+
160
+ function changedFiles(root, scope, base, ref = 'HEAD') {
161
+ const args = ['diff', '--name-only', '--no-renames', '-z'];
162
+ if (scope === 'index') args.push('--cached');
163
+ if (base) args.push(base);
164
+ if (scope === 'commit') {
165
+ if (!base) throw new Error('BASE_REQUIRED: 提交校验必须明确提供比较基线');
166
+ args.push(ref);
167
+ }
168
+ args.push('--');
169
+ const result = git(root, args).split('\0').filter(Boolean);
170
+ if (scope === 'worktree') result.push(...git(root, ['ls-files', '--others', '--exclude-standard', '-z']).split('\0').filter(Boolean));
171
+ return [...new Set(result)].sort();
172
+ }
173
+
174
+ function atomicJson(file, value) {
175
+ fs.mkdirSync(path.dirname(file), { recursive: true });
176
+ const temp = `${file}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
177
+ fs.writeFileSync(temp, JSON.stringify(value, null, 2) + '\n');
178
+ fs.renameSync(temp, file);
179
+ }
180
+
181
+ module.exports = { RULE_VERSION, digest, hashJson, git, gitEnv, repoIdentity, entriesAt, readBlobs, fileMeta, codeSnapshot, specSnapshot, changedFiles, atomicJson, excluded };
@@ -1,3 +1,5 @@
1
+ # 默认仅安装 commit-msg;pre-commit/pre-push 段仅供已有显式集成使用。
2
+ # Apply 自动运行 delivery-check;安装不会替团队开启 CI 门禁。
1
3
  # KLD SDD Hook 插件配置文件
2
4
  # 每行一个脚本名,空行和 # 开头的行会被忽略
3
5
  # 注释某行 = 禁用该插件,取消注释 = 启用该插件
@@ -9,13 +11,13 @@ commit-msg-sdd-trailer.cjs
9
11
  [pre-commit]
10
12
  # 检查 tasks.md 中是否有未完成的归档任务
11
13
  pre-commit-sdd-check.cjs
12
- # 检查 Spec 一致性校验报告(置信度、过期、人工确认等)
14
+ # 检查 Spec 一致性校验报告(输入指纹、覆盖与实际检查结果)
13
15
  pre-commit-consistency-check.cjs
14
16
 
15
17
  [pre-push]
16
18
  # 运行 doctor 完整诊断(配置、Trailer 协议、活动变更等)
17
19
  pre-push-sdd-check.cjs
18
- # 检查待推送范围的一致性报告(存在性、过期;置信度由 pre-commit 负责)
20
+ # 检查待推送范围的一致性报告(实际 push refs;需显式 Change 和真实测试回执)
19
21
  pre-push-consistency-check.cjs
20
22
 
21
23
  [consistency]
@@ -24,15 +26,15 @@ pre-push-consistency-check.cjs
24
26
  # Go / Kotlin 等项目按需修改:
25
27
  # 注意:新增语言的语言特定构建文件(如 Rust 的 build.rs)不在内置排除规则内,需自行确认
26
28
  # 本配置同时被 git hook(isBusinessCode)与 opsx-consistency-check skill 读取,单一来源
27
- # ⚠️ 清空本行 = 不拦截任何业务代码文件(一致性门禁对提交/推送失效,仅剩报告存在性/过期检查)
29
+ # 清空本行时严格交付检查会失败,不能将范围未知视为通过。
28
30
  business_code_exts = .java,.ts,.tsx,.js,.jsx,.vue,.py
29
31
 
30
32
  # 一致性校验模式(项目级默认值,环境变量优先覆盖;三个互斥,只能启用一个)
31
33
  # 环境变量:SDD_SKIP_CONSISTENCY_CHECK=1 / SDD_STRICT_CONSISTENCY=1 / SDD_WARN_ONLY=1
32
34
  # - skip = 1:跳过一致性校验(紧急绕过,如 hotfix 仓不需要 spec 校验)
33
- # - strict = 1:严格模式,medium 置信度也阻止提交(仅 pre-commit 生效,pre-push 忽略)
35
+ # - strict = 1:保留旧项目显式门禁选择;交付检查始终要求完整事实证据
34
36
  # - warn_only = 1:仅告警不拦截(CI 调试期,问题全列但不阻断提交/推送)
35
- # 三项均不设或均设为 0 = 默认模式(low 阻止,medium 放行,high 放行)
37
+ # 三项均不设或均设为 0 = 检查回执、输入指纹与覆盖;不按置信度放行
36
38
  skip = 0
37
39
  strict = 0
38
- warn_only = 0
40
+ warn_only = 0
@@ -1,45 +1,27 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * pre-commit-consistency-check.cjs
4
- *
5
- * Pre-commit 门禁:检查本次提交 diff 涉及的 Spec 一致性校验报告。
6
- * - diff 驱动:仅检查 diff 文件与报告 coveredFiles 有交集的 change,无关 change 跳过
7
- * - 报告存在性:活跃 change 无报告 → 阻止提交
8
- * - 报告过期:代码在报告生成后被修改 → 阻止提交
9
- * - 置信度门禁:confidence=low → 阻止提交;strict 模式下 medium 也阻止
10
- *
11
- * 通过环境变量控制:
12
- * - SDD_SKIP_CONSISTENCY_CHECK=1 → 跳过校验
13
- * - SDD_STRICT_CONSISTENCY=1 → 严格模式(medium 也阻止)
14
- * - SDD_WARN_ONLY=1 → 仅告警不拦截
15
- * - 注意:三个环境变量互斥,只能启用一个
16
- *
17
- * 共享逻辑见同目录 consistency-check-core.cjs。
2
+ /** Optional strict index verification. Not installed on daily commits by default.
3
+ * Checks complete coverage, input fingerprints and actual review dimensions.
4
+ * SDD_SKIP_CONSISTENCY_CHECK / SDD_WARN_ONLY remain explicit legacy overrides.
18
5
  */
19
6
  'use strict';
20
7
 
21
8
  const core = require('./consistency-check-core.cjs');
22
- const { gitDiffNameStatus } = core;
23
9
 
24
- /**
25
- * 单仓 diff 采集:工作区相对 HEAD 的变更(含 staged 与未 staged)。
26
- * files=修改/新增(排除删除);deleted=删除文件。
27
- * 空仓(无任何 commit)时 HEAD 不存在:降级用 staged 文件列表。
28
- * @returns {{files: string[], deleted: string[], ok: boolean}} ok=false 表示无法确定变更范围
29
- */
10
+ /** Inspect the actual staged content, including an unborn repository and deletions. */
30
11
  function gitDiffHead(repoRoot) {
31
- const bases = [['diff', 'HEAD'], ['diff', '--cached']];
32
- for (const base of bases) {
33
- const r = gitDiffNameStatus(repoRoot, base);
34
- if (r.ok) return r;
35
- }
36
- return { files: [], deleted: [], ok: false };
12
+ try {
13
+ const snap = require('./delivery-snapshot.cjs');
14
+ const changed = snap.changedFiles(repoRoot, 'index');
15
+ const indexed = new Set(snap.entriesAt(repoRoot, 'index').map(e => e.path));
16
+ return { files: changed.filter(f => indexed.has(f)), deleted: changed.filter(f => !indexed.has(f)), ok: true };
17
+ } catch { return { files: [], deleted: [], ok: false }; }
37
18
  }
38
19
 
39
20
  core.runConsistencyCheck({
40
21
  label: '[SDD commit-consistency-check]',
41
22
  actionNoun: '提交',
42
23
  diffFn: gitDiffHead,
43
- checkConfidence: true, // 置信度校验由 pre-commit 完成
44
- includeUntracked: true, // 新文件将随本次提交入库,需检查
24
+ checkConfidence: true, // Legacy option; factual review dimensions now decide readiness.
25
+ includeUntracked: false, // Only the index is part of this commit.
26
+ staleFromIndex: true,
45
27
  });
@@ -1,88 +1,41 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * pre-push-consistency-check.cjs
4
- *
5
- * Pre-push 轻量门禁:仅检查待推送范围涉及报告的存在性、是否过期。
6
- * - diff 驱动:仅检查待推送文件与报告 coveredFiles 有交集的 change,无关 change 跳过
7
- * - 完整的置信度校验由 pre-commit 完成,push 阶段不再重复。
8
- * - 过期判断读取 HEAD 已提交内容(而非工作区),避免工作区未提交修改造成误判。
9
- *
10
- * 待推送范围采集(逐级降级):
11
- * 1. git diff @{push}..HEAD(本地领先远程的提交)
12
- * 2. git diff origin/HEAD..HEAD
13
- * 3. 远程引用不存在(新分支/新仓库首次推送):git diff <空树>..HEAD,
14
- * 语义 = 首推时 HEAD 全部文件都是新增。空树 SHA 是 git 内置常量,
15
- * 不依赖任何远程引用,避免「首推因无远程引用被拦 → 引用永远建立不起来」的死锁
16
- * 4. 连空树 diff 都失败(如空仓无 HEAD):返回 ok=false,由 core 拦截推送而非静默放行
17
- *
18
- * 通过环境变量控制:
19
- * - SDD_SKIP_CONSISTENCY_CHECK=1 → 跳过校验
20
- * - SDD_WARN_ONLY=1 → 仅告警不拦截
21
- * - 注意:两个环境变量互斥,只能启用一个
22
- *
23
- * 共享逻辑见同目录 consistency-check-core.cjs。
24
- */
25
2
  'use strict';
26
-
27
- const core = require('./consistency-check-core.cjs');
28
- const { gitDiffNameStatus } = core;
29
- const { execFileSync } = require('child_process');
30
-
31
- // git 内置空树对象 SHA(SHA-1 仓库恒定不变)
32
- const EMPTY_TREE_SHA = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
33
-
34
- /**
35
- * 单仓待推送范围采集:本地领先远程的提交所涉及的文件。
36
- * files=修改/新增(排除删除);deleted=删除文件。
37
- * @returns {{files: string[], deleted: string[], ok: boolean}}
38
- */
39
- function gitPushRangeDiff(repoRoot) {
40
- const ranges = ['@{push}..HEAD', 'origin/HEAD..HEAD'];
41
- for (const range of ranges) {
42
- const r = gitDiffNameStatus(repoRoot, ['diff', range]);
43
- if (r.ok) return r;
44
- }
45
- // 首推降级:远程引用不存在(@{push}/origin/HEAD 均无法解析)时,
46
- // 用 git 内置空树 SHA 做 diff —— 语义为「首推 = HEAD 全部文件都是新增」。
47
- // 空树对象不依赖任何远程引用,打破「首推被拦 → 远程引用永远建立不起来」的死锁。
48
- const emptyTree = gitEmptyTreeSha(repoRoot);
49
- if (emptyTree) {
50
- const r = gitDiffNameStatus(repoRoot, ['diff', `${emptyTree}..HEAD`]);
51
- if (r.ok) return r;
52
- }
53
- // 连空树 diff 都失败(如空仓无 HEAD):返回失败,由 core 拦截而非静默放行
54
- return { files: [], deleted: [], ok: false };
3
+ // Optional legacy integration. New installs do not validate on every push.
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const snap = require('./delivery-snapshot.cjs');
7
+ const delivery = require('./delivery-check.cjs');
8
+ const mode = require('./consistency-check-core.cjs').loadConsistencyMode();
9
+ const skip = process.env.SDD_SKIP_CONSISTENCY_CHECK === undefined ? mode.skip : process.env.SDD_SKIP_CONSISTENCY_CHECK === '1';
10
+ const warnOnly = process.env.SDD_WARN_ONLY === undefined ? mode.warnOnly : process.env.SDD_WARN_ONLY === '1';
11
+ const strict = process.env.SDD_STRICT_CONSISTENCY === undefined ? mode.strict : process.env.SDD_STRICT_CONSISTENCY === '1';
12
+ if ([skip, warnOnly, strict].filter(Boolean).length > 1) {
13
+ console.error('[SDD] skip / warn_only / strict 互斥'); process.exit(1);
55
14
  }
56
-
57
- /**
58
- * 读取 git 内置空树 SHA(4b825dc642cb6eb9a060e54bf8d69288fbee4904)。
59
- * 优先用 git mktree 动态计算(兼容 SHA-256 仓库),失败时回退内置常量。
60
- * @param {string} repoRoot
61
- * @returns {string|null}
62
- */
63
- function gitEmptyTreeSha(repoRoot) {
64
- try {
65
- const sha = execFileSync('git', ['mktree'], {
66
- cwd: repoRoot, encoding: 'utf8', input: '', stdio: ['pipe', 'pipe', 'ignore'],
67
- }).trim();
68
- if (/^[0-9a-f]{40,64}$/.test(sha)) return sha;
69
- } catch {
70
- // 降级到内置常量
15
+ if (skip) { console.log('[SDD] 按显式配置跳过推送检查(未验证)'); process.exit(0); }
16
+ try {
17
+ const project = path.resolve(process.argv.find(a => a.startsWith('--project='))?.slice(10) || process.cwd());
18
+ const specRoot = path.resolve(__dirname, '..', '..');
19
+ const changes = String(process.env.KLD_SDD_CHANGE || '').split(/[,\s]+/).filter(Boolean);
20
+ const refs = (process.env.SDD_PRE_PUSH_REFS || (process.stdin.isTTY ? '' : fs.readFileSync(0, 'utf8'))).replace(/\\n/g, '\n').trim();
21
+ if (!refs) throw new Error('PUSH_REFS_REQUIRED: 需要真实 pre-push refs;手动验证请运行 delivery-check verify');
22
+ for (const line of refs.split(/\r?\n/)) {
23
+ const fields = line.trim().split(/\s+/);
24
+ if (fields.length !== 4 || fields.some((f, i) => i % 2 === 1 && !/^[a-f0-9]{40}$|^[a-f0-9]{64}$/.test(f))) throw new Error('PUSH_REFS_INVALID');
25
+ const [, localSha, , remoteSha] = fields;
26
+ if (/^0+$/.test(localSha)) continue;
27
+ if (!changes.length) throw new Error('CHANGE_REQUIRED: 严格推送检查需要 KLD_SDD_CHANGE 明确关联');
28
+ const base = /^0+$/.test(remoteSha) ? snap.git(project, ['mktree'], { input: '' }).trim() : remoteSha;
29
+ const revisions = snap.git(project, ['show', '-s', '--format=%(trailers:key=Spec-Revision,valueonly)', localSha]).trim().split(/\s+/).filter(Boolean);
30
+ for (const change of changes) {
31
+ const result = delivery.verify({ project, 'spec-root': specRoot, change, scope: 'commit', ref: localSha, base,
32
+ 'spec-ref': revisions.length === 1 ? revisions[0] : undefined });
33
+ if (!result.ok) throw new Error(result.errors.join('; '));
34
+ }
71
35
  }
72
- return /^[0-9a-f]{40}$/.test(EMPTY_TREE_SHA) ? EMPTY_TREE_SHA : null;
73
- }
74
-
75
- // strict 模式仅 pre-commit 支持:显式提示而非静默忽略
76
- const _mode = core.loadConsistencyMode();
77
- if (process.env.SDD_STRICT_CONSISTENCY === '1' || _mode.strict) {
78
- console.log('[SDD push-consistency-check] ℹ️ SDD_STRICT_CONSISTENCY 仅在 pre-commit 阶段生效,push 阶段忽略');
36
+ console.log('[SDD] 指定推送提交的交付证据验证通过');
37
+ } catch (error) {
38
+ console.error(`[SDD] ${error.message}`);
39
+ if (warnOnly) console.error('[SDD] 仅告警模式,推送继续(交付仍未验证)');
40
+ process.exitCode = warnOnly ? 0 : 1;
79
41
  }
80
-
81
- core.runConsistencyCheck({
82
- label: '[SDD push-consistency-check]',
83
- actionNoun: '推送',
84
- diffFn: gitPushRangeDiff,
85
- checkConfidence: false, // 置信度校验由 pre-commit 完成,push 阶段不重复
86
- includeUntracked: false, // untracked 文件不在任何 commit 中,不会被推送
87
- staleFromHead: true, // 过期判断读 HEAD 已提交内容,避免工作区未提交修改误判
88
- });
@@ -7,7 +7,7 @@ module: "<module>" # modules.yaml 中的模块代号;跨模块用 cross
7
7
  # affected-modules: # 仅 module=cross 时必填,至少两个已注册模块
8
8
  # - fi
9
9
  # - mm
10
- entity-id: "<8位十六进制ID>" # Change 的全局逻辑实体 ID,added 时生成
10
+ entity-id: "<完整UUID>" # Change 的全局逻辑实体 ID,added 时生成
11
11
  version-id: "<UUID>" # 本次 Change 版本 UUID
12
12
  delta-state: "added"
13
13
  predecessor-version: "" # added 留空;modified/removed 指向直接前序版本
@@ -293,33 +293,35 @@ g. **继续下一个层级** — 重新检查 DAG,找出依赖已满足的下
293
293
 
294
294
  ### 6. 完成或暂停时显示状态
295
295
 
296
- 展示 DAG 执行摘要,引导下一步:
297
- > "📊 **实施进度摘要**:
298
- > - 已完成:[N/M] 任务
299
- > - 当前层级:Layer [X]
300
- > - 下一步建议:[继续实施 / 运行测试 / 归档]"
301
-
302
- **【已授权连续处理完成】**
303
- 若本次为用户已授权的连续处理,完成时展示一次整体交付结果:
304
- > "✅ **本轮连续处理完成**
305
- > - propose → spec → design → task → check → apply 已全部执行完毕
306
- > - 各阶段文档已生成,质量门禁已执行
307
- > - 建议下一步:运行测试 / 归档变更"
308
-
309
- > **💡 代码评审提示**:
310
- > 建议执行 `/kld-review --local` 对本次变更的代码进行评审。
311
- > 该技能可在公司内部 SkillHub 下载安装。
312
- >
313
- > **🧹 Worktree 收尾**:若本次使用了 worktree,见 **§6.1**;若判定未建 worktree,跳过 §6.1。
314
- > **其他 capability**:若并行 apply 中仍有未 finish 的 worktree,提示按 proposal 依赖顺序继续 `finish`。
296
+ 先执行下面的收尾验证,再展示实施摘要。验证未完成时显示「代码已保存,交付待验证」并列出缺口,不展示「全部完成」或把测试留给用户手动执行。
297
+
298
+ ### 6.0 实施收尾:自动检查、测试与就绪判定
299
+
300
+ 此节由 Agent 在当前授权范围内连续执行,无需用户串联多个 Skills。`opsx-check` 的文档结果不替代代码检查。
301
+
302
+ 1. 整理任务状态和最终 Spec 后,调用 `opsx-consistency-check`,明确本次 Change、代码仓以及实施前基线。回执有效则复用;失效时定向重查。多仓只检查本次涉及的仓,每仓独立保留证据。
303
+ 2. 使用下列入口执行最终回归测试;它内部调用原 `test-run`,保留严格测试事件、原始日志和任务关联,不重复手写相同测试事件。已有任务级 RED/GREEN 证据继续保留。命令必须是真实测试或项目认可的验证命令,不能用 `echo`、空测试冒充验证。
304
+
305
+ ```bash
306
+ node "<spec-root>/skywalk-sdd/git-hooks/delivery-check.cjs" test --project="<代码仓>" --change=<change> --session-id=<会话ID> --task-ids=<任务ID列表> --command-file="<测试命令JSON数组文件>"
307
+ ```
308
+
309
+ 命令文件放在 prepare 输出的状态目录,内容如 `["node","--test","test/order.test.cjs"]`。脚本取证前后核对输入;测试修改代码或 Spec 时,本次回执失效。详细 RED/GREEN 与任务完成证据仍见 `./reference.md`「§6.0」和 `./checklist.md`。
310
+ 3. 执行就绪判断,失败就修复并补足对应证据;正常 Git 保存和本地编辑仍可继续。
311
+
312
+ ```bash
313
+ node "<spec-root>/skywalk-sdd/git-hooks/delivery-check.cjs" verify --project="<代码仓>" --change=<change>
314
+ ```
315
+
316
+ `status=ready` 只表示该快照的本地检查和测试证据完整。不得把它表述为已入库、已发布或已合并。只有本次无业务代码变化、四个检查维度均有依据判为不适用,且检查回执显式写入 `tests: {"status":"na","reason":"具体理由"}` 时,工具才允许省略测试并显示 `testStatus=not_applicable`。其余缺少可执行验证命令的项目标记待验证,不凭模型文字造测试。`test-strategy=none` 免除任务级 TDD,不自动证明交付验证通过。
315
317
 
316
- ### 6.0 【条件必做】单元测试真实执行(`test-strategy` 非 `none`)
318
+ 收尾合并或提交后,若需验证最终提交,运行 `verify --scope=commit --ref=<目标SHA> --base=<实施前SHA>`;多仓再提供 `--spec-ref=<实际验证的完整Spec提交SHA>`。工具读取指定 Git 对象,不读取别的 HEAD 或工作区替代。冲突修复改变内容时重新检查和测试。
317
319
 
318
- 当 `proposal.md` 的 `test-strategy` 为 **`tdd`** 或 **`impl-first`** 时,在结束 apply 或执行 §6.1 收尾之前,必须真实运行单元测试并留 telemetry 证据。**详细执行要求与证据形式见 `./reference.md`「§6.0」+ `./checklist.md`「§6.0 单元测试真实执行自检」**。${HOOK_GATE_DESCRIPTION}
320
+ 所有涉及仓验证完成后,才输出整体结果:完成范围、真实验证结果、知识发布状态和剩余问题。现有 start/end、final_output_snapshot、使用上报照常执行。没有自动启用 CI;团队可将同一严格命令接入实际合并流程,并提供对应回执和测试原始日志。
319
321
 
320
322
  ### 6.1 【条件必做】Worktree 收尾(仅当 §1.5 已创建 worktree)
321
323
 
322
- > **⛔ 本次 Apply 若创建了 worktree**:DAG 全部完成且 §6.0 测试门禁(若适用)通过后,必须在**主仓库根目录**执行收尾脚本(禁止在 `.worktrees/...` 内执行)。
324
+ > **⛔ 本次 Apply 若创建了 worktree**:DAG 全部完成且 §6.0 交付验证通过后,必须在**主仓库根目录**执行收尾脚本(禁止在 `.worktrees/...` 内执行)。
323
325
  > 若 §1.5 判定未建 worktree(串行、强依赖、沙箱回退),跳过本节。
324
326
 
325
327
  **前置条件、执行命令、脚本行为、常用 flags、回退方式见 `./reference.md`「§6.1 Worktree 收尾」+ `./checklist.md`「§6.1 Worktree 收尾前置条件自检」**。