kld-sdd 2.6.21 → 2.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +12 -5
  2. package/USABILITY.md +84 -0
  3. package/bin/kld-sdd-init.js +3 -9
  4. package/kld-sdd-guide.html +16 -4
  5. package/lib/hook-gate-core.js +327 -0
  6. package/lib/init.js +241 -94
  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 +49 -16
  12. package/skywalk-sdd/kb-sync-identity.cjs +99 -451
  13. package/skywalk-sdd/kb-upload.cjs +67 -44
  14. package/skywalk-sdd/lib/git-identity.cjs +270 -0
  15. package/skywalk-sdd/lib/usage-contract.cjs +13 -81
  16. package/skywalk-sdd/lib/usage-reporter.cjs +235 -104
  17. package/skywalk-sdd/lib/user-config.cjs +21 -46
  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 +8 -6
  22. package/skywalk-sdd/ontology/id.cjs +5 -4
  23. package/skywalk-sdd/ontology/identity-index.cjs +4 -3
  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 +71 -27
  31. package/templates/git-hooks/consistency-check-core.cjs +1087 -0
  32. package/templates/git-hooks/hooks.config +38 -0
  33. package/templates/git-hooks/pre-commit +62 -27
  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 +70 -27
  37. package/templates/git-hooks/pre-push-consistency-check.cjs +61 -299
  38. package/templates/hooks/codebuddy/hooks/sdd-tdd-rhythm-gate.cjs +1 -1
  39. package/templates/openspec/tasks.md +3 -3
  40. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +43 -6
  41. package/templates/skills/kld-sdd/opsx-apply/checklist.md +1 -1
  42. package/templates/skills/kld-sdd/opsx-apply/reference.md +1 -1
  43. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +9 -13
  44. package/templates/skills/kld-sdd/opsx-check/SKILL.md +54 -328
  45. package/templates/skills/kld-sdd/opsx-check/checklist.md +7 -4
  46. package/templates/skills/kld-sdd/opsx-check/reference.md +58 -0
  47. package/templates/skills/kld-sdd/opsx-check/result-template.json +52 -0
  48. package/templates/skills/kld-sdd/opsx-check/reviewer.md +100 -0
  49. package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +82 -451
  50. package/templates/skills/kld-sdd/opsx-consistency-check/{reference.md → references/reference.md} +0 -1
  51. package/templates/skills/kld-sdd/opsx-consistency-check/scripts/scripts.cjs +517 -0
  52. package/templates/skills/kld-sdd/opsx-design/SKILL.md +10 -30
  53. package/templates/skills/kld-sdd/opsx-design/checklist.md +3 -4
  54. package/templates/skills/kld-sdd/opsx-design/reference.md +1 -1
  55. package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +12 -74
  56. package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +7 -5
  57. package/templates/skills/kld-sdd/opsx-ontology-query/phase-1-prechange.md +2 -2
  58. package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +1 -1
  59. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +28 -73
  60. package/templates/skills/kld-sdd/opsx-propose/checklist.md +8 -8
  61. package/templates/skills/kld-sdd/opsx-propose/interaction-policy.md +28 -0
  62. package/templates/skills/kld-sdd/opsx-propose/reference.md +12 -46
  63. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +14 -61
  64. package/templates/skills/kld-sdd/opsx-spec/checklist.md +3 -3
  65. package/templates/skills/kld-sdd/opsx-task/SKILL.md +38 -32
  66. package/templates/skills/kld-sdd/opsx-task/checklist.md +5 -6
  67. package/templates/skills/kld-sdd/opsx-test/SKILL.md +2 -0
  68. package/templates/skills/kld-sdd/tdd-rules/rules/tdd-strategy-selection.md +1 -1
  69. package/lib/device-auth-cli.js +0 -130
  70. package/lib/device-auth.js +0 -345
  71. package/lib/init-account-binding.js +0 -108
@@ -3,7 +3,7 @@
3
3
  const crypto = require('crypto');
4
4
  const { ID_PREFIX_TYPES, entityTypeForId } = require('./schema.cjs');
5
5
 
6
- const ID_BYTE_LENGTH = 4;
6
+ const ID_BYTE_LENGTH = 16;
7
7
  const ID_HEX_LENGTH = ID_BYTE_LENGTH * 2;
8
8
 
9
9
  function normalizeSlug(value) {
@@ -52,15 +52,16 @@ function isValidEntityId(entityId, expectedType = '') {
52
52
  }
53
53
 
54
54
  function generateIdentityId() {
55
- return crypto.randomBytes(ID_BYTE_LENGTH).toString('hex');
55
+ return crypto.randomUUID();
56
56
  }
57
57
 
58
58
  function generateUuidV7() {
59
+ // Compatibility name retained for existing callers; new identities are standard UUIDs.
59
60
  return generateIdentityId();
60
61
  }
61
62
 
62
63
  /**
63
- * Normalize a UUID value to the internal 8-hex short format.
64
+ * Preserve full UUIDs; collapse only the legacy zero-padded 8-hex representation.
64
65
  *
65
66
  * KB backend (PostgreSQL) serializes UUID columns as full 36-char format
66
67
  * (e.g. "3518f551-0000-0000-0000-000000000000"). The SDD toolchain uses
@@ -69,7 +70,7 @@ function generateUuidV7() {
69
70
  */
70
71
  function normalizeUuid(value) {
71
72
  const v = String(value || '').trim().toLowerCase();
72
- const fullUuidMatch = v.match(/^([0-9a-f]{8})-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
73
+ const fullUuidMatch = v.match(/^([0-9a-f]{8})-0000-0000-0000-000000000000$/);
73
74
  if (fullUuidMatch) {
74
75
  return fullUuidMatch[1];
75
76
  }
@@ -168,11 +168,11 @@ function buildCatalogDiagnostics(records) {
168
168
  const byPredecessor = groupBy(records, 'predecessor_version_id');
169
169
 
170
170
  for (const [entityId, items] of byEntityId.entries()) {
171
- const signatures = new Set(items.map((item) => `${item.anchor_id}|${item.type}`));
171
+ const signatures = new Set(items.map((item) => item.type));
172
172
  if (signatures.size > 1) {
173
173
  diagnostics.push(catalogDiagnostic(
174
174
  DIAGNOSTIC_CODES.ENTITY_IDENTITY_CONFLICT,
175
- `历史集合中同一 entity-id 对应多个锚点或类型: ${entityId}`,
175
+ `历史集合中同一 entity-id 对应多个类型: ${entityId}`,
176
176
  { entity_id: items[0].anchor_id, file: items[0].source && items[0].source.file },
177
177
  ));
178
178
  }
@@ -206,7 +206,8 @@ function buildCatalogDiagnostics(records) {
206
206
  diagnostics.push(catalogDiagnostic(
207
207
  DIAGNOSTIC_CODES.VERSION_LINEAGE_CONFLICT,
208
208
  `历史集合中同一 predecessor 存在多个后继: ${predecessorId}`,
209
- { entity_id: items[0].anchor_id, file: items[0].source && items[0].source.file },
209
+ { severity: items.every(item => item.scope === "active") ? "warning" : "error",
210
+ entity_id: items[0].anchor_id, file: items[0].source && items[0].source.file },
210
211
  ));
211
212
  }
212
213
  }
@@ -15,7 +15,7 @@ function listActiveChanges(projectRoot) {
15
15
  const full = path.join(changesDir, name);
16
16
  return fs.statSync(full).isDirectory();
17
17
  })
18
- .sort();
18
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
19
19
  }
20
20
 
21
21
  function describeChange(projectRoot, changeName, registry) {
@@ -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
@@ -1,46 +1,90 @@
1
1
  #!/bin/sh
2
2
  # KLD SDD Commit-MSG Hook
3
- # 自动查找 commit-msg-sdd-trailer.cjs,兼容单仓库/多仓库/单仓mono布局
3
+ # 在下方 SCRIPTS 列表中声明要加载的 .cjs 脚本
4
+ # 删除某行 = 禁用该插件
4
5
  # marker: KLD SDD quality gate
5
6
 
6
- # hook 所在目录推算 git 根目录
7
- hook_dir="$(cd "$(dirname "$0")" && pwd)"
8
- git_dir="$(dirname "$hook_dir")"
9
- git_root="$(dirname "$git_dir")"
10
-
11
- script=""
12
-
13
- # 方法1: git config sdd.specPath(多仓布局:spec 是独立 clone)
14
- spec_path=$(git config --local sdd.specPath 2>/dev/null)
15
- if [ -n "$spec_path" ] && [ -f "$spec_path/skywalk-sdd/git-hooks/commit-msg-sdd-trailer.cjs" ]; then
16
- script="$spec_path/skywalk-sdd/git-hooks/commit-msg-sdd-trailer.cjs"
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
+ hooks_dir=""
13
+ spec_path=$(git -C "$git_root" config --local sdd.specPath 2>/dev/null)
14
+ if [ -n "$spec_path" ]; then
15
+ case "$spec_path" in /*) ;; *) spec_path="$git_root/$spec_path" ;; esac
16
+ hooks_dir="$spec_path/skywalk-sdd/git-hooks"
17
+ elif [ -f "$git_root/.sdd-spec-root" ]; then
18
+ spec_rel=$(head -1 "$git_root/.sdd-spec-root" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
19
+ case "$spec_rel" in /*) hooks_dir="$spec_rel/skywalk-sdd/git-hooks" ;; *) hooks_dir="$git_root/$spec_rel/skywalk-sdd/git-hooks" ;; esac
17
20
  fi
18
-
19
- # 方法2: .sdd-spec-root(单仓 mono 布局:skywalk-sdd 在 spec 包裹包子目录内)
20
- if [ -z "$script" ] && [ -f "$git_root/.sdd-spec-root" ]; then
21
- spec_rel=$(cat "$git_root/.sdd-spec-root" | head -1 | tr -d '[:space:]')
22
- if [ -n "$spec_rel" ] && [ -f "$git_root/$spec_rel/skywalk-sdd/git-hooks/commit-msg-sdd-trailer.cjs" ]; then
23
- script="$git_root/$spec_rel/skywalk-sdd/git-hooks/commit-msg-sdd-trailer.cjs"
24
- fi
21
+ if [ -n "$hooks_dir" ] && [ ! -d "$hooks_dir" ]; then
22
+ echo "[SDD] 已配置的 Spec 路径失效:$hooks_dir"
23
+ echo "[SDD] 请恢复 Spec 仓,或修正 sdd.specPath / .sdd-spec-root 后重试"
24
+ exit 1
25
25
  fi
26
26
 
27
27
  # 方法3: 从 git 根向上搜索 skywalk-sdd/git-hooks(spec 仓本身或父级)
28
- if [ -z "$script" ]; then
28
+ if [ -z "$hooks_dir" ]; then
29
29
  search_dir="$git_root"
30
30
  while [ "$search_dir" != "/" ] && [ "$search_dir" != "" ]; do
31
- if [ -f "$search_dir/skywalk-sdd/git-hooks/commit-msg-sdd-trailer.cjs" ]; then
32
- script="$search_dir/skywalk-sdd/git-hooks/commit-msg-sdd-trailer.cjs"
31
+ if [ -d "$search_dir/skywalk-sdd/git-hooks" ]; then
32
+ hooks_dir="$search_dir/skywalk-sdd/git-hooks"
33
33
  break
34
34
  fi
35
35
  search_dir="$(dirname "$search_dir")"
36
36
  done
37
37
  fi
38
38
 
39
- if [ -z "$script" ]; then
40
- echo "[SDD] 未找到 commit-msg-sdd-trailer.cjs,跳过 commit-msg trailer"
39
+ if [ -z "$hooks_dir" ]; then
40
+ echo "[SDD] 未找到 spec 仓的 skywalk-sdd/git-hooks 目录,跳过 commit-msg"
41
41
  exit 0
42
42
  fi
43
43
 
44
- # $1 = Git 传入的 commit message 文件路径
45
- # --project 告诉 .cjs 脚本代码仓根目录(用于解析 spec 布局)
46
- node "$script" "$1" --project="$git_root"
44
+ # 从配置文件读取插件列表
45
+ config_file="$hooks_dir/hooks.config"
46
+ if [ ! -f "$config_file" ]; then
47
+ echo "[SDD] ⚠️ 配置文件不存在:hooks.config,使用默认插件"
48
+ SCRIPTS="commit-msg-sdd-trailer.cjs"
49
+ else
50
+ # 读取 [commit-msg] 段落的配置
51
+ in_section=0
52
+ SCRIPTS=""
53
+ CR=$(printf '\r')
54
+ while IFS= read -r line; do
55
+ line=${line%"$CR"}
56
+ case "$line" in
57
+ "[commit-msg]") in_section=1 ;;
58
+ "["*) in_section=0 ;;
59
+ "#"*|"") ;;
60
+ *)
61
+ if [ "$in_section" -eq 1 ]; then
62
+ SCRIPTS="$SCRIPTS $line"
63
+ fi
64
+ ;;
65
+ esac
66
+ done < "$config_file"
67
+ fi
68
+
69
+ executed=0
70
+ failed=0
71
+ for script_name in $SCRIPTS; do
72
+ script="$hooks_dir/$script_name"
73
+ if [ ! -f "$script" ]; then
74
+ echo "[SDD] ❌ 已配置的插件不存在:$script_name"
75
+ failed=$((failed + 1))
76
+ continue
77
+ fi
78
+ executed=$((executed + 1))
79
+ echo "[SDD] 执行 commit-msg 插件:$script_name"
80
+ node "$script" "$1" --project="$git_root" || failed=$((failed + 1))
81
+ done
82
+
83
+ if [ "$failed" -gt 0 ]; then
84
+ echo "[SDD] ❌ 共执行 $executed 个 commit-msg 插件,$failed 个失败"
85
+ exit 1
86
+ elif [ "$executed" -eq 0 ]; then
87
+ echo "[SDD] 配置已禁用 commit-msg 插件"
88
+ else
89
+ echo "[SDD] ✅ 共执行 $executed 个 commit-msg 插件,全部通过"
90
+ fi