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.
- package/README.md +12 -5
- package/USABILITY.md +84 -0
- package/bin/kld-sdd-init.js +3 -9
- package/kld-sdd-guide.html +16 -4
- package/lib/hook-gate-core.js +327 -0
- package/lib/init.js +241 -94
- 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 +49 -16
- package/skywalk-sdd/kb-sync-identity.cjs +99 -451
- package/skywalk-sdd/kb-upload.cjs +67 -44
- package/skywalk-sdd/lib/git-identity.cjs +270 -0
- package/skywalk-sdd/lib/usage-contract.cjs +13 -81
- package/skywalk-sdd/lib/usage-reporter.cjs +235 -104
- package/skywalk-sdd/lib/user-config.cjs +21 -46
- 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 +8 -6
- package/skywalk-sdd/ontology/id.cjs +5 -4
- package/skywalk-sdd/ontology/identity-index.cjs +4 -3
- 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 +71 -27
- package/templates/git-hooks/consistency-check-core.cjs +1087 -0
- package/templates/git-hooks/hooks.config +38 -0
- package/templates/git-hooks/pre-commit +62 -27
- 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 +70 -27
- package/templates/git-hooks/pre-push-consistency-check.cjs +61 -299
- 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 +43 -6
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +1 -1
- package/templates/skills/kld-sdd/opsx-apply/reference.md +1 -1
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +9 -13
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +54 -328
- package/templates/skills/kld-sdd/opsx-check/checklist.md +7 -4
- package/templates/skills/kld-sdd/opsx-check/reference.md +58 -0
- package/templates/skills/kld-sdd/opsx-check/result-template.json +52 -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-ingest/SKILL.md +12 -74
- 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 +28 -73
- package/templates/skills/kld-sdd/opsx-propose/checklist.md +8 -8
- package/templates/skills/kld-sdd/opsx-propose/interaction-policy.md +28 -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 +38 -32
- 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
- package/lib/device-auth-cli.js +0 -130
- package/lib/device-auth.js +0 -345
- package/lib/init-account-binding.js +0 -108
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* opsx-consistency-check 辅助脚本
|
|
6
|
+
*
|
|
7
|
+
* 封装 SKILL.md 中需要跨平台执行的命令(非 git 命令),
|
|
8
|
+
* 避免 node -e 单行命令的可读性和引号转义问题。
|
|
9
|
+
*
|
|
10
|
+
* 用法:
|
|
11
|
+
* node scripts.cjs <command> [args...]
|
|
12
|
+
*
|
|
13
|
+
* 命令列表:
|
|
14
|
+
* list-changes [--spec-root=<路径>] 列出活跃变更(注册表优先:sdd.config.yaml 的 active_changes;
|
|
15
|
+
* 无注册表时降级扫描目录,排除 archive 和隐藏目录——与 hook 同源)
|
|
16
|
+
* find-specs <change-name> [--spec-root=<路径>]
|
|
17
|
+
* 递归查找变更级 + 全局 spec.md 文件
|
|
18
|
+
* (支持嵌套 specs/ 子目录与平铺 spec.md 两种结构)
|
|
19
|
+
* clean-reports <change-dir> 清理指定 change 下所有旧报告文件(删除结果可见,失败退出 1)
|
|
20
|
+
* file-hash <file> 计算单个文件的 size + sha256
|
|
21
|
+
* batch-hash [--files-from=<列表文件>] [--files=<逗号分隔列表>]
|
|
22
|
+
* 批量计算 size + sha256。输入优先级:
|
|
23
|
+
* --files-from=(UTF-8 列表文件,每行一个路径,适合上百文件的大变更)
|
|
24
|
+
* > --files=(逗号分隔,适合少量文件)> stdin(注意 PowerShell 管道编码问题)
|
|
25
|
+
* grep-code <pattern> [dir] [--glob=<通配>] [-i]
|
|
26
|
+
* 跨平台内容搜索(Node 实现,IDE 搜索工具故障时的兜底)。
|
|
27
|
+
* 输出 相对路径:行号: 内容,最多 200 条后截断
|
|
28
|
+
* prepare-report <change-dir> [--files-from=<列表文件>|--files=<csv>]
|
|
29
|
+
* 一体化命令:清理旧报告 + 批量哈希(可选)+ 输出本地时间,
|
|
30
|
+
* 单个 JSON 输出(cleaned/hashes/unreadable/timestamp),减少报告阶段往返。
|
|
31
|
+
* 不带 --files-from=/--files= 时跳过哈希(hashes 为空),
|
|
32
|
+
* 适用于哈希已由 --collect --hash 提供的场景;绝不读 stdin(防挂起)
|
|
33
|
+
* now 输出当前本机本地时间(YYYY-MM-DD HH:mm:ss)
|
|
34
|
+
*
|
|
35
|
+
* spec 仓根解析(list-changes / find-specs 共用):
|
|
36
|
+
* --spec-root= 参数 > .sdd-spec-root 标记文件 > .sdd.yaml mono 的 spec_path
|
|
37
|
+
* > cwd 有 openspec/ > .sdd-workspace.yaml 的 spec_path > 向上找父目录
|
|
38
|
+
* 单仓(.sdd.yaml layout: mono + .sdd-spec-root)与多仓(.sdd-workspace.yaml + .sdd-spec-root)
|
|
39
|
+
* 均自动解析,无需手动传 --spec-root。与权威实现 resolve-spec-root.cjs 策略对齐。
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
const fs = require('fs');
|
|
43
|
+
const path = require('path');
|
|
44
|
+
const crypto = require('crypto');
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 解析 spec 仓根目录(含 openspec/ 的目录)。
|
|
48
|
+
* 解析顺序(与权威实现 resolve-spec-root.cjs 的 resolveSpecProjectRoot 对齐):
|
|
49
|
+
* 1. --spec-root= 命令行参数(显式指定,验证 openspec/ 存在)
|
|
50
|
+
* 2. .sdd-spec-root 标记文件(cwd 及祖先目录向上遍历,读内容 resolve)
|
|
51
|
+
* 3. .sdd.yaml 且 layout: mono(cwd 及祖先目录,读 spec_path resolve)
|
|
52
|
+
* 4. cwd 下存在 openspec/ → cwd 即 spec 根(单仓场景,兼容现状)
|
|
53
|
+
* 5. cwd 或祖先目录存在 .sdd-workspace.yaml → 读 spec_path 解析(多仓场景)
|
|
54
|
+
* 6. 向上逐级父目录找 openspec/(兜底)
|
|
55
|
+
* 找不到返回 null(调用方报错退出,不静默)。
|
|
56
|
+
*/
|
|
57
|
+
function resolveSpecRoot() {
|
|
58
|
+
// 1. 显式参数
|
|
59
|
+
const argPrefix = '--spec-root=';
|
|
60
|
+
const arg = process.argv.find(a => a.startsWith(argPrefix));
|
|
61
|
+
if (arg) {
|
|
62
|
+
const p = path.resolve(arg.slice(argPrefix.length));
|
|
63
|
+
if (fs.existsSync(path.join(p, 'openspec'))) return p;
|
|
64
|
+
console.error(`ERROR: --spec-root=${arg.slice(argPrefix.length)} 下不存在 openspec/`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2. .sdd-spec-root 标记文件(cwd 及祖先目录)— 与权威 resolveHint 对齐
|
|
69
|
+
const hinted = resolveSpecRootHint(process.cwd());
|
|
70
|
+
if (hinted) return hinted;
|
|
71
|
+
|
|
72
|
+
// 3. .sdd.yaml mono 布局(cwd 及祖先目录)— 与权威 isMonoLayout + resolveMonoSpecRoot 对齐
|
|
73
|
+
const monoRoot = resolveMonoSpecRootFrom(process.cwd());
|
|
74
|
+
if (monoRoot) return monoRoot;
|
|
75
|
+
|
|
76
|
+
// 4. cwd 即 spec 根
|
|
77
|
+
if (fs.existsSync(path.join(process.cwd(), 'openspec'))) return process.cwd();
|
|
78
|
+
|
|
79
|
+
// 5. .sdd-workspace.yaml 的 spec_path(cwd 及祖先目录)
|
|
80
|
+
let dir = process.cwd();
|
|
81
|
+
for (let i = 0; i < 10; i++) {
|
|
82
|
+
const wsYaml = path.join(dir, '.sdd-workspace.yaml');
|
|
83
|
+
if (fs.existsSync(wsYaml)) {
|
|
84
|
+
try {
|
|
85
|
+
const content = fs.readFileSync(wsYaml, 'utf8');
|
|
86
|
+
const m = /^spec_path:\s*(\S+)/m.exec(content);
|
|
87
|
+
if (m) {
|
|
88
|
+
const p = path.resolve(dir, m[1].replace(/["']/g, ''));
|
|
89
|
+
if (fs.existsSync(path.join(p, 'openspec'))) return p;
|
|
90
|
+
}
|
|
91
|
+
} catch { /* 继续兜底 */ }
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
const parent = path.dirname(dir);
|
|
95
|
+
if (parent === dir) break;
|
|
96
|
+
dir = parent;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 6. 向上找 openspec/(兜底)
|
|
100
|
+
dir = process.cwd();
|
|
101
|
+
for (let i = 0; i < 10; i++) {
|
|
102
|
+
if (fs.existsSync(path.join(dir, 'openspec'))) return dir;
|
|
103
|
+
const parent = path.dirname(dir);
|
|
104
|
+
if (parent === dir) break;
|
|
105
|
+
dir = parent;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 从 dir 向上遍历找 .sdd-spec-root 标记文件。
|
|
113
|
+
* 读其内容作为相对路径,path.resolve 得到 spec 根;验证路径存在即返回。
|
|
114
|
+
* 与 resolve-spec-root.cjs 的 resolveHint 同策略。
|
|
115
|
+
*/
|
|
116
|
+
function resolveSpecRootHint(dir) {
|
|
117
|
+
let current = path.resolve(dir);
|
|
118
|
+
const root = path.parse(current).root;
|
|
119
|
+
for (;;) {
|
|
120
|
+
const hintPath = path.join(current, '.sdd-spec-root');
|
|
121
|
+
if (fs.existsSync(hintPath)) {
|
|
122
|
+
try {
|
|
123
|
+
const hint = fs.readFileSync(hintPath, 'utf8').trim();
|
|
124
|
+
if (hint) {
|
|
125
|
+
const resolved = path.resolve(current, hint);
|
|
126
|
+
if (fs.existsSync(resolved)) return resolved;
|
|
127
|
+
}
|
|
128
|
+
} catch { /* 忽略读取异常,继续兜底 */ }
|
|
129
|
+
}
|
|
130
|
+
if (current === root) break;
|
|
131
|
+
current = path.dirname(current);
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 从 dir 向上遍历找 .sdd.yaml 且 layout: mono,读 spec_path resolve。
|
|
138
|
+
* 与 sdd-config.cjs 的 isMonoLayout + resolveMonoSpecRoot 同策略。
|
|
139
|
+
* 逐行去注释再匹配(与现有 parseActiveChangesRegistry / sdd-config.cjs 风格一致),
|
|
140
|
+
* 避免 `layout: mono# comment` 等边缘情况误匹配。
|
|
141
|
+
*/
|
|
142
|
+
function resolveMonoSpecRootFrom(dir) {
|
|
143
|
+
let current = path.resolve(dir);
|
|
144
|
+
const root = path.parse(current).root;
|
|
145
|
+
for (;;) {
|
|
146
|
+
const yamlPath = path.join(current, '.sdd.yaml');
|
|
147
|
+
if (fs.existsSync(yamlPath)) {
|
|
148
|
+
try {
|
|
149
|
+
const lines = fs.readFileSync(yamlPath, 'utf8').split(/\r?\n/);
|
|
150
|
+
let layout = null;
|
|
151
|
+
let specPathRel = null;
|
|
152
|
+
for (const raw of lines) {
|
|
153
|
+
const line = raw.replace(/#.*$/, '').trim();
|
|
154
|
+
if (!line) continue;
|
|
155
|
+
const layoutM = /^layout:\s*(.+?)\s*$/.exec(line);
|
|
156
|
+
if (layoutM) { layout = layoutM[1].replace(/["']/g, '').toLowerCase(); continue; }
|
|
157
|
+
const pathM = /^spec_path:\s*(.+?)\s*$/.exec(line);
|
|
158
|
+
if (pathM) { specPathRel = pathM[1].replace(/["']/g, ''); continue; }
|
|
159
|
+
}
|
|
160
|
+
if (layout === 'mono' && specPathRel) {
|
|
161
|
+
const p = path.resolve(current, specPathRel);
|
|
162
|
+
if (fs.existsSync(p)) return p;
|
|
163
|
+
}
|
|
164
|
+
} catch { /* 忽略,继续兜底 */ }
|
|
165
|
+
// 找到 .sdd.yaml 但非 mono 或解析失败,不再向上找(.sdd.yaml 是仓库级标记)
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
if (current === root) break;
|
|
169
|
+
current = path.dirname(current);
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// --- 活跃变更注册表解析(与 consistency-check-core.cjs parseActiveChangesFromYaml 同规则) ---
|
|
175
|
+
function parseActiveChangesRegistry(specRoot) {
|
|
176
|
+
const configPath = path.join(specRoot, 'sdd.config.yaml');
|
|
177
|
+
if (!fs.existsSync(configPath)) return { keys: [], sawSection: false };
|
|
178
|
+
try {
|
|
179
|
+
const keys = [];
|
|
180
|
+
let inActive = false;
|
|
181
|
+
for (const raw of fs.readFileSync(configPath, 'utf8').split(/\r?\n/)) {
|
|
182
|
+
const line = raw.replace(/#.*$/, '').replace(/\s+$/, '');
|
|
183
|
+
if (!line.trim()) continue;
|
|
184
|
+
if (/^active_changes:\s*(\[\s*\])?\s*$/.test(line)) { inActive = true; continue; }
|
|
185
|
+
if (inActive) {
|
|
186
|
+
const m = /^\s+-\s+change-key:\s*(\S+)/.exec(line);
|
|
187
|
+
if (m) keys.push(m[1]);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { keys, sawSection: inActive };
|
|
191
|
+
} catch {
|
|
192
|
+
return { keys: [], sawSection: false };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// --- list-changes ---
|
|
197
|
+
function listChanges() {
|
|
198
|
+
const specRoot = resolveSpecRoot();
|
|
199
|
+
if (!specRoot) {
|
|
200
|
+
console.error('ERROR: 未找到 spec 仓根目录(含 openspec/)。可用 --spec-root=<路径> 显式指定');
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
// 1. 注册表优先(与 hook discoverActiveChanges 同源,避免两边发现结果不一致)
|
|
204
|
+
const { keys, sawSection } = parseActiveChangesRegistry(specRoot);
|
|
205
|
+
if (sawSection) {
|
|
206
|
+
if (keys.length) console.log(keys.join('\n'));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// 2. 降级:目录扫描
|
|
210
|
+
const dir = path.join(specRoot, 'openspec', 'changes');
|
|
211
|
+
if (!fs.existsSync(dir)) {
|
|
212
|
+
process.exit(0);
|
|
213
|
+
}
|
|
214
|
+
const names = fs.readdirSync(dir)
|
|
215
|
+
.filter(n => !n.startsWith('.') && n !== 'archive')
|
|
216
|
+
.filter(n => fs.statSync(path.join(dir, n)).isDirectory());
|
|
217
|
+
if (names.length) console.log(names.join('\n'));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// --- find-specs ---
|
|
221
|
+
function findSpecs(changeName) {
|
|
222
|
+
const specRoot = resolveSpecRoot();
|
|
223
|
+
if (!specRoot) {
|
|
224
|
+
console.error('ERROR: 未找到 spec 仓根目录(含 openspec/)。可用 --spec-root=<路径> 显式指定');
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
const results = [];
|
|
228
|
+
// 平铺结构:changes/<name>/spec.md(部分项目变更目录下无 specs/ 子目录,spec.md 直接平铺)
|
|
229
|
+
const flat = path.join(specRoot, 'openspec', 'changes', changeName, 'spec.md');
|
|
230
|
+
if (fs.existsSync(flat)) results.push(flat);
|
|
231
|
+
const targets = [
|
|
232
|
+
path.join(specRoot, 'openspec', 'changes', changeName, 'specs'),
|
|
233
|
+
path.join(specRoot, 'openspec', 'specs'),
|
|
234
|
+
];
|
|
235
|
+
for (const target of targets) {
|
|
236
|
+
if (!fs.existsSync(target)) continue;
|
|
237
|
+
walkDir(target).forEach(f => {
|
|
238
|
+
if (f.endsWith('spec.md') && !results.includes(f)) results.push(f);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (results.length) console.log(results.join('\n'));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function walkDir(dir) {
|
|
245
|
+
const results = [];
|
|
246
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
247
|
+
const full = path.join(dir, entry.name);
|
|
248
|
+
if (entry.isDirectory()) {
|
|
249
|
+
results.push(...walkDir(full));
|
|
250
|
+
} else {
|
|
251
|
+
results.push(full);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return results;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// --- clean-reports ---
|
|
258
|
+
function cleanReports(changeDir) {
|
|
259
|
+
const files = [
|
|
260
|
+
'consistency-report.md',
|
|
261
|
+
'consistency-report-result.json',
|
|
262
|
+
];
|
|
263
|
+
let deleted = 0;
|
|
264
|
+
for (const f of files) {
|
|
265
|
+
const fp = path.join(changeDir, f);
|
|
266
|
+
try {
|
|
267
|
+
if (fs.existsSync(fp)) {
|
|
268
|
+
fs.unlinkSync(fp);
|
|
269
|
+
console.log('deleted: ' + fp);
|
|
270
|
+
deleted++;
|
|
271
|
+
}
|
|
272
|
+
} catch (e) {
|
|
273
|
+
// 失败不静默:删除失败会导致后续 Write 要求先 Read(多一轮工具调用),必须可见
|
|
274
|
+
console.error('ERROR: failed to delete ' + fp + ': ' + e.message);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (!deleted) console.log('no stale reports to clean');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// --- 行尾归一化(与 consistency-check-core.cjs normalizeEolLf 保持一致) ---
|
|
282
|
+
// 报告生成与 hook 校验均对内容做 LF 归一化后再算 size/sha256,
|
|
283
|
+
// 消除 autocrlf=true 下 CRLF→LF→CRLF 往返不完美、混合行尾等导致的哈希不一致。
|
|
284
|
+
function normalizeEolLf(buf) {
|
|
285
|
+
return Buffer.from(buf.toString('binary').replace(/\r\n/g, '\n'), 'binary');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// --- file-hash ---
|
|
289
|
+
function fileHash(file) {
|
|
290
|
+
const buf = normalizeEolLf(fs.readFileSync(file));
|
|
291
|
+
console.log(JSON.stringify({
|
|
292
|
+
size: buf.length,
|
|
293
|
+
sha256: crypto.createHash('sha256').update(buf).digest('hex'),
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// --- 哈希输入解析(batch-hash / prepare-report 共用) ---
|
|
298
|
+
// 输入优先级:--files-from= > --files= > stdin。
|
|
299
|
+
// - --files-from=:UTF-8 列表文件(每行一个路径),适合上百文件的大变更——同时绕开
|
|
300
|
+
// stdin 管道编码问题(PowerShell 默认 UTF-16)与命令行长度上限(cmd.exe 8191 字符 / CreateProcess 32KB)
|
|
301
|
+
// - --files=:逗号分隔,适合少量文件(命令行长度上限约 8K 字符,约 60~80 个典型路径)
|
|
302
|
+
// - stdin 兜底:交互式终端(TTY)下无重定向输入会永久阻塞(等待 EOF),直接报错退出而非挂起
|
|
303
|
+
function readHashInput() {
|
|
304
|
+
const filesFromPrefix = '--files-from=';
|
|
305
|
+
const filesFromArg = process.argv.find(a => a.startsWith(filesFromPrefix));
|
|
306
|
+
const filesPrefix = '--files=';
|
|
307
|
+
const filesArg = process.argv.find(a => a.startsWith(filesPrefix));
|
|
308
|
+
if (filesFromArg) {
|
|
309
|
+
return fs.readFileSync(filesFromArg.slice(filesFromPrefix.length), 'utf8')
|
|
310
|
+
.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
|
311
|
+
}
|
|
312
|
+
if (filesArg) {
|
|
313
|
+
return filesArg.slice(filesPrefix.length).split(',').map(s => s.trim()).filter(Boolean);
|
|
314
|
+
}
|
|
315
|
+
if (process.stdin.isTTY) {
|
|
316
|
+
console.error('ERROR: 未提供 --files-from= 或 --files=,且 stdin 为交互式终端(等待管道输入会永久挂起)。');
|
|
317
|
+
console.error('用法: node scripts.cjs batch-hash --files-from=<列表文件> 或 --files=<逗号分隔路径>');
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
return fs.readFileSync(0, 'utf8').split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// --- batch-hash ---
|
|
324
|
+
function batchHash() {
|
|
325
|
+
const input = readHashInput();
|
|
326
|
+
for (const f of input) {
|
|
327
|
+
const buf = normalizeEolLf(fs.readFileSync(f));
|
|
328
|
+
console.log(JSON.stringify({
|
|
329
|
+
path: f,
|
|
330
|
+
size: buf.length,
|
|
331
|
+
sha256: crypto.createHash('sha256').update(buf).digest('hex'),
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// --- 本地时间(now / prepare-report 共用) ---
|
|
337
|
+
function nowString() {
|
|
338
|
+
const d = new Date();
|
|
339
|
+
const p = n => String(n).padStart(2, '0');
|
|
340
|
+
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) +
|
|
341
|
+
' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// --- now ---
|
|
345
|
+
function now() {
|
|
346
|
+
console.log(nowString());
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// --- grep-code(跨平台内容搜索,IDE 搜索工具故障时的兜底) ---
|
|
350
|
+
const GREP_SKIP_DIRS = new Set([
|
|
351
|
+
'node_modules', 'dist', 'build', 'target', 'out', 'vendor',
|
|
352
|
+
'.git', '.svn', '.hg', '.idea', '.vscode',
|
|
353
|
+
]);
|
|
354
|
+
const GREP_BINARY_EXT_RE = /\.(png|jpe?g|gif|bmp|ico|webp|pdf|zip|gz|tgz|tar|jar|class|woff2?|ttf|eot|mp4|mp3|avi|mov|xlsx?|docx?|pptx?|bin|so|dll|exe)$/i;
|
|
355
|
+
const GREP_MAX_MATCHES = 200;
|
|
356
|
+
const GREP_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
357
|
+
|
|
358
|
+
function globToRegExp(glob) {
|
|
359
|
+
const escaped = glob
|
|
360
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
361
|
+
.replace(/\*\*/g, '\u0000')
|
|
362
|
+
.replace(/\*/g, '[^/]*')
|
|
363
|
+
.replace(/\u0000/g, '.*')
|
|
364
|
+
.replace(/\?/g, '.');
|
|
365
|
+
return new RegExp('^' + escaped + '$', 'i');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function grepCode() {
|
|
369
|
+
const pattern = process.argv[3];
|
|
370
|
+
if (!pattern) {
|
|
371
|
+
console.error('ERROR: 缺少搜索模式。用法: node scripts.cjs grep-code <pattern> [dir] [--glob=<通配>] [-i]');
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
let dir = process.argv[4] ? path.resolve(process.argv[4]) : process.cwd();
|
|
375
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
376
|
+
console.error('ERROR: 搜索目录不存在: ' + dir);
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
const globPrefix = '--glob=';
|
|
380
|
+
const globArg = process.argv.find(a => a.startsWith(globPrefix));
|
|
381
|
+
const globRe = globArg ? globToRegExp(globArg.slice(globPrefix.length)) : null;
|
|
382
|
+
const globHasSlash = globArg ? globArg.slice(globPrefix.length).includes('/') : false;
|
|
383
|
+
const caseInsensitive = process.argv.includes('-i') || process.argv.includes('--ignore-case');
|
|
384
|
+
let re;
|
|
385
|
+
try {
|
|
386
|
+
re = new RegExp(pattern, caseInsensitive ? 'i' : '');
|
|
387
|
+
} catch (e) {
|
|
388
|
+
console.error('ERROR: 非法正则: ' + e.message);
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const out = [];
|
|
393
|
+
let truncated = false;
|
|
394
|
+
const walk = (d, relBase) => {
|
|
395
|
+
if (truncated) return;
|
|
396
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
397
|
+
if (truncated) return;
|
|
398
|
+
const full = path.join(d, entry.name);
|
|
399
|
+
const rel = relBase ? relBase + '/' + entry.name : entry.name;
|
|
400
|
+
if (entry.isDirectory()) {
|
|
401
|
+
if (!GREP_SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) walk(full, rel);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (GREP_BINARY_EXT_RE.test(entry.name)) continue;
|
|
405
|
+
if (globRe && !globRe.test(globHasSlash ? rel : entry.name)) continue;
|
|
406
|
+
let stat;
|
|
407
|
+
try { stat = fs.statSync(full); } catch { continue; }
|
|
408
|
+
if (stat.size > GREP_MAX_FILE_BYTES) continue;
|
|
409
|
+
let content;
|
|
410
|
+
try { content = fs.readFileSync(full, 'utf8'); } catch { continue; }
|
|
411
|
+
if (content.includes('\u0000')) continue; // 二进制内容跳过
|
|
412
|
+
const lines = content.split(/\r?\n/);
|
|
413
|
+
for (let i = 0; i < lines.length; i++) {
|
|
414
|
+
if (re.test(lines[i])) {
|
|
415
|
+
out.push(rel + ':' + (i + 1) + ': ' + lines[i].trim());
|
|
416
|
+
if (out.length >= GREP_MAX_MATCHES) { truncated = true; break; }
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
walk(dir, '');
|
|
422
|
+
if (out.length) console.log(out.join('\n'));
|
|
423
|
+
else console.log('no matches');
|
|
424
|
+
if (truncated) console.error('NOTE: 结果超过 ' + GREP_MAX_MATCHES + ' 条已截断,请缩小搜索范围(加 --glob= 或指定更精确目录)');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// --- prepare-report(一体化:清理旧报告 + 批量哈希 + 本地时间,单 JSON 输出减少往返) ---
|
|
428
|
+
function prepareReport(changeDir) {
|
|
429
|
+
if (!changeDir) {
|
|
430
|
+
console.error('ERROR: 缺少 change 目录。用法: node scripts.cjs prepare-report <change-dir> [--files-from=<列表文件>|--files=<csv>]');
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
// 相对路径解析:报告固定落在 spec 根下(hook findReportJson 只认 specRoot/openspec/changes/<name>/),
|
|
434
|
+
// 相对路径优先按 spec 根解析——cwd 可能是项目根/代码仓根而非 spec 根(多仓/子目录布局),
|
|
435
|
+
// 按 cwd 解析会清错目录(曾实际发生:清理未作用于真实 change 目录,AI 被迫读源码自查浪费多轮)。
|
|
436
|
+
// 绝对路径原样使用;spec 根解析失败时回退 cwd(向后兼容)。
|
|
437
|
+
let dir = path.resolve(changeDir);
|
|
438
|
+
if (!path.isAbsolute(changeDir)) {
|
|
439
|
+
const specRoot = resolveSpecRoot();
|
|
440
|
+
if (specRoot) dir = path.resolve(specRoot, changeDir);
|
|
441
|
+
}
|
|
442
|
+
// 1. 清理旧报告(失败不静默:残留旧报告会导致后续 Write 要求先 Read)
|
|
443
|
+
const reportFiles = ['consistency-report.md', 'consistency-report-result.json'];
|
|
444
|
+
const cleaned = [];
|
|
445
|
+
for (const f of reportFiles) {
|
|
446
|
+
const fp = path.join(dir, f);
|
|
447
|
+
try {
|
|
448
|
+
if (fs.existsSync(fp)) {
|
|
449
|
+
fs.unlinkSync(fp);
|
|
450
|
+
cleaned.push(fp.replace(/\\/g, '/'));
|
|
451
|
+
}
|
|
452
|
+
} catch (e) {
|
|
453
|
+
console.error('ERROR: failed to delete ' + fp + ': ' + e.message);
|
|
454
|
+
process.exit(1);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// 2. 批量哈希(可选):仅当显式提供 --files-from=/--files= 时执行,绝不读 stdin(防交互式终端挂起)。
|
|
458
|
+
// 无输入源 = "只清理 + 取时间戳"合法模式(哈希已由 --collect --hash 提供时无需重复传入),hashes 输出空数组
|
|
459
|
+
const hashes = [];
|
|
460
|
+
const unreadable = [];
|
|
461
|
+
const hasHashInput = process.argv.some(a => a.startsWith('--files-from=') || a.startsWith('--files='));
|
|
462
|
+
if (hasHashInput) {
|
|
463
|
+
for (const f of readHashInput()) {
|
|
464
|
+
try {
|
|
465
|
+
const buf = normalizeEolLf(fs.readFileSync(f));
|
|
466
|
+
hashes.push({
|
|
467
|
+
path: f,
|
|
468
|
+
size: buf.length,
|
|
469
|
+
sha256: crypto.createHash('sha256').update(buf).digest('hex'),
|
|
470
|
+
});
|
|
471
|
+
} catch {
|
|
472
|
+
unreadable.push(f);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// 3. 单 JSON 输出:cleaned + hashes + timestamp + 实际目录一次拿全
|
|
477
|
+
// changeDir 带回解析后的绝对路径,调用方无需再自行推导报告落点(写报告直接用它)
|
|
478
|
+
console.log(JSON.stringify({
|
|
479
|
+
changeDir: dir.replace(/\\/g, '/'),
|
|
480
|
+
cleaned,
|
|
481
|
+
hashes,
|
|
482
|
+
unreadable,
|
|
483
|
+
timestamp: nowString(),
|
|
484
|
+
}, null, 2));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// --- main ---
|
|
488
|
+
const command = process.argv[2];
|
|
489
|
+
switch (command) {
|
|
490
|
+
case 'list-changes':
|
|
491
|
+
listChanges();
|
|
492
|
+
break;
|
|
493
|
+
case 'find-specs':
|
|
494
|
+
findSpecs(process.argv[3]);
|
|
495
|
+
break;
|
|
496
|
+
case 'clean-reports':
|
|
497
|
+
cleanReports(process.argv[3]);
|
|
498
|
+
break;
|
|
499
|
+
case 'file-hash':
|
|
500
|
+
fileHash(process.argv[3]);
|
|
501
|
+
break;
|
|
502
|
+
case 'batch-hash':
|
|
503
|
+
batchHash();
|
|
504
|
+
break;
|
|
505
|
+
case 'now':
|
|
506
|
+
now();
|
|
507
|
+
break;
|
|
508
|
+
case 'grep-code':
|
|
509
|
+
grepCode();
|
|
510
|
+
break;
|
|
511
|
+
case 'prepare-report':
|
|
512
|
+
prepareReport(process.argv[3]);
|
|
513
|
+
break;
|
|
514
|
+
default:
|
|
515
|
+
console.error('Usage: node scripts.cjs <list-changes|find-specs|clean-reports|file-hash|batch-hash|now|grep-code|prepare-report> [args...]');
|
|
516
|
+
process.exit(1);
|
|
517
|
+
}
|
|
@@ -14,6 +14,8 @@ allowed-tools:
|
|
|
14
14
|
- Edit
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
+
> **执行方式**:先读 [交互与执行契约](../opsx-propose/interaction-policy.md);同一会话已读则复用。用户已授权连续处理时按范围推进,不重复索要阶段口令;只请求单阶段时完成即停。
|
|
18
|
+
|
|
17
19
|
你是一个 SDD(Specification-Driven Development)技术设计专家。激活本技能后,你将引导用户为**单一 Capability** 创建 **design.md** 文档。
|
|
18
20
|
|
|
19
21
|
> **⚠️ 阶段边界约束**
|
|
@@ -21,20 +23,12 @@ allowed-tools:
|
|
|
21
23
|
> 当前处于 **Design(设计)阶段**:
|
|
22
24
|
> - ✅ **允许**:创建/编辑 design.md 文档、读取代码/文档作为上下文分析
|
|
23
25
|
> - ❌ **禁止**:创建/修改任何代码文件、执行代码生成、运行测试
|
|
24
|
-
> -
|
|
26
|
+
> - **执行范围**:单阶段请求完成即停;已授权连续处理则按交互契约加载下一阶段,check 通过后方可进入 apply,不能超出授权范围。
|
|
25
27
|
>
|
|
26
28
|
> 代码实现将在 `/opsx-apply` 阶段进行。
|
|
27
|
-
> **完成本阶段后,绝对禁止自动继续执行 task 等后续阶段。**
|
|
28
29
|
> 阶段边界自检见 `./checklist.md`「阶段边界⛔」。
|
|
29
30
|
|
|
30
|
-
> **KB
|
|
31
|
-
> 1. 检查 proposal.md frontmatter `kb-status`:若 `degraded(by-user-choice)` → 跳过 KB,仅用本地上下文。
|
|
32
|
-
> 2. 否则运行 KB 就绪检查:
|
|
33
|
-
> ```bash
|
|
34
|
-
> node "$(cat .sdd-spec-root)/skywalk-sdd/context-client.cjs" --check-only
|
|
35
|
-
> ```
|
|
36
|
-
> - `"available": true` → 若需查历史设计参考,先 `Read` `opsx-ontology-query/phase-2-during.md` §1
|
|
37
|
-
> - `"available": false` → **KB 不可用不阻塞 design 流程**,仅跳过历史设计参考查询
|
|
31
|
+
> **KB 上下文**:仅在需要历史设计参考时查询,优先复用本轮已有结果;不逐阶段执行 `--check-only`。未配置、断连或无权限时记录原因,继续本地流程,不重复询问配置。
|
|
38
32
|
|
|
39
33
|
> **⚠️ 渐进式上下文加载原则**
|
|
40
34
|
>
|
|
@@ -43,7 +37,7 @@ allowed-tools:
|
|
|
43
37
|
> - **输出路径**(Full 模式):`changes/<name>/specs/<capability>/design.md`
|
|
44
38
|
> - **输入路径**(Simple 模式):`changes/<name>/spec.md`
|
|
45
39
|
> - **输出路径**(Simple 模式):`changes/<name>/design.md`
|
|
46
|
-
> - ⛔
|
|
40
|
+
> - ⛔ **隔离红线**:默认聚焦当前 Capability;为核对已声明依赖可读取相关 Capability 契约并注明来源,不全量加载或混写其他能力。
|
|
47
41
|
|
|
48
42
|
> **🖥️ 跨平台执行规则**
|
|
49
43
|
> - **SDD 文档根** = `*-sdd-specs` 包裹包(含 `openspec/`、`modules.yaml`),不是 Git 根或工作区根。
|
|
@@ -111,7 +105,7 @@ openspec list
|
|
|
111
105
|
|
|
112
106
|
### 3. 渐进式上下文加载
|
|
113
107
|
|
|
114
|
-
> ⛔ 必须严格按 overview.md → proposal.md → 当前 capability spec.md
|
|
108
|
+
> ⛔ 必须严格按 overview.md → proposal.md → 当前 capability spec.md 顺序加载;隔离红线:默认聚焦当前 Capability;为核对已声明依赖可读取相关 Capability 契约并注明来源,不全量加载或混写其他能力。**完整层级图见 `./reference.md`「§3 渐进式上下文加载层级图」**。
|
|
115
109
|
|
|
116
110
|
### 4. 【关键步骤】读取本地模板文件
|
|
117
111
|
|
|
@@ -157,23 +151,9 @@ design.md 中若使用 version 正则约束(如格式校验 `^\d+\.\d+\.\d+$`
|
|
|
157
151
|
|
|
158
152
|
> 写入文档前逐项确认,完整 7 项自检清单见 `./checklist.md`「§7 质量红线自检」。如有任意一项未满足,重新生成对应章节,直至全部通过。
|
|
159
153
|
|
|
160
|
-
###
|
|
161
|
-
|
|
162
|
-
生成文档后,向用户展示概要:
|
|
163
|
-
> "已生成 design.md,概要如下:
|
|
164
|
-
> - Capability:[name]
|
|
165
|
-
> - 核心模块:[模块列表]
|
|
166
|
-
> - 设计模式:[使用的设计模式]
|
|
167
|
-
> - 预估复杂度:[高/中/低]
|
|
168
|
-
>
|
|
169
|
-
> 请确认:
|
|
170
|
-
> - A. 确认无误,继续创建 tasks.md
|
|
171
|
-
> - B. 需要修改设计方案
|
|
172
|
-
> - C. 补充更多代码上下文"
|
|
154
|
+
### 7. 展示设计差异并继续
|
|
173
155
|
|
|
174
|
-
|
|
175
|
-
- 文档路径
|
|
176
|
-
- 下一步提示:"运行 `/opsx-task <name> <capability>` 创建任务拆解文档"
|
|
156
|
+
展示关键设计决定、依赖、验收和未决问题。Agent 能确定的缺失字段或依赖先自行修复;实质业务歧义再集中询问。已授权连续处理时加载下一阶段 task。单阶段请求完成即停,不重复询问模式或后续阶段口令。
|
|
177
157
|
|
|
178
158
|
---
|
|
179
159
|
|
|
@@ -194,11 +174,11 @@ design.md 中若使用 version 正则约束(如格式校验 `^\d+\.\d+\.\d+$`
|
|
|
194
174
|
|
|
195
175
|
- **必须以 `openspec-templates/design.md` 为模板基准**。
|
|
196
176
|
- **⛔ 渐进式加载**:严格按 overview.md → proposal.md → spec.md 顺序加载。
|
|
197
|
-
- **⛔
|
|
177
|
+
- **⛔ 隔离红线**:默认聚焦当前 Capability;为核对已声明依赖可读取相关 Capability 契约并注明来源,不全量加载或混写其他能力。
|
|
198
178
|
- design.md 聚焦【How】,是 spec.md → tasks.md 的桥梁;必须 100% 覆盖 spec.md 定义的需求项。
|
|
199
179
|
- 代码锚点必须具体到类/方法级别。
|
|
200
180
|
- **⛔ 阶段边界**:禁止执行任何代码创建/修改操作。
|
|
201
|
-
-
|
|
181
|
+
- **执行范围**:单阶段请求完成即停;已授权连续处理则按交互契约加载下一阶段,check 通过后方可进入 apply,不能超出授权范围。
|
|
202
182
|
|
|
203
183
|
---
|
|
204
184
|
|
|
@@ -13,9 +13,8 @@ description: opsx-design 的阶段强制检查点与自检清单。仅在执行
|
|
|
13
13
|
|
|
14
14
|
- [ ] ✅ 允许:创建/编辑 design.md 文档、读取代码/文档作为上下文分析
|
|
15
15
|
- [ ] ❌ 禁止:创建/修改任何代码文件、执行代码生成、运行测试
|
|
16
|
-
- [ ]
|
|
16
|
+
- [ ] **执行范围**:单阶段请求完成即停;已授权连续处理则按交互契约加载下一阶段,check 通过后方可进入 apply,不能超出授权范围。
|
|
17
17
|
- [ ] 代码实现将在 `/opsx-apply` 阶段进行
|
|
18
|
-
- [ ] ⛔ 完成本阶段后绝对禁止自动继续执行 task 等后续阶段
|
|
19
18
|
|
|
20
19
|
---
|
|
21
20
|
|
|
@@ -39,9 +38,9 @@ description: opsx-design 的阶段强制检查点与自检清单。仅在执行
|
|
|
39
38
|
|
|
40
39
|
- [ ] 必须以 `openspec-templates/design.md` 为模板基准
|
|
41
40
|
- [ ] ⛔ **渐进式加载**:严格按 overview.md → proposal.md → spec.md 顺序加载(层级图见 `./reference.md`「§3」)
|
|
42
|
-
- [ ] ⛔
|
|
41
|
+
- [ ] ⛔ **隔离红线**:默认聚焦当前 Capability;为核对已声明依赖可读取相关 Capability 契约并注明来源,不全量加载或混写其他能力。
|
|
43
42
|
- [ ] design.md 聚焦【How】,是 spec.md → tasks.md 的桥梁
|
|
44
43
|
- [ ] 必须 100% 覆盖 spec.md 定义的需求项
|
|
45
44
|
- [ ] 代码锚点必须具体到类/方法级别
|
|
46
45
|
- [ ] ⛔ **阶段边界**:禁止执行任何代码创建/修改操作
|
|
47
|
-
- [ ]
|
|
46
|
+
- [ ] **执行范围**:单阶段请求完成即停;已授权连续处理则按交互契约加载下一阶段,check 通过后方可进入 apply,不能超出授权范围。
|