kld-sdd 2.7.3 → 2.7.8-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +12 -5
  2. package/USABILITY.md +104 -0
  3. package/bin/kld-sdd-init.js +1 -1
  4. package/kld-sdd-guide.html +16 -4
  5. package/lib/hook-gate-core.js +327 -0
  6. package/lib/init.js +228 -37
  7. package/lib/scale-thresholds.json +19 -0
  8. package/lib/skills-bundle.js +2 -1
  9. package/package.json +5 -3
  10. package/skywalk-sdd/context-client.cjs +50 -30
  11. package/skywalk-sdd/index.cjs +159 -40
  12. package/skywalk-sdd/kb-sync-identity.cjs +99 -451
  13. package/skywalk-sdd/kb-upload.cjs +67 -44
  14. package/skywalk-sdd/lib/check-review.cjs +41 -0
  15. package/skywalk-sdd/lib/test-execution.cjs +110 -0
  16. package/skywalk-sdd/lib/usage-contract.cjs +3 -2
  17. package/skywalk-sdd/lib/usage-reporter.cjs +27 -2
  18. package/skywalk-sdd/metrics-v3.cjs +2 -2
  19. package/skywalk-sdd/ontology/active-changes.cjs +2 -1
  20. package/skywalk-sdd/ontology/archive-package.cjs +1 -1
  21. package/skywalk-sdd/ontology/artifact-parser.cjs +7 -4
  22. package/skywalk-sdd/ontology/id.cjs +5 -4
  23. package/skywalk-sdd/ontology/identity-index.cjs +9 -4
  24. package/skywalk-sdd/ontology/list-changes.cjs +1 -1
  25. package/skywalk-sdd/ontology/runtime.cjs +7 -3
  26. package/skywalk-sdd/ontology/schema.cjs +2 -0
  27. package/skywalk-sdd/ontology/traceability-validator.cjs +74 -4
  28. package/skywalk-sdd/ontology/workspace-layout.cjs +25 -5
  29. package/skywalk-sdd/reporting/change-report-model.cjs +4 -3
  30. package/templates/git-hooks/commit-msg +39 -24
  31. package/templates/git-hooks/consistency-check-core.cjs +1097 -0
  32. package/templates/git-hooks/hooks.config +20 -1
  33. package/templates/git-hooks/pre-commit +39 -24
  34. package/templates/git-hooks/pre-commit-consistency-check.cjs +29 -332
  35. package/templates/git-hooks/pre-commit-sdd-check.cjs +98 -0
  36. package/templates/git-hooks/pre-push +39 -24
  37. package/templates/git-hooks/pre-push-consistency-check.cjs +58 -406
  38. package/templates/hooks/claude/hooks/sdd-post-tool.cjs +2 -2
  39. package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +2 -2
  40. package/templates/hooks/codebuddy/hooks/sdd-tdd-rhythm-gate.cjs +1 -1
  41. package/templates/openspec/tasks.md +3 -3
  42. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +44 -6
  43. package/templates/skills/kld-sdd/opsx-apply/checklist.md +1 -1
  44. package/templates/skills/kld-sdd/opsx-apply/reference.md +20 -2
  45. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +19 -21
  46. package/templates/skills/kld-sdd/opsx-check/SKILL.md +55 -327
  47. package/templates/skills/kld-sdd/opsx-check/checklist.md +7 -4
  48. package/templates/skills/kld-sdd/opsx-check/reference.md +60 -0
  49. package/templates/skills/kld-sdd/opsx-check/result-template.json +52 -0
  50. package/templates/skills/kld-sdd/opsx-check/review-template.json +22 -0
  51. package/templates/skills/kld-sdd/opsx-check/reviewer.md +100 -0
  52. package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +82 -451
  53. package/templates/skills/kld-sdd/opsx-consistency-check/{reference.md → references/reference.md} +0 -1
  54. package/templates/skills/kld-sdd/opsx-consistency-check/scripts/scripts.cjs +517 -0
  55. package/templates/skills/kld-sdd/opsx-design/SKILL.md +10 -30
  56. package/templates/skills/kld-sdd/opsx-design/checklist.md +3 -4
  57. package/templates/skills/kld-sdd/opsx-design/reference.md +1 -1
  58. package/templates/skills/kld-sdd/opsx-kb-config/SKILL.md +29 -154
  59. package/templates/skills/kld-sdd/opsx-kb-config/reference.md +8 -11
  60. package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +12 -74
  61. package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +2 -2
  62. package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +7 -5
  63. package/templates/skills/kld-sdd/opsx-ontology-query/phase-1-prechange.md +2 -2
  64. package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +1 -1
  65. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +29 -74
  66. package/templates/skills/kld-sdd/opsx-propose/checklist.md +8 -8
  67. package/templates/skills/kld-sdd/opsx-propose/interaction-policy.md +35 -0
  68. package/templates/skills/kld-sdd/opsx-propose/reference.md +12 -46
  69. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +14 -61
  70. package/templates/skills/kld-sdd/opsx-spec/checklist.md +3 -3
  71. package/templates/skills/kld-sdd/opsx-task/SKILL.md +40 -34
  72. package/templates/skills/kld-sdd/opsx-task/checklist.md +5 -6
  73. package/templates/skills/kld-sdd/opsx-test/SKILL.md +2 -0
  74. package/templates/skills/kld-sdd/tdd-rules/rules/tdd-strategy-selection.md +1 -1
@@ -417,8 +417,10 @@ function createArchiveSnapshot(projectRoot, changeName, archiveDir, inputState)
417
417
  }
418
418
  const snapshotId = `SNAP-${sha256(`${changeName}:${state.revision}`).slice(0, 16).toUpperCase()}`;
419
419
  const targetPath = path.join(targetDir, 'archive-ontology.json');
420
- const previous = fs.existsSync(targetPath)
421
- ? JSON.parse(fs.readFileSync(targetPath, 'utf8'))
420
+ const cachePath = path.join(statePaths(projectRoot, changeName).dir, 'snapshots', `${state.revision}.json`);
421
+ const previousPath = fs.existsSync(cachePath) ? cachePath : targetPath;
422
+ const previous = fs.existsSync(previousPath)
423
+ ? JSON.parse(fs.readFileSync(previousPath, 'utf8'))
422
424
  : null;
423
425
  const previousEntity = previous && previous.source_revision === state.revision
424
426
  ? (previous.entities || []).find((entity) => entity.type === 'OntologySnapshot')
@@ -458,12 +460,14 @@ function createArchiveSnapshot(projectRoot, changeName, archiveDir, inputState)
458
460
  snapshot_id: snapshotId,
459
461
  snapshot_entity_id: snapshotEntity.entity_id,
460
462
  source_revision: state.revision,
461
- confirmed_at: new Date().toISOString(),
463
+ confirmed_at: previous && previous.source_revision === state.revision
464
+ ? previous.confirmed_at : new Date().toISOString(),
462
465
  entities: [
463
466
  ...(state.entities || []).filter((entity) => entity.type !== 'OntologySnapshot'),
464
467
  snapshotEntity,
465
468
  ],
466
469
  };
470
+ atomicWriteIfChanged(cachePath, snapshot);
467
471
  atomicWriteIfChanged(targetPath, snapshot);
468
472
  return snapshot;
469
473
  }
@@ -114,6 +114,8 @@ const DIAGNOSTIC_CODES = Object.freeze({
114
114
  NUMBERING_WAIVER_ACTIVE: 'NUMBERING_WAIVER_ACTIVE',
115
115
  CONTINUITY_DECISION_REQUIRED: 'CONTINUITY_DECISION_REQUIRED',
116
116
  CONTINUITY_IDENTITY_MISMATCH: 'CONTINUITY_IDENTITY_MISMATCH',
117
+ TASK_DEPENDENCY_LAYER_GAP: 'SEM_TASK_DEPENDENCY_LAYER_GAP',
118
+ TASK_DEPENDENCY_MISSING_EDGE: 'SEM_TASK_DEPENDENCY_MISSING_EDGE',
117
119
  });
118
120
 
119
121
  function normalizeProfile(profile, inferredProfile = 'simple') {
@@ -113,7 +113,7 @@ function validateIdentitySemantics(facts, diagnostics, identityHistory) {
113
113
  }
114
114
 
115
115
  const entityOwner = currentByEntityId.get(entity.entity_id);
116
- if (entityOwner && (entityOwner.id !== entity.id || entityOwner.type !== entity.type)) {
116
+ if (entityOwner && (entityOwner.type !== entity.type)) {
117
117
  diagnostics.push(diagnostic(
118
118
  DIAGNOSTIC_CODES.ENTITY_IDENTITY_CONFLICT,
119
119
  'error',
@@ -165,7 +165,7 @@ function validateIdentitySemantics(facts, diagnostics, identityHistory) {
165
165
  const priorAnchors = historyByAnchor.get(entity.anchor_id || entity.id) || [];
166
166
  const priorVersion = historyByVersionId.get(entity.version_id);
167
167
 
168
- if (priorEntities.some((record) => record.anchor_id !== entity.id || record.type !== entity.type)
168
+ if (priorEntities.some((record) => record.type !== entity.type)
169
169
  || priorAnchors.some((record) => record.entity_id !== entity.entity_id)) {
170
170
  diagnostics.push(diagnostic(
171
171
  DIAGNOSTIC_CODES.ENTITY_IDENTITY_CONFLICT,
@@ -220,8 +220,8 @@ function validateIdentitySemantics(facts, diagnostics, identityHistory) {
220
220
  if (knownSuccessor) {
221
221
  diagnostics.push(diagnostic(
222
222
  DIAGNOSTIC_CODES.VERSION_LINEAGE_CONFLICT,
223
- 'error',
224
- `同一 predecessor 已存在后继版本,禁止并行或陈旧分支: ${entity.id}`,
223
+ knownSuccessor.scope === 'active' ? 'warning' : 'error',
224
+ `同一 predecessor 存在其他后继;草稿可并行,发布前必须解决基线冲突: ${entity.id}`,
225
225
  { ...context, suggestion: `以 ${knownSuccessor.change} 的版本为直接前序;不要再次继承已消费的 predecessor` },
226
226
  ));
227
227
  }
@@ -922,6 +922,8 @@ function validateTraceability(facts, options = {}) {
922
922
  ));
923
923
  }
924
924
 
925
+ validateTaskLayerSemantics(graphEntities, graphRelations, diagnostics);
926
+
925
927
  diagnostics.sort((left, right) => (
926
928
  String(left.code).localeCompare(String(right.code))
927
929
  || String(left.file || '').localeCompare(String(right.file || ''))
@@ -943,10 +945,78 @@ function validateTraceability(facts, options = {}) {
943
945
  };
944
946
  }
945
947
 
948
+ function inferTaskLayer(task) {
949
+ const attrs = task.attributes || {};
950
+ if (typeof attrs.layer === 'number') return attrs.layer;
951
+
952
+ const id = String(task.id || '').toUpperCase();
953
+ const desc = String(task.description || task.title || '').toUpperCase();
954
+ const text = `${id} ${desc}`;
955
+
956
+ const patterns = [
957
+ { layer: 0, keywords: ['DEPS', 'DEPEND', 'CONFIG', 'POM', 'GRADLE', 'CONST'] },
958
+ { layer: 1, keywords: ['ENTITY', 'DTO', 'ENUM', 'VO', 'MODEL'] },
959
+ { layer: 2, keywords: ['MAPPER', 'REPO', 'REPOSITORY', 'DAO'] },
960
+ { layer: 3, keywords: ['SERVICE-IF', 'SERVICE_IF', 'INTERFACE'] },
961
+ { layer: 4, keywords: ['IMPL', 'SERVICE', 'HANDLER', 'PROCESSOR'] },
962
+ { layer: 5, keywords: ['CONTROLLER', 'API', 'RESOURCE', 'ENDPOINT'] },
963
+ { layer: 6, keywords: ['TEST', 'VERIFY', 'VALIDATE', 'SPEC'] },
964
+ ];
965
+
966
+ for (const { layer, keywords } of patterns) {
967
+ if (keywords.some((kw) => text.includes(kw))) return layer;
968
+ }
969
+ return null;
970
+ }
971
+
972
+ function validateTaskLayerSemantics(graphEntities, graphRelations, diagnostics) {
973
+ const tasks = graphEntities.filter((entity) => entity.type === 'Task' && entity.delta_state !== 'removed');
974
+ if (tasks.length === 0) return;
975
+
976
+ const taskById = new Map();
977
+ for (const task of tasks) taskById.set(task.id, task);
978
+
979
+ const dependsOnMap = new Map();
980
+ for (const relation of graphRelations) {
981
+ if (relation.type !== 'dependsOn') continue;
982
+ if (!dependsOnMap.has(relation.from)) dependsOnMap.set(relation.from, new Set());
983
+ dependsOnMap.get(relation.from).add(relation.to);
984
+ }
985
+
986
+ const layerByTask = new Map();
987
+ for (const task of tasks) {
988
+ const layer = inferTaskLayer(task);
989
+ if (layer !== null) layerByTask.set(task.id, layer);
990
+ }
991
+
992
+ for (const [taskId, layer] of layerByTask.entries()) {
993
+ if (layer <= 0) continue;
994
+ const deps = dependsOnMap.get(taskId) || new Set();
995
+
996
+ for (const [otherId, otherLayer] of layerByTask.entries()) {
997
+ if (otherId === taskId) continue;
998
+ if (otherLayer >= layer) continue;
999
+ if (!deps.has(otherId)) {
1000
+ diagnostics.push(diagnostic(
1001
+ DIAGNOSTIC_CODES.TASK_DEPENDENCY_LAYER_GAP,
1002
+ 'warning',
1003
+ `Task ${taskId} (Layer ${layer}) 可能缺少对 Layer ${otherLayer} 任务 ${otherId} 的依赖`,
1004
+ {
1005
+ entity_id: taskId,
1006
+ suggestion: `若 ${taskId} 引用或依赖 ${otherId} 的定义,建议添加 dependsOn: ${otherId}`,
1007
+ },
1008
+ ));
1009
+ }
1010
+ }
1011
+ }
1012
+ }
1013
+
946
1014
  module.exports = {
947
1015
  diagnostic,
948
1016
  findTaskCycles,
949
1017
  validateIdentitySemantics,
950
1018
  validateInheritedReferences,
951
1019
  validateTraceability,
1020
+ validateTaskLayerSemantics,
1021
+ inferTaskLayer,
952
1022
  };
@@ -26,11 +26,17 @@ function isGitRepo(dir) {
26
26
 
27
27
  function looksLikeSpecRepo(dir) {
28
28
  const name = path.basename(dir).toLowerCase();
29
+ // 命名约定:*-sdd-specs 目录名直接命中
29
30
  if (/-sdd-specs$/.test(name) || name.endsWith('sdd-specs')) return true;
30
- if (fs.existsSync(path.join(dir, 'modules.yaml'))) return true;
31
- if (fs.existsSync(path.join(dir, 'openspec', 'changes'))) return true;
32
- if (fs.existsSync(path.join(dir, 'openspec', 'specs', 'overview.md'))) return true;
33
- return false;
31
+ // 结构判据(四项同时满足):
32
+ // modules.yaml(模块登记)+ sdd.config.yaml(活动变更登记)
33
+ // + openspec/changes(变更结构)+ openspec/specs/overview.md(全局契约)
34
+ return (
35
+ fs.existsSync(path.join(dir, 'modules.yaml'))
36
+ && fs.existsSync(path.join(dir, 'sdd.config.yaml'))
37
+ && fs.existsSync(path.join(dir, 'openspec', 'changes'))
38
+ && fs.existsSync(path.join(dir, 'openspec', 'specs', 'overview.md'))
39
+ );
34
40
  }
35
41
 
36
42
  function listChildGitRepos(workspaceRoot) {
@@ -231,7 +237,10 @@ function findNestedSpecPackage(parentDir) {
231
237
  function ensureSpecPackage(parentDir, options = {}) {
232
238
  const { execFileSync } = require('child_process');
233
239
  const root = path.resolve(parentDir);
234
- const existing = findNestedSpecPackage(root);
240
+ // 显式指定目录名(--spec-path)时,以该路径为准,不再回退到"探测到的其它 spec 包",
241
+ // 避免用户想指定 erp-sdd-specs 却被同级已存在的 *-sdd-specs 抢占。
242
+ const explicit = Boolean(options.name);
243
+ const existing = explicit ? null : findNestedSpecPackage(root);
235
244
  if (existing) {
236
245
  return {
237
246
  name: existing.name,
@@ -243,6 +252,17 @@ function ensureSpecPackage(parentDir, options = {}) {
243
252
 
244
253
  const name = options.name || deriveSpecPackageName(root);
245
254
  const abs = path.join(root, name);
255
+
256
+ // 显式路径且该目录本身已存在并具备 spec 包特征 → 直接复用(幂等)
257
+ if (explicit && fs.existsSync(abs) && looksLikeSpecRepo(abs)) {
258
+ return {
259
+ name,
260
+ abs,
261
+ created: false,
262
+ isGit: isGitRepo(abs),
263
+ };
264
+ }
265
+
246
266
  const existed = fs.existsSync(abs);
247
267
  fs.mkdirSync(abs, { recursive: true });
248
268
 
@@ -273,7 +273,8 @@ function normalizeEvidenceSummary(report) {
273
273
  })),
274
274
  ];
275
275
  const issueByKey = new Map();
276
- history.map(normalizeIssue).forEach((issue) => {
276
+ // 显式只传元素,避免 map index/array 参数透传(S6544 类)
277
+ history.map(issue => normalizeIssue(issue)).forEach((issue) => {
277
278
  const stableKey = issue.issue_id
278
279
  || `${issue.code}:${issue.task_id || issue.pair_id || issue.subject || 'change'}`;
279
280
  const previous = issueByKey.get(stableKey);
@@ -369,8 +370,8 @@ function normalizeChangedFiles(changedFiles = {}) {
369
370
  ?? null;
370
371
  const taskSet = new Set(taskEventFiles || []);
371
372
  const finalSet = new Set(finalOutputFiles || []);
372
- const onlyInTaskEvents = [...taskSet].filter(file => !finalSet.has(file)).sort();
373
- const onlyInFinalOutput = [...finalSet].filter(file => !taskSet.has(file)).sort();
373
+ const onlyInTaskEvents = [...taskSet].filter(file => !finalSet.has(file)).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
374
+ const onlyInFinalOutput = [...finalSet].filter(file => !taskSet.has(file)).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
374
375
  const countsDiffer = taskCount != null && finalCount != null && taskCount !== finalCount;
375
376
  const listsDiffer = taskEventFiles != null
376
377
  && finalOutputFiles != null
@@ -4,36 +4,49 @@
4
4
  # 删除某行 = 禁用该插件
5
5
  # marker: KLD SDD quality gate
6
6
 
7
- # hook 所在目录推算 git 根目录
8
- hook_dir="$(cd "$(dirname "$0")" && pwd)"
9
- git_dir="$(dirname "$hook_dir")"
10
- git_root="$(dirname "$git_dir")"
11
-
7
+ # Git supplies the working tree context; this also supports worktrees and core.hooksPath.
8
+ git_root=$(git rev-parse --show-toplevel 2>/dev/null) || {
9
+ echo "[SDD] 无法定位当前 Git 工作区"
10
+ exit 1
11
+ }
12
12
  hooks_dir=""
13
-
14
- # 方法1: git config sdd.specPath(多仓布局:spec 是独立 clone)
15
- spec_path=$(git config --local sdd.specPath 2>/dev/null)
16
- if [ -n "$spec_path" ] && [ -d "$spec_path/skywalk-sdd/git-hooks" ]; then
17
- hooks_dir="$spec_path/skywalk-sdd/git-hooks"
13
+ # 统一反斜杠为正斜杠,保证 Windows 盘符路径(P:\a\b → P:/a/b)能被 case 判定为绝对路径
14
+ normalize_path() {
15
+ printf '%s' "$1" | tr '\\' '/'
16
+ }
17
+ # 绝对路径(/ 开头或盘符 X: 开头)直接使用;相对路径基于 git 根拼接
18
+ resolve_spec_dir() {
19
+ raw=$(normalize_path "$1")
20
+ case "$raw" in
21
+ /*|[A-Za-z]:*) printf '%s' "$raw" ;;
22
+ *) printf '%s' "$git_root/$raw" ;;
23
+ esac
24
+ }
25
+ spec_path=$(git -C "$git_root" config --local sdd.specPath 2>/dev/null)
26
+ if [ -n "$spec_path" ]; then
27
+ hooks_dir="$(resolve_spec_dir "$spec_path")/skywalk-sdd/git-hooks"
28
+ elif [ -f "$git_root/.sdd-spec-root" ]; then
29
+ spec_rel=$(head -1 "$git_root/.sdd-spec-root" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
30
+ hooks_dir="$(resolve_spec_dir "$spec_rel")/skywalk-sdd/git-hooks"
18
31
  fi
19
-
20
- # 方法2: .sdd-spec-root(单仓 mono 布局:skywalk-sdd 在 spec 包裹包子目录内)
21
- if [ -z "$hooks_dir" ] && [ -f "$git_root/.sdd-spec-root" ]; then
22
- spec_rel=$(cat "$git_root/.sdd-spec-root" | head -1 | tr -d '[:space:]')
23
- if [ -n "$spec_rel" ] && [ -d "$git_root/$spec_rel/skywalk-sdd/git-hooks" ]; then
24
- hooks_dir="$git_root/$spec_rel/skywalk-sdd/git-hooks"
25
- fi
32
+ if [ -n "$hooks_dir" ] && [ ! -d "$hooks_dir" ]; then
33
+ echo "[SDD] 已配置的 Spec 路径失效:$hooks_dir"
34
+ echo "[SDD] 请恢复 Spec 仓,或修正 sdd.specPath / .sdd-spec-root 后重试"
35
+ exit 1
26
36
  fi
27
37
 
28
38
  # 方法3: 从 git 根向上搜索 skywalk-sdd/git-hooks(spec 仓本身或父级)
29
39
  if [ -z "$hooks_dir" ]; then
30
40
  search_dir="$git_root"
31
- while [ "$search_dir" != "/" ] && [ "$search_dir" != "" ]; do
41
+ while [ -n "$search_dir" ] && [ "$search_dir" != "/" ]; do
32
42
  if [ -d "$search_dir/skywalk-sdd/git-hooks" ]; then
33
43
  hooks_dir="$search_dir/skywalk-sdd/git-hooks"
34
44
  break
35
45
  fi
36
- search_dir="$(dirname "$search_dir")"
46
+ # Windows 盘符根(P:/)dirname 后不变,需显式收敛,避免死循环
47
+ parent_dir="$(dirname "$search_dir")"
48
+ [ "$parent_dir" = "$search_dir" ] && break
49
+ search_dir="$parent_dir"
37
50
  done
38
51
  fi
39
52
 
@@ -51,8 +64,9 @@ else
51
64
  # 读取 [commit-msg] 段落的配置
52
65
  in_section=0
53
66
  SCRIPTS=""
67
+ CR=$(printf '\r')
54
68
  while IFS= read -r line; do
55
- line=$(printf '%s' "$line" | tr -d '\r')
69
+ line=${line%"$CR"}
56
70
  case "$line" in
57
71
  "[commit-msg]") in_section=1 ;;
58
72
  "["*) in_section=0 ;;
@@ -71,7 +85,8 @@ failed=0
71
85
  for script_name in $SCRIPTS; do
72
86
  script="$hooks_dir/$script_name"
73
87
  if [ ! -f "$script" ]; then
74
- echo "[SDD] ⚠️ 插件不存在:$script_name"
88
+ echo "[SDD] ❌ 已配置的插件不存在:$script_name"
89
+ failed=$((failed + 1))
75
90
  continue
76
91
  fi
77
92
  executed=$((executed + 1))
@@ -79,11 +94,11 @@ for script_name in $SCRIPTS; do
79
94
  node "$script" "$1" --project="$git_root" || failed=$((failed + 1))
80
95
  done
81
96
 
82
- if [ "$executed" -eq 0 ]; then
83
- echo "[SDD] 无 commit-msg 插件,跳过"
84
- elif [ "$failed" -gt 0 ]; then
97
+ if [ "$failed" -gt 0 ]; then
85
98
  echo "[SDD] ❌ 共执行 $executed 个 commit-msg 插件,$failed 个失败"
86
99
  exit 1
100
+ elif [ "$executed" -eq 0 ]; then
101
+ echo "[SDD] 配置已禁用 commit-msg 插件"
87
102
  else
88
103
  echo "[SDD] ✅ 共执行 $executed 个 commit-msg 插件,全部通过"
89
104
  fi