kld-sdd 2.7.8 → 2.8.0
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 +26 -0
- package/USABILITY.md +72 -0
- package/bin/kld-sdd-init.js +718 -34
- package/lib/command-bridge.js +26 -17
- package/lib/deploy-codebuddy-hooks.js +19 -26
- package/lib/deploy-strategy.js +293 -0
- package/lib/hook-gate-core.js +7 -8
- package/lib/hook-install.js +75 -0
- package/lib/init.js +507 -726
- package/lib/maintenance.js +221 -0
- package/lib/managed-hook-hashes.json +24 -0
- package/lib/settings-merge.js +102 -25
- package/lib/skills-bundle.js +5 -5
- package/lib/spec-choice.js +85 -0
- package/lib/tool-profiles.js +2 -2
- package/lib/uninstall.js +453 -0
- package/package.json +7 -4
- package/skywalk-sdd/context-client.cjs +38 -7
- package/skywalk-sdd/index.cjs +153 -27
- package/skywalk-sdd/kb-upload.cjs +224 -82
- package/skywalk-sdd/lib/check-review.cjs +41 -0
- package/skywalk-sdd/lib/shared.cjs +62 -41
- package/skywalk-sdd/lib/test-execution.cjs +130 -0
- package/skywalk-sdd/lib/usage-reporter.cjs +17 -8
- package/skywalk-sdd/ontology/archive-package.cjs +141 -0
- package/skywalk-sdd/ontology/artifact-parser.cjs +24 -3
- package/skywalk-sdd/ontology/cli.cjs +53 -0
- package/skywalk-sdd/ontology/id.cjs +1 -1
- package/skywalk-sdd/ontology/identity-index.cjs +5 -1
- package/skywalk-sdd/ontology/resolve-spec-root.cjs +49 -0
- package/skywalk-sdd/ontology/schema.cjs +6 -0
- package/skywalk-sdd/ontology/sdd-config.cjs +33 -5
- package/skywalk-sdd/ontology/spec-detect.cjs +421 -0
- package/skywalk-sdd/ontology/spec-ontology.cjs +507 -0
- package/skywalk-sdd/ontology/workspace-layout.cjs +65 -17
- package/templates/git-hooks/commit-msg +17 -85
- package/templates/git-hooks/commit-msg-runner.cjs +44 -0
- package/templates/git-hooks/commit-msg-sdd-trailer.cjs +60 -69
- package/templates/git-hooks/consistency-check-core.cjs +92 -261
- package/templates/git-hooks/delivery-check.cjs +231 -0
- package/templates/git-hooks/delivery-snapshot.cjs +181 -0
- package/templates/git-hooks/hooks.config +8 -6
- package/templates/git-hooks/pre-commit +19 -5
- package/templates/git-hooks/pre-commit-consistency-check.cjs +13 -31
- package/templates/git-hooks/pre-push +19 -5
- package/templates/git-hooks/pre-push-consistency-check.cjs +36 -83
- package/templates/hooks/claude/hooks/sdd-post-tool.cjs +2 -2
- package/templates/hooks/codebuddy/hooks/hook-gate-core.cjs +7 -8
- package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +2 -2
- package/templates/openspec/proposal.md +1 -1
- package/templates/openspec/spec.md +8 -0
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +28 -24
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +12 -3
- package/templates/skills/kld-sdd/opsx-apply/reference.md +21 -1
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +20 -18
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +18 -13
- package/templates/skills/kld-sdd/opsx-check/reference.md +3 -1
- package/templates/skills/kld-sdd/opsx-check/review-template.json +22 -0
- package/templates/skills/kld-sdd/opsx-check/reviewer.md +27 -6
- package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +34 -112
- package/templates/skills/kld-sdd/opsx-consistency-check/scripts/scripts.cjs +15 -14
- package/templates/skills/kld-sdd/opsx-kb-config/SKILL.md +82 -61
- package/templates/skills/kld-sdd/opsx-kb-config/reference.md +8 -11
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +3 -2
- package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +2 -2
- package/templates/skills/kld-sdd/opsx-ontology-ingest/SKILL.md +77 -0
- package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +5 -4
- package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +14 -1
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-propose/interaction-policy.md +9 -0
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +5 -3
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +2 -2
- package/index.js +0 -13
|
@@ -1,26 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* 两个入口脚本通过 require 引用本模块,注入各自差异配置:
|
|
9
|
-
* - diff 采集方式(pre-commit: git diff HEAD;pre-push: 待推送范围 @{push}..HEAD)
|
|
10
|
-
* - 是否检查置信度(pre-commit 检查;pre-push 轻量跳过)
|
|
11
|
-
* - strict 模式(仅 pre-commit 支持 SDD_STRICT_CONSISTENCY)
|
|
12
|
-
* - 过期比对来源(pre-commit: 工作区内容;pre-push: HEAD 已提交内容,staleFromHead)
|
|
13
|
-
* - 提示文案(提交 / 推送)
|
|
14
|
-
*
|
|
15
|
-
* 核心行为(简化版):
|
|
16
|
-
* - diff 驱动:仅检查 diff 文件与报告 coveredFiles 有交集的 change,无关 change 跳过
|
|
17
|
-
* - 多仓归属过滤:报告 repositories 已声明覆盖仓库且不含当前仓的 change 静默跳过(不输出、不拦截)
|
|
18
|
-
* - 纯注释/空行/空格变更过滤:diff 中所有变更行均为注释/空行/空格的文件直接跳过,不参与校验
|
|
19
|
-
* - 报告存在性:活跃 change 无报告 → 阻止
|
|
20
|
-
* - 报告过期:diff 业务代码与 coveredFiles 交集文件内容变化(size 短路 + SHA-256)→ 阻止
|
|
21
|
-
* - 置信度门禁(仅 pre-commit):low → 阻止;strict 时 medium 也阻止
|
|
22
|
-
* - 无活跃 change 但 diff 有业务代码 → 阻止(提示先执行 opsx-propose)
|
|
23
|
-
* - 变更范围采集失败 → 阻止(避免静默放行的 fail-open)
|
|
4
|
+
/** Shared collection utilities and the optional strict index entry.
|
|
5
|
+
* Delivery readiness uses delivery-check.cjs receipts with complete input fingerprints.
|
|
6
|
+
* Legacy JSON parsing helpers remain available for readers; old confidence-only reports
|
|
7
|
+
* cannot satisfy the strict entry. Daily commit/push do not call this module by default.
|
|
24
8
|
*/
|
|
25
9
|
|
|
26
10
|
const fs = require('fs');
|
|
@@ -49,7 +33,7 @@ function parseActiveChangesFromYaml(content) {
|
|
|
49
33
|
for (const raw of content.split(/\r?\n/)) {
|
|
50
34
|
const line = raw.replace(/#.*$/, '').replace(/\s+$/, '');
|
|
51
35
|
if (!line.trim()) continue;
|
|
52
|
-
if (/^active_changes:\s*(\[\s
|
|
36
|
+
if (/^active_changes:\s*(\[\s*])?\s*$/.test(line)) { inActive = true; continue; }
|
|
53
37
|
if (inActive) {
|
|
54
38
|
const m = /^\s+-\s+change-key:\s*(\S+)/.exec(line);
|
|
55
39
|
if (m) keys.push(m[1]);
|
|
@@ -105,7 +89,7 @@ function parseReport(reportPath) {
|
|
|
105
89
|
: raw;
|
|
106
90
|
const t = new Date(normalized.replace(/-/g, '/')).getTime();
|
|
107
91
|
if (!isNaN(t)) {
|
|
108
|
-
generatedAtMs = t
|
|
92
|
+
generatedAtMs = t;
|
|
109
93
|
} else {
|
|
110
94
|
generatedAtFormatError = `generatedAt 无法解析(实际: ${raw},应为 YYYY-MM-DD HH:mm:ss 本地时间)`;
|
|
111
95
|
}
|
|
@@ -144,17 +128,31 @@ function validateReportFields(report) {
|
|
|
144
128
|
// --- 变更文件采集 ---
|
|
145
129
|
|
|
146
130
|
/**
|
|
147
|
-
* 简易 YAML 解析器:仅提取 code_repos
|
|
131
|
+
* 简易 YAML 解析器:仅提取 code_repos 列表。
|
|
132
|
+
* 逐行状态机解析(无正则量词嵌套),天然免疫 ReDoS(Sonar S5852)。
|
|
133
|
+
* 容忍条目间注释、行尾注释;遇到下一个顶层 key(非缩进行)即停止。
|
|
148
134
|
*/
|
|
149
135
|
function parseCodeRepos(yamlPath) {
|
|
150
136
|
try {
|
|
151
137
|
const content = fs.readFileSync(yamlPath, 'utf8');
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
.
|
|
156
|
-
|
|
157
|
-
|
|
138
|
+
const repos = [];
|
|
139
|
+
let inList = false;
|
|
140
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
141
|
+
const line = raw.replace(/#.*$/, '').replace(/\s+$/, '');
|
|
142
|
+
if (!line.trim()) continue;
|
|
143
|
+
if (!/^\s/.test(line)) {
|
|
144
|
+
// 顶层 key:进入 code_repos 列表 / 离开列表
|
|
145
|
+
inList = /^code_repos:\s*$/.test(line);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (!inList) continue;
|
|
149
|
+
const item = line.trim();
|
|
150
|
+
if (item.startsWith('- ')) {
|
|
151
|
+
const value = item.slice(2).trim();
|
|
152
|
+
if (value) repos.push(value);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return repos.length > 0 ? repos : null;
|
|
158
156
|
} catch {
|
|
159
157
|
return null;
|
|
160
158
|
}
|
|
@@ -185,10 +183,8 @@ function isTrivialDiffLine(line) {
|
|
|
185
183
|
if (trimmed.startsWith('#')) {
|
|
186
184
|
// 排除 C/C++ 预处理指令
|
|
187
185
|
const rest = trimmed.slice(1).trimStart();
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
return true; // Python/Shell/YAML 注释
|
|
186
|
+
return !/^(include|define|ifdef|ifndef|endif|else|elif|undef|pragma|error|warning|line|region|endregion)\b/.test(rest);
|
|
187
|
+
// Python/Shell/YAML 注释
|
|
192
188
|
}
|
|
193
189
|
if (trimmed.startsWith('/*') || trimmed.endsWith('*/')) return true; // 块注释开始/结束
|
|
194
190
|
if (trimmed.startsWith('*')) return true; // 块注释续行 (* ... 或 ** ...)
|
|
@@ -252,7 +248,7 @@ function parseDiffForTrivialFiles(diffOutput) {
|
|
|
252
248
|
if (line.startsWith('-') && !line.startsWith('---')) {
|
|
253
249
|
hasAnyChange = true;
|
|
254
250
|
if (!isTrivialDiffLine(line.slice(1))) hasSubstantive = true;
|
|
255
|
-
|
|
251
|
+
|
|
256
252
|
}
|
|
257
253
|
}
|
|
258
254
|
if (currentFile !== null && hasAnyChange && !hasSubstantive) {
|
|
@@ -405,15 +401,14 @@ function getModifiedFiles(projectRoot, diffFn, options = {}) {
|
|
|
405
401
|
/**
|
|
406
402
|
* 解析 INI 风格的 [consistency] 段,返回段内所有 key=value。
|
|
407
403
|
*/
|
|
408
|
-
function parseConsistencySection() {
|
|
409
|
-
const configPath = path.join(__dirname, 'hooks.config');
|
|
404
|
+
function parseConsistencySection(configPath = path.join(__dirname, 'hooks.config')) {
|
|
410
405
|
const values = {};
|
|
411
406
|
try {
|
|
412
407
|
const content = fs.readFileSync(configPath, 'utf8');
|
|
413
408
|
let inSection = false;
|
|
414
409
|
for (const raw of content.split(/\r?\n/)) {
|
|
415
410
|
const line = raw.trim();
|
|
416
|
-
if (/^\[
|
|
411
|
+
if (/^\[.+]$/.test(line)) {
|
|
417
412
|
if (inSection) break;
|
|
418
413
|
inSection = line.toLowerCase() === '[consistency]';
|
|
419
414
|
continue;
|
|
@@ -436,8 +431,8 @@ const CONSISTENCY_CONFIG = parseConsistencySection();
|
|
|
436
431
|
* 业务代码扩展名白名单(business_code_exts,唯一来源为 hooks.config,无内置默认值)。
|
|
437
432
|
* @returns {{exts: Set<string>, source: 'config'|'missing'|'empty', configPath: string|null}}
|
|
438
433
|
*/
|
|
439
|
-
function loadBusinessCodeExts() {
|
|
440
|
-
const { configPath, values } = CONSISTENCY_CONFIG;
|
|
434
|
+
function loadBusinessCodeExts(file) {
|
|
435
|
+
const { configPath, values } = file ? parseConsistencySection(file) : CONSISTENCY_CONFIG;
|
|
441
436
|
const rawValue = Object.prototype.hasOwnProperty.call(values, 'business_code_exts')
|
|
442
437
|
? values.business_code_exts : null;
|
|
443
438
|
|
|
@@ -478,14 +473,14 @@ const CONSISTENCY_MODE = loadConsistencyMode();
|
|
|
478
473
|
const TOOLCHAIN_CONFIG_RE = /^(?:vite|vitest|next|nuxt|astro|svelte|remix|webpack|rollup|babel|jest|playwright|cypress|eslint|prettier|tailwind|postcss|karma|wrangler|nest-cli|angular|vue|craco|rspack|metro|tsconfig|jsconfig|turbo|nx)\.(?:config|conf)\.[a-z]+$/i;
|
|
479
474
|
const DOTFILE_CONFIG_RE = /^\.?(?:eslintrc|babelrc|prettierrc|stylelintrc)\.[a-z]+$/i;
|
|
480
475
|
|
|
481
|
-
function isBusinessCode(file) {
|
|
476
|
+
function isBusinessCode(file, exts = BUSINESS_CODE_EXTS) {
|
|
482
477
|
const normalized = String(file).replace(/\\/g, '/');
|
|
483
478
|
const base = normalized.split('/').pop() || '';
|
|
484
479
|
|
|
485
480
|
const dot = base.lastIndexOf('.');
|
|
486
481
|
if (dot < 0) return false;
|
|
487
482
|
const ext = base.slice(dot).toLowerCase();
|
|
488
|
-
if (!BUSINESS_CODE_EXTS.has(ext)) return false;
|
|
483
|
+
if (!(exts instanceof Set ? exts : BUSINESS_CODE_EXTS).has(ext)) return false;
|
|
489
484
|
|
|
490
485
|
if (/\.d\.tsx?$/i.test(base)) return false;
|
|
491
486
|
if (/(^|\/)(dist|build|target|out|node_modules)\//.test(normalized)) return false;
|
|
@@ -493,9 +488,9 @@ function isBusinessCode(file) {
|
|
|
493
488
|
if (/(^|\/)(__tests__|tests?|e2e|cypress|fixtures|mocks?)(\/|$)/i.test(normalized)) return false;
|
|
494
489
|
if (/(^|\/)[^/]*(?:\.test|\.spec|_test)\.[a-z]+$/i.test(normalized)) return false;
|
|
495
490
|
if (TOOLCHAIN_CONFIG_RE.test(base)) return false;
|
|
496
|
-
|
|
491
|
+
return !DOTFILE_CONFIG_RE.test(base);
|
|
492
|
+
|
|
497
493
|
|
|
498
|
-
return true;
|
|
499
494
|
}
|
|
500
495
|
|
|
501
496
|
// --- 路径归一化:报告 coveredFiles → 代码仓相对 ---
|
|
@@ -622,25 +617,12 @@ function normalizeEolLf(buf) {
|
|
|
622
617
|
return Buffer.from(str.replace(/\r\n/g, '\n'), 'binary');
|
|
623
618
|
}
|
|
624
619
|
|
|
625
|
-
/**
|
|
626
|
-
* 单文件过期判断(mtime 快速跳过 + size 短路 + SHA-256,行尾归一化)。
|
|
627
|
-
* mtime 快速路径:文件修改时间早于报告生成时间 → 报告生成后从未被碰过 → 内容必然未变,
|
|
628
|
-
* 跳过读取和哈希(大变更场景省 100-300ms/百文件)。mtime 晚于报告时间才做精确哈希判断,
|
|
629
|
-
* 因此 git pull/checkout 重置 mtime(内容未变)只会多算一次哈希,不会误判过期。
|
|
630
|
-
*/
|
|
620
|
+
/** 内容哈希是有效性依据,mtime 不参与正确性判断。 */
|
|
631
621
|
function isFileChangedOnDisk(fp, metas, reportTime, readContent = readWorkingFile, relFile = '') {
|
|
632
622
|
const list = Array.isArray(metas) ? metas : (metas ? [metas] : []);
|
|
633
623
|
const hashed = list.filter(m => m && m.sha256);
|
|
634
624
|
|
|
635
625
|
if (hashed.length) {
|
|
636
|
-
// mtime 快速跳过:仅在能拿到本地文件时(readContent 为工作区读取)且 mtime 可信时启用
|
|
637
|
-
if (readContent === readWorkingFile) {
|
|
638
|
-
try {
|
|
639
|
-
if (fs.statSync(fp).mtimeMs <= reportTime) return false;
|
|
640
|
-
} catch {
|
|
641
|
-
return true; // 文件不存在 → 过期
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
626
|
const content = readContent(fp, relFile);
|
|
645
627
|
if (content === null) return true;
|
|
646
628
|
const normalized = normalizeEolLf(content);
|
|
@@ -651,14 +633,8 @@ function isFileChangedOnDisk(fp, metas, reportTime, readContent = readWorkingFil
|
|
|
651
633
|
return !candidates.some(m => hash === String(m.sha256).toLowerCase());
|
|
652
634
|
}
|
|
653
635
|
|
|
654
|
-
//
|
|
655
|
-
|
|
656
|
-
try {
|
|
657
|
-
stat = fs.statSync(fp);
|
|
658
|
-
} catch {
|
|
659
|
-
return true;
|
|
660
|
-
}
|
|
661
|
-
return stat.mtimeMs > reportTime;
|
|
636
|
+
// mtime cannot prove content equality. Legacy entries without hashes need review.
|
|
637
|
+
return true;
|
|
662
638
|
}
|
|
663
639
|
|
|
664
640
|
/**
|
|
@@ -761,205 +737,62 @@ function generateInstructions(opts, result, warnOnly) {
|
|
|
761
737
|
* @param {object} opts 入口差异配置
|
|
762
738
|
*/
|
|
763
739
|
function runConsistencyCheck(opts) {
|
|
764
|
-
const {
|
|
765
|
-
label, actionNoun, diffFn,
|
|
766
|
-
checkConfidence = false, includeUntracked = true,
|
|
767
|
-
staleFromHead = false,
|
|
768
|
-
} = opts;
|
|
740
|
+
const { label, actionNoun, diffFn, includeUntracked = false, staleFromHead = false, staleFromIndex = false } = opts;
|
|
769
741
|
const projectRoot = path.resolve(readArg('project') || process.env.SDD_PROJECT || process.cwd());
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
const
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
const strict = envStrict !== undefined ? envStrict === '1' : CONSISTENCY_MODE.strict;
|
|
777
|
-
const warnOnly = envWarnOnly !== undefined ? envWarnOnly === '1' : CONSISTENCY_MODE.warnOnly;
|
|
778
|
-
|
|
779
|
-
// 互斥检查
|
|
780
|
-
const activeModes = [skip, strict, warnOnly].filter(Boolean).length;
|
|
781
|
-
if (activeModes > 1) {
|
|
782
|
-
const names = [
|
|
783
|
-
...(skip ? ['SDD_SKIP_CONSISTENCY_CHECK' + (envSkip === undefined ? '(hooks.config)' : '')] : []),
|
|
784
|
-
...(strict ? ['SDD_STRICT_CONSISTENCY' + (envStrict === undefined ? '(hooks.config)' : '')] : []),
|
|
785
|
-
...(warnOnly ? ['SDD_WARN_ONLY' + (envWarnOnly === undefined ? '(hooks.config)' : '')] : []),
|
|
786
|
-
];
|
|
787
|
-
console.error(`${label} 错误:${names.join('、')} 互斥,只能启用一个`);
|
|
742
|
+
const mode = loadConsistencyMode();
|
|
743
|
+
const skip = process.env.SDD_SKIP_CONSISTENCY_CHECK === undefined ? mode.skip : process.env.SDD_SKIP_CONSISTENCY_CHECK === '1';
|
|
744
|
+
const strict = process.env.SDD_STRICT_CONSISTENCY === undefined ? mode.strict : process.env.SDD_STRICT_CONSISTENCY === '1';
|
|
745
|
+
const warnOnly = process.env.SDD_WARN_ONLY === undefined ? mode.warnOnly : process.env.SDD_WARN_ONLY === '1';
|
|
746
|
+
if ([skip, strict, warnOnly].filter(Boolean).length > 1) {
|
|
747
|
+
console.error(`${label} skip / strict / warn_only 互斥`);
|
|
788
748
|
process.exit(1);
|
|
789
749
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
if (
|
|
797
|
-
|
|
798
|
-
// 配置缺失/空清单显式告警
|
|
799
|
-
if (BUSINESS_CODE_EXTS_INFO.source !== 'config') {
|
|
800
|
-
const reason = BUSINESS_CODE_EXTS_INFO.source === 'missing'
|
|
801
|
-
? '未配置 business_code_exts(hooks.config 缺失或无 [consistency] 段)'
|
|
802
|
-
: 'business_code_exts 为空清单';
|
|
803
|
-
console.warn(`${label} ⚠️ ${reason},一致性校验不拦截任何业务代码文件(仅保留报告存在性/过期检查)`);
|
|
804
|
-
console.warn(`${label} ℹ️ 请在 ${BUSINESS_CODE_EXTS_INFO.configPath || path.join(__dirname, 'hooks.config')} 的 [consistency] 段配置 business_code_exts`);
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
const specRoot = resolveSpecRoot();
|
|
808
|
-
|
|
809
|
-
// 1. 采集 diff 文件
|
|
810
|
-
const { files: modifiedFiles, deleted: deletedFiles, failed: diffFailed } =
|
|
811
|
-
getModifiedFiles(projectRoot, diffFn, { includeUntracked });
|
|
812
|
-
|
|
813
|
-
if (diffFailed) {
|
|
814
|
-
console.log(`${label} ❌ 无法确定本次${actionNoun}的变更范围(git diff 采集失败),拒绝${actionNoun}`);
|
|
815
|
-
if (warnOnly) {
|
|
816
|
-
console.log(`${label} 仅告警模式,放行${actionNoun}`);
|
|
817
|
-
process.exit(0);
|
|
818
|
-
}
|
|
819
|
-
process.exit(1);
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
// 2. 筛选业务代码文件
|
|
823
|
-
const diffBusinessFiles = modifiedFiles.filter(isBusinessCode);
|
|
824
|
-
const deletedBusinessFiles = deletedFiles.filter(isBusinessCode);
|
|
825
|
-
|
|
826
|
-
// 3. 无业务代码文件 → 放行
|
|
827
|
-
if (diffBusinessFiles.length === 0 && deletedBusinessFiles.length === 0) {
|
|
750
|
+
if (skip) { console.log(`${label} 跳过校验`); process.exit(0); }
|
|
751
|
+
const problems = [];
|
|
752
|
+
const { files, deleted, failed } = getModifiedFiles(projectRoot, diffFn, { includeUntracked });
|
|
753
|
+
if (failed) problems.push('无法确定变更范围');
|
|
754
|
+
if (BUSINESS_CODE_EXTS_INFO.source !== 'config') problems.push('business_code_exts 未配置,不能证明检查完整');
|
|
755
|
+
const required = [...new Set([...files, ...deleted])].filter(isBusinessCode);
|
|
756
|
+
if (!problems.length && !required.length) {
|
|
828
757
|
console.log(`${label} 本次${actionNoun}无业务代码文件,跳过一致性校验`);
|
|
829
758
|
process.exit(0);
|
|
830
759
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
console.log(`${label} 仅告警模式,放行${actionNoun}`);
|
|
845
|
-
process.exit(0);
|
|
846
|
-
}
|
|
847
|
-
process.exit(1);
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
// 5. 逐 change 解析报告
|
|
851
|
-
const parseError = [];
|
|
852
|
-
const invalid = [];
|
|
853
|
-
const lowConfidence = [];
|
|
854
|
-
const stale = [];
|
|
855
|
-
const missing = [];
|
|
856
|
-
const parsedReports = new Map();
|
|
857
|
-
|
|
858
|
-
// 当前代码仓目录名(多仓布局下用于报告归属过滤 + normalizeCoveredFiles 兜底前缀剥离)
|
|
859
|
-
const currentRepoName = path.basename(projectRoot).toLowerCase();
|
|
860
|
-
|
|
861
|
-
for (const cn of changes) {
|
|
862
|
-
const rp = findReportJson(specRoot, cn);
|
|
863
|
-
if (!rp) {
|
|
864
|
-
missing.push(cn);
|
|
865
|
-
continue;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
const report = parseReport(rp);
|
|
869
|
-
if (report.parseError) {
|
|
870
|
-
console.warn(`${label} ⚠️ ${cn} 报告解析失败`);
|
|
871
|
-
parseError.push(cn);
|
|
872
|
-
continue;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
// 旧格式报告(无 coveredFiles)
|
|
876
|
-
if (!report.coveredFiles) {
|
|
877
|
-
console.log(`${label} ⚠️ ${cn}: 报告为旧格式(缺少 coveredFiles),建议重新生成`);
|
|
878
|
-
missing.push(cn);
|
|
879
|
-
continue;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
// 字段合法性校验(仅 confidence + generatedAt)
|
|
883
|
-
const fieldErrors = validateReportFields(report);
|
|
884
|
-
if (fieldErrors.length) {
|
|
885
|
-
console.warn(`${label} ⚠️ ${cn} 报告字段不合法:${fieldErrors.join(';')}`);
|
|
886
|
-
invalid.push({ cn, errors: fieldErrors });
|
|
887
|
-
continue;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
const covered = normalizeCoveredFiles(report, currentRepoName);
|
|
891
|
-
parsedReports.set(cn, { report, covered });
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// 6. 对 diff(含删除)与 coveredFiles 有交集的 change 检查报告
|
|
895
|
-
const checkFiles = [...new Set([...diffBusinessFiles, ...deletedBusinessFiles])];
|
|
896
|
-
const checkFilesLower = checkFiles.map(f => f.toLowerCase());
|
|
897
|
-
|
|
898
|
-
let headContentCache = null;
|
|
899
|
-
const readContent = staleFromHead
|
|
900
|
-
? (fp, relFile) => {
|
|
901
|
-
if (headContentCache === null) {
|
|
902
|
-
headContentCache = gitShowHeadContentBatch(projectRoot, checkFiles);
|
|
903
|
-
}
|
|
904
|
-
const fromHead = headContentCache.get(relFile);
|
|
905
|
-
return fromHead !== undefined && fromHead !== null ? fromHead : readWorkingFile(fp);
|
|
906
|
-
}
|
|
907
|
-
: readWorkingFile;
|
|
908
|
-
|
|
909
|
-
for (const [cn, { report, covered }] of parsedReports) {
|
|
910
|
-
// 多仓归属过滤:报告显式声明了覆盖仓库且不含当前仓 → 与本仓无关,静默跳过
|
|
911
|
-
// (repositories 为空视为未声明,保持原交集判断行为,兼容单仓与旧报告)
|
|
912
|
-
const declaredRepos = (report.repositories || [])
|
|
913
|
-
.map(r => String(r && r.path || '').replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase())
|
|
914
|
-
.filter(Boolean);
|
|
915
|
-
if (declaredRepos.length && !declaredRepos.includes(currentRepoName)) {
|
|
916
|
-
continue;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
const hasIntersection = checkFilesLower.some(f => covered.has(f));
|
|
920
|
-
if (!hasIntersection) {
|
|
921
|
-
console.log(`${label} ⏭️ ${cn}: 与本次${actionNoun}无交集,跳过`);
|
|
922
|
-
continue;
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
if (isReportStale(report, covered, checkFiles, projectRoot, readContent)) {
|
|
926
|
-
console.log(`${label} ⚠️ ${cn}: 报告已过期`);
|
|
927
|
-
stale.push(cn);
|
|
928
|
-
continue;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
if (checkConfidence) {
|
|
932
|
-
const c = report.confidence;
|
|
933
|
-
if (c === 'low' || (c === 'medium' && strict)) {
|
|
934
|
-
console.log(`${label} ❌ ${cn}: 置信度 ${c}`);
|
|
935
|
-
lowConfidence.push(cn);
|
|
760
|
+
const delivery = require('./delivery-check.cjs');
|
|
761
|
+
const snap = require('./delivery-snapshot.cjs');
|
|
762
|
+
const specRoot = resolveSpecRoot();
|
|
763
|
+
const explicit = String(readArg('change') || process.env.KLD_SDD_CHANGE || '').split(/[,\s]+/).filter(Boolean);
|
|
764
|
+
const changes = explicit.length ? explicit : discoverActiveChanges(specRoot);
|
|
765
|
+
const covered = new Set();
|
|
766
|
+
for (const change of changes) {
|
|
767
|
+
try {
|
|
768
|
+
const identity = snap.repoIdentity(projectRoot);
|
|
769
|
+
const rp = delivery.reviewPath({ specRoot, change, identity });
|
|
770
|
+
if (!fs.existsSync(rp)) {
|
|
771
|
+
// Unrelated active changes do not require a report for this commit.
|
|
772
|
+
if (explicit.includes(change)) problems.push(`${change}: 缺少一致性检查回执,请运行 opsx-consistency-check`);
|
|
936
773
|
continue;
|
|
937
774
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
if (
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
process.exit(1);
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
console.log(`\n${label} ✅ 本次${actionNoun}涉及的业务代码均已通过一致性校验,放行${actionNoun}`);
|
|
775
|
+
const report = JSON.parse(fs.readFileSync(rp, 'utf8'));
|
|
776
|
+
if (!explicit.includes(change) && !required.some(f => (report.reviewedFiles || []).includes(f))) continue;
|
|
777
|
+
const ctx = delivery.context({ project: projectRoot, 'spec-root': specRoot, change,
|
|
778
|
+
scope: staleFromHead ? 'commit' : staleFromIndex ? 'index' : 'worktree',
|
|
779
|
+
base: opts.base || report.base, ref: opts.ref || 'HEAD', 'spec-ref': opts.specRef });
|
|
780
|
+
// This report need only cover its own files; union coverage is asserted below.
|
|
781
|
+
ctx.required = required.filter(f => (report.reviewedFiles || []).includes(f));
|
|
782
|
+
const errors = delivery.reviewErrors(ctx, report);
|
|
783
|
+
if (errors.length) problems.push(`${change}: 报告过期或未完成:${errors.join(';')}`);
|
|
784
|
+
else for (const file of ctx.required) covered.add(file);
|
|
785
|
+
} catch (error) { problems.push(`${change}: ${error.message}`); }
|
|
786
|
+
}
|
|
787
|
+
const uncovered = required.filter(file => !covered.has(file));
|
|
788
|
+
if (uncovered.length) problems.push(`缺少有效检查覆盖(UNCOVERED_FILES):${uncovered.join(', ')}`);
|
|
789
|
+
if (problems.length) {
|
|
790
|
+
console.log(`${label} 未通过:\n${problems.map(p => ` - ${p}`).join('\n')}`);
|
|
791
|
+
console.log('请由 opsx-consistency-check 更新回执;日常提交默认不运行此严格入口。');
|
|
792
|
+
if (warnOnly) console.log(`${label} 仅告警模式,放行${actionNoun}(不代表已通过检查)`);
|
|
793
|
+
process.exit(warnOnly ? 0 : 1);
|
|
794
|
+
}
|
|
795
|
+
console.log(`${label} 本次${actionNoun}涉及的业务代码均已通过一致性校验`);
|
|
963
796
|
process.exit(0);
|
|
964
797
|
}
|
|
965
798
|
|
|
@@ -968,7 +801,7 @@ function runConsistencyCheck(opts) {
|
|
|
968
801
|
function runFilterMode() {
|
|
969
802
|
const filesArg = readArg('files');
|
|
970
803
|
|
|
971
|
-
let input
|
|
804
|
+
let input;
|
|
972
805
|
if (filesArg) {
|
|
973
806
|
if (filesArg.startsWith('@')) {
|
|
974
807
|
input = fs.readFileSync(filesArg.slice(1), 'utf8').split(/\r?\n/);
|
|
@@ -1021,11 +854,9 @@ function runCollectMode() {
|
|
|
1021
854
|
const businessFiles = files.filter(isBusinessCode);
|
|
1022
855
|
const excluded = files.filter(f => !isBusinessCode(f));
|
|
1023
856
|
|
|
1024
|
-
|
|
1025
|
-
|
|
857
|
+
const hashedBusiness = [];
|
|
858
|
+
const unreadableFiles = [];
|
|
1026
859
|
if (withHash) {
|
|
1027
|
-
hashedBusiness = [];
|
|
1028
|
-
unreadableFiles = [];
|
|
1029
860
|
for (const rel of businessFiles) {
|
|
1030
861
|
const content = readWorkingFile(path.join(projectRoot, rel));
|
|
1031
862
|
if (content === null) {
|