kld-sdd 2.7.8 → 2.8.1
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 +26 -0
- package/USABILITY.md +72 -0
- package/bin/kld-sdd-init.js +718 -34
- package/lib/command-bridge.js +26 -17
- package/lib/deploy-codebuddy-hooks.js +19 -26
- package/lib/deploy-strategy.js +293 -0
- package/lib/hook-gate-core.js +7 -8
- package/lib/hook-install.js +75 -0
- package/lib/init.js +507 -726
- package/lib/maintenance.js +221 -0
- package/lib/managed-hook-hashes.json +24 -0
- package/lib/settings-merge.js +102 -25
- package/lib/skills-bundle.js +5 -5
- package/lib/spec-choice.js +85 -0
- package/lib/tool-profiles.js +2 -2
- package/lib/uninstall.js +453 -0
- package/package.json +7 -4
- package/skywalk-sdd/context-client.cjs +38 -7
- package/skywalk-sdd/index.cjs +153 -27
- package/skywalk-sdd/kb-sync-identity.cjs +1 -1
- package/skywalk-sdd/kb-upload.cjs +242 -85
- package/skywalk-sdd/lib/check-review.cjs +41 -0
- package/skywalk-sdd/lib/shared.cjs +62 -41
- package/skywalk-sdd/lib/test-execution.cjs +130 -0
- package/skywalk-sdd/lib/usage-reporter.cjs +17 -8
- package/skywalk-sdd/ontology/archive-package.cjs +141 -0
- package/skywalk-sdd/ontology/artifact-parser.cjs +24 -3
- package/skywalk-sdd/ontology/cli.cjs +53 -0
- package/skywalk-sdd/ontology/id.cjs +1 -1
- package/skywalk-sdd/ontology/identity-index.cjs +5 -1
- package/skywalk-sdd/ontology/resolve-spec-root.cjs +49 -0
- package/skywalk-sdd/ontology/schema.cjs +6 -0
- package/skywalk-sdd/ontology/sdd-config.cjs +33 -5
- package/skywalk-sdd/ontology/spec-detect.cjs +421 -0
- package/skywalk-sdd/ontology/spec-ontology.cjs +507 -0
- package/skywalk-sdd/ontology/workspace-layout.cjs +65 -17
- package/templates/git-hooks/commit-msg +17 -85
- package/templates/git-hooks/commit-msg-runner.cjs +44 -0
- package/templates/git-hooks/commit-msg-sdd-trailer.cjs +60 -69
- package/templates/git-hooks/consistency-check-core.cjs +92 -261
- package/templates/git-hooks/delivery-check.cjs +231 -0
- package/templates/git-hooks/delivery-snapshot.cjs +181 -0
- package/templates/git-hooks/hooks.config +8 -6
- package/templates/git-hooks/pre-commit +19 -5
- package/templates/git-hooks/pre-commit-consistency-check.cjs +13 -31
- package/templates/git-hooks/pre-push +19 -5
- package/templates/git-hooks/pre-push-consistency-check.cjs +36 -83
- package/templates/hooks/claude/hooks/sdd-post-tool.cjs +2 -2
- package/templates/hooks/codebuddy/hooks/hook-gate-core.cjs +7 -8
- package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +2 -2
- package/templates/openspec/proposal.md +1 -1
- package/templates/openspec/spec.md +8 -0
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +28 -24
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +12 -3
- package/templates/skills/kld-sdd/opsx-apply/reference.md +21 -1
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +20 -18
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +18 -13
- package/templates/skills/kld-sdd/opsx-check/reference.md +3 -1
- package/templates/skills/kld-sdd/opsx-check/review-template.json +22 -0
- package/templates/skills/kld-sdd/opsx-check/reviewer.md +27 -6
- package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +34 -112
- package/templates/skills/kld-sdd/opsx-consistency-check/scripts/scripts.cjs +15 -14
- package/templates/skills/kld-sdd/opsx-kb-config/SKILL.md +93 -61
- package/templates/skills/kld-sdd/opsx-kb-config/reference.md +14 -12
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +16 -5
- package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +13 -5
- package/templates/skills/kld-sdd/opsx-kb-ingest/state.example.json +18 -0
- package/templates/skills/kld-sdd/opsx-ontology-ingest/SKILL.md +77 -0
- package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +5 -4
- package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +14 -1
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-propose/interaction-policy.md +9 -0
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +5 -3
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +2 -2
- package/index.js +0 -13
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* node skywalk-sdd/kb-upload.cjs --folder=<path> [--minimum-level=propose-spec] [--validate-only]
|
|
13
13
|
* node skywalk-sdd/kb-upload.cjs --package=<zip-path>
|
|
14
14
|
* node skywalk-sdd/kb-upload.cjs --folder=<path> --project=<spec-root> --change=<name>
|
|
15
|
+
* node skywalk-sdd/kb-upload.cjs --folder=<path> --require-review
|
|
16
|
+
*
|
|
17
|
+
* 历史产物(缺 entity-id / version-id):
|
|
18
|
+
* 自动旁路物化 spec-ontology.json,无 semantic_key 冲突时自动确认并发布为 current;
|
|
19
|
+
* 有冲突或 --require-review 时只上传候选,需人工 approve-spec-ontology 后再上传。
|
|
15
20
|
*
|
|
16
21
|
* project_id 解析:
|
|
17
22
|
* 1. $SPEC_ROOT/skywalk-sdd/project-identity.json 的 project_id
|
|
@@ -28,18 +33,34 @@ const {
|
|
|
28
33
|
ensureDir,
|
|
29
34
|
copyDirRecursive,
|
|
30
35
|
resolveSpecRootFast,
|
|
31
|
-
|
|
36
|
+
KB_STATE_FILENAME,
|
|
37
|
+
isKbStateHostDir,
|
|
32
38
|
uploadFile,
|
|
33
39
|
httpGetJson,
|
|
34
40
|
} = require('./lib/shared.cjs');
|
|
35
41
|
|
|
36
42
|
function resolveUploadTarget(state, options = {}) {
|
|
37
43
|
if (Boolean(options.spaceId) !== Boolean(options.kbId)) throw new Error('必须同时指定 --space-id 和 --kb-id');
|
|
38
|
-
const targets = state.targets
|
|
44
|
+
const targets = Array.isArray(state?.targets) ? state.targets : [];
|
|
39
45
|
if (options.spaceId && options.kbId) {
|
|
40
46
|
return targets.find(t => t.spaceId === options.spaceId && t.kbId === options.kbId) || options;
|
|
41
47
|
}
|
|
42
|
-
|
|
48
|
+
|
|
49
|
+
const configured = state?.uploadTarget;
|
|
50
|
+
if (configured != null) {
|
|
51
|
+
if (!configured.spaceId || !configured.kbId) {
|
|
52
|
+
throw new Error('kb-state.json 的 uploadTarget 不完整,请在 SDD 助手中重新选择 Spec 上传知识库');
|
|
53
|
+
}
|
|
54
|
+
const selected = targets.find(t => t.spaceId === configured.spaceId && t.kbId === configured.kbId);
|
|
55
|
+
if (!selected) {
|
|
56
|
+
throw new Error('kb-state.json 的 uploadTarget 已不在 targets 读取范围中,请在 SDD 助手中重新选择 Spec 上传知识库');
|
|
57
|
+
}
|
|
58
|
+
return selected;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (targets.length !== 1) {
|
|
62
|
+
throw new Error('请明确上传目标:在 SDD 助手中选择“Spec 上传知识库”,或同时指定 --space-id 和 --kb-id;不会自动分发到多个 KB');
|
|
63
|
+
}
|
|
43
64
|
if (!targets[0].spaceId || !targets[0].kbId) throw new Error('知识库目标缺少 spaceId/kbId');
|
|
44
65
|
return targets[0];
|
|
45
66
|
}
|
|
@@ -49,18 +70,33 @@ function cacheKey(changeName, context) {
|
|
|
49
70
|
return JSON.stringify([context.api.replace(/\/+$/, ''), context.spaceId, context.kbId, changeName]);
|
|
50
71
|
}
|
|
51
72
|
|
|
52
|
-
function assertIngestionResult(result) {
|
|
73
|
+
function assertIngestionResult(result, options = {}) {
|
|
53
74
|
if (result?.code != null && result.code !== 0) throw new Error(result.message || 'KB API 拒绝入库');
|
|
54
75
|
const job = result?.data || result;
|
|
55
76
|
if (!job || !['projecting', 'succeeded'].includes(job.status) || job.relationStatus !== 'committed') {
|
|
56
77
|
throw new Error(`INGEST_NOT_COMMITTED: ${job?.errorCode || job?.status || 'unknown'} ${job?.errorMessage || ''} job=${job?.jobId || 'unknown'}`);
|
|
57
78
|
}
|
|
58
|
-
if (Number(job.report?.version_conflicts || 0) > 0
|
|
79
|
+
if (Number(job.report?.version_conflicts || 0) > 0) {
|
|
59
80
|
throw new Error(`VERSION_CONFLICT: 事实包已保存为候选,当前事实未更新;请合并基线后重新发布。job=${job.jobId}`);
|
|
60
81
|
}
|
|
82
|
+
if (job.report?.publication_status === 'needs_review' && !options.allowNeedsReview) {
|
|
83
|
+
throw new Error(`REVIEW_REQUIRED: 事实包已保存为候选,当前事实未更新;请审核本体后重新发布。job=${job.jobId}`);
|
|
84
|
+
}
|
|
61
85
|
return job;
|
|
62
86
|
}
|
|
63
87
|
|
|
88
|
+
/**
|
|
89
|
+
* 解析 kb-state.json 的权威路径:`<specRoot>/kb-state.json`。
|
|
90
|
+
*
|
|
91
|
+
* 位置校验已由 `resolveSpecRoot` 在入口完成(本函数只做路径拼接)。
|
|
92
|
+
*
|
|
93
|
+
* @param {string} specRoot spec 包裹包根目录
|
|
94
|
+
* @returns {string} kb-state.json 绝对路径
|
|
95
|
+
*/
|
|
96
|
+
function resolveKbStatePath(specRoot) {
|
|
97
|
+
return path.join(path.resolve(specRoot), KB_STATE_FILENAME);
|
|
98
|
+
}
|
|
99
|
+
|
|
64
100
|
/**
|
|
65
101
|
* 读取 kb-state.json 中指定 change 的 archive 缓存。
|
|
66
102
|
* @param {string} specRoot
|
|
@@ -68,9 +104,7 @@ function assertIngestionResult(result) {
|
|
|
68
104
|
* @returns {object|null} { archive_id, content_hash, uploaded_at }
|
|
69
105
|
*/
|
|
70
106
|
function readArchiveCache(specRoot, changeName, context) {
|
|
71
|
-
const
|
|
72
|
-
const legacyPath = path.join(specRoot, '..', '.shared', 'kb-state.json');
|
|
73
|
-
const statePath = fs.existsSync(directPath) ? directPath : legacyPath;
|
|
107
|
+
const statePath = resolveKbStatePath(specRoot);
|
|
74
108
|
const state = readJsonSafe(statePath);
|
|
75
109
|
if (!state || !state.archiveCache) return null;
|
|
76
110
|
const key = cacheKey(changeName, context);
|
|
@@ -85,9 +119,7 @@ function readArchiveCache(specRoot, changeName, context) {
|
|
|
85
119
|
* @param {string} contentHash
|
|
86
120
|
*/
|
|
87
121
|
function updateKbStateCache(specRoot, changeName, archiveId, contentHash, context) {
|
|
88
|
-
const
|
|
89
|
-
const legacyPath = path.join(specRoot, '..', '.shared', 'kb-state.json');
|
|
90
|
-
const statePath = fs.existsSync(directPath) ? directPath : legacyPath;
|
|
122
|
+
const statePath = resolveKbStatePath(specRoot);
|
|
91
123
|
const state = readJsonSafe(statePath) || {};
|
|
92
124
|
if (!state.archiveCache) state.archiveCache = {};
|
|
93
125
|
const key = cacheKey(changeName, context);
|
|
@@ -116,8 +148,18 @@ const DEFAULT_LEVEL = 'propose-spec';
|
|
|
116
148
|
// ── 工具函数 ──────────────────────────────────────────
|
|
117
149
|
|
|
118
150
|
function resolveSpecRoot(explicit) {
|
|
119
|
-
|
|
120
|
-
|
|
151
|
+
const root = explicit ? path.resolve(explicit) : resolveSpecRootFast(__dirname);
|
|
152
|
+
// 位置校验前移:specRoot 必须是 spec 包裹包根,后续所有 kb-state.json /
|
|
153
|
+
// project-identity.json 读取都建立在它之上。在此 fail-fast,避免流程走到
|
|
154
|
+
// 中途才因「文件不存在」报出误导性错误(真正原因是路径指错了地方)。
|
|
155
|
+
if (!isKbStateHostDir(root)) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`kb-state.json 位置校验失败:${root} 不是 spec 包裹包根目录。\n` +
|
|
158
|
+
'kb-state.json 只允许位于 spec 包裹包根(由 /opsx-kb-config 写入)。\n' +
|
|
159
|
+
'请确认 --project 指向 *-sdd-specs 包裹包,或运行 /opsx-kb-config 重新配置。'
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return root;
|
|
121
163
|
}
|
|
122
164
|
|
|
123
165
|
/**
|
|
@@ -139,36 +181,28 @@ function resolveProjectId(specRoot) {
|
|
|
139
181
|
}
|
|
140
182
|
|
|
141
183
|
/**
|
|
142
|
-
* 读取 KB 配置(api, apiKey, tenantKey, targets
|
|
143
|
-
*
|
|
184
|
+
* 读取 KB 配置(api, apiKey, tenantKey, targets)。
|
|
185
|
+
*
|
|
186
|
+
* 位置校验:只读 `<specRoot>/kb-state.json`(specRoot 的 spec 根身份已由
|
|
187
|
+
* resolveSpecRoot 在入口校验)。
|
|
144
188
|
*/
|
|
145
189
|
function readKbState(specRoot) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// 兼容旧路径:specRoot 同级 .shared/kb-state.json
|
|
149
|
-
const legacyPath = path.join(specRoot, '..', '.shared', 'kb-state.json');
|
|
150
|
-
if (fs.existsSync(directPath)) {
|
|
151
|
-
try {
|
|
152
|
-
return readJson(directPath);
|
|
153
|
-
} catch (e) {
|
|
154
|
-
throw new Error(
|
|
155
|
-
`KB 配置解析失败 (${directPath}): ${e.message}\n` +
|
|
156
|
-
'请运行 /opsx-kb-config 重新配置知识库。'
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
// 退回到多路径搜索(从 specRoot 向上查找)
|
|
161
|
-
const { config, path: foundPath, warnings } = loadKbState(specRoot);
|
|
162
|
-
if (warnings.length > 0) {
|
|
163
|
-
for (const w of warnings) console.warn(`⚠️ ${w}`);
|
|
164
|
-
}
|
|
165
|
-
if (!config) {
|
|
190
|
+
const statePath = resolveKbStatePath(specRoot);
|
|
191
|
+
if (!fs.existsSync(statePath)) {
|
|
166
192
|
throw new Error(
|
|
167
193
|
'KB 配置不存在(kb-state.json 缺失)。\n' +
|
|
194
|
+
`期望位置: ${statePath}\n` +
|
|
168
195
|
'请运行 /opsx-kb-config 配置知识库。'
|
|
169
196
|
);
|
|
170
197
|
}
|
|
171
|
-
|
|
198
|
+
try {
|
|
199
|
+
return readJson(statePath);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`KB 配置解析失败 (${statePath}): ${e.message}\n` +
|
|
203
|
+
'请运行 /opsx-kb-config 重新配置知识库。'
|
|
204
|
+
);
|
|
205
|
+
}
|
|
172
206
|
}
|
|
173
207
|
|
|
174
208
|
/**
|
|
@@ -204,7 +238,7 @@ function preCheckProjectId(projectId, state, selectedTarget) {
|
|
|
204
238
|
}
|
|
205
239
|
const spaceKey = (selectedTarget || resolveUploadTarget(state)).spaceKey;
|
|
206
240
|
if (!spaceKey) {
|
|
207
|
-
console.warn('⚠️ kb-state.json
|
|
241
|
+
console.warn('⚠️ kb-state.json 的上传目标缺少 spaceKey,跳过本地预检');
|
|
208
242
|
return;
|
|
209
243
|
}
|
|
210
244
|
if (projectId !== spaceKey) {
|
|
@@ -229,9 +263,65 @@ function preCheckProjectId(projectId, state, selectedTarget) {
|
|
|
229
263
|
* 5. materializeArchivePackage(生成 manifest + canonical-facts + conversion-report + zip)
|
|
230
264
|
* 6. 返回 zip 路径
|
|
231
265
|
*/
|
|
232
|
-
|
|
266
|
+
/**
|
|
267
|
+
* 历史产物的 Spec 本体解析:
|
|
268
|
+
* - 已审核且与当前 Markdown 一致 → 直接复用
|
|
269
|
+
* - 否则重新物化。重复锚点、非法 UUID、身份冲突由物化自动改写为稳定身份并确认发布
|
|
270
|
+
* - 只有显式 requireReview,或正文没有结构化事实时,才保留候选
|
|
271
|
+
* 全程不改写 Markdown,只读写 <change>/ontology/spec-ontology.json 与 legacy-mapping.json。
|
|
272
|
+
*/
|
|
273
|
+
function resolveLegacySpecOntology(specRoot, changeName, changeDir, facts, options = {}) {
|
|
274
|
+
const {
|
|
275
|
+
buildSpecOntology,
|
|
276
|
+
materializeChangeSpecOntology,
|
|
277
|
+
approveSpecOntologyFile,
|
|
278
|
+
specOntologyContentSignature,
|
|
279
|
+
} = require('./ontology/spec-ontology.cjs');
|
|
280
|
+
const { ensureProjectIdentity } = require('./ontology/archive-package.cjs');
|
|
281
|
+
const identity = ensureProjectIdentity(specRoot);
|
|
282
|
+
const ontologyPath = path.join(changeDir, 'ontology', 'spec-ontology.json');
|
|
283
|
+
|
|
284
|
+
const fresh = buildSpecOntology(facts, {
|
|
285
|
+
projectId: identity.project_id,
|
|
286
|
+
compatibilityMode: 'LEGACY_MATERIALIZED',
|
|
287
|
+
reviewStatus: 'pending',
|
|
288
|
+
});
|
|
289
|
+
const freshSignature = specOntologyContentSignature(fresh);
|
|
290
|
+
const existing = readJsonSafe(ontologyPath);
|
|
291
|
+
const existingApproved = existing
|
|
292
|
+
&& existing.compatibility_mode === 'LEGACY_MATERIALIZED'
|
|
293
|
+
&& existing.review_status === 'confirmed'
|
|
294
|
+
&& existing.publication_eligible === true
|
|
295
|
+
&& existing.project_id === identity.project_id;
|
|
296
|
+
if (existingApproved && specOntologyContentSignature(existing) === freshSignature) {
|
|
297
|
+
return { approved: existing, status: 'reused', path: ontologyPath, collisions: [] };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const materialized = materializeChangeSpecOntology(specRoot, changeName, {
|
|
301
|
+
changeDir,
|
|
302
|
+
projectId: identity.project_id,
|
|
303
|
+
compatibilityMode: 'LEGACY_MATERIALIZED',
|
|
304
|
+
reviewStatus: 'pending',
|
|
305
|
+
});
|
|
306
|
+
if (!materialized.validation.valid) {
|
|
307
|
+
throw new Error(`历史 Spec 本体物化失败: ${materialized.validation.errors.join('; ')}`);
|
|
308
|
+
}
|
|
309
|
+
const repairs = (materialized.payload.warnings || [])
|
|
310
|
+
.filter((warning) => warning.startsWith('LEGACY_DUPLICATE_ANCHOR:') || warning.startsWith('LEGACY_IDENTITY_REPAIRED:'));
|
|
311
|
+
if (options.requireReview) {
|
|
312
|
+
return { approved: null, status: 'pending_review', path: ontologyPath, collisions: repairs };
|
|
313
|
+
}
|
|
314
|
+
if (materialized.payload.entities.length === 0) {
|
|
315
|
+
return { approved: null, status: 'needs_review', path: ontologyPath, collisions: repairs };
|
|
316
|
+
}
|
|
317
|
+
const approved = approveSpecOntologyFile(materialized.output_path, { allowCollisions: true });
|
|
318
|
+
return { approved: approved.payload, status: 'auto_approved', path: ontologyPath, collisions: repairs };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function buildPackageFromFolder(specRoot, changeName, options = {}) {
|
|
233
322
|
const runtime = require('./ontology/runtime.cjs');
|
|
234
323
|
const archivePackage = require('./ontology/archive-package.cjs');
|
|
324
|
+
const { classifyLegacyDiagnostics } = require('./ontology/spec-ontology.cjs');
|
|
235
325
|
|
|
236
326
|
// 1. 语义管线
|
|
237
327
|
console.log('→ 运行语义管线 (semantic-reconcile)...');
|
|
@@ -239,13 +329,20 @@ function buildPackageFromFolder(specRoot, changeName, options) {
|
|
|
239
329
|
profile: 'auto',
|
|
240
330
|
markPending: true,
|
|
241
331
|
});
|
|
332
|
+
const legacy = classifyLegacyDiagnostics(semanticResult.diagnostics);
|
|
333
|
+
const legacyIdentityOnly = legacy.legacyCompatible;
|
|
242
334
|
if (!semanticResult.valid) {
|
|
243
|
-
|
|
244
|
-
.
|
|
245
|
-
|
|
246
|
-
|
|
335
|
+
if (!legacyIdentityOnly) {
|
|
336
|
+
const errors = legacy.blocking.map(d => `${d.code}: ${d.message}`);
|
|
337
|
+
throw new Error(`语义校验失败:\n${errors.join('\n')}`);
|
|
338
|
+
}
|
|
339
|
+
console.warn('⚠️ 检测到历史产物缺少身份标识,将生成旁路映射与本体包,不改写 Markdown。');
|
|
340
|
+
for (const item of legacy.tolerated) {
|
|
341
|
+
console.warn(` ↳ 兼容放行 ${item.code}: ${item.message}`);
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
console.log(`✓ 语义校验通过 (profile: ${semanticResult.profile}, revision: ${semanticResult.revision})`);
|
|
247
345
|
}
|
|
248
|
-
console.log(`✓ 语义校验通过 (profile: ${semanticResult.profile}, revision: ${semanticResult.revision})`);
|
|
249
346
|
|
|
250
347
|
// 2. 创建临时 staging 目录
|
|
251
348
|
const stagingDir = path.join(
|
|
@@ -258,28 +355,63 @@ function buildPackageFromFolder(specRoot, changeName, options) {
|
|
|
258
355
|
const changeDir = path.join(specRoot, 'openspec', 'changes', changeName);
|
|
259
356
|
copyDirRecursive(changeDir, stagingDir);
|
|
260
357
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
358
|
+
let result;
|
|
359
|
+
let legacyStatus = null;
|
|
360
|
+
if (legacyIdentityOnly) {
|
|
361
|
+
const resolved = resolveLegacySpecOntology(specRoot, changeName, changeDir, semanticResult.state, {
|
|
362
|
+
requireReview: Boolean(options.requireReview),
|
|
363
|
+
});
|
|
364
|
+
legacyStatus = resolved.status;
|
|
365
|
+
const messages = {
|
|
366
|
+
reused: '✓ 已发现审核通过且与当前 Markdown 一致的历史 Spec 本体,直接发布',
|
|
367
|
+
auto_approved: '✓ 历史 Spec 本体已自动物化并确认,身份和重复锚点已在旁路改写,将发布为 current 事实',
|
|
368
|
+
needs_review: '⚠️ 历史正文没有可发布的结构化事实,仅保留为证据候选',
|
|
369
|
+
pending_review: 'ℹ️ 按 --require-review 要求,历史 Spec 本体保留为待审核候选',
|
|
370
|
+
};
|
|
371
|
+
console.log(messages[resolved.status]);
|
|
372
|
+
result = archivePackage.materializeCompatibilityPackage({
|
|
373
|
+
projectRoot: specRoot,
|
|
374
|
+
archiveDir: stagingDir,
|
|
375
|
+
changeName,
|
|
376
|
+
facts: semanticResult.state,
|
|
377
|
+
specOntology: resolved.approved,
|
|
378
|
+
toleratedDiagnostics: legacy.tolerated,
|
|
379
|
+
packagePath: zipPath,
|
|
380
|
+
reason: options.reason || '历史 Spec 兼容入库',
|
|
381
|
+
method: 'kb-upload-legacy-folder',
|
|
382
|
+
});
|
|
383
|
+
console.log(resolved.approved
|
|
384
|
+
? '✓ 已生成审核通过的 LEGACY_MATERIALIZED 发布包'
|
|
385
|
+
: '✓ 已生成 LEGACY_MATERIALIZED 本体候选包');
|
|
386
|
+
} else {
|
|
387
|
+
// 严格产物继续生成 confirmed snapshot 和双协议事实包。
|
|
388
|
+
runtime.createArchiveSnapshot(specRoot, changeName, stagingDir, semanticResult.state);
|
|
389
|
+
console.log('✓ 已生成 archive-ontology.json');
|
|
390
|
+
archivePackage.ensureProjectIdentity(specRoot);
|
|
391
|
+
result = archivePackage.materializeArchivePackage({
|
|
392
|
+
projectRoot: specRoot,
|
|
393
|
+
archiveDir: stagingDir,
|
|
394
|
+
archivePath: stagingDir,
|
|
395
|
+
changeName: changeName,
|
|
396
|
+
packagePath: zipPath,
|
|
397
|
+
reason: options.reason || '随时入库(文件夹上传)',
|
|
398
|
+
method: 'kb-upload-folder',
|
|
399
|
+
});
|
|
400
|
+
}
|
|
276
401
|
|
|
277
402
|
console.log(`✓ Archive Package 已生成`);
|
|
278
403
|
console.log(` archive_id: ${result.manifest.archive_id}`);
|
|
279
404
|
console.log(` project_id: ${result.manifest.project_id}`);
|
|
280
405
|
console.log(` files: ${result.manifest.files.length}`);
|
|
281
406
|
|
|
282
|
-
return {
|
|
407
|
+
return {
|
|
408
|
+
zipPath,
|
|
409
|
+
manifest: result.manifest,
|
|
410
|
+
stagingDir,
|
|
411
|
+
requiresReview: result.conversion_report?.status === 'materialized_pending_review',
|
|
412
|
+
legacyStatus,
|
|
413
|
+
toleratedDiagnostics: legacyIdentityOnly ? legacy.tolerated : [],
|
|
414
|
+
};
|
|
283
415
|
} catch (err) {
|
|
284
416
|
// 出错时清理 staging
|
|
285
417
|
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
@@ -302,7 +434,7 @@ async function uploadZip(zipPath, state, options) {
|
|
|
302
434
|
|
|
303
435
|
try {
|
|
304
436
|
const result = await uploadFile(uploadUrl, zipPath, state.apiKey);
|
|
305
|
-
assertIngestionResult(result);
|
|
437
|
+
assertIngestionResult(result, options);
|
|
306
438
|
return result;
|
|
307
439
|
} catch (err) {
|
|
308
440
|
throw new Error(`上传失败: ${err.message}`);
|
|
@@ -319,6 +451,7 @@ async function main() {
|
|
|
319
451
|
console.error(' node skywalk-sdd/kb-upload.cjs --folder=<path> [--minimum-level=propose-spec] [--validate-only]');
|
|
320
452
|
console.error(' node skywalk-sdd/kb-upload.cjs --package=<zip-path>');
|
|
321
453
|
console.error(' node skywalk-sdd/kb-upload.cjs --folder=<path> --project=<spec-root> --change=<name>');
|
|
454
|
+
console.error(' node skywalk-sdd/kb-upload.cjs --folder=<path> --require-review # 历史产物只上传候选,不自动确认本体');
|
|
322
455
|
process.exit(1);
|
|
323
456
|
}
|
|
324
457
|
|
|
@@ -332,6 +465,7 @@ async function main() {
|
|
|
332
465
|
try {
|
|
333
466
|
// ── Zip 模式 ──
|
|
334
467
|
if (args.package) {
|
|
468
|
+
if (args['validate-only']) throw new Error('--validate-only 仅支持 --folder/--change 本地语义校验;未执行归档上传');
|
|
335
469
|
const zipPath = path.resolve(args.package);
|
|
336
470
|
if (!fs.existsSync(zipPath)) {
|
|
337
471
|
throw new Error(`zip 文件不存在: ${zipPath}`);
|
|
@@ -370,9 +504,12 @@ async function main() {
|
|
|
370
504
|
changeName = path.basename(folderPath);
|
|
371
505
|
}
|
|
372
506
|
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
507
|
+
const changesRoot = fs.realpathSync(path.resolve(specRoot, 'openspec', 'changes'));
|
|
508
|
+
const expectedPath = path.resolve(changesRoot, changeName);
|
|
509
|
+
const expectedFolder = fs.existsSync(expectedPath) ? fs.realpathSync(expectedPath) : expectedPath;
|
|
510
|
+
const suppliedFolder = fs.existsSync(folderPath) ? fs.realpathSync(folderPath) : path.resolve(folderPath);
|
|
511
|
+
if (suppliedFolder !== expectedFolder || !expectedFolder.startsWith(changesRoot + path.sep)) {
|
|
512
|
+
throw new Error('文件夹上传仅接受当前 Spec 仓 openspec/changes/<change>;已归档内容请用 --package 指定归档包');
|
|
376
513
|
}
|
|
377
514
|
|
|
378
515
|
if (!fs.existsSync(folderPath)) {
|
|
@@ -389,19 +526,6 @@ async function main() {
|
|
|
389
526
|
// 1. 校验必需文件
|
|
390
527
|
validateRequiredFiles(folderPath, minimumLevel);
|
|
391
528
|
|
|
392
|
-
// 2. 解析 project_id
|
|
393
|
-
const projectId = resolveProjectId(specRoot);
|
|
394
|
-
console.log(`✓ project_id: ${projectId}`);
|
|
395
|
-
|
|
396
|
-
// 3. 读取 KB 配置 + 预检
|
|
397
|
-
const state = readKbState(specRoot);
|
|
398
|
-
if (!state.apiKey) {
|
|
399
|
-
throw new Error('KB 配置中无 apiKey,请运行 /opsx-kb-config 配置知识库。');
|
|
400
|
-
}
|
|
401
|
-
const target = resolveUploadTarget(state, { spaceId: args['space-id'], kbId: args['kb-id'] });
|
|
402
|
-
preCheckProjectId(projectId, state, target);
|
|
403
|
-
const cacheContext = { api: state.api || 'http://localhost:8090/api', spaceId: target.spaceId, kbId: target.kbId };
|
|
404
|
-
|
|
405
529
|
// 4. 仅校验模式(含语义校验)
|
|
406
530
|
if (args['validate-only']) {
|
|
407
531
|
const runtime = require('./ontology/runtime.cjs');
|
|
@@ -411,20 +535,39 @@ async function main() {
|
|
|
411
535
|
markPending: true,
|
|
412
536
|
});
|
|
413
537
|
if (!semanticResult.valid) {
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
538
|
+
const { classifyLegacyDiagnostics } = require('./ontology/spec-ontology.cjs');
|
|
539
|
+
const legacy = classifyLegacyDiagnostics(semanticResult.diagnostics);
|
|
540
|
+
if (!legacy.legacyCompatible) {
|
|
541
|
+
const errors = legacy.blocking.map(d => `${d.code}: ${d.message}`);
|
|
542
|
+
throw new Error(`语义校验失败:\n${errors.join('\n')}`);
|
|
543
|
+
}
|
|
544
|
+
console.log('⚠️ 历史产物缺少身份标识,正式上传时将走 LEGACY_MATERIALIZED 旁路物化,不改写 Markdown');
|
|
545
|
+
for (const item of legacy.tolerated) console.log(` ↳ 兼容放行 ${item.code}: ${item.message}`);
|
|
546
|
+
} else {
|
|
547
|
+
console.log(`✓ 语义校验通过 (profile: ${semanticResult.profile}, revision: ${semanticResult.revision})`);
|
|
418
548
|
}
|
|
419
|
-
console.log(
|
|
420
|
-
console.log('\n✓ 校验完成(--validate-only,未上传)');
|
|
549
|
+
console.log('\n✓ 本地语义校验完成(--validate-only,未联网;不代表权限、发布基线或入库已验证)');
|
|
421
550
|
return;
|
|
422
551
|
}
|
|
423
552
|
|
|
553
|
+
// 2. 解析 project_id
|
|
554
|
+
const projectId = resolveProjectId(specRoot);
|
|
555
|
+
console.log(`✓ project_id: ${projectId}`);
|
|
556
|
+
|
|
557
|
+
// 3. 读取 KB 配置 + 预检
|
|
558
|
+
const state = readKbState(specRoot);
|
|
559
|
+
if (!state.apiKey) {
|
|
560
|
+
throw new Error('KB 配置中无 apiKey,请运行 /opsx-kb-config 配置知识库。');
|
|
561
|
+
}
|
|
562
|
+
const target = resolveUploadTarget(state, { spaceId: args['space-id'], kbId: args['kb-id'] });
|
|
563
|
+
preCheckProjectId(projectId, state, target);
|
|
564
|
+
const cacheContext = { api: state.api || 'http://localhost:8090/api', spaceId: target.spaceId, kbId: target.kbId };
|
|
565
|
+
|
|
424
566
|
// 5. 构建 Archive Package
|
|
425
567
|
console.log('');
|
|
426
|
-
const { zipPath, manifest, stagingDir } = buildPackageFromFolder(specRoot, changeName, {
|
|
568
|
+
const { zipPath, manifest, stagingDir, requiresReview, legacyStatus } = buildPackageFromFolder(specRoot, changeName, {
|
|
427
569
|
reason: args.reason,
|
|
570
|
+
requireReview: Boolean(args['require-review']),
|
|
428
571
|
});
|
|
429
572
|
|
|
430
573
|
try {
|
|
@@ -477,6 +620,7 @@ async function main() {
|
|
|
477
620
|
const result = await uploadZip(zipPath, state, {
|
|
478
621
|
spaceId: target.spaceId,
|
|
479
622
|
kbId: target.kbId,
|
|
623
|
+
allowNeedsReview: requiresReview,
|
|
480
624
|
});
|
|
481
625
|
|
|
482
626
|
console.log('\n── 上传结果 ──');
|
|
@@ -489,8 +633,21 @@ async function main() {
|
|
|
489
633
|
console.log(` change_id: ${manifest.change_id}`);
|
|
490
634
|
console.log(` files: ${manifest.files.length}`);
|
|
491
635
|
|
|
492
|
-
|
|
493
|
-
|
|
636
|
+
const publicationStatus = result?.data?.report?.publication_status
|
|
637
|
+
|| result?.report?.publication_status
|
|
638
|
+
|| '';
|
|
639
|
+
if (publicationStatus === 'needs_review') {
|
|
640
|
+
console.log('\n⚠️ 历史 Spec 已安全保存为候选,尚未进入 current 召回');
|
|
641
|
+
console.log(` → 审核 ${path.join(folderPath, 'ontology', 'spec-ontology.json')}`);
|
|
642
|
+
if (legacyStatus === 'needs_review') {
|
|
643
|
+
console.log(' → 正文没有足够的结构化事实,只能作为证据保留');
|
|
644
|
+
} else {
|
|
645
|
+
console.log(' → 运行 approve-spec-ontology 后再次上传即可发布;不带 --require-review 上传则自动确认');
|
|
646
|
+
}
|
|
647
|
+
} else {
|
|
648
|
+
// Published commit has been verified; a receipt does not imply projection completion.
|
|
649
|
+
updateKbStateCache(specRoot, changeName, archiveId, contentHash, cacheContext);
|
|
650
|
+
}
|
|
494
651
|
|
|
495
652
|
// === 投影状态提示 ===
|
|
496
653
|
// 上传成功后,KB 后端异步执行 vector embedding + AGE graph 投影。
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const CATEGORIES = ['completeness', 'consistency', 'executability', 'tdd_compliance'];
|
|
4
|
+
|
|
5
|
+
function assembleReview(review, taskCompletion, options = {}) {
|
|
6
|
+
if (!review || !Array.isArray(review.items) || !review.items.length) throw new Error('CHECK_ITEMS_REQUIRED: 提供实际评审项,不能提交空模板');
|
|
7
|
+
const categories = Object.fromEntries(CATEGORIES.map(key => [key, { passed: 0, total: 0 }]));
|
|
8
|
+
const warnings = [], suggestions = [], ids = new Set();
|
|
9
|
+
for (const item of review.items) {
|
|
10
|
+
if (!item.id || ids.has(item.id)) throw new Error('CHECK_ITEM_ID_INVALID: 检查项必须有唯一 ID');
|
|
11
|
+
ids.add(item.id);
|
|
12
|
+
if (!CATEGORIES.includes(item.category) || !['passed', 'error', 'warning', 'suggestion', 'na'].includes(item.status)) throw new Error('CHECK_ITEM_STATUS_INVALID');
|
|
13
|
+
if (typeof item.evidence !== 'string' || !item.evidence.trim()) throw new Error(`CHECK_ITEM_EVIDENCE_REQUIRED: ${item.id} 缺少来源证据或不适用原因`);
|
|
14
|
+
if (item.status === 'na') continue;
|
|
15
|
+
categories[item.category].total++;
|
|
16
|
+
if (item.status !== 'error') categories[item.category].passed++;
|
|
17
|
+
if (item.status === 'warning') warnings.push({ category: item.category, warning: item.id, description: item.description || item.evidence, target: item.evidence });
|
|
18
|
+
if (item.status === 'suggestion') suggestions.push(item);
|
|
19
|
+
}
|
|
20
|
+
const total = Object.values(categories).reduce((n, c) => n + c.total, 0);
|
|
21
|
+
const passed = Object.values(categories).reduce((n, c) => n + c.passed, 0);
|
|
22
|
+
if (!total) throw new Error('CHECK_ITEMS_REQUIRED: 至少有一个实际适用的评审项');
|
|
23
|
+
if (taskCompletion.has_incomplete) warnings.push({ category: 'task_completion', warning: 'TASKS_INCOMPLETE', description: `${taskCompletion.incomplete} 个任务尚未完成`, target: 'tasks.md' });
|
|
24
|
+
const result = total !== passed ? 'failure' : (warnings.length ? 'partial' : 'success');
|
|
25
|
+
return {
|
|
26
|
+
result,
|
|
27
|
+
details: { check_results: {
|
|
28
|
+
...review.reviewer,
|
|
29
|
+
total, errors: total - passed, warnings: warnings.length, suggestions: suggestions.length,
|
|
30
|
+
categories, warning_items: warnings, warning_dispositions: review.warning_dispositions || [],
|
|
31
|
+
// This field means items satisfied before implementation, not defect fixes.
|
|
32
|
+
fixed_before_apply: options.beforeApply ? passed : Math.min(total, options.previousBeforeApply || 0),
|
|
33
|
+
consistency_score: null,
|
|
34
|
+
task_completion: { ...taskCompletion, checked_for_archive_readiness: !options.beforeApply },
|
|
35
|
+
review_items: review.items,
|
|
36
|
+
review_revision: options.revision,
|
|
37
|
+
} },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { assembleReview };
|