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.
- package/README.md +12 -5
- package/USABILITY.md +104 -0
- package/bin/kld-sdd-init.js +1 -1
- package/kld-sdd-guide.html +16 -4
- package/lib/hook-gate-core.js +327 -0
- package/lib/init.js +228 -37
- package/lib/scale-thresholds.json +19 -0
- package/lib/skills-bundle.js +2 -1
- package/package.json +5 -3
- package/skywalk-sdd/context-client.cjs +50 -30
- package/skywalk-sdd/index.cjs +159 -40
- package/skywalk-sdd/kb-sync-identity.cjs +99 -451
- package/skywalk-sdd/kb-upload.cjs +67 -44
- package/skywalk-sdd/lib/check-review.cjs +41 -0
- package/skywalk-sdd/lib/test-execution.cjs +110 -0
- package/skywalk-sdd/lib/usage-contract.cjs +3 -2
- package/skywalk-sdd/lib/usage-reporter.cjs +27 -2
- package/skywalk-sdd/metrics-v3.cjs +2 -2
- package/skywalk-sdd/ontology/active-changes.cjs +2 -1
- package/skywalk-sdd/ontology/archive-package.cjs +1 -1
- package/skywalk-sdd/ontology/artifact-parser.cjs +7 -4
- package/skywalk-sdd/ontology/id.cjs +5 -4
- package/skywalk-sdd/ontology/identity-index.cjs +9 -4
- package/skywalk-sdd/ontology/list-changes.cjs +1 -1
- package/skywalk-sdd/ontology/runtime.cjs +7 -3
- package/skywalk-sdd/ontology/schema.cjs +2 -0
- package/skywalk-sdd/ontology/traceability-validator.cjs +74 -4
- package/skywalk-sdd/ontology/workspace-layout.cjs +25 -5
- package/skywalk-sdd/reporting/change-report-model.cjs +4 -3
- package/templates/git-hooks/commit-msg +39 -24
- package/templates/git-hooks/consistency-check-core.cjs +1097 -0
- package/templates/git-hooks/hooks.config +20 -1
- package/templates/git-hooks/pre-commit +39 -24
- package/templates/git-hooks/pre-commit-consistency-check.cjs +29 -332
- package/templates/git-hooks/pre-commit-sdd-check.cjs +98 -0
- package/templates/git-hooks/pre-push +39 -24
- package/templates/git-hooks/pre-push-consistency-check.cjs +58 -406
- package/templates/hooks/claude/hooks/sdd-post-tool.cjs +2 -2
- package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +2 -2
- package/templates/hooks/codebuddy/hooks/sdd-tdd-rhythm-gate.cjs +1 -1
- package/templates/openspec/tasks.md +3 -3
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +44 -6
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +1 -1
- package/templates/skills/kld-sdd/opsx-apply/reference.md +20 -2
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +19 -21
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +55 -327
- package/templates/skills/kld-sdd/opsx-check/checklist.md +7 -4
- package/templates/skills/kld-sdd/opsx-check/reference.md +60 -0
- package/templates/skills/kld-sdd/opsx-check/result-template.json +52 -0
- package/templates/skills/kld-sdd/opsx-check/review-template.json +22 -0
- package/templates/skills/kld-sdd/opsx-check/reviewer.md +100 -0
- package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +82 -451
- package/templates/skills/kld-sdd/opsx-consistency-check/{reference.md → references/reference.md} +0 -1
- package/templates/skills/kld-sdd/opsx-consistency-check/scripts/scripts.cjs +517 -0
- package/templates/skills/kld-sdd/opsx-design/SKILL.md +10 -30
- package/templates/skills/kld-sdd/opsx-design/checklist.md +3 -4
- package/templates/skills/kld-sdd/opsx-design/reference.md +1 -1
- package/templates/skills/kld-sdd/opsx-kb-config/SKILL.md +29 -154
- package/templates/skills/kld-sdd/opsx-kb-config/reference.md +8 -11
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +12 -74
- package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +2 -2
- package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +7 -5
- package/templates/skills/kld-sdd/opsx-ontology-query/phase-1-prechange.md +2 -2
- package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +1 -1
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +29 -74
- package/templates/skills/kld-sdd/opsx-propose/checklist.md +8 -8
- package/templates/skills/kld-sdd/opsx-propose/interaction-policy.md +35 -0
- package/templates/skills/kld-sdd/opsx-propose/reference.md +12 -46
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +14 -61
- package/templates/skills/kld-sdd/opsx-spec/checklist.md +3 -3
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +40 -34
- package/templates/skills/kld-sdd/opsx-task/checklist.md +5 -6
- package/templates/skills/kld-sdd/opsx-test/SKILL.md +2 -0
- 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
|
|
421
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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"
|
|
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
|
-
|
|
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=$
|
|
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]
|
|
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 [ "$
|
|
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
|