kld-sdd 2.6.6 → 2.6.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.
- package/bin/kld-sdd-init.js +39 -3
- package/lib/init.js +320 -45
- package/lib/workspace-layout.js +2 -0
- package/package.json +2 -2
- package/skywalk-sdd/context-client.cjs +12 -1
- package/skywalk-sdd/ontology/active-changes.cjs +297 -0
- package/skywalk-sdd/ontology/archive-package.cjs +95 -6
- package/skywalk-sdd/ontology/artifact-parser.cjs +50 -7
- package/skywalk-sdd/ontology/change-key.cjs +241 -0
- package/skywalk-sdd/ontology/cli.cjs +154 -0
- package/skywalk-sdd/ontology/external-key.cjs +159 -0
- package/skywalk-sdd/ontology/list-changes.cjs +110 -0
- package/skywalk-sdd/ontology/modules.cjs +167 -0
- package/skywalk-sdd/ontology/naming-diagnose.cjs +594 -0
- package/skywalk-sdd/ontology/normalizer.cjs +4 -0
- package/skywalk-sdd/ontology/schema.cjs +5 -0
- package/skywalk-sdd/ontology/sdd-config.cjs +335 -0
- package/skywalk-sdd/ontology/traceability-validator.cjs +175 -0
- package/skywalk-sdd/ontology/workspace-layout.cjs +194 -0
- package/templates/dot-sdd.yaml +8 -0
- package/templates/git-hooks/commit-msg-sdd-trailer.cjs +224 -0
- package/templates/modules.yaml +13 -0
- package/templates/openspec/proposal.md +12 -3
- package/templates/openspec/spec.md +3 -3
- package/templates/sdd.config.yaml +12 -0
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +3 -3
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +1 -1
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +10 -0
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +47 -3
- package/templates/skills/kld-sdd/opsx-explore/SKILL.md +37 -17
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +13 -14
- package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +13 -0
- package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +26 -15
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +61 -23
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +8 -2
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +1 -0
- package/templates/skills/kld-sdd/opsx-tdd-core/checklist.md +1 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const WORKSPACE_FILE = '.sdd-workspace.yaml';
|
|
7
|
+
const IGNORE_DIR_NAMES = new Set([
|
|
8
|
+
'node_modules',
|
|
9
|
+
'.git',
|
|
10
|
+
'.claude',
|
|
11
|
+
'.codebuddy',
|
|
12
|
+
'.kunlunzhima',
|
|
13
|
+
'.cursor',
|
|
14
|
+
'.opencode',
|
|
15
|
+
'skywalk-sdd',
|
|
16
|
+
'openspec',
|
|
17
|
+
'openspec-templates',
|
|
18
|
+
'dist',
|
|
19
|
+
'build',
|
|
20
|
+
'coverage',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function isGitRepo(dir) {
|
|
24
|
+
return fs.existsSync(path.join(dir, '.git'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function looksLikeSpecRepo(dir) {
|
|
28
|
+
const name = path.basename(dir).toLowerCase();
|
|
29
|
+
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;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function listChildGitRepos(workspaceRoot) {
|
|
37
|
+
if (!fs.existsSync(workspaceRoot)) return [];
|
|
38
|
+
return fs.readdirSync(workspaceRoot)
|
|
39
|
+
.filter((name) => !name.startsWith('.') && !IGNORE_DIR_NAMES.has(name))
|
|
40
|
+
.map((name) => ({ name, abs: path.join(workspaceRoot, name) }))
|
|
41
|
+
.filter((entry) => {
|
|
42
|
+
try {
|
|
43
|
+
return fs.statSync(entry.abs).isDirectory() && isGitRepo(entry.abs);
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Detect personal workspace layout: cwd hosts multiple/sibling git repos,
|
|
53
|
+
* and skills should live once at workspace root (not in every code repo).
|
|
54
|
+
*/
|
|
55
|
+
function detectWorkspaceLayout(workspaceRoot = process.cwd()) {
|
|
56
|
+
const root = path.resolve(workspaceRoot);
|
|
57
|
+
const children = listChildGitRepos(root);
|
|
58
|
+
const rootIsGit = isGitRepo(root);
|
|
59
|
+
const rootLooksSpec = looksLikeSpecRepo(root);
|
|
60
|
+
|
|
61
|
+
const specCandidates = children.filter((c) => looksLikeSpecRepo(c.abs));
|
|
62
|
+
let spec = null;
|
|
63
|
+
if (specCandidates.length === 1) {
|
|
64
|
+
spec = specCandidates[0];
|
|
65
|
+
} else if (specCandidates.length > 1) {
|
|
66
|
+
spec = specCandidates.find((c) => fs.existsSync(path.join(c.abs, 'modules.yaml')))
|
|
67
|
+
|| specCandidates.find((c) => /-sdd-specs$/i.test(c.name))
|
|
68
|
+
|| specCandidates[0];
|
|
69
|
+
} else if (rootLooksSpec && rootIsGit) {
|
|
70
|
+
spec = { name: path.basename(root), abs: root };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const codeRepos = children.filter((c) => !spec || c.abs !== spec.abs);
|
|
74
|
+
|
|
75
|
+
const isWorkspace = (!rootIsGit && children.length >= 1)
|
|
76
|
+
|| (children.length >= 2)
|
|
77
|
+
|| (Boolean(spec) && codeRepos.length >= 1 && !rootLooksSpec);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
root,
|
|
81
|
+
isWorkspace: Boolean(isWorkspace),
|
|
82
|
+
rootIsGit,
|
|
83
|
+
specRepo: spec,
|
|
84
|
+
codeRepos,
|
|
85
|
+
allChildGitRepos: children,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseWorkspaceYaml(text) {
|
|
90
|
+
const result = {
|
|
91
|
+
version: 1,
|
|
92
|
+
spec_path: '',
|
|
93
|
+
code_repos: [],
|
|
94
|
+
};
|
|
95
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
96
|
+
let inCode = false;
|
|
97
|
+
for (const raw of lines) {
|
|
98
|
+
const line = raw.replace(/#.*$/, '').replace(/\s+$/, '');
|
|
99
|
+
if (!line.trim()) continue;
|
|
100
|
+
const version = /^version:\s*(\d+)\s*$/.exec(line);
|
|
101
|
+
if (version) {
|
|
102
|
+
result.version = Number(version[1]);
|
|
103
|
+
inCode = false;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const spec = /^spec_path:\s*(.+?)\s*$/.exec(line);
|
|
107
|
+
if (spec) {
|
|
108
|
+
let value = spec[1].trim();
|
|
109
|
+
if (
|
|
110
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
111
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
112
|
+
) {
|
|
113
|
+
value = value.slice(1, -1);
|
|
114
|
+
}
|
|
115
|
+
result.spec_path = value;
|
|
116
|
+
inCode = false;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (/^code_repos:\s*$/.test(line)) {
|
|
120
|
+
inCode = true;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (inCode) {
|
|
124
|
+
const item = /^\s*-\s+(.+?)\s*$/.exec(line);
|
|
125
|
+
if (item) {
|
|
126
|
+
result.code_repos.push(item[1].trim());
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (/^\S/.test(line)) inCode = false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function loadWorkspaceFile(workspaceRoot) {
|
|
136
|
+
const filePath = path.join(workspaceRoot, WORKSPACE_FILE);
|
|
137
|
+
if (!fs.existsSync(filePath)) {
|
|
138
|
+
return { ok: false, path: filePath, data: null };
|
|
139
|
+
}
|
|
140
|
+
const data = parseWorkspaceYaml(fs.readFileSync(filePath, 'utf8'));
|
|
141
|
+
return { ok: true, path: filePath, data };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function writeWorkspaceFile(workspaceRoot, layout) {
|
|
145
|
+
const specRel = layout.specRepo
|
|
146
|
+
? path.relative(workspaceRoot, layout.specRepo.abs) || '.'
|
|
147
|
+
: '';
|
|
148
|
+
const codeRels = (layout.codeRepos || []).map((c) =>
|
|
149
|
+
path.relative(workspaceRoot, c.abs) || c.name,
|
|
150
|
+
);
|
|
151
|
+
const lines = [
|
|
152
|
+
'# 个人工作目录编排清单(不替代各仓 .sdd.yaml)',
|
|
153
|
+
'# skills 只部署在本工作目录;代码仓仅挂 Hook / 关联 spec。',
|
|
154
|
+
'version: 1',
|
|
155
|
+
`spec_path: ${specRel}`,
|
|
156
|
+
'code_repos:',
|
|
157
|
+
...codeRels.map((name) => ` - ${name}`),
|
|
158
|
+
'',
|
|
159
|
+
];
|
|
160
|
+
const filePath = path.join(workspaceRoot, WORKSPACE_FILE);
|
|
161
|
+
fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
|
|
162
|
+
return filePath;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Compare discovery with saved workspace file; return newly found code repos.
|
|
167
|
+
*/
|
|
168
|
+
function findNewCodeRepos(workspaceRoot) {
|
|
169
|
+
const layout = detectWorkspaceLayout(workspaceRoot);
|
|
170
|
+
const saved = loadWorkspaceFile(workspaceRoot);
|
|
171
|
+
const known = new Set(
|
|
172
|
+
saved.ok && saved.data
|
|
173
|
+
? saved.data.code_repos.map((item) => path.resolve(workspaceRoot, item))
|
|
174
|
+
: [],
|
|
175
|
+
);
|
|
176
|
+
const newcomers = layout.codeRepos.filter((c) => !known.has(path.resolve(c.abs)));
|
|
177
|
+
return {
|
|
178
|
+
layout,
|
|
179
|
+
saved,
|
|
180
|
+
newcomers,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
WORKSPACE_FILE,
|
|
186
|
+
isGitRepo,
|
|
187
|
+
looksLikeSpecRepo,
|
|
188
|
+
listChildGitRepos,
|
|
189
|
+
detectWorkspaceLayout,
|
|
190
|
+
parseWorkspaceYaml,
|
|
191
|
+
loadWorkspaceFile,
|
|
192
|
+
writeWorkspaceFile,
|
|
193
|
+
findNewCodeRepos,
|
|
194
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* commit-msg hook,两种布局:
|
|
6
|
+
*
|
|
7
|
+
* - 多仓(代码仓有 .sdd.yaml,spec 是另一个 clone):
|
|
8
|
+
* 写 Spec-Revision(spec 干净 HEAD 的完整 SHA)+ Spec-Change(活动 change,可多条)。
|
|
9
|
+
* - 单仓(openspec 与代码同仓):
|
|
10
|
+
* spec 与代码在同一次 commit 里,指针自指无意义 → 只写 Spec-Change,不写 Spec-Revision,
|
|
11
|
+
* 且不做「spec 工作区干净」检查(提交进行中必然是脏的)。
|
|
12
|
+
*
|
|
13
|
+
* Spec-Change 来源:KLD_SDD_CHANGE(显式覆盖)> sdd.config.yaml active_changes。
|
|
14
|
+
* Not from branch name, ever.
|
|
15
|
+
*
|
|
16
|
+
* Authority: 10-最小化多仓库关联与变更命名方案.md
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
function resolveOntologyDir() {
|
|
23
|
+
const deployed = path.join(__dirname, '..', 'ontology');
|
|
24
|
+
if (fs.existsSync(path.join(deployed, 'change-key.cjs'))) return deployed;
|
|
25
|
+
const fromPackageTemplates = path.join(__dirname, '..', '..', 'skywalk-sdd', 'ontology');
|
|
26
|
+
if (fs.existsSync(path.join(fromPackageTemplates, 'change-key.cjs'))) return fromPackageTemplates;
|
|
27
|
+
throw new Error('无法定位 skywalk-sdd/ontology');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ontologyDir = resolveOntologyDir();
|
|
31
|
+
const changeKey = require(path.join(ontologyDir, 'change-key.cjs'));
|
|
32
|
+
const sddConfig = require(path.join(ontologyDir, 'sdd-config.cjs'));
|
|
33
|
+
const activeChanges = require(path.join(ontologyDir, 'active-changes.cjs'));
|
|
34
|
+
const { parseFrontmatter } = require(path.join(ontologyDir, 'artifact-parser.cjs'));
|
|
35
|
+
|
|
36
|
+
function projectRootFromHook() {
|
|
37
|
+
const explicit = process.argv.find((arg) => arg.startsWith('--project='));
|
|
38
|
+
if (explicit) return path.resolve(explicit.slice('--project='.length));
|
|
39
|
+
return path.resolve(__dirname, '..', '..');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fail(message) {
|
|
43
|
+
console.error(`[kld-sdd commit-msg] ${message}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isSddLinkedCodeRepo(codeRepoRoot) {
|
|
48
|
+
return fs.existsSync(path.join(codeRepoRoot, '.sdd.yaml'));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 决定本次提交按哪种布局处理。布局来自 .sdd.yaml 显式声明,不做启发式推断
|
|
53
|
+
* (否则单独诊断的 spec 仓 clone 会被误判成单仓)。
|
|
54
|
+
*
|
|
55
|
+
* 单仓优先于 specPath:即使用户把 sdd.specPath 指回本仓,也走单仓路径,
|
|
56
|
+
* 否则会因为「spec 工作区不干净」永久阻断提交。
|
|
57
|
+
*/
|
|
58
|
+
function resolveLayout(repoRoot, env = process.env) {
|
|
59
|
+
if (!isSddLinkedCodeRepo(repoRoot)) {
|
|
60
|
+
return { mode: 'unlinked' };
|
|
61
|
+
}
|
|
62
|
+
if (sddConfig.isMonoLayout(repoRoot)) {
|
|
63
|
+
return { mode: 'mono', specPath: repoRoot };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const context = sddConfig.resolveCodeRepoContext(repoRoot, env);
|
|
67
|
+
if (!context.specPath) {
|
|
68
|
+
fail(context.message || '无法解析 spec clone 路径。请执行 kld-sdd link-spec --path=<spec>');
|
|
69
|
+
}
|
|
70
|
+
if (context.code === sddConfig.CODES.SPEC_PATH_INVALID) {
|
|
71
|
+
fail(context.message);
|
|
72
|
+
}
|
|
73
|
+
// specPath 指回本仓:等同单仓,不能按外部 spec 校验干净度
|
|
74
|
+
if (path.resolve(context.specPath) === path.resolve(repoRoot)) {
|
|
75
|
+
return { mode: 'mono', specPath: repoRoot, context };
|
|
76
|
+
}
|
|
77
|
+
return { mode: 'multi', specPath: context.specPath, context };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Explicit override: KLD_SDD_CHANGE, comma/space separated for several changes.
|
|
82
|
+
* Not from branch name, not from repo-wide git config.
|
|
83
|
+
*/
|
|
84
|
+
function parseEnvChangeKeys(env = process.env) {
|
|
85
|
+
const raw = env.KLD_SDD_CHANGE && String(env.KLD_SDD_CHANGE).trim();
|
|
86
|
+
if (!raw) return null;
|
|
87
|
+
const keys = [];
|
|
88
|
+
for (const token of raw.split(/[,\s]+/).filter(Boolean)) {
|
|
89
|
+
const validated = changeKey.validate(token);
|
|
90
|
+
if (!validated.ok) {
|
|
91
|
+
fail(`KLD_SDD_CHANGE 无效: ${validated.message}`);
|
|
92
|
+
}
|
|
93
|
+
if (!keys.includes(validated.normalized)) keys.push(validated.normalized);
|
|
94
|
+
}
|
|
95
|
+
return keys.length ? keys : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Spec-Change values: explicit env wins, otherwise every active change
|
|
100
|
+
* registered in the spec repo's sdd.config.yaml.
|
|
101
|
+
*/
|
|
102
|
+
function resolveChangeKeys(specRoot, env = process.env) {
|
|
103
|
+
const fromEnv = parseEnvChangeKeys(env);
|
|
104
|
+
if (fromEnv) {
|
|
105
|
+
for (const key of fromEnv) {
|
|
106
|
+
requireChangeDir(specRoot, key, { strict: true });
|
|
107
|
+
}
|
|
108
|
+
return { keys: fromEnv, source: 'env' };
|
|
109
|
+
}
|
|
110
|
+
const registered = activeChanges.activeChangeKeys(specRoot);
|
|
111
|
+
const keys = registered.filter((key) => requireChangeDir(specRoot, key, { strict: false }));
|
|
112
|
+
return { keys, source: 'sdd.config.yaml' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {{ strict: boolean }} options strict=true 阻断提交;false 仅警告并跳过该条
|
|
117
|
+
*/
|
|
118
|
+
function requireChangeDir(specRoot, key, options = { strict: true }) {
|
|
119
|
+
const proposalPath = path.join(specRoot, 'openspec', 'changes', key, 'proposal.md');
|
|
120
|
+
if (!fs.existsSync(proposalPath)) {
|
|
121
|
+
const message = `spec 中不存在 change: openspec/changes/${key}/proposal.md`;
|
|
122
|
+
if (options.strict) fail(message);
|
|
123
|
+
console.error(`[kld-sdd commit-msg] 警告: ${message}(已跳过该 Spec-Change)`);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const lines = fs.readFileSync(proposalPath, 'utf8').split(/\r?\n/);
|
|
127
|
+
const fm = parseFrontmatter(lines);
|
|
128
|
+
const declared = (fm['change-key'] || '').toLowerCase();
|
|
129
|
+
if (declared && declared !== key) {
|
|
130
|
+
const message = `proposal change-key (${declared}) 与目录 (${key}) 不一致`;
|
|
131
|
+
if (options.strict) fail(message);
|
|
132
|
+
console.error(`[kld-sdd commit-msg] 警告: ${message}(已跳过该 Spec-Change)`);
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 重写 Trailer 区块:同名旧行先全部摘除,再作为连续区块追加到末尾。
|
|
140
|
+
*
|
|
141
|
+
* @param {Record<string, string|string[]>} trailers 数组值写成多条同名 Trailer
|
|
142
|
+
*/
|
|
143
|
+
function upsertTrailers(message, trailers) {
|
|
144
|
+
let body = String(message || '').replace(/\s+$/g, '');
|
|
145
|
+
const block = [];
|
|
146
|
+
|
|
147
|
+
for (const [name, value] of Object.entries(trailers)) {
|
|
148
|
+
const linePattern = new RegExp(`^${name}:[^\\n]*\\n?`, 'gim');
|
|
149
|
+
body = body.replace(linePattern, '');
|
|
150
|
+
const values = (Array.isArray(value) ? value : [value]).filter(
|
|
151
|
+
(item) => item != null && String(item).length > 0,
|
|
152
|
+
);
|
|
153
|
+
for (const item of values) {
|
|
154
|
+
block.push(`${name}: ${item}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
body = body.replace(/\s+$/g, '');
|
|
159
|
+
if (block.length) {
|
|
160
|
+
body += `\n\n${block.join('\n')}`;
|
|
161
|
+
}
|
|
162
|
+
body = body.replace(/\n{3,}/g, '\n\n');
|
|
163
|
+
if (!body.endsWith('\n')) body += '\n';
|
|
164
|
+
return body;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function main(env = process.env) {
|
|
168
|
+
const msgFile = process.argv[2];
|
|
169
|
+
if (!msgFile) {
|
|
170
|
+
fail('缺少 commit message 文件参数');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const repoRoot = projectRootFromHook();
|
|
174
|
+
const layout = resolveLayout(repoRoot, env);
|
|
175
|
+
|
|
176
|
+
// 未接入 SDD 的普通代码仓:不写 Trailer
|
|
177
|
+
if (layout.mode === 'unlinked') {
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const trailers = {
|
|
182
|
+
// 多个活动 change 时写多条 Spec-Change
|
|
183
|
+
'Spec-Change': resolveChangeKeys(layout.specPath, env).keys,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
if (layout.mode === 'multi') {
|
|
187
|
+
const context = layout.context;
|
|
188
|
+
// remote mismatch:仍允许写本地 Revision(溯源本地快照);仅警告
|
|
189
|
+
if (context.code === sddConfig.CODES.REMOTE_MISMATCH) {
|
|
190
|
+
console.error(`[kld-sdd commit-msg] 警告: ${context.message}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!sddConfig.gitWorkingTreeClean(layout.specPath)) {
|
|
194
|
+
fail('spec 工作区不干净。请先提交或暂存 spec 变更后再提交代码。');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const revision = sddConfig.gitHeadSha(layout.specPath);
|
|
198
|
+
if (!revision || revision.length < 40) {
|
|
199
|
+
fail(`无法读取完整 Spec-Revision(需要完整 SHA): ${revision}`);
|
|
200
|
+
}
|
|
201
|
+
trailers['Spec-Revision'] = revision;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const original = fs.readFileSync(msgFile, 'utf8');
|
|
205
|
+
const updated = upsertTrailers(original, trailers);
|
|
206
|
+
fs.writeFileSync(msgFile, updated, 'utf8');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (require.main === module) {
|
|
210
|
+
try {
|
|
211
|
+
main();
|
|
212
|
+
} catch (error) {
|
|
213
|
+
fail(error.message || String(error));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = {
|
|
218
|
+
upsertTrailers,
|
|
219
|
+
parseEnvChangeKeys,
|
|
220
|
+
resolveChangeKeys,
|
|
221
|
+
isSddLinkedCodeRepo,
|
|
222
|
+
resolveLayout,
|
|
223
|
+
main,
|
|
224
|
+
};
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
---
|
|
2
2
|
# 【用户选择配置 - 由 /opsx:propose 引导填写】
|
|
3
|
-
change-id: "CHG-<
|
|
3
|
+
change-id: "CHG-<MODULE>-<YYMMDD>-<SLUG>" # 创建时由 change-key 派生一次,后续不得修改
|
|
4
|
+
change-key: "<module>-<yymmdd>-<slug>" # spec 仓目录名;可选写入代码 commit 的 Spec-Change(需 KLD_SDD_CHANGE)
|
|
5
|
+
title: "<中文标题>" # 人读名称,opsx-explore / 知识平台展示
|
|
6
|
+
module: "<module>" # modules.yaml 中的模块代号;跨模块用 cross
|
|
7
|
+
# affected-modules: # 仅 module=cross 时必填,至少两个已注册模块
|
|
8
|
+
# - fi
|
|
9
|
+
# - mm
|
|
4
10
|
entity-id: "<8位十六进制ID>" # Change 的全局逻辑实体 ID,added 时生成
|
|
5
11
|
version-id: "<UUID>" # 本次 Change 版本 UUID
|
|
6
12
|
delta-state: "added"
|
|
7
13
|
predecessor-version: "" # added 留空;modified/removed 指向直接前序版本
|
|
8
14
|
mode: "" # full=分 Capability 产物,simple=根目录精简产物
|
|
9
15
|
test-strategy: "" # tdd=测试先行, impl-first=实现优先, none=无测试
|
|
10
|
-
#
|
|
16
|
+
# 外部需求键(默认必填 ≥1 个合规 REQ;探索性/纯内部重构可走 numbering-waiver)
|
|
11
17
|
requirement-refs:
|
|
12
18
|
- system: requirement-mgmt
|
|
13
19
|
object-type: requirement
|
|
14
|
-
external-id: REQ-<DOMAIN>-<
|
|
20
|
+
external-id: REQ-<DOMAIN>-<YEAR>-<SEQ> # 例 REQ-FI-2024-001
|
|
21
|
+
feature-id: FEAT-<DOMAIN>-<SEQ> # 可选,需求所属功能(需求系统权威)
|
|
22
|
+
# numbering-waiver:
|
|
23
|
+
# reason: "探索性原型,本轮不种桥"
|
|
15
24
|
# Continuity:字段一律来自知识库 resolve,禁止本地 archive 文件夹名
|
|
16
25
|
continuity:
|
|
17
26
|
kind: new # iteration | similar-reference | new
|
|
@@ -34,7 +34,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与 proposal.md 中的 Capability ID
|
|
|
34
34
|
- **version-id**: <UUID>
|
|
35
35
|
- **delta-state**: added
|
|
36
36
|
- **predecessor-version**: 无
|
|
37
|
-
- **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<
|
|
37
|
+
- **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<YEAR>-<SEQ>:SCN-<slug>-<NNN>
|
|
38
38
|
- **当** <!-- 触发条件 -->
|
|
39
39
|
- **预期** <!-- 预期结果 -->
|
|
40
40
|
|
|
@@ -43,7 +43,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与 proposal.md 中的 Capability ID
|
|
|
43
43
|
- **version-id**: <UUID>
|
|
44
44
|
- **delta-state**: added
|
|
45
45
|
- **predecessor-version**: 无
|
|
46
|
-
- **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<
|
|
46
|
+
- **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<YEAR>-<SEQ>:SCN-<slug>-<NNN>
|
|
47
47
|
- **当** <!-- 触发条件 -->
|
|
48
48
|
- **预期** <!-- 预期结果 -->
|
|
49
49
|
|
|
@@ -63,7 +63,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与 proposal.md 中的 Capability ID
|
|
|
63
63
|
- **version-id**: <新 UUID>
|
|
64
64
|
- **delta-state**: <modified|added>
|
|
65
65
|
- **predecessor-version**: <modified 时填写;added 为无>
|
|
66
|
-
- **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<
|
|
66
|
+
- **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<YEAR>-<SEQ>:SCN-<slug>-<NNN>
|
|
67
67
|
- **当** <!-- 触发条件 -->
|
|
68
68
|
- **预期** <!-- 预期结果 -->
|
|
69
69
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# 活动变更登记表(spec 仓库根目录)
|
|
2
|
+
# 由 opsx-propose 隐式写入、opsx-archive 隐式移除,供代码仓 AI 与 commit-msg Hook 读取。
|
|
3
|
+
# 不登记代码仓清单、路径 glob 或 CI 配置。
|
|
4
|
+
version: 1
|
|
5
|
+
|
|
6
|
+
active_changes:
|
|
7
|
+
# 示例(propose 会自动写入,无需手填):
|
|
8
|
+
# - change-key: fi-260727-account-doc-head-create
|
|
9
|
+
# change-id: CHG-FI-260727-ACCOUNT-DOC-HEAD-CREATE
|
|
10
|
+
# title: 会计凭证头创建
|
|
11
|
+
# module: fi
|
|
12
|
+
# summary: 支持凭证头创建与字段校验
|
|
@@ -183,8 +183,8 @@ openspec list --json
|
|
|
183
183
|
|
|
184
184
|
**执行每个任务**(a-g 步骤):
|
|
185
185
|
|
|
186
|
-
a. **DAG 依赖检查 & 层级收集** —
|
|
187
|
-
b. **显示当前层级任务** — `📍 层级 [X]:准备派发 [K] 个子代理并行处理 [M]
|
|
186
|
+
a. **DAG 依赖检查 & 层级收集** — 收集依赖已满足的待执行任务;同层多个独立任务可并行派发子代理。**⛔ TDD 模式豁免**:当 `test-strategy=tdd` 时,同层的 RED→GREEN 对**不可并行**,必须逐对串行执行(见下方 S2)。仅同层的非 TDD 模块任务(UI/配置/SQL DDL)可并行。
|
|
187
|
+
b. **显示当前层级任务** — `📍 层级 [X]:准备派发 [K] 个子代理并行处理 [M] 个任务`。**TDD 模式下**:若本层含 RED→GREEN 对,显示 `📍 层级 [X]:[M] 个 RED→GREEN 对须逐对串行执行(TDD 串行约束),[K] 个非 TDD 任务可并行`。
|
|
188
188
|
c. **🤖 派发子代理实现代码** — 使用 Agent 工具并行派发同层任务,⛔ 子代理阶段不创建 worktree/分支。**派发模板与状态处理见 `./reference.md`「§5c 子代理派发模板」+ `./implementer-prompt.md`**。
|
|
189
189
|
d. **⛔ 编译检查门禁** — 每完成一个任务后必须编译通过;**详细见 `./checklist.md`「§5d 编译检查门禁」**。
|
|
190
190
|
e. **⛔ 测试执行门禁** — 按 `proposal.md` 的 `test-strategy` 决定(tdd=强制, impl-first=警告, none=跳过);**详细见 `./checklist.md`「§5e 测试执行门禁」**。
|
|
@@ -285,7 +285,7 @@ g. **继续下一个层级** — 重新检查 DAG,找出依赖已满足的下
|
|
|
285
285
|
- **⛔ 必须实时更新任务状态**:每完成一个任务立即改 tasks.md,两种格式同步。
|
|
286
286
|
- **⛔ apply 结束前 checkbox 全量同步校验**:`stage_end` 前对比 telemetry `task_update` 记录数与 tasks.md `[x]` 数量,不一致则补齐(见 `./checklist.md` §5f.1)。
|
|
287
287
|
- **⛔ task_update 后必须验证 checkbox 已更新**:执行 `check-task` 确认 tasks.md 对应行已变更;未更新则手动修改。
|
|
288
|
-
- **⛔ TDD RED→GREEN 严格串行**:不适用同层并行派发;RED-N 确认失败后必须执行中断声明再进入 GREEN-N;GREEN 完成后必须通过 Scope 门禁再进入下一个 RED
|
|
288
|
+
- **⛔ TDD RED→GREEN 严格串行**:不适用同层并行派发;RED-N 确认失败后必须执行中断声明再进入 GREEN-N;GREEN 完成后必须通过 Scope 门禁再进入下一个 RED。同层存在多个 RED→GREEN 对时,必须逐对串行完成,禁止并行派发子代理处理多个 RED→GREEN 对。同层非 TDD 模块任务(UI/配置/SQL DDL)仍可并行。
|
|
289
289
|
- **Git 只读策略**:禁止为了度量自动初始化 Git、创建分支或提交 commit;非 Git 项目用 `vcs_mode=no-git` 继续执行。
|
|
290
290
|
- **⛔ Step 0.1 隔离校验必做**:建 worktree / 建议分支名前必须完成 proposal + 跨 cap spec 依赖校验并输出报告。
|
|
291
291
|
- **Worktree 为加速手段,非必选项**:校验通过且解耦方可多 worktree;有依赖或共享修改面则串行。
|
|
@@ -130,7 +130,7 @@ description: opsx-apply 的阶段强制检查点与自检清单。仅在执行 a
|
|
|
130
130
|
- [ ] ⛔ **必须实时更新任务状态**:每完成一个任务立即改 tasks.md,两种格式(`- [ ]`→`- [x]` 与 `**状态**: [ ]`→`[x]`)同步
|
|
131
131
|
- [ ] ⛔ **apply 结束前 checkbox 全量同步校验**:见 §5f.1,`stage_end` 前对比 telemetry `task_update` 记录数与 tasks.md `[x]` 数量
|
|
132
132
|
- [ ] ⛔ **task_update 后必须验证 checkbox 已更新**:执行 `check-task` 确认 tasks.md 对应行已变更;未更新则手动修改
|
|
133
|
-
- [ ] ⛔ **TDD RED→GREEN 严格串行**:不适用同层并行派发;RED-N 确认失败后必须执行中断声明再进入 GREEN-N;GREEN 完成后必须通过 Scope 门禁再进入下一个 RED
|
|
133
|
+
- [ ] ⛔ **TDD RED→GREEN 严格串行**:不适用同层并行派发;RED-N 确认失败后必须执行中断声明再进入 GREEN-N;GREEN 完成后必须通过 Scope 门禁再进入下一个 RED。同层多个 RED→GREEN 对须逐对串行,禁止并行派发;同层非 TDD 模块任务仍可并行
|
|
134
134
|
- [ ] ⛔ **TDD 节奏校验**:见 §5e.1,连续 RED-N/GREEN-N 的 `task_update` 时间戳须有可验证间距
|
|
135
135
|
- [ ] **Git 只读策略**:禁止为了度量自动初始化 Git、创建分支或提交 commit;非 Git 项目用 `vcs_mode=no-git` 继续执行
|
|
136
136
|
- [ ] ⛔ **Step 0.1 隔离校验必做**:建 worktree / 建议分支名前必须完成 proposal + 跨 cap spec 依赖校验并输出报告;未通过不得按 full 并行策略拆 `kld-sdd/<change>/<cap>`
|
|
@@ -118,6 +118,16 @@ node skywalk-sdd/log.cjs archive-docs --project=. --change=<变更名称> --reas
|
|
|
118
118
|
- 最终中文报告生成到 `openspec/changes/archive/<日期>-<name>/reports/<name>-report.md` 及同名 `<name>-report.html`(默认同时生成 .md 与 .html 双产物,默认归档后 archive 目录,可用 --report-output 自定义)。
|
|
119
119
|
- 执行日志 `openspec/changes/archive/<日期>-<name>/logs/execution-log.md` 随归档整目录迁移(人读审计层)。
|
|
120
120
|
|
|
121
|
+
### 5.4 隐式注销活动变更
|
|
122
|
+
|
|
123
|
+
归档成功后立即执行(不询问用户):
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
node skywalk-sdd/ontology/cli.cjs active-change --remove --change=<变更名称> --project=.
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
把该 change 从 spec 仓 `sdd.config.yaml` 的 `active_changes` 移除,避免已归档 change 继续被代码 commit 写成 `Spec-Change`。该操作幂等;条目本就不存在时不报错。
|
|
130
|
+
|
|
121
131
|
### 5.5 收尾入库(opsx-kb-ingest)
|
|
122
132
|
|
|
123
133
|
1. 确认 `${AGENT_SKILL_DIR}/opsx-kb-ingest/SKILL.md` 存在并 Read;按该技能完成 API Key / targets。
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
2
|
name: opsx-check
|
|
3
3
|
description: "质量检查技能 - 验证文档完整性、一致性、算法正确性及可执行性"
|
|
4
4
|
argument-hint: "[change-name] [上下文文件...]"
|
|
@@ -69,6 +69,43 @@ openspec list
|
|
|
69
69
|
- `specs/<capability>/design.md`(实现方案)
|
|
70
70
|
- `specs/<capability>/tasks.md`(或 task.md,兼容旧格式)(任务拆解)
|
|
71
71
|
|
|
72
|
+
### 2.5 【多仓库关联配置诊断】(在五维检查之前)
|
|
73
|
+
|
|
74
|
+
先执行轻量配置诊断,**不执行代码 diff**,只检查命名与引用配置:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# 在 spec 仓库:
|
|
78
|
+
node skywalk-sdd/ontology/cli.cjs diagnose-naming --project=. --change=<change-key>
|
|
79
|
+
|
|
80
|
+
# 在代码仓库:
|
|
81
|
+
node skywalk-sdd/ontology/cli.cjs diagnose-naming --project=. --mode=code-repo
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
诊断结果三类:
|
|
85
|
+
|
|
86
|
+
| 状态 | 含义 |
|
|
87
|
+
|------|------|
|
|
88
|
+
| PASS | 配置完整 |
|
|
89
|
+
| FIXABLE | 修复方式唯一,用户确认后可自动修复(如安装 commit-msg Hook、设置 sdd.specPath) |
|
|
90
|
+
| NEEDS_INPUT | 多候选或团队级配置,必须询问用户 |
|
|
91
|
+
|
|
92
|
+
规则:
|
|
93
|
+
- 只问缺失项,不重复询问已合法配置
|
|
94
|
+
- 本地 Git config / Hook 可在确认后修复
|
|
95
|
+
- `.sdd.yaml`、`modules.yaml`、proposal frontmatter 修改前必须展示变更摘要
|
|
96
|
+
- 已被代码 commit 引用的 change-id/change-key **禁止静默重命名**
|
|
97
|
+
- 用户拒绝修复时按严重程度记 warning/failure,然后继续后续文档检查
|
|
98
|
+
- spec 仓诊断跳过代码仓 Hook;代码仓诊断跳过 modules/change 命名(入口分离)
|
|
99
|
+
- 单仓(`.sdd.yaml` 声明 `layout: mono`,openspec 与代码同仓):`mode=mono-repo`,命名与 Hook 一并就地检查;该布局 commit 只写 `Spec-Change`,不写 `Spec-Revision`
|
|
100
|
+
- 若在个人工作目录发现未接入的新代码仓(缺 `.sdd.yaml` 或 commit-msg Hook):
|
|
101
|
+
1. 向用户展示仓名列表;
|
|
102
|
+
2. 用户确认后执行 `kld-sdd sync-repos`(只装 Hook/关联,不装 skills);
|
|
103
|
+
3. 用户拒绝则记 warning,不阻断五维文档检查
|
|
104
|
+
- Trailer 协议:已接入仓 commit 必写 `Spec-Revision`;`Spec-Change` 来自 spec 仓 `sdd.config.yaml` 的 `active_changes`(多个则写多条),`KLD_SDD_CHANGE` 可显式覆盖,**不从分支名推断**
|
|
105
|
+
- 活动变更登记表诊断:`sdd.config.yaml` 是否存在且格式合法;当前 change 是否已登记(未登记则提示执行 `active-change --register`);登记项对应目录是否仍存在(已归档应执行 `active-change --remove`)
|
|
106
|
+
|
|
107
|
+
最终检查报告必须包含「多仓库关联配置」章节(命令输出已按此格式打印)。
|
|
108
|
+
|
|
72
109
|
### 3. 【上下文加载】识别并读取用户提供的文件
|
|
73
110
|
|
|
74
111
|
**自动识别上下文文件**:
|
|
@@ -117,7 +154,7 @@ openspec list
|
|
|
117
154
|
|
|
118
155
|
#### 4.4a TDD 合规性检查(仅 test-strategy=tdd 时执行)
|
|
119
156
|
|
|
120
|
-
⛔ 执行 `opsx-tdd-core/checklist.md` §B(
|
|
157
|
+
⛔ 执行 `opsx-tdd-core/checklist.md` §B(15 项)逐项检查。
|
|
121
158
|
|
|
122
159
|
> 不在此内联复制,以 opsx-tdd-core/checklist.md §B 为唯一真相源。
|
|
123
160
|
> 额外补充:还需检查 `opsx-tdd-rules/rules/exception-path-coverage.md`(异常路径覆盖门禁)。
|
|
@@ -220,7 +257,7 @@ node skywalk-sdd/log.cjs record --type=conformance_review --command=check --proj
|
|
|
220
257
|
|
|
221
258
|
---
|
|
222
259
|
|
|
223
|
-
## Continuity / external_ref
|
|
260
|
+
## Continuity / external_ref / 编号确定性门禁
|
|
224
261
|
|
|
225
262
|
`opsx-check` **不联网提问**。Agent 应在 propose/spec 已问完;本阶段只验证并入既有 apply 前门禁:
|
|
226
263
|
|
|
@@ -228,6 +265,13 @@ node skywalk-sdd/log.cjs record --type=conformance_review --command=check --proj
|
|
|
228
265
|
- 已写 spec 的 Capability:场景 `external-ref` 与 `continuity-resolution.json` 决议一致;同 key+同锚点未偷偷换 entity_id
|
|
229
266
|
- 用户选「原对象」却仍用新 id、或选「新对象」却仍共用旧锚点 → 失败(`CONTINUITY_IDENTITY_MISMATCH` / `EXTERNAL_REF_CONFLICT`)
|
|
230
267
|
- 决议缺失 / pending / 与产物不一致 → `CONTINUITY_DECISION_REQUIRED`
|
|
268
|
+
- **编号诊断码**(并入五维报告与 apply gate):
|
|
269
|
+
- `EXTERNAL_KEY_FORMAT_INVALID` — external_id / feature_id 不合规
|
|
270
|
+
- `EXTERNAL_KEY_SCOPE_MISMATCH` — 场景键 REQ 前缀不在 `requirement-refs`
|
|
271
|
+
- `EXTERNAL_KEY_FEATURE_UNDECLARED` — feature↔requirement.`feature_id` 配对断裂
|
|
272
|
+
- `EXTERNAL_KEY_SEQ_REUSED` — 本 Change 内 SCN 重复或复用 removed 墓碑号
|
|
273
|
+
- `NUMBERING_WAIVER_ACTIVE`(warning)— 豁免生效,本轮不种桥
|
|
274
|
+
- `requirement-refs` 为空且无有效 waiver → `CONTINUITY_DECISION_REQUIRED` 级阻断
|
|
231
275
|
- CI/非交互:失败即非零退出并打印修复说明,不挂起等待输入
|
|
232
276
|
|
|
233
277
|
## 本体语义关系门禁
|