kld-sdd 2.7.8 → 2.8.0
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-upload.cjs +224 -82
- 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 +82 -61
- package/templates/skills/kld-sdd/opsx-kb-config/reference.md +8 -11
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +3 -2
- package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +2 -2
- 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,7 +33,8 @@ 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');
|
|
@@ -49,18 +55,33 @@ function cacheKey(changeName, context) {
|
|
|
49
55
|
return JSON.stringify([context.api.replace(/\/+$/, ''), context.spaceId, context.kbId, changeName]);
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
function assertIngestionResult(result) {
|
|
58
|
+
function assertIngestionResult(result, options = {}) {
|
|
53
59
|
if (result?.code != null && result.code !== 0) throw new Error(result.message || 'KB API 拒绝入库');
|
|
54
60
|
const job = result?.data || result;
|
|
55
61
|
if (!job || !['projecting', 'succeeded'].includes(job.status) || job.relationStatus !== 'committed') {
|
|
56
62
|
throw new Error(`INGEST_NOT_COMMITTED: ${job?.errorCode || job?.status || 'unknown'} ${job?.errorMessage || ''} job=${job?.jobId || 'unknown'}`);
|
|
57
63
|
}
|
|
58
|
-
if (Number(job.report?.version_conflicts || 0) > 0
|
|
64
|
+
if (Number(job.report?.version_conflicts || 0) > 0) {
|
|
59
65
|
throw new Error(`VERSION_CONFLICT: 事实包已保存为候选,当前事实未更新;请合并基线后重新发布。job=${job.jobId}`);
|
|
60
66
|
}
|
|
67
|
+
if (job.report?.publication_status === 'needs_review' && !options.allowNeedsReview) {
|
|
68
|
+
throw new Error(`REVIEW_REQUIRED: 事实包已保存为候选,当前事实未更新;请审核本体后重新发布。job=${job.jobId}`);
|
|
69
|
+
}
|
|
61
70
|
return job;
|
|
62
71
|
}
|
|
63
72
|
|
|
73
|
+
/**
|
|
74
|
+
* 解析 kb-state.json 的权威路径:`<specRoot>/kb-state.json`。
|
|
75
|
+
*
|
|
76
|
+
* 位置校验已由 `resolveSpecRoot` 在入口完成(本函数只做路径拼接)。
|
|
77
|
+
*
|
|
78
|
+
* @param {string} specRoot spec 包裹包根目录
|
|
79
|
+
* @returns {string} kb-state.json 绝对路径
|
|
80
|
+
*/
|
|
81
|
+
function resolveKbStatePath(specRoot) {
|
|
82
|
+
return path.join(path.resolve(specRoot), KB_STATE_FILENAME);
|
|
83
|
+
}
|
|
84
|
+
|
|
64
85
|
/**
|
|
65
86
|
* 读取 kb-state.json 中指定 change 的 archive 缓存。
|
|
66
87
|
* @param {string} specRoot
|
|
@@ -68,9 +89,7 @@ function assertIngestionResult(result) {
|
|
|
68
89
|
* @returns {object|null} { archive_id, content_hash, uploaded_at }
|
|
69
90
|
*/
|
|
70
91
|
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;
|
|
92
|
+
const statePath = resolveKbStatePath(specRoot);
|
|
74
93
|
const state = readJsonSafe(statePath);
|
|
75
94
|
if (!state || !state.archiveCache) return null;
|
|
76
95
|
const key = cacheKey(changeName, context);
|
|
@@ -85,9 +104,7 @@ function readArchiveCache(specRoot, changeName, context) {
|
|
|
85
104
|
* @param {string} contentHash
|
|
86
105
|
*/
|
|
87
106
|
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;
|
|
107
|
+
const statePath = resolveKbStatePath(specRoot);
|
|
91
108
|
const state = readJsonSafe(statePath) || {};
|
|
92
109
|
if (!state.archiveCache) state.archiveCache = {};
|
|
93
110
|
const key = cacheKey(changeName, context);
|
|
@@ -116,8 +133,18 @@ const DEFAULT_LEVEL = 'propose-spec';
|
|
|
116
133
|
// ── 工具函数 ──────────────────────────────────────────
|
|
117
134
|
|
|
118
135
|
function resolveSpecRoot(explicit) {
|
|
119
|
-
|
|
120
|
-
|
|
136
|
+
const root = explicit ? path.resolve(explicit) : resolveSpecRootFast(__dirname);
|
|
137
|
+
// 位置校验前移:specRoot 必须是 spec 包裹包根,后续所有 kb-state.json /
|
|
138
|
+
// project-identity.json 读取都建立在它之上。在此 fail-fast,避免流程走到
|
|
139
|
+
// 中途才因「文件不存在」报出误导性错误(真正原因是路径指错了地方)。
|
|
140
|
+
if (!isKbStateHostDir(root)) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`kb-state.json 位置校验失败:${root} 不是 spec 包裹包根目录。\n` +
|
|
143
|
+
'kb-state.json 只允许位于 spec 包裹包根(由 /opsx-kb-config 写入)。\n' +
|
|
144
|
+
'请确认 --project 指向 *-sdd-specs 包裹包,或运行 /opsx-kb-config 重新配置。'
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return root;
|
|
121
148
|
}
|
|
122
149
|
|
|
123
150
|
/**
|
|
@@ -139,36 +166,28 @@ function resolveProjectId(specRoot) {
|
|
|
139
166
|
}
|
|
140
167
|
|
|
141
168
|
/**
|
|
142
|
-
* 读取 KB 配置(api, apiKey, tenantKey, targets
|
|
143
|
-
*
|
|
169
|
+
* 读取 KB 配置(api, apiKey, tenantKey, targets)。
|
|
170
|
+
*
|
|
171
|
+
* 位置校验:只读 `<specRoot>/kb-state.json`(specRoot 的 spec 根身份已由
|
|
172
|
+
* resolveSpecRoot 在入口校验)。
|
|
144
173
|
*/
|
|
145
174
|
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) {
|
|
175
|
+
const statePath = resolveKbStatePath(specRoot);
|
|
176
|
+
if (!fs.existsSync(statePath)) {
|
|
166
177
|
throw new Error(
|
|
167
178
|
'KB 配置不存在(kb-state.json 缺失)。\n' +
|
|
179
|
+
`期望位置: ${statePath}\n` +
|
|
168
180
|
'请运行 /opsx-kb-config 配置知识库。'
|
|
169
181
|
);
|
|
170
182
|
}
|
|
171
|
-
|
|
183
|
+
try {
|
|
184
|
+
return readJson(statePath);
|
|
185
|
+
} catch (e) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`KB 配置解析失败 (${statePath}): ${e.message}\n` +
|
|
188
|
+
'请运行 /opsx-kb-config 重新配置知识库。'
|
|
189
|
+
);
|
|
190
|
+
}
|
|
172
191
|
}
|
|
173
192
|
|
|
174
193
|
/**
|
|
@@ -229,9 +248,65 @@ function preCheckProjectId(projectId, state, selectedTarget) {
|
|
|
229
248
|
* 5. materializeArchivePackage(生成 manifest + canonical-facts + conversion-report + zip)
|
|
230
249
|
* 6. 返回 zip 路径
|
|
231
250
|
*/
|
|
232
|
-
|
|
251
|
+
/**
|
|
252
|
+
* 历史产物的 Spec 本体解析:
|
|
253
|
+
* - 已审核且与当前 Markdown 一致 → 直接复用
|
|
254
|
+
* - 否则重新物化。重复锚点、非法 UUID、身份冲突由物化自动改写为稳定身份并确认发布
|
|
255
|
+
* - 只有显式 requireReview,或正文没有结构化事实时,才保留候选
|
|
256
|
+
* 全程不改写 Markdown,只读写 <change>/ontology/spec-ontology.json 与 legacy-mapping.json。
|
|
257
|
+
*/
|
|
258
|
+
function resolveLegacySpecOntology(specRoot, changeName, changeDir, facts, options = {}) {
|
|
259
|
+
const {
|
|
260
|
+
buildSpecOntology,
|
|
261
|
+
materializeChangeSpecOntology,
|
|
262
|
+
approveSpecOntologyFile,
|
|
263
|
+
specOntologyContentSignature,
|
|
264
|
+
} = require('./ontology/spec-ontology.cjs');
|
|
265
|
+
const { ensureProjectIdentity } = require('./ontology/archive-package.cjs');
|
|
266
|
+
const identity = ensureProjectIdentity(specRoot);
|
|
267
|
+
const ontologyPath = path.join(changeDir, 'ontology', 'spec-ontology.json');
|
|
268
|
+
|
|
269
|
+
const fresh = buildSpecOntology(facts, {
|
|
270
|
+
projectId: identity.project_id,
|
|
271
|
+
compatibilityMode: 'LEGACY_MATERIALIZED',
|
|
272
|
+
reviewStatus: 'pending',
|
|
273
|
+
});
|
|
274
|
+
const freshSignature = specOntologyContentSignature(fresh);
|
|
275
|
+
const existing = readJsonSafe(ontologyPath);
|
|
276
|
+
const existingApproved = existing
|
|
277
|
+
&& existing.compatibility_mode === 'LEGACY_MATERIALIZED'
|
|
278
|
+
&& existing.review_status === 'confirmed'
|
|
279
|
+
&& existing.publication_eligible === true
|
|
280
|
+
&& existing.project_id === identity.project_id;
|
|
281
|
+
if (existingApproved && specOntologyContentSignature(existing) === freshSignature) {
|
|
282
|
+
return { approved: existing, status: 'reused', path: ontologyPath, collisions: [] };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const materialized = materializeChangeSpecOntology(specRoot, changeName, {
|
|
286
|
+
changeDir,
|
|
287
|
+
projectId: identity.project_id,
|
|
288
|
+
compatibilityMode: 'LEGACY_MATERIALIZED',
|
|
289
|
+
reviewStatus: 'pending',
|
|
290
|
+
});
|
|
291
|
+
if (!materialized.validation.valid) {
|
|
292
|
+
throw new Error(`历史 Spec 本体物化失败: ${materialized.validation.errors.join('; ')}`);
|
|
293
|
+
}
|
|
294
|
+
const repairs = (materialized.payload.warnings || [])
|
|
295
|
+
.filter((warning) => warning.startsWith('LEGACY_DUPLICATE_ANCHOR:') || warning.startsWith('LEGACY_IDENTITY_REPAIRED:'));
|
|
296
|
+
if (options.requireReview) {
|
|
297
|
+
return { approved: null, status: 'pending_review', path: ontologyPath, collisions: repairs };
|
|
298
|
+
}
|
|
299
|
+
if (materialized.payload.entities.length === 0) {
|
|
300
|
+
return { approved: null, status: 'needs_review', path: ontologyPath, collisions: repairs };
|
|
301
|
+
}
|
|
302
|
+
const approved = approveSpecOntologyFile(materialized.output_path, { allowCollisions: true });
|
|
303
|
+
return { approved: approved.payload, status: 'auto_approved', path: ontologyPath, collisions: repairs };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function buildPackageFromFolder(specRoot, changeName, options = {}) {
|
|
233
307
|
const runtime = require('./ontology/runtime.cjs');
|
|
234
308
|
const archivePackage = require('./ontology/archive-package.cjs');
|
|
309
|
+
const { classifyLegacyDiagnostics } = require('./ontology/spec-ontology.cjs');
|
|
235
310
|
|
|
236
311
|
// 1. 语义管线
|
|
237
312
|
console.log('→ 运行语义管线 (semantic-reconcile)...');
|
|
@@ -239,13 +314,20 @@ function buildPackageFromFolder(specRoot, changeName, options) {
|
|
|
239
314
|
profile: 'auto',
|
|
240
315
|
markPending: true,
|
|
241
316
|
});
|
|
317
|
+
const legacy = classifyLegacyDiagnostics(semanticResult.diagnostics);
|
|
318
|
+
const legacyIdentityOnly = legacy.legacyCompatible;
|
|
242
319
|
if (!semanticResult.valid) {
|
|
243
|
-
|
|
244
|
-
.
|
|
245
|
-
|
|
246
|
-
|
|
320
|
+
if (!legacyIdentityOnly) {
|
|
321
|
+
const errors = legacy.blocking.map(d => `${d.code}: ${d.message}`);
|
|
322
|
+
throw new Error(`语义校验失败:\n${errors.join('\n')}`);
|
|
323
|
+
}
|
|
324
|
+
console.warn('⚠️ 检测到历史产物缺少身份标识,将生成旁路映射与本体包,不改写 Markdown。');
|
|
325
|
+
for (const item of legacy.tolerated) {
|
|
326
|
+
console.warn(` ↳ 兼容放行 ${item.code}: ${item.message}`);
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
console.log(`✓ 语义校验通过 (profile: ${semanticResult.profile}, revision: ${semanticResult.revision})`);
|
|
247
330
|
}
|
|
248
|
-
console.log(`✓ 语义校验通过 (profile: ${semanticResult.profile}, revision: ${semanticResult.revision})`);
|
|
249
331
|
|
|
250
332
|
// 2. 创建临时 staging 目录
|
|
251
333
|
const stagingDir = path.join(
|
|
@@ -258,28 +340,63 @@ function buildPackageFromFolder(specRoot, changeName, options) {
|
|
|
258
340
|
const changeDir = path.join(specRoot, 'openspec', 'changes', changeName);
|
|
259
341
|
copyDirRecursive(changeDir, stagingDir);
|
|
260
342
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
343
|
+
let result;
|
|
344
|
+
let legacyStatus = null;
|
|
345
|
+
if (legacyIdentityOnly) {
|
|
346
|
+
const resolved = resolveLegacySpecOntology(specRoot, changeName, changeDir, semanticResult.state, {
|
|
347
|
+
requireReview: Boolean(options.requireReview),
|
|
348
|
+
});
|
|
349
|
+
legacyStatus = resolved.status;
|
|
350
|
+
const messages = {
|
|
351
|
+
reused: '✓ 已发现审核通过且与当前 Markdown 一致的历史 Spec 本体,直接发布',
|
|
352
|
+
auto_approved: '✓ 历史 Spec 本体已自动物化并确认,身份和重复锚点已在旁路改写,将发布为 current 事实',
|
|
353
|
+
needs_review: '⚠️ 历史正文没有可发布的结构化事实,仅保留为证据候选',
|
|
354
|
+
pending_review: 'ℹ️ 按 --require-review 要求,历史 Spec 本体保留为待审核候选',
|
|
355
|
+
};
|
|
356
|
+
console.log(messages[resolved.status]);
|
|
357
|
+
result = archivePackage.materializeCompatibilityPackage({
|
|
358
|
+
projectRoot: specRoot,
|
|
359
|
+
archiveDir: stagingDir,
|
|
360
|
+
changeName,
|
|
361
|
+
facts: semanticResult.state,
|
|
362
|
+
specOntology: resolved.approved,
|
|
363
|
+
toleratedDiagnostics: legacy.tolerated,
|
|
364
|
+
packagePath: zipPath,
|
|
365
|
+
reason: options.reason || '历史 Spec 兼容入库',
|
|
366
|
+
method: 'kb-upload-legacy-folder',
|
|
367
|
+
});
|
|
368
|
+
console.log(resolved.approved
|
|
369
|
+
? '✓ 已生成审核通过的 LEGACY_MATERIALIZED 发布包'
|
|
370
|
+
: '✓ 已生成 LEGACY_MATERIALIZED 本体候选包');
|
|
371
|
+
} else {
|
|
372
|
+
// 严格产物继续生成 confirmed snapshot 和双协议事实包。
|
|
373
|
+
runtime.createArchiveSnapshot(specRoot, changeName, stagingDir, semanticResult.state);
|
|
374
|
+
console.log('✓ 已生成 archive-ontology.json');
|
|
375
|
+
archivePackage.ensureProjectIdentity(specRoot);
|
|
376
|
+
result = archivePackage.materializeArchivePackage({
|
|
377
|
+
projectRoot: specRoot,
|
|
378
|
+
archiveDir: stagingDir,
|
|
379
|
+
archivePath: stagingDir,
|
|
380
|
+
changeName: changeName,
|
|
381
|
+
packagePath: zipPath,
|
|
382
|
+
reason: options.reason || '随时入库(文件夹上传)',
|
|
383
|
+
method: 'kb-upload-folder',
|
|
384
|
+
});
|
|
385
|
+
}
|
|
276
386
|
|
|
277
387
|
console.log(`✓ Archive Package 已生成`);
|
|
278
388
|
console.log(` archive_id: ${result.manifest.archive_id}`);
|
|
279
389
|
console.log(` project_id: ${result.manifest.project_id}`);
|
|
280
390
|
console.log(` files: ${result.manifest.files.length}`);
|
|
281
391
|
|
|
282
|
-
return {
|
|
392
|
+
return {
|
|
393
|
+
zipPath,
|
|
394
|
+
manifest: result.manifest,
|
|
395
|
+
stagingDir,
|
|
396
|
+
requiresReview: result.conversion_report?.status === 'materialized_pending_review',
|
|
397
|
+
legacyStatus,
|
|
398
|
+
toleratedDiagnostics: legacyIdentityOnly ? legacy.tolerated : [],
|
|
399
|
+
};
|
|
283
400
|
} catch (err) {
|
|
284
401
|
// 出错时清理 staging
|
|
285
402
|
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
@@ -302,7 +419,7 @@ async function uploadZip(zipPath, state, options) {
|
|
|
302
419
|
|
|
303
420
|
try {
|
|
304
421
|
const result = await uploadFile(uploadUrl, zipPath, state.apiKey);
|
|
305
|
-
assertIngestionResult(result);
|
|
422
|
+
assertIngestionResult(result, options);
|
|
306
423
|
return result;
|
|
307
424
|
} catch (err) {
|
|
308
425
|
throw new Error(`上传失败: ${err.message}`);
|
|
@@ -319,6 +436,7 @@ async function main() {
|
|
|
319
436
|
console.error(' node skywalk-sdd/kb-upload.cjs --folder=<path> [--minimum-level=propose-spec] [--validate-only]');
|
|
320
437
|
console.error(' node skywalk-sdd/kb-upload.cjs --package=<zip-path>');
|
|
321
438
|
console.error(' node skywalk-sdd/kb-upload.cjs --folder=<path> --project=<spec-root> --change=<name>');
|
|
439
|
+
console.error(' node skywalk-sdd/kb-upload.cjs --folder=<path> --require-review # 历史产物只上传候选,不自动确认本体');
|
|
322
440
|
process.exit(1);
|
|
323
441
|
}
|
|
324
442
|
|
|
@@ -332,6 +450,7 @@ async function main() {
|
|
|
332
450
|
try {
|
|
333
451
|
// ── Zip 模式 ──
|
|
334
452
|
if (args.package) {
|
|
453
|
+
if (args['validate-only']) throw new Error('--validate-only 仅支持 --folder/--change 本地语义校验;未执行归档上传');
|
|
335
454
|
const zipPath = path.resolve(args.package);
|
|
336
455
|
if (!fs.existsSync(zipPath)) {
|
|
337
456
|
throw new Error(`zip 文件不存在: ${zipPath}`);
|
|
@@ -370,9 +489,12 @@ async function main() {
|
|
|
370
489
|
changeName = path.basename(folderPath);
|
|
371
490
|
}
|
|
372
491
|
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
492
|
+
const changesRoot = fs.realpathSync(path.resolve(specRoot, 'openspec', 'changes'));
|
|
493
|
+
const expectedPath = path.resolve(changesRoot, changeName);
|
|
494
|
+
const expectedFolder = fs.existsSync(expectedPath) ? fs.realpathSync(expectedPath) : expectedPath;
|
|
495
|
+
const suppliedFolder = fs.existsSync(folderPath) ? fs.realpathSync(folderPath) : path.resolve(folderPath);
|
|
496
|
+
if (suppliedFolder !== expectedFolder || !expectedFolder.startsWith(changesRoot + path.sep)) {
|
|
497
|
+
throw new Error('文件夹上传仅接受当前 Spec 仓 openspec/changes/<change>;已归档内容请用 --package 指定归档包');
|
|
376
498
|
}
|
|
377
499
|
|
|
378
500
|
if (!fs.existsSync(folderPath)) {
|
|
@@ -389,19 +511,6 @@ async function main() {
|
|
|
389
511
|
// 1. 校验必需文件
|
|
390
512
|
validateRequiredFiles(folderPath, minimumLevel);
|
|
391
513
|
|
|
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
514
|
// 4. 仅校验模式(含语义校验)
|
|
406
515
|
if (args['validate-only']) {
|
|
407
516
|
const runtime = require('./ontology/runtime.cjs');
|
|
@@ -411,20 +520,39 @@ async function main() {
|
|
|
411
520
|
markPending: true,
|
|
412
521
|
});
|
|
413
522
|
if (!semanticResult.valid) {
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
523
|
+
const { classifyLegacyDiagnostics } = require('./ontology/spec-ontology.cjs');
|
|
524
|
+
const legacy = classifyLegacyDiagnostics(semanticResult.diagnostics);
|
|
525
|
+
if (!legacy.legacyCompatible) {
|
|
526
|
+
const errors = legacy.blocking.map(d => `${d.code}: ${d.message}`);
|
|
527
|
+
throw new Error(`语义校验失败:\n${errors.join('\n')}`);
|
|
528
|
+
}
|
|
529
|
+
console.log('⚠️ 历史产物缺少身份标识,正式上传时将走 LEGACY_MATERIALIZED 旁路物化,不改写 Markdown');
|
|
530
|
+
for (const item of legacy.tolerated) console.log(` ↳ 兼容放行 ${item.code}: ${item.message}`);
|
|
531
|
+
} else {
|
|
532
|
+
console.log(`✓ 语义校验通过 (profile: ${semanticResult.profile}, revision: ${semanticResult.revision})`);
|
|
418
533
|
}
|
|
419
|
-
console.log(
|
|
420
|
-
console.log('\n✓ 校验完成(--validate-only,未上传)');
|
|
534
|
+
console.log('\n✓ 本地语义校验完成(--validate-only,未联网;不代表权限、发布基线或入库已验证)');
|
|
421
535
|
return;
|
|
422
536
|
}
|
|
423
537
|
|
|
538
|
+
// 2. 解析 project_id
|
|
539
|
+
const projectId = resolveProjectId(specRoot);
|
|
540
|
+
console.log(`✓ project_id: ${projectId}`);
|
|
541
|
+
|
|
542
|
+
// 3. 读取 KB 配置 + 预检
|
|
543
|
+
const state = readKbState(specRoot);
|
|
544
|
+
if (!state.apiKey) {
|
|
545
|
+
throw new Error('KB 配置中无 apiKey,请运行 /opsx-kb-config 配置知识库。');
|
|
546
|
+
}
|
|
547
|
+
const target = resolveUploadTarget(state, { spaceId: args['space-id'], kbId: args['kb-id'] });
|
|
548
|
+
preCheckProjectId(projectId, state, target);
|
|
549
|
+
const cacheContext = { api: state.api || 'http://localhost:8090/api', spaceId: target.spaceId, kbId: target.kbId };
|
|
550
|
+
|
|
424
551
|
// 5. 构建 Archive Package
|
|
425
552
|
console.log('');
|
|
426
|
-
const { zipPath, manifest, stagingDir } = buildPackageFromFolder(specRoot, changeName, {
|
|
553
|
+
const { zipPath, manifest, stagingDir, requiresReview, legacyStatus } = buildPackageFromFolder(specRoot, changeName, {
|
|
427
554
|
reason: args.reason,
|
|
555
|
+
requireReview: Boolean(args['require-review']),
|
|
428
556
|
});
|
|
429
557
|
|
|
430
558
|
try {
|
|
@@ -477,6 +605,7 @@ async function main() {
|
|
|
477
605
|
const result = await uploadZip(zipPath, state, {
|
|
478
606
|
spaceId: target.spaceId,
|
|
479
607
|
kbId: target.kbId,
|
|
608
|
+
allowNeedsReview: requiresReview,
|
|
480
609
|
});
|
|
481
610
|
|
|
482
611
|
console.log('\n── 上传结果 ──');
|
|
@@ -489,8 +618,21 @@ async function main() {
|
|
|
489
618
|
console.log(` change_id: ${manifest.change_id}`);
|
|
490
619
|
console.log(` files: ${manifest.files.length}`);
|
|
491
620
|
|
|
492
|
-
|
|
493
|
-
|
|
621
|
+
const publicationStatus = result?.data?.report?.publication_status
|
|
622
|
+
|| result?.report?.publication_status
|
|
623
|
+
|| '';
|
|
624
|
+
if (publicationStatus === 'needs_review') {
|
|
625
|
+
console.log('\n⚠️ 历史 Spec 已安全保存为候选,尚未进入 current 召回');
|
|
626
|
+
console.log(` → 审核 ${path.join(folderPath, 'ontology', 'spec-ontology.json')}`);
|
|
627
|
+
if (legacyStatus === 'needs_review') {
|
|
628
|
+
console.log(' → 正文没有足够的结构化事实,只能作为证据保留');
|
|
629
|
+
} else {
|
|
630
|
+
console.log(' → 运行 approve-spec-ontology 后再次上传即可发布;不带 --require-review 上传则自动确认');
|
|
631
|
+
}
|
|
632
|
+
} else {
|
|
633
|
+
// Published commit has been verified; a receipt does not imply projection completion.
|
|
634
|
+
updateKbStateCache(specRoot, changeName, archiveId, contentHash, cacheContext);
|
|
635
|
+
}
|
|
494
636
|
|
|
495
637
|
// === 投影状态提示 ===
|
|
496
638
|
// 上传成功后,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 };
|
|
@@ -289,61 +289,81 @@ function resolveSpecRootFast(anchorDir = __dirname) {
|
|
|
289
289
|
// 7. KB 配置加载
|
|
290
290
|
// ═══════════════════════════════════════════════════════════════
|
|
291
291
|
|
|
292
|
-
/**
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
292
|
+
/** kb-state.json 文件名(唯一权威位置:spec 包裹包根目录) */
|
|
293
|
+
const KB_STATE_FILENAME = 'kb-state.json';
|
|
294
|
+
|
|
295
|
+
/** 向上搜索 kb-state.json 的最大层数(从 skywalk-sdd/lib/ 到 spec 根最多 3 层) */
|
|
296
|
+
const MAX_KB_STATE_LEVELS = 6;
|
|
297
|
+
|
|
298
|
+
let _isSpecPackageRoot = null;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* 惰性加载 resolve-spec-root.cjs 的 isSpecPackageRoot(避免加载期循环依赖)。
|
|
302
|
+
*/
|
|
303
|
+
function getIsSpecPackageRoot() {
|
|
304
|
+
if (!_isSpecPackageRoot) {
|
|
305
|
+
const mod = require(path.join(__dirname, '..', 'ontology', 'resolve-spec-root.cjs'));
|
|
306
|
+
_isSpecPackageRoot = mod.isSpecPackageRoot;
|
|
307
|
+
}
|
|
308
|
+
return _isSpecPackageRoot;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* kb-state.json 位置校验:宿主目录必须是 spec 包裹包根。
|
|
313
|
+
*
|
|
314
|
+
* 判据复用 `ontology/resolve-spec-root.cjs#isSpecPackageRoot`(与全仓 spec 识别
|
|
315
|
+
* 单一真相源对齐),不在本模块另立一套结构判据。
|
|
316
|
+
*
|
|
317
|
+
* @param {string} dir 候选宿主目录
|
|
318
|
+
* @returns {boolean}
|
|
319
|
+
*/
|
|
320
|
+
function isKbStateHostDir(dir) {
|
|
321
|
+
try {
|
|
322
|
+
return Boolean(getIsSpecPackageRoot()(dir));
|
|
323
|
+
} catch {
|
|
324
|
+
// ontology 层不可用(理论上部署布局下不会发生):保守判否,避免误用未知位置的配置
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
297
328
|
|
|
298
329
|
/**
|
|
299
330
|
* 加载 KB 配置(kb-state.json)。
|
|
300
331
|
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
332
|
+
* **唯一权威位置**:spec 包裹包根目录 `<spec-root>/kb-state.json`
|
|
333
|
+
* —— 由 `/opsx-kb-config` 写入(见 opsx-kb-config/reference.md §kb-state.json),
|
|
334
|
+
* spec 仓根、已 gitignore。
|
|
335
|
+
*
|
|
336
|
+
* 位置校验(本函数同时承担「位置正确性」判定,而不只是「能否读到」):
|
|
337
|
+
* 从 anchorDir 逐级向上查找同名文件,但**只接受宿主目录为 spec 包裹包根**的命中
|
|
338
|
+
* (isKbStateHostDir)。因此从 `<spec-root>/skywalk-sdd/`、`<spec-root>/skywalk-sdd/lib/`
|
|
339
|
+
* 等子目录调用都能正确命中 spec 根;而落在工作区根、代码仓根等错误位置的文件
|
|
340
|
+
* 不会被误用 —— 读到「不该读的配置」比读不到更危险。
|
|
306
341
|
*
|
|
307
342
|
* @param {string} [anchorDir=__dirname] — 搜索起点
|
|
308
|
-
* @param {object} [env=process.env] —
|
|
343
|
+
* @param {object} [env=process.env] — 保留参数(环境变量兜底由 resolveKbParams 处理)
|
|
309
344
|
* @returns {{ config: object|null, path: string|null, warnings: string[] }}
|
|
310
345
|
*/
|
|
311
346
|
function loadKbState(anchorDir = __dirname, env = process.env) {
|
|
347
|
+
void env;
|
|
312
348
|
const warnings = [];
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
let current =
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
candidatePaths.push(path.join(current, '.shared', 'kb-state.json'));
|
|
327
|
-
for (const ide of IDE_DIRS) {
|
|
328
|
-
candidatePaths.push(path.join(current, ide, 'skills', '.shared', 'kb-state.json'));
|
|
349
|
+
const start = path.resolve(anchorDir);
|
|
350
|
+
const fileRoot = path.parse(start).root;
|
|
351
|
+
|
|
352
|
+
let current = start;
|
|
353
|
+
for (let level = 0; level < MAX_KB_STATE_LEVELS; level += 1) {
|
|
354
|
+
const candidate = path.join(current, KB_STATE_FILENAME);
|
|
355
|
+
if (fs.existsSync(candidate) && isKbStateHostDir(current)) {
|
|
356
|
+
try {
|
|
357
|
+
const config = JSON.parse(stripBom(fs.readFileSync(candidate, 'utf8')));
|
|
358
|
+
return { config, path: candidate, warnings };
|
|
359
|
+
} catch (e) {
|
|
360
|
+
warnings.push(`kb-state.json 解析失败 (${candidate}): ${e.message}`);
|
|
361
|
+
}
|
|
329
362
|
}
|
|
330
363
|
if (current === fileRoot) break;
|
|
331
364
|
current = path.dirname(current);
|
|
332
365
|
}
|
|
333
366
|
|
|
334
|
-
for (const p of candidatePaths) {
|
|
335
|
-
try {
|
|
336
|
-
if (fs.existsSync(p)) {
|
|
337
|
-
let raw = fs.readFileSync(p, 'utf8');
|
|
338
|
-
raw = stripBom(raw);
|
|
339
|
-
const config = JSON.parse(raw);
|
|
340
|
-
return { config, path: p, warnings };
|
|
341
|
-
}
|
|
342
|
-
} catch (e) {
|
|
343
|
-
warnings.push(`kb-state.json 解析失败 (${p}): ${e.message}`);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
367
|
return { config: null, path: null, warnings };
|
|
348
368
|
}
|
|
349
369
|
|
|
@@ -797,7 +817,8 @@ module.exports = {
|
|
|
797
817
|
// Spec root
|
|
798
818
|
resolveSpecRootFast,
|
|
799
819
|
// KB state
|
|
800
|
-
|
|
820
|
+
KB_STATE_FILENAME,
|
|
821
|
+
isKbStateHostDir,
|
|
801
822
|
loadKbState,
|
|
802
823
|
resolveKbParams,
|
|
803
824
|
resolveKbScopes,
|